diff --git a/.github/workflows/e2e_tests.yaml b/.github/workflows/e2e_tests.yaml index 4bfe69e09..5e14d19cf 100644 --- a/.github/workflows/e2e_tests.yaml +++ b/.github/workflows/e2e_tests.yaml @@ -54,8 +54,7 @@ jobs: E2E_OPENAI_MODEL: ${{ vars.E2E_OPENAI_MODEL }} E2E_DEPLOYMENT_MODE: ${{ matrix.mode }} FAISS_VECTOR_STORE_ID: ${{ vars.FAISS_VECTOR_STORE_ID }} - # Override via repo Actions variable E2E_LLAMA_HOSTNAME; default matches server-mode lightspeed-stack.yaml - E2E_LLAMA_HOSTNAME: ${{ vars.E2E_LLAMA_HOSTNAME || 'llama-stack' }} + E2E_OGX_HOSTNAME: ogx steps: - uses: actions/checkout@v7 @@ -289,8 +288,8 @@ jobs: echo "=== Test failure logs ===" if [ "${{ matrix.mode }}" == "server" ]; then - echo "=== llama-stack logs ===" - docker compose logs llama-stack + echo "=== OGX logs ===" + docker compose logs ogx echo "" echo "=== lightspeed-stack logs ===" docker compose logs lightspeed-stack diff --git a/.github/workflows/e2e_tests_providers.yaml b/.github/workflows/e2e_tests_providers.yaml index 3d988e2b3..284ccb87f 100644 --- a/.github/workflows/e2e_tests_providers.yaml +++ b/.github/workflows/e2e_tests_providers.yaml @@ -355,8 +355,8 @@ jobs: echo "=== Test failure logs ===" if [ "${{ matrix.mode }}" == "server" ]; then - echo "=== llama-stack logs ===" - docker compose logs llama-stack + echo "=== OGX logs ===" + docker compose logs ogx echo "" echo "=== lightspeed-stack logs ===" docker compose logs lightspeed-stack diff --git a/.github/workflows/e2e_tests_rhaiis.yaml b/.github/workflows/e2e_tests_rhaiis.yaml index da2fe0497..0b1c81c2b 100644 --- a/.github/workflows/e2e_tests_rhaiis.yaml +++ b/.github/workflows/e2e_tests_rhaiis.yaml @@ -249,8 +249,8 @@ jobs: echo "=== Test failure logs ===" if [ "${{ matrix.mode }}" == "server" ]; then - echo "=== llama-stack logs ===" - docker compose logs llama-stack + echo "=== OGX logs ===" + docker compose logs ogx echo "" echo "=== lightspeed-stack logs ===" docker compose logs lightspeed-stack diff --git a/.gitignore b/.gitignore index 15f06684a..3d7a1f08f 100644 --- a/.gitignore +++ b/.gitignore @@ -183,7 +183,7 @@ dev/ # VSCode .vscode/ -# Llama related - when running the stack as lib client +# OGX related - when running the stack as lib client .llama # Database files @@ -195,7 +195,7 @@ requirements.*.backup # Local run files local-run.yaml -# Synthesized Llama Stack run.yaml written by unified library mode (LCORE-2336) +# Synthesized OGX run.yaml written by unified library mode (LCORE-2336) .generated/ # Sisyphus planning files diff --git a/.konflux/build-args-konflux.conf b/.konflux/build-args-konflux.conf index 02915eeb5..23ddf89d2 100644 --- a/.konflux/build-args-konflux.conf +++ b/.konflux/build-args-konflux.conf @@ -1,4 +1,4 @@ -BUILDER_BASE_IMAGE=quay.io/aipcc/base-images/cpu:3.5.1-1787661100 +BUILDER_BASE_IMAGE=quay.io/aipcc/base-images/cpu:3.5.1-1789137687 BUILDER_DNF_COMMAND=dnf -RUNTIME_BASE_IMAGE=quay.io/aipcc/base-images/cpu:3.5.1-1787661100 +RUNTIME_BASE_IMAGE=quay.io/aipcc/base-images/cpu:3.5.1-1789137687 RUNTIME_DNF_COMMAND=dnf diff --git a/.konflux/requirements.hashes.wheel.txt b/.konflux/requirements.hashes.wheel.txt index 8fc832288..51c516820 100644 --- a/.konflux/requirements.hashes.wheel.txt +++ b/.konflux/requirements.hashes.wheel.txt @@ -59,9 +59,9 @@ chevron==0.14.0 \ --hash=sha256:4cbb3abb3803127a24947e075384f21bde2e00d43b8b8026f4ba9af7f866a423 click==8.4.2 \ --hash=sha256:368d4af9976de9fc5efc4d8a26c64d23c187c21a2eba9336254832d466dd6198 -cryptography==49.0.0 \ - --hash=sha256:272f6cf35bd11146fb7182ed94b7123b9547174e944a0ec41fb547d1ad363982 \ - --hash=sha256:44e7dda87cce4e64a4eada2469ade616471772ee8203d7331967ef7ba2fee2bd +cryptography==50.0.1 \ + --hash=sha256:3e29639a33400721f710beeba7741e275bdb4c5bc53107c9dfef240d251c0590 \ + --hash=sha256:8ae962f84e7f36290feca5f8ba7bfc01fea9cd9a48f446bca52ef115657cca3d datasets==5.0.0 \ --hash=sha256:0fbc08ef020b03a22d91ad38b3b22091a1e18c1335f2963d357ea540bfb032e7 defusedxml==0.7.1 \ @@ -215,8 +215,8 @@ narwhals==2.24.0 \ --hash=sha256:30285014e6754ed468b29358bc0aebff0117d6a1d64383254ae55ceef31ffb25 networkx==3.6.1 \ --hash=sha256:56b687ad58bed743066f1b7d5e6f56a72d2f193c8eaf35064abaedda4fba3745 -nltk==3.10.0 \ - --hash=sha256:a15da2911adca5c7b4574b902f6b344f3f65db72ded7f79b30617bbe7100d318 +nltk==3.10.3 \ + --hash=sha256:fb9ae77ba878bf1f09e6cca649f2c296f20b1c83143932ae06c12018b505afd7 numpy==2.3.5 \ --hash=sha256:927205c8882f7a53543a8fc5b13fcb05771d6bfff35e06daf8030e57549cc7be \ --hash=sha256:a08c6c26c26d7530d09ac93bc177593958c2e3ce4a9b5750c14a80329cca157c diff --git a/.konflux/rpms.in.yaml b/.konflux/rpms.in.yaml index 2fa7d1470..3866b1d54 100644 --- a/.konflux/rpms.in.yaml +++ b/.konflux/rpms.in.yaml @@ -9,9 +9,8 @@ packages: ] upgradePackages: [ - libxslt, - libarchive, - python3-urllib3, + acl, + ffmpeg-free-rhai, ] contentOrigin: repofiles: ["./redhat.repo"] diff --git a/.konflux/rpms.lock.yaml b/.konflux/rpms.lock.yaml index 5cfaa7ad2..039f44652 100644 --- a/.konflux/rpms.lock.yaml +++ b/.konflux/rpms.lock.yaml @@ -4,6 +4,69 @@ lockfileVendor: redhat arches: - arch: aarch64 packages: + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/f/ffmpeg-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 1515025 + checksum: sha256:c6bfee462ad3b3f5b4fd19cf96367d549ea7bfeea0ab4ffe634dd497ba5847ba + name: ffmpeg-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libavcodec-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 4031909 + checksum: sha256:9ca273eea853bd5c0781add66b1620978468ce9e54a288e972bb668b4d6d7360 + name: libavcodec-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libavdevice-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 75640 + checksum: sha256:4f32d0216ddc8e6f83732aece446b0e618f0bcd358f85c86fb9d5e8b760c3da7 + name: libavdevice-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libavfilter-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 1446844 + checksum: sha256:795dd36bacac138d1efb3f1a41e8cb6e9f13843a856fd9f6ef6fe6e9c0b4bf9b + name: libavfilter-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libavformat-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 1136615 + checksum: sha256:c17182dfc59bda4af4bda6da1efcd13c860c11af50523910e708fd5debfa177c + name: libavformat-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libavutil-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 345879 + checksum: sha256:00d27c22f8aed4753ef047771597c7f263030e06549d445e22a9206abd1f3664 + name: libavutil-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libpostproc-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 39547 + checksum: sha256:8b1a8cc49a38014789179fb39eb3bbe81ae74b58da2d078f2d8581461222f03e + name: libpostproc-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libswresample-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 60307 + checksum: sha256:bb0259b949e925f3eed54e75cde70e76e0992938ee24c82cf1bae0b85a461a00 + name: libswresample-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/aarch64/rhelai/3.5/os/Packages/l/libswscale-free-rhai-6.1.6-9.el9ai.aarch64.rpm + repoid: rhelai-3.5-for-rhel-9-aarch64-rpms + size: 162587 + checksum: sha256:bd4e3882c78fffd4ffac7d99bdcea42d7eebd337a6da75223580eeb828ac55be + name: libswscale-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm - url: https://cdn.redhat.com/content/eus/rhel9/9.6/aarch64/appstream/os/Packages/c/cargo-1.84.1-1.el9.aarch64.rpm repoid: rhel-9-for-aarch64-appstream-eus-rpms__9_DOT_6 size: 7744425 @@ -60,6 +123,13 @@ arches: name: rust-std-static evr: 1.84.1-1.el9 sourcerpm: rust-1.84.1-1.el9.src.rpm + - url: https://cdn.redhat.com/content/eus/rhel9/9.6/aarch64/baseos/os/Packages/a/acl-2.4.0-0.el9_6.1.aarch64.rpm + repoid: rhel-9-for-aarch64-baseos-eus-rpms__9_DOT_6 + size: 77159 + checksum: sha256:3b88acdb403b28e3d9b72010d6794d2306a3a92295e264f9ca6447883886ac8b + name: acl + evr: 2.4.0-0.el9_6.1 + sourcerpm: acl-2.4.0-0.el9_6.1.src.rpm - url: https://cdn.redhat.com/content/eus/rhel9/9.6/aarch64/baseos/os/Packages/e/ed-1.14.2-12.el9.aarch64.rpm repoid: rhel-9-for-aarch64-baseos-eus-rpms__9_DOT_6 size: 78931 @@ -74,17 +144,80 @@ arches: name: info evr: 6.7-15.el9 sourcerpm: texinfo-6.7-15.el9.src.rpm - - url: https://cdn.redhat.com/content/eus/rhel9/9.6/aarch64/baseos/os/Packages/p/python3-urllib3-1.26.5-6.el9_6.2.noarch.rpm + - url: https://cdn.redhat.com/content/eus/rhel9/9.6/aarch64/baseos/os/Packages/l/libacl-2.4.0-0.el9_6.1.aarch64.rpm repoid: rhel-9-for-aarch64-baseos-eus-rpms__9_DOT_6 - size: 223128 - checksum: sha256:177899cd61996a1a459f23508428fe8be31d174cf998c624155e13137a10a4e9 - name: python3-urllib3 - evr: 1.26.5-6.el9_6.2 - sourcerpm: python-urllib3-1.26.5-6.el9_6.2.src.rpm + size: 24904 + checksum: sha256:44a0d95be843d5b5104a6182e13de076c3ed83321977aee6f69148a1d5fb2780 + name: libacl + evr: 2.4.0-0.el9_6.1 + sourcerpm: acl-2.4.0-0.el9_6.1.src.rpm source: [] module_metadata: [] - arch: x86_64 packages: + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/f/ffmpeg-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 1518752 + checksum: sha256:cd24a91037cbbdc29277a574666fc5e49dcfd582afd63f6f6fe0099ea1470c36 + name: ffmpeg-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libavcodec-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 4213330 + checksum: sha256:7786b455d74f44cc89b7a8017da2d606e3cbfa7f07b64ad575527e583bdbd178 + name: libavcodec-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libavdevice-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 76627 + checksum: sha256:620c454f720454d13e881966d770e598554b9db1f9e5daeed03a4c87aaf20e98 + name: libavdevice-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libavfilter-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 1531710 + checksum: sha256:32c1f98752cf9e40d18a6171ab68f7a3f9a5c435ae44e1beb6450ce1af183e9c + name: libavfilter-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libavformat-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 1122106 + checksum: sha256:aee63758df3f99f5962a5ba61ca07c6c9b5d1ccde4677dd6ddc4c999599cd921 + name: libavformat-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libavutil-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 351489 + checksum: sha256:3338518a95b0931f570cadea63983faaee1a053e382c6fe7b09cd6e5c629275d + name: libavutil-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libpostproc-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 46878 + checksum: sha256:31356012fb0d14be447a9357e4bb9be23de095072bdd5654d70efe2643fe1d11 + name: libpostproc-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libswresample-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 66438 + checksum: sha256:6252004c850c7741fb49e18c5eb86896192051caa6623c96051ac30c6e0c0a70 + name: libswresample-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm + - url: https://cdn.redhat.com/content/dist/layered/rhel9/x86_64/rhelai/3.5/os/Packages/l/libswscale-free-rhai-6.1.6-9.el9ai.x86_64.rpm + repoid: rhelai-3.5-for-rhel-9-x86_64-rpms + size: 188983 + checksum: sha256:f114a579b7cdc4458baecc516ca93f4abf64a66fa21eda0f186d5ef9799a8640 + name: libswscale-free-rhai + evr: 6.1.6-9.el9ai + sourcerpm: ffmpeg-6.1.6-9.el9ai.src.rpm - url: https://cdn.redhat.com/content/eus/rhel9/9.6/x86_64/appstream/os/Packages/c/cargo-1.84.1-1.el9.x86_64.rpm repoid: rhel-9-for-x86_64-appstream-eus-rpms__9_DOT_6 size: 8292467 @@ -141,6 +274,13 @@ arches: name: rust-std-static evr: 1.84.1-1.el9 sourcerpm: rust-1.84.1-1.el9.src.rpm + - url: https://cdn.redhat.com/content/eus/rhel9/9.6/x86_64/baseos/os/Packages/a/acl-2.4.0-0.el9_6.1.x86_64.rpm + repoid: rhel-9-for-x86_64-baseos-eus-rpms__9_DOT_6 + size: 77918 + checksum: sha256:7922ff8b02bdd4f0771e3036c3d293939bc2d182505772d7fccfb28a650fedc6 + name: acl + evr: 2.4.0-0.el9_6.1 + sourcerpm: acl-2.4.0-0.el9_6.1.src.rpm - url: https://cdn.redhat.com/content/eus/rhel9/9.6/x86_64/baseos/os/Packages/e/ed-1.14.2-12.el9.x86_64.rpm repoid: rhel-9-for-x86_64-baseos-eus-rpms__9_DOT_6 size: 79993 @@ -155,12 +295,12 @@ arches: name: info evr: 6.7-15.el9 sourcerpm: texinfo-6.7-15.el9.src.rpm - - url: https://cdn.redhat.com/content/eus/rhel9/9.6/x86_64/baseos/os/Packages/p/python3-urllib3-1.26.5-6.el9_6.2.noarch.rpm + - url: https://cdn.redhat.com/content/eus/rhel9/9.6/x86_64/baseos/os/Packages/l/libacl-2.4.0-0.el9_6.1.x86_64.rpm repoid: rhel-9-for-x86_64-baseos-eus-rpms__9_DOT_6 - size: 223128 - checksum: sha256:177899cd61996a1a459f23508428fe8be31d174cf998c624155e13137a10a4e9 - name: python3-urllib3 - evr: 1.26.5-6.el9_6.2 - sourcerpm: python-urllib3-1.26.5-6.el9_6.2.src.rpm + size: 25027 + checksum: sha256:3fefd0c83d5b54dab6f88bd85fd00493983fc26656c5e1f62f5ea56ce1711388 + name: libacl + evr: 2.4.0-0.el9_6.1 + sourcerpm: acl-2.4.0-0.el9_6.1.src.rpm source: [] module_metadata: [] diff --git a/.tekton/integration-tests/pipeline/lightspeed-stack-integration-test.yaml b/.tekton/integration-tests/pipeline/lightspeed-stack-integration-test.yaml index b522c6479..c1eda7285 100644 --- a/.tekton/integration-tests/pipeline/lightspeed-stack-integration-test.yaml +++ b/.tekton/integration-tests/pipeline/lightspeed-stack-integration-test.yaml @@ -15,7 +15,7 @@ spec: default: '{"components": [{"name":"lightspeed-stack-0-7", "containerImage": "quay.io/example/lightspeed-stack-0-7:latest"}]}' type: string - name: llama-stack-image - description: 'Llama Stack runs from source on UBI (init container clones repo and installs deps). Kept for logging/backwards compatibility.' + description: 'OGX runs from source on UBI (init container clones repo and installs deps). Parameter name kept for backwards compatibility.' default: 'run-from-source (UBI)' type: string - name: test-name @@ -81,7 +81,7 @@ spec: - name: instanceType value: "m5.large" - name: get-stack-images - description: Extract lightspeed-stack image and commit from SNAPSHOT (Llama Stack runs from source in-pod) + description: Extract lightspeed-stack image and commit from SNAPSHOT (OGX runs from source in-pod) runAfter: - provision-cluster params: diff --git a/Makefile b/Makefile index 3a1cd4f8a..2fa421ada 100644 --- a/Makefile +++ b/Makefile @@ -339,9 +339,9 @@ docstyle: ## Check the docstring style using Docstyle checker ruff: ## Check source code using Ruff linter uv run ruff check src tests --per-file-ignores=tests/*:S101 --per-file-ignores=scripts/*:S101 -lint-openapi: ## Lint docs/openapi.json (Spectral OAS ruleset; fail on error) +lint-openapi: ## Lint docs/devel_doc/openapi.json (Spectral OAS ruleset; fail on error) @if command -v npx >/dev/null 2>&1; then \ - npx --yes @stoplight/spectral-cli@6 lint docs/openapi.json --fail-severity error --display-only-failures; \ + npx --yes @stoplight/spectral-cli@6 lint docs/devel_doc/openapi.json --fail-severity error --display-only-failures; \ else \ echo "lint-openapi: skipping Spectral (npx not found). Install Node.js for OpenAPI lint locally; CI still runs it."; \ fi diff --git a/README.md b/README.md index 402995ada..d2bd85afb 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,7 @@ Lightspeed Core Stack is based on the FastAPI framework (Uvicorn). The service i # Prerequisites -* Python 3.12, or 3.13 - - please note that currently Python 3.14 is not officially supported +* Python 3.12, 3.13, or 3.14 - all sources are made (backward) compatible with Python 3.12; it is checked on CI * OpenAI API Key (Recommended for Getting Started) @@ -206,7 +205,7 @@ To quickly get hands on LCS, we can run it using the default configurations prov ``` 4. access LCS web UI at [http://localhost:8080/](http://localhost:8080/) -**Note**: `make run` uses containerized OGX (service mode). For details on container lifecycle management, customization, and troubleshooting, see the [Container Orchestration Guide](docs/devel_doc/container_orchestration.md). To run llama-stack manually instead, see the [OGX as separate server](#ogx-as-separate-server) section below. +**Note**: `make run` uses containerized OGX (service mode). For details on container lifecycle management, customization, and troubleshooting, see the [Container Orchestration Guide](docs/devel_doc/container_orchestration.md). To run OGX manually instead, see the [OGX as separate server](#ogx-as-separate-server) section below. ## Container Runtime Requirements @@ -337,7 +336,7 @@ Kotlin, which "wraps" the REST API stack in a suitable way, which is easier for many applications. -![Integration with OGX](docs/core2llama-stack_interface.png) +![Integration with OGX](docs/devel_doc/core2ogx_interface.png) @@ -354,7 +353,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: use_as_library_client: false url: http://localhost:8321 user_data_collection: @@ -366,7 +365,7 @@ user_data_collection: ### Degraded mode -Lightspeed core is able to continue operating in a _degraded but safe_ mode if the LLS service is not started or fails to start. When degraded, the `/health` endpoint report the LLS status and any relevant impacts so operators and automation can detect and respond. +Lightspeed core is able to continue operating in a _degraded but safe_ mode if the OGX service is not started or fails to start. When degraded, the `/health` endpoint report the OGX status and any relevant impacts so operators and automation can detect and respond. Degraded mode need to be enabled in `lightspeed-stack.yaml` configuration file: @@ -620,12 +619,12 @@ To run OGX in separate process, you need to have all dependencies installed. The ```toml [project] -name = "llama-stack-runner" +name = "ogx-runner" version = "0.1.0" description = "OGX runner" authors = [] dependencies = [ - "llama-stack==0.2.22", + "ogx==1.2.5", "fastapi>=0.115.12", "opentelemetry-sdk>=1.34.0", "opentelemetry-exporter-otlp>=1.34.0", @@ -659,7 +658,7 @@ To run OGX perform these two commands: ``` export OPENAI_API_KEY="sk-{YOUR-KEY}" -uv run llama stack run run.yaml +uv run ogx stack run run.yaml ``` ### Check connection to OGX @@ -683,14 +682,14 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: use_as_library_client: true # Unified mode (recommended): LCORE synthesizes the OGX run.yaml. # Point profile at a run.yaml-shaped file you author, or omit the config # block and drive everything from the top-level inference.providers # section over the built-in default baseline. config: - profile: + profile: user_data_collection: feedback_enabled: true feedback_storage: "/tmp/data/feedback" @@ -706,7 +705,7 @@ user_data_collection: ## OGX version check -During Lightspeed Core Stack service startup, the OGX version is retrieved. The version is tested against two constants `MINIMAL_SUPPORTED_LLAMA_STACK_VERSION` and `MAXIMAL_SUPPORTED_LLAMA_STACK_VERSION` which are defined in `src/constants.py`. If the actual OGX version is outside the range defined by these two constants, the service won't start and administrator will be informed about this problem. +During Lightspeed Core Stack service startup, the OGX version is retrieved. The version is tested against two constants `MINIMAL_SUPPORTED_OGX_VERSION` and `MAXIMAL_SUPPORTED_OGX_VERSION` which are defined in `src/constants.py`. If the actual OGX version is outside the range defined by these two constants, the service won't start and administrator will be informed about this problem. @@ -888,7 +887,7 @@ options: path where the synthesized OGX run.yaml is written in unified library mode (overwritten each boot, mode 0600; default: ./.generated/run.yaml) --migrate-config migrate a legacy two-file config to a unified single file and exit. Lifts the run.yaml given by --run-yaml - into the llama_stack.config.native_override of the -c lightspeed-stack.yaml and writes the result to + into the ogx.config.native_override of the -c lightspeed-stack.yaml and writes the result to --migrate-output. Replace literal secrets with ${env.VAR} references before or after migrating. --run-yaml RUN_YAML path to the legacy OGX run.yaml to migrate (used with --migrate-config) --migrate-output MIGRATE_OUTPUT @@ -918,15 +917,15 @@ Usage: make ... Available targets are: -run-stack Run lightspeed-stack directly, without building dependent service/s +run-ogx Run lightspeed-stack directly, without building dependent service/s run Run the service locally with dependent services -build-llama-stack-image Build OGX container image -stop-llama-stack-container Gracefully stop OGX container -remove-llama-stack-container Remove OGX container (saves logs first) -start-llama-stack-container Start OGX container -wait-for-llama-stack-health Wait for OGX container to be healthy -clean-llama-stack Remove container and image -run-llama-stack Start OGX with enriched config (for local service mode) +build-ogx-image Build OGX container image +stop-ogx-container Gracefully stop OGX container +remove-ogx-container Remove OGX container (saves logs first) +start-ogx-container Start OGX container +wait-for-ogx-health Wait for OGX container to be healthy +clean-ogx Remove container and image +run-ogx-local Start OGX with enriched config (for local service mode) test-unit Run the unit tests test-integration Run integration tests tests test-e2e Run end to end tests for the service @@ -1018,9 +1017,9 @@ When using OGX as a separate service, the existing `docker-compose.yaml` provide **Configuration** (`lightspeed-stack.yaml`): ```yaml -llama_stack: +ogx: use_as_library_client: false - url: http://llama-stack:8321 # container name from docker-compose.yaml + url: http://ogx:8321 # service name from docker-compose.yaml api_key: xyzzy ``` @@ -1057,7 +1056,7 @@ When embedding OGX directly in the container, use the existing `deploy/lightspee **Configuration** (`lightspeed-stack.yaml`): ```yaml -llama_stack: +ogx: use_as_library_client: true # Unified mode: the mounted run.yaml is the synthesis profile. (The # legacy library_client_config_path equivalent is deprecated, removed diff --git a/deploy/ogx/README.md b/deploy/ogx/README.md index 5554a1aca..27749f138 100644 --- a/deploy/ogx/README.md +++ b/deploy/ogx/README.md @@ -1,9 +1,9 @@ -# Llama Stack container image +# OGX container image -`test.containerfile` builds the Llama Stack server image used by -`docker-compose.yaml` (server mode, e.g. for the e2e suite). Besides the -Llama Stack distribution itself, the image bundles the pieces needed to -generate its run configuration at container start: +`test.containerfile` builds the OGX server image used by `docker-compose.yaml` +(server mode, e.g. for the e2e suite). Besides the OGX distribution itself, the +image bundles the pieces needed to generate its run configuration at container +start: - `/opt/app-root/ogx_configuration.py` — the config-generation script (copied from `src/ogx_configuration.py`). @@ -21,11 +21,12 @@ shape: - **Unified mode** — the `lightspeed-stack.yaml` carries a *synthesis input* (a non-empty `inference.providers` or `vector_store.providers`, - or a `llama_stack.config` block). The full `run.yaml` is synthesized - from it; no external `run.yaml` mount is needed. + or an `ogx.config` block). The full `run.yaml` is synthesized from it; + no external `run.yaml` mount is needed. - **Legacy mode** — no synthesis input present. The mounted `run.yaml` - (`$LLAMA_STACK_CONFIG`, default `/opt/app-root/run.yaml`) is enriched - with lightspeed dynamic values (BYOK RAG, Solr/OKP, Azure Entra ID). + (`$OGX_CONFIG`, with deprecated fallback `$LLAMA_STACK_CONFIG`, default + `/opt/app-root/run.yaml`) is enriched with lightspeed dynamic values + (BYOK RAG, Solr/OKP, Azure Entra ID). The repository `docker-compose.yaml` mounts both files and works for either mode — with a unified `lightspeed-stack.yaml` the `run.yaml` @@ -33,7 +34,7 @@ mount is simply ignored. A unified-only deployment needs just: ```yaml services: - llama-stack: + ogx: build: context: . dockerfile: deploy/ogx/test.containerfile @@ -48,5 +49,5 @@ services: The compose file also mounts host copies of the script, the baseline data directory, and the entrypoint over their baked-in counterparts, so `docker compose up` picks up local changes to any of them without an -image rebuild. Rebuild (`docker compose build llama-stack`) when +image rebuild. Rebuild (`docker compose build ogx`) when dependencies (`pyproject.toml` / `uv.lock`) or the providers change. diff --git a/dev-tools/fetch-jira.sh b/dev-tools/fetch-jira.sh index af1790246..fee9fbd67 100755 --- a/dev-tools/fetch-jira.sh +++ b/dev-tools/fetch-jira.sh @@ -86,34 +86,16 @@ fi # leading and trailing spaces so substring matching works cleanly). FETCHED_KEYS=" " -fetch_ticket() { - local key="$1" - local indent="${2:-}" - local depth="${3:-0}" - - # Cycle / dup protection - case "$FETCHED_KEYS" in - *" $key "*) return 0 ;; - esac - FETCHED_KEYS="$FETCHED_KEYS$key " - - local data - data=$(curl -sS --connect-timeout 10 --max-time 30 \ - -u "$JIRA_EMAIL:$JIRA_TOKEN" \ - "$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>/dev/null) - - # Optional: fetch comments (only if --comments was passed). Empty - # JSON object signals "no comments fetched" to the Python printer. - local comments_data='{}' - if [ "$FETCH_COMMENTS" -eq 1 ]; then - comments_data=$(curl -sS --connect-timeout 10 --max-time 30 \ - -u "$JIRA_EMAIL:$JIRA_TOKEN" \ - "$JIRA_INSTANCE/rest/api/3/issue/$key/comment" 2>/dev/null) || comments_data='{}' - fi +# The Python programs below are held in quoted heredocs rather than +# passed inline to 'python3 -c'. An inline program sits inside a +# double-quoted shell word, which makes every quote, dollar sign and +# backtick in it shell syntax first and Python second. That is what broke +# this file with an SC2140 warning, and it stays a hazard for anyone +# editing the extractor. A <<'EOF' heredoc is passed through verbatim, so +# the Python can be written exactly as Python. - if echo "$data" | python3 -c "import sys,json; json.load(sys.stdin)['key']" >/dev/null 2>&1; then - python3 -c " -import json, sys, textwrap +PRINT_TICKET_PY=$(cat <<'PYEOF_PRINT_TICKET_PY' +import datetime, json, sys, textwrap data = json.loads(sys.argv[1]) indent = sys.argv[2] @@ -133,6 +115,17 @@ if parent_key: print() +# Jira appends its own '#icft=KEY' tracking fragment to internal smart +# links and issue mentions. It is never part of the target and only adds +# noise to the rendered line, so drop it from every URL we print. +def clean_url(url): + return url.split('#icft=')[0] if '#icft=' in url else url + + +# List kinds that may appear nested inside a listItem. +NESTED_LIST_TYPES = ('bulletList', 'orderedList', 'taskList') + + # ADF (Atlassian Document Format) → markdown-ish text extractor. # Hoisted to top-level so both description and comments can use it. def extract_text(node, depth=0): @@ -142,23 +135,147 @@ def extract_text(node, depth=0): if ntype == 'text': text = node.get('text', '') marks = node.get('marks', []) + href = '' for m in marks: if m.get('type') == 'strong': text = f'**{text}**' elif m.get('type') == 'code': - text = f'\`{text}\`' + # A code run may itself contain backticks, which would + # end the span early. Fence it with one more backtick + # than its longest internal run, padding when the text + # starts or ends with one. + longest = 0 + run = 0 + for char in text: + run = run + 1 if char == '`' else 0 + longest = max(longest, run) + fence = '`' * (longest + 1) + pad = ' ' if text.startswith('`') or text.endswith('`') else '' + text = fence + pad + text + pad + fence + elif m.get('type') == 'em': + text = f'_{text}_' + elif m.get('type') == 'strike': + text = f'~~{text}~~' + elif m.get('type') == 'link': + href = m.get('attrs', {}).get('href', '') + # Keep the target: a link whose text differs from its href + # (ticket keys, 'here', PR titles) is otherwise lost. Appended + # after the other marks so the URL never lands inside a code + # span or bold run, whatever order the marks arrived in, and + # compared against the raw text so an autolinked bare URL that + # also carries a mark is still recognised as its own target. + href = clean_url(href) + if href and href != node.get('text', ''): + text = text + ' <' + href + '>' return [text] + if ntype in ('inlineCard', 'blockCard', 'embedCard'): + # Smart links (pasted Jira/GitHub URLs) carry the URL only here, + # either directly or inside the resolved JSON-LD 'data' blob. + # blockCard and embedCard are the block forms Jira produces when + # a URL is pasted on a line of its own, which is how most + # tickets reference a PR or another ticket. + attrs = node.get('attrs', {}) + data = attrs.get('data') + url = attrs.get('url') or (data.get('url', '') if isinstance(data, dict) else '') + url = clean_url(url) + return ['<' + url + '>'] if url else [] + if ntype == 'mention': + return [node.get('attrs', {}).get('text', '@?')] + if ntype == 'emoji': + attrs = node.get('attrs', {}) + return [attrs.get('text') or attrs.get('shortName') or ''] + if ntype == 'status': + # A status lozenge carries meaning no other node repeats, so a + # ticket saying a step is DONE reads as empty without this. + label = node.get('attrs', {}).get('text', '') + return ['[' + label + ']'] if label else [] + if ntype == 'date': + timestamp = node.get('attrs', {}).get('timestamp', '') + if not timestamp: + return [] + try: + moment = datetime.datetime.fromtimestamp( + int(timestamp) / 1000, datetime.timezone.utc + ) + return [moment.strftime('%Y-%m-%d')] + except (ValueError, TypeError, OverflowError, OSError): + return [str(timestamp)] + if ntype in ('media', 'mediaInline'): + # Attachments have no text of their own; name them so a ticket + # that argues from a screenshot does not read as if it argued + # from nothing. + attrs = node.get('attrs', {}) + name = attrs.get('alt') or attrs.get('id') or '' + return ['[attachment: ' + name + ']'] if name else ['[attachment]'] + if ntype in ('mediaSingle', 'mediaGroup'): + for c in node.get('content', []): + lines.extend(extract_text(c, depth)) + return lines if ntype == 'hardBreak': return ['\n'] + if ntype == 'rule': + return ['---'] + if ntype == 'paragraph': + # A paragraph's children are inline runs — text, smart links, + # mentions. The generic block walk below puts every child on its + # own line, which chops any sentence containing a link into + # fragments, so join them into one flowing line instead and let + # an explicit hardBreak be the only thing that splits it. + joined = ''.join( + piece + for c in node.get('content', []) + for piece in extract_text(c, depth) + ) + if not joined.strip(): + return [] + return joined.split('\n') + [''] if ntype == 'listItem': - child_text = [] + own_text = [] + nested = [] for c in node.get('content', []): - child_text.extend(extract_text(c, depth)) - return [' ' * depth + '- ' + ''.join(child_text).strip()] + if isinstance(c, dict) and c.get('type') in NESTED_LIST_TYPES: + # A nested list has already indented its own lines; folding + # it into the parent's text would flatten the whole tree + # onto one line. + nested.extend(extract_text(c, depth)) + else: + own_text.extend(extract_text(c, depth)) + # Join on a newline before collapsing so an item holding two + # paragraphs keeps a space between them instead of running the + # last word of one into the first word of the next. The marker + # is added by the enclosing list, which is the only place that + # knows whether the item needs a bullet or a number. + return [' '.join('\n'.join(own_text).split())] + nested if ntype in ('bulletList', 'orderedList'): + ordered = ntype == 'orderedList' + first = node.get('attrs', {}).get('order', 1) if ordered else 1 + try: + first = int(first) + except (ValueError, TypeError): + first = 1 + for number, c in enumerate(node.get('content', []), start=first): + item_lines = [item for item in extract_text(c, depth + 1) if item] + if not item_lines: + continue + marker = f'{number}. ' if ordered else '- ' + lines.append(' ' * (depth + 1) + marker + item_lines[0]) + # Anything after the first line is a nested list that brought + # its own marker and indentation. + lines.extend(item_lines[1:]) + return lines + if ntype == 'taskList': for c in node.get('content', []): lines.extend(extract_text(c, depth + 1)) return lines + if ntype == 'taskItem': + # Without the box a done item and an open one render identically. + child_text = [] + for c in node.get('content', []): + child_text.extend(extract_text(c, depth)) + body = ' '.join('\n'.join(child_text).split()) + state = node.get('attrs', {}).get('state', 'TODO') + box = '[x]' if state == 'DONE' else '[ ]' + return [' ' * depth + '- ' + box + ' ' + body] if body else [] if ntype == 'heading': level = node.get('attrs', {}).get('level', 1) child_text = [] @@ -166,14 +283,86 @@ def extract_text(node, depth=0): child_text.extend(extract_text(c, depth)) return ['#' * level + ' ' + ''.join(child_text).strip()] if ntype == 'codeBlock': + language = node.get('attrs', {}).get('language', '') or '' child_text = [] for c in node.get('content', []): child_text.extend(extract_text(c, depth)) - return ['\`\`\`\n' + ''.join(child_text) + '\n\`\`\`'] + return ['```' + language + '\n' + ''.join(child_text) + '\n```'] + if ntype in ('expand', 'nestedExpand'): + # The title is usually the only summary of what the collapsed + # block holds, and collapsing is exactly what an author does + # with long logs and stack traces. + title = node.get('attrs', {}).get('title', '') + child_text = [] + for c in node.get('content', []): + child_text.extend(extract_text(c, depth)) + body = '\n'.join(child_text).strip('\n') + head = ['**' + title + '**'] if title else [] + return head + (body.split('\n') if body else []) + [''] + if ntype == 'decisionList': + for c in node.get('content', []): + lines.extend(extract_text(c, depth + 1)) + return lines + if ntype == 'decisionItem': + child_text = [] + for c in node.get('content', []): + child_text.extend(extract_text(c, depth)) + body = ' '.join('\n'.join(child_text).split()) + return [' ' * depth + '- [decision] ' + body] if body else [] + if ntype == 'panel': + # Jira panels carry their severity in the attrs, not the text, + # so an info note and a warning read the same without this. + panel_type = node.get('attrs', {}).get('panelType', 'info') + child_text = [] + for c in node.get('content', []): + child_text.extend(extract_text(c, depth)) + body = '\n'.join(child_text).strip() + if not body: + return [] + body_lines = body.split('\n') + head = '**[' + panel_type.upper() + ']** ' + body_lines[0] + return [head] + body_lines[1:] + [''] + if ntype == 'blockquote': + child_text = [] + for c in node.get('content', []): + child_text.extend(extract_text(c, depth)) + # Children already come back one line each; joining on '' would + # run every paragraph of the quote together into a single line. + quoted = '\n'.join(child_text).strip() + if not quoted: + return [] + return ['> ' + ln if ln else '>' for ln in quoted.split('\n')] + if ntype == 'table': + rows = [] + for row in node.get('content', []): + cells = [] + header = False + for cell in row.get('content', []): + if cell.get('type') == 'tableHeader': + header = True + cell_text = [] + for c in cell.get('content', []): + cell_text.extend(extract_text(c, depth)) + # Collapse to a single line: a cell holding two + # paragraphs would otherwise inject a newline into the + # middle of the row and break the whole table. Join on a + # newline first so the paragraph boundary survives as the + # space that separates them. + cell = ' '.join('\n'.join(cell_text).split()) + # An unescaped pipe in the text would open a new column + # and shift every cell after it. + cell = cell.replace('\\', '\\\\').replace('|', '\\|') + cells.append(cell) + if not cells: + continue + rows.append(' | '.join(cells)) + # Emit the markdown rule under a header row so the result is + # a real table rather than pipe-separated lines. + if header and len(rows) == 1: + rows.append(' | '.join(['---'] * len(cells))) + return rows + [''] if rows else [] for c in node.get('content', []): lines.extend(extract_text(c, depth)) - if ntype == 'paragraph' and lines: - lines.append('') return lines @@ -181,7 +370,10 @@ def extract_text(node, depth=0): desc = fields.get('description') if desc and isinstance(desc, dict): text_lines = extract_text(desc) - desc_text = '\n'.join(text_lines).strip() + # Strip newlines only: a plain strip() would also eat the indent of the + # first line, so a description opening with a list lost its leading + # bullet indent while every later item kept it. + desc_text = '\n'.join(text_lines).strip('\n') if desc_text: for line in desc_text.split('\n'): print(f'{indent}{line}') @@ -222,7 +414,13 @@ if subtasks: # comments_data is the empty {} sentinel.) comments = comments_data.get('comments', []) if isinstance(comments_data, dict) else [] if comments: - print(f'{indent}Comments ({len(comments)}):') + # The endpoint caps a page at 100 comments. Saying so beats letting a + # reader believe a truncated thread is the whole discussion. + total = comments_data.get('total', len(comments)) + if isinstance(total, int) and total > len(comments): + print(f'{indent}Comments ({len(comments)} of {total}; rest not fetched):') + else: + print(f'{indent}Comments ({len(comments)}):') for c in comments: author = c.get('author', {}).get('displayName') or c.get('author', {}).get('emailAddress') or 'unknown' created = c.get('created', '')[:10] # YYYY-MM-DD @@ -231,7 +429,7 @@ if comments: if isinstance(body, dict): # Reuse the same ADF extractor used for descriptions. text_lines = extract_text(body) - text = '\n'.join(text_lines).strip() + text = '\n'.join(text_lines).strip('\n') if not text: text = '(comment body in ADF format; no text extracted)' elif isinstance(body, str): @@ -241,18 +439,69 @@ if comments: for line in text.split('\n'): print(f'{indent} {line}') print() -" "$data" "$indent" "$comments_data" - else - echo "${indent}Error fetching $key" - echo "$data" | head -3 - return 1 - fi +PYEOF_PRINT_TICKET_PY +) - # Recurse into related tickets if depth > 0 - if [ "$depth" -gt 0 ]; then - # Extract subtask + linked-issue keys from already-fetched data - local related_keys - related_keys=$(echo "$data" | python3 -c " +# Prints the next startAt offset for a comment page, or nothing when the +# thread is exhausted. +COMMENT_NEXT_PY=$(cat <<'PYEOF_COMMENT_NEXT_PY' +import json, sys +try: + page = json.load(open(sys.argv[1])) + start = int(page.get('startAt', 0)) + got = len(page.get('comments', [])) + total = int(page.get('total', 0)) + if got and start + got < total: + print(start + got) +except Exception: + pass +PYEOF_COMMENT_NEXT_PY +) + +# Merges numbered comment pages back into one response-shaped document. +MERGE_COMMENTS_PY=$(cat <<'PYEOF_MERGE_COMMENTS_PY' +import json, os, sys +directory = sys.argv[1] +names = sorted(os.listdir(directory), key=lambda f: int(f.split('.')[0])) +comments = [] +total = 0 +for name in names: + page = json.load(open(os.path.join(directory, name))) + comments.extend(page.get('comments', [])) + total = max(total, int(page.get('total', 0))) +print(json.dumps({'comments': comments, 'total': max(total, len(comments))})) +PYEOF_MERGE_COMMENTS_PY +) + +# Prints the search cursor for the next page, or nothing when it is the last. +SEARCH_NEXT_PY=$(cat <<'PYEOF_SEARCH_NEXT_PY' +import json, sys +try: + page = json.load(open(sys.argv[1])) + token = page.get('nextPageToken') + if token and page.get('isLast') is not True: + print(token) +except Exception: + pass +PYEOF_SEARCH_NEXT_PY +) + +# Merges numbered search pages into one issues list. +MERGE_ISSUES_PY=$(cat <<'PYEOF_MERGE_ISSUES_PY' +import json, os, sys +directory = sys.argv[1] +names = sorted(os.listdir(directory), key=lambda f: int(f.split('.')[0])) +issues = [] +last = True +for name in names: + page = json.load(open(os.path.join(directory, name))) + issues.extend(page.get('issues', [])) + last = page.get('isLast') is not False and not page.get('nextPageToken') +print(json.dumps({'issues': issues, 'isLast': last})) +PYEOF_MERGE_ISSUES_PY +) + +RELATED_KEYS_PY=$(cat <<'PYEOF_RELATED_KEYS_PY' import json, sys try: d = json.load(sys.stdin) @@ -268,14 +517,10 @@ try: print(' '.join(out)) except Exception: pass -" 2>/dev/null) +PYEOF_RELATED_KEYS_PY +) - # Also fetch JQL parent= children - local jql_kids - jql_kids=$(curl -sS --connect-timeout 10 --max-time 30 \ - -u "$JIRA_EMAIL:$JIRA_TOKEN" \ - "$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${key}&fields=key&maxResults=20" 2>/dev/null | \ - python3 -c " +JQL_KIDS_PY=$(cat <<'PYEOF_JQL_KIDS_PY' import json, sys try: d = json.load(sys.stdin) @@ -283,41 +528,156 @@ try: print(issue['key']) except Exception: pass -" 2>/dev/null | tr '\n' ' ') +PYEOF_JQL_KIDS_PY +) + +CHILD_KEYS_PY=$(cat <<'PYEOF_CHILD_KEYS_PY' +import json, sys +try: + data = json.load(sys.stdin) + for issue in data.get('issues', []): + key = issue['key'] + summary = issue['fields']['summary'] + status = issue['fields']['status']['name'] + itype = issue['fields']['issuetype']['name'] + print(f'{key} ({itype}) [{status}]: {summary}') + # maxResults caps this request; a parent with more children would + # otherwise show a short list indistinguishable from a complete one. + if data.get('isLast') is False or data.get('nextPageToken'): + print('(more children exist than were fetched)') +except Exception: + pass +PYEOF_CHILD_KEYS_PY +) + +# Jira serves list endpoints one page at a time. Pages are collected as files +# under a scratch directory and merged, so a long comment thread or a parent +# with many children is returned whole rather than silently cut off at the +# first page. PAGE_LIMIT is a stop so a malformed cursor cannot spin forever. +PAGE_LIMIT=50 +PAGE_DIR=$(mktemp -d) +trap 'rm -rf "$PAGE_DIR"' EXIT + +fetch_all_comments() { + local key="$1" + local dir="$PAGE_DIR/comments" + rm -rf "$dir" + mkdir -p "$dir" + local start=0 + local page_no=0 + while [ "$page_no" -lt "$PAGE_LIMIT" ]; do + curl -sS --connect-timeout 10 --max-time 30 \ + -u "$JIRA_EMAIL:$JIRA_TOKEN" \ + "$JIRA_INSTANCE/rest/api/3/issue/$key/comment?startAt=$start&maxResults=100" \ + -o "$dir/$page_no.json" 2>/dev/null || return 1 + start=$(python3 -c "$COMMENT_NEXT_PY" "$dir/$page_no.json" 2>/dev/null) || return 1 + page_no=$((page_no + 1)) + if [ -z "$start" ]; then + break + fi + done + python3 -c "$MERGE_COMMENTS_PY" "$dir" 2>/dev/null || return 1 +} + +fetch_all_children() { + local parent="$1" + local fields="$2" + local dir="$PAGE_DIR/children" + rm -rf "$dir" + mkdir -p "$dir" + local token='' + local page_no=0 + local url + while [ "$page_no" -lt "$PAGE_LIMIT" ]; do + url="$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${parent}&fields=${fields}&maxResults=100" + if [ -n "$token" ]; then + url="$url&nextPageToken=$token" + fi + curl -sS --connect-timeout 10 --max-time 30 \ + -u "$JIRA_EMAIL:$JIRA_TOKEN" "$url" -o "$dir/$page_no.json" 2>/dev/null || return 1 + token=$(python3 -c "$SEARCH_NEXT_PY" "$dir/$page_no.json" 2>/dev/null) || return 1 + page_no=$((page_no + 1)) + if [ -z "$token" ]; then + break + fi + done + python3 -c "$MERGE_ISSUES_PY" "$dir" 2>/dev/null || return 1 +} + +# Every requested ticket is attempted even when an earlier one fails, so one +# unreachable key does not hide the rest of the output; the script still exits +# non-zero if any of them failed, including a failure deep in the recursion. +EXIT_STATUS=0 + +fetch_ticket() { + local key="$1" + local indent="${2:-}" + local depth="${3:-0}" + + # Cycle / dup protection + case "$FETCHED_KEYS" in + *" $key "*) return 0 ;; + esac + FETCHED_KEYS="$FETCHED_KEYS$key " + + # The '|| data=' guard matters: under 'set -e' an unguarded assignment + # from a failing curl aborts the whole script, so a single unreachable + # ticket would kill a multi-ticket or recursive run instead of falling + # through to the 'Error fetching' branch below and carrying on. + local data + data=$(curl -sS --connect-timeout 10 --max-time 30 \ + -u "$JIRA_EMAIL:$JIRA_TOKEN" \ + "$JIRA_INSTANCE/rest/api/3/issue/$key?fields=summary,status,issuetype,description,issuelinks,subtasks,parent" 2>/dev/null) || data='' + + # Optional: fetch comments (only if --comments was passed). Empty + # JSON object signals "no comments fetched" to the Python printer. + local comments_data='{}' + if [ "$FETCH_COMMENTS" -eq 1 ]; then + comments_data=$(fetch_all_comments "$key") || comments_data='{}' + fi + + if echo "$data" | python3 -c "import sys,json; json.load(sys.stdin)['key']" >/dev/null 2>&1; then + python3 -c "$PRINT_TICKET_PY" "$data" "$indent" "$comments_data" + else + echo "${indent}Error fetching $key" + echo "$data" | head -3 + return 1 + fi + + # Recurse into related tickets if depth > 0 + if [ "$depth" -gt 0 ]; then + # Extract subtask + linked-issue keys from already-fetched data + local related_keys + related_keys=$(echo "$data" | python3 -c "$RELATED_KEYS_PY" 2>/dev/null) + + # Also fetch JQL parent= children + local jql_kids + jql_kids=$(fetch_all_children "$key" "key" 2>/dev/null | \ + python3 -c "$JQL_KIDS_PY" 2>/dev/null | tr '\n' ' ') || jql_kids='' local rk for rk in $related_keys $jql_kids; do [ -z "$rk" ] && continue echo - fetch_ticket "$rk" "${indent} " $((depth - 1)) + # A relation we cannot fetch must not abandon the rest of the + # recursion, but it is still a failure: record it rather than + # swallowing it, or the run reports success having printed an + # error. + fetch_ticket "$rk" "${indent} " $((depth - 1)) || EXIT_STATUS=1 done fi } # Fetch main ticket (with depth recursion if requested) -fetch_ticket "$TICKET" "" "$LINKED_DEPTH" +fetch_ticket "$TICKET" "" "$LINKED_DEPTH" || EXIT_STATUS=1 # At depth 0, also list JQL parent= children as a flat summary (legacy # behavior — useful as a quick "what's underneath" overview without # fetching each one). At depth > 0, the recursive fetch_ticket already # pulled them in, so skip this listing to avoid duplication. if [ "$LINKED_DEPTH" -eq 0 ]; then - CHILD_KEYS=$(curl -sS --connect-timeout 10 --max-time 30 \ - -u "$JIRA_EMAIL:$JIRA_TOKEN" \ - "$JIRA_INSTANCE/rest/api/3/search/jql?jql=parent%3D${TICKET}&fields=key,summary,status,issuetype&maxResults=20" 2>/dev/null | \ - python3 -c " -import json, sys -try: - data = json.load(sys.stdin) - for issue in data.get('issues', []): - key = issue['key'] - summary = issue['fields']['summary'] - status = issue['fields']['status']['name'] - itype = issue['fields']['issuetype']['name'] - print(f'{key} ({itype}) [{status}]: {summary}') -except Exception: - pass -" 2>/dev/null) + CHILD_KEYS=$(fetch_all_children "$TICKET" "key,summary,status,issuetype" 2>/dev/null | \ + python3 -c "$CHILD_KEYS_PY" 2>/dev/null) || CHILD_KEYS='' if [ -n "$CHILD_KEYS" ]; then echo "Child issues:" @@ -336,5 +696,7 @@ for extra in "$@"; do fi echo "────────────────────────────────────────────────────────" echo "" - fetch_ticket "$extra" "" "$LINKED_DEPTH" + fetch_ticket "$extra" "" "$LINKED_DEPTH" || EXIT_STATUS=1 done + +exit "$EXIT_STATUS" diff --git a/docker-compose.yaml b/docker-compose.yaml index 16dcda1bc..6dff730b7 100755 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,11 +1,11 @@ services: # Red Hat OGX distribution with FAISS - llama-stack: + ogx: build: context: . dockerfile: deploy/ogx/test.containerfile platform: linux/amd64 - container_name: llama-stack + container_name: ogx ports: - "8321:8321" # Expose OGX on 8321 (adjust if needed) depends_on: @@ -14,7 +14,7 @@ services: volumes: # Used in legacy mode only: with a unified lightspeed-stack.yaml (one # carrying inference.providers / vector_store.providers / a - # llama_stack.config block) the entrypoint synthesizes the run config + # ogx.config block) the entrypoint synthesizes the run config # from lightspeed-stack.yaml and this mount is ignored - ./run.yaml:/opt/app-root/run.yaml:z # Host copies so `docker compose up` picks up script changes without rebuilding OGX @@ -24,7 +24,7 @@ services: - ${GCP_KEYS_PATH:-./tmp/.gcp-keys-dummy}:/opt/app-root/.gcp-keys:ro - ./lightspeed-stack.yaml:/opt/app-root/lightspeed-stack.yaml:ro,z # Writable OGX storage (rag/ work copy is re-seeded by entrypoint each start) - - llama-storage:/opt/app-root/src/.llama/storage + - ogx-storage:/opt/app-root/src/.llama/storage # Read-only e2e FAISS fixtures — never mount these as the live KV_RAG_PATH - ./tests/e2e/rag:/opt/app-root/src/.llama/storage/.e2e-rag-seed:ro,z - mock-tls-certs:/certs:ro @@ -80,8 +80,8 @@ services: - SOLR_CONTENT_FIELD=${SOLR_CONTENT_FIELD:-} - SOLR_EMBEDDING_MODEL=${SOLR_EMBEDDING_MODEL:-} - SOLR_EMBEDDING_DIM=${SOLR_EMBEDDING_DIM:-} - #required for enrichment - - E2E_LLAMA_HOSTNAME=${E2E_LLAMA_HOSTNAME:-llama-stack} + # Required for enrichment; E2E_LLAMA_HOSTNAME is the external/GitHub Actions name + - E2E_OGX_HOSTNAME=${E2E_OGX_HOSTNAME:-${E2E_LLAMA_HOSTNAME:-ogx}} networks: - lightspeednet healthcheck: @@ -111,9 +111,9 @@ services: - CLIENT_SECRET=${CLIENT_SECRET:-} # FAISS vector store ID (used by inline RAG config) - FAISS_VECTOR_STORE_ID=${FAISS_VECTOR_STORE_ID:-} - # Substituted in mounted lightspeed-stack.yaml (llama_stack.url); GitHub Actions sets via vars - - E2E_LLAMA_HOSTNAME=${E2E_LLAMA_HOSTNAME:-llama-stack} - - E2E_LLAMA_PORT=${E2E_LLAMA_PORT:-8321} + # Substituted in mounted lightspeed-stack.yaml (ogx.url); GitHub Actions sets E2E_LLAMA_HOSTNAME + - E2E_OGX_HOSTNAME=${E2E_OGX_HOSTNAME:-${E2E_LLAMA_HOSTNAME:-ogx}} + - E2E_OGX_PORT=${E2E_OGX_PORT:-${E2E_LLAMA_PORT:-8321}} # OpenTelemetry configuration (tracing disabled by default) - OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT:-} - OTEL_EXPORTER_OTLP_PROTOCOL=${OTEL_EXPORTER_OTLP_PROTOCOL:-} @@ -121,7 +121,7 @@ services: - OTEL_ANONYMIZATION_SECRET=${OTEL_ANONYMIZATION_SECRET:-lightspeed-stack-otel-anonymization-dev-default} - OTEL_SDK_DISABLED=${OTEL_SDK_DISABLED:-true} depends_on: - llama-stack: + ogx: condition: service_healthy mock-mcp: condition: service_healthy @@ -186,7 +186,7 @@ services: volumes: - llama-storage: + ogx-storage: mock-tls-certs: networks: diff --git a/docs/basic_info/getting_started.md b/docs/basic_info/getting_started.md index 0b17e6e7f..4377501c7 100644 --- a/docs/basic_info/getting_started.md +++ b/docs/basic_info/getting_started.md @@ -24,7 +24,9 @@ It is possible to run Lightspeed Core Stack service with OGX "embedded" as a Pyt 1. Add and install all required dependencies ```bash uv add \ - "llama-stack==0.2.22" \ + "ogx==1.2.5" \ + "ogx-api==1.2.5" \ + "ogx-client==1.2.5" \ "fastapi>=0.115.12" \ "opentelemetry-sdk>=1.34.0" \ "opentelemetry-exporter-otlp>=1.34.0" \ diff --git a/docs/basic_info/overview.svg b/docs/basic_info/overview.svg index 02dd918d4..3f32b9bc5 100644 --- a/docs/basic_info/overview.svg +++ b/docs/basic_info/overview.svg @@ -1,7 +1,7 @@ - + @@ -168,7 +168,7 @@
-
Llama Stack
+
OGX
(library or
service mode)
@@ -453,7 +453,7 @@
-
Llama Stack
Configuration
+
OGX
Configuration
diff --git a/docs/benchmarks/tokenizer/10000_lines.svg b/docs/benchmarks/tokenizer/10000_lines.svg index 7c2b11558..11dda4264 100644 --- a/docs/benchmarks/tokenizer/10000_lines.svg +++ b/docs/benchmarks/tokenizer/10000_lines.svg @@ -1,14 +1,14 @@ - - + + -
-
Llama Stack
+
OGX
(library or
service mode)
@@ -677,7 +677,7 @@
-
Llama Stack
Configuration
+
OGX
Configuration
@@ -1319,7 +1319,7 @@
-
Llama Stack
+
OGX
configuration
generator
diff --git a/docs/demos/lcore/images/llama_stack_as_library.svg b/docs/demos/lcore/images/ogx_as_library.svg similarity index 99% rename from docs/demos/lcore/images/llama_stack_as_library.svg rename to docs/demos/lcore/images/ogx_as_library.svg index bc2efe117..67f7a9d16 100644 --- a/docs/demos/lcore/images/llama_stack_as_library.svg +++ b/docs/demos/lcore/images/ogx_as_library.svg @@ -102,12 +102,12 @@
-
Llama Stack as a library (Python package)
+
OGX as a library (Python package)
- Llama Stack library + OGX library diff --git a/docs/demos/lcore/images/llama_stack_as_service.svg b/docs/demos/lcore/images/ogx_as_service.svg similarity index 99% rename from docs/demos/lcore/images/llama_stack_as_service.svg rename to docs/demos/lcore/images/ogx_as_service.svg index 1a53f2b11..2bb303837 100644 --- a/docs/demos/lcore/images/llama_stack_as_service.svg +++ b/docs/demos/lcore/images/ogx_as_service.svg @@ -126,12 +126,12 @@
-
Llama Stack service
+
OGX service
- Llama Stack service + OGX service diff --git a/docs/demos/lcore/images/llama_stack_in_container.svg b/docs/demos/lcore/images/ogx_in_container.svg similarity index 96% rename from docs/demos/lcore/images/llama_stack_in_container.svg rename to docs/demos/lcore/images/ogx_in_container.svg index a17f6aa76..c26ab020d 100644 --- a/docs/demos/lcore/images/llama_stack_in_container.svg +++ b/docs/demos/lcore/images/ogx_in_container.svg @@ -1,7 +1,7 @@ - + @@ -150,7 +150,7 @@
-
Llama Stack server
+
OGX server
diff --git a/docs/demos/lcore/images/ogx_providers.svg b/docs/demos/lcore/images/ogx_providers.svg new file mode 100644 index 000000000..0c7038ae2 --- /dev/null +++ b/docs/demos/lcore/images/ogx_providers.svg @@ -0,0 +1,4 @@ + + + +
Persistent storage
Service that calls
OGX
ogx
Agents
RAG
Conversation cache
OKP
Question validators
Answer redactors
Summarizers
MCP servers
OGX
Configuration






PostgreSQL






SQLite
Persistent storage






PostgreSQL






SQLite
Persistent storage






PostgreSQL






SQLite
diff --git a/docs/demos/lcore/lcore.md b/docs/demos/lcore/lcore.md index 9b7395278..fd4886487 100644 --- a/docs/demos/lcore/lcore.md +++ b/docs/demos/lcore/lcore.md @@ -22,7 +22,7 @@ ptisnovs@redhat.com ## OGX -![LCORE](images/llama_stack_logo.png) +![OGX](images/ogx_arch.png) --- @@ -38,7 +38,7 @@ ptisnovs@redhat.com --- -![LS1](images/llama_stack.png) +![OGX architecture](images/ogx_arch.svg) --- @@ -135,10 +135,6 @@ ptisnovs@redhat.com --- -![LS-providers](images/llama_stack_providers.svg) - ---- - ### Communication with OGX * CLI @@ -161,19 +157,19 @@ ptisnovs@redhat.com ### OGX as a library -![LS1](images/llama_stack_as_library.svg) +![LS1](images/ogx_as_library.svg) --- ### OGX as a service -![LS1](images/llama_stack_as_service.svg) +![LS1](images/ogx_as_service.svg) --- ### Run inside container -![LS1](images/llama_stack_in_container.svg) +![LS1](images/ogx_in_container.svg) --- @@ -185,7 +181,7 @@ Python ecosystem ``` pdm init -pdm add llama-stack fastapi opentelemetry-sdk \ +pdm add ogx ogx-api ogx-client fastapi opentelemetry-sdk \ opentelemetry-exporter-otlp opentelemetry-instrumentation \ aiosqlite litellm uvicorn blobfile ``` @@ -196,13 +192,13 @@ aiosqlite litellm uvicorn blobfile ```toml [project] -name = "llama-stack-demo" +name = "ogx-demo" version = "0.1.0" description = "Default template for PDM package" authors = [] dependencies = [ - "llama-stack==0.2.20", - "llama-stack-client==0.2.20", + "ogx==1.2.5", + "ogx-client==1.2.5", "opentelemetry-sdk>=1.34.0", "opentelemetry-exporter-otlp>=1.34.0", "opentelemetry-instrumentation>=0.55b0", @@ -224,7 +220,7 @@ distribution = false ### Starting OGX ```bash -uv run llama stack run +uv run ogx stack run ``` --- @@ -232,7 +228,7 @@ uv run llama stack run ### List of OGX API ```bash -uv run llama stack list-apis +uv run ogx stack list-apis ``` --- @@ -290,7 +286,7 @@ uv run llama stack list-apis ### List of providers ```bash -uv run llama stack list-providers +uv run ogx stack list-providers ``` --- @@ -343,7 +339,7 @@ uv run llama stack list-providers --- -![LS1](images/llama_stack_arch.svg) +![LS1](images/ogx_arch.svg) --- diff --git a/docs/design/byok-confluence-import/byok-confluence-import.md b/docs/design/byok-confluence-import/byok-confluence-import.md index 6277490d6..60a6686f7 100644 --- a/docs/design/byok-confluence-import/byok-confluence-import.md +++ b/docs/design/byok-confluence-import/byok-confluence-import.md @@ -44,7 +44,7 @@ unchanged pages). - **R4:** Every chunk in the built store carries the source page's title and canonical Confluence URL, so answers can cite the page. - **R5:** A single command performs fetch → build → artifact - (`llamastack-faiss` file by default; optional OCI image via the existing + (`llamastack-faiss` rag-content artifact name; optional OCI image via the existing `--output-image`). - **R6:** Incremental refresh: a re-run against unchanged spaces performs no page-body fetches and no re-embeddings; changed pages are re-imported; diff --git a/docs/design/byok-pdf/byok-pdf.md b/docs/design/byok-pdf/byok-pdf.md index 1a82db734..b2ad11ee6 100644 --- a/docs/design/byok-pdf/byok-pdf.md +++ b/docs/design/byok-pdf/byok-pdf.md @@ -114,7 +114,7 @@ These are baked into `PDFReader.__init__`. **No CLI flags expose them in v1.** I PDFs go through the same chunking path as HTML and Markdown. Two predicates in `document_processor.py` need to learn `"pdf"`: - The `Settings.node_parser = MarkdownNodeParser()` branch in `_BaseDB.__init__`. -- The same branch in `_LlamaStackDB.__init__` (currently a separate copy of the predicate). +- The same branch in `_LlamaStackDB.__init__` (rag-content class name; currently a separate copy of the predicate). To prevent these two tuples from drifting, extract a single module-level constant and use it from both call sites: diff --git a/docs/design/conversation-compaction/conversation-compaction.md b/docs/design/conversation-compaction/conversation-compaction.md index 6cdb9991d..455d9acb0 100644 --- a/docs/design/conversation-compaction/conversation-compaction.md +++ b/docs/design/conversation-compaction/conversation-compaction.md @@ -352,7 +352,7 @@ Use the same pattern as `conversations_v1.py:240-246`: ``` python items_response = await client.conversations.items.list( - conversation_id=llama_stack_conv_id, + conversation_id=ogx_conv_id, after=None, include=None, limit=None, diff --git a/docs/design/human-in-the-loop/human-in-the-loop-spike.md b/docs/design/human-in-the-loop/human-in-the-loop-spike.md index 7195b430e..cb1b901d5 100644 --- a/docs/design/human-in-the-loop/human-in-the-loop-spike.md +++ b/docs/design/human-in-the-loop/human-in-the-loop-spike.md @@ -325,8 +325,8 @@ Reference existing docs in docs/ for style. No PoC was built for this spike. The core mechanisms are already validated: -1. **OGX approval types exist**: `MCPApprovalRequest` and - `MCPApprovalResponse` are defined in `llama_stack_api.openai_responses` +1. **OGX approval types exist**: `OpenAIResponseMCPApprovalRequest` and + `OpenAIResponseMCPApprovalResponse` are defined in `ogx_api.openai_responses` 2. **LCS already parses approval events**: `build_tool_call_summary()` in [responses.py:1067-1094](../../../src/utils/responses.py#L1067-L1094) handles both `mcp_approval_request` and `mcp_approval_response` types @@ -369,7 +369,7 @@ async def get_mcp_tools(...) -> list[InputToolMCP]: ### OGX Support -From `llama_stack_api.openai_responses`: +From `ogx_api.openai_responses`: ```python class ApprovalFilter(BaseModel): diff --git a/docs/design/human-in-the-loop/human-in-the-loop.md b/docs/design/human-in-the-loop/human-in-the-loop.md index dfb884f78..a513b683b 100644 --- a/docs/design/human-in-the-loop/human-in-the-loop.md +++ b/docs/design/human-in-the-loop/human-in-the-loop.md @@ -7,7 +7,7 @@ | **Authors** | Lightspeed Core Team | | **Feature** | [LCORE-268](https://redhat.atlassian.net/browse/LCORE-268) | | **Spike** | [LCORE-1589](https://redhat.atlassian.net/browse/LCORE-1589) | -| **Links** | [MCP Spec](https://modelcontextprotocol.io), [OGX](https://github.com/meta-llama/llama-stack) | +| **Links** | [MCP Spec](https://modelcontextprotocol.io), [OGX](https://github.com/ogx-ai/ogx) | ## What @@ -527,10 +527,10 @@ async def get_mcp_tools(...) -> list[InputToolMCP]: require_approval = mcp_server.require_approval if isinstance(require_approval, ApprovalFilter): # Convert to OGX's ApprovalFilter format - require_approval = LlamaStackApprovalFilter( + require_approval = ApprovalFilter( always=require_approval.always or None, never=require_approval.never or None, - ) + ) # ogx_api.openai_responses.ApprovalFilter tools.append( InputToolMCP( @@ -596,7 +596,7 @@ Example config files go in `examples/`. ## Appendix A: OGX Types Reference -From `llama_stack_api.openai_responses`: +From `ogx_api.openai_responses`: ```python class ApprovalFilter(BaseModel): diff --git a/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.puml b/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.puml index c522c2087..1e443aab2 100644 --- a/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.puml +++ b/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.puml @@ -11,8 +11,8 @@ alt Startup admin ->> runner: Start runner ->> cfg_1: Read\nglobal\nconfiguration cfg_1 ->> runner: Global\nconfiguration -runner ->> cfg_2: Generate\nLlama\nStack\nconfiguration -runner ->> ls_runner: Start\nLlama Stack +runner ->> cfg_2: Generate\nOGX\nconfiguration +runner ->> ls_runner: Start\nOGX ls_runner ->> ls_service: Startup ls_service ->> ls_runner: Status + PID ls_runner ->> runner: Status + PID diff --git a/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.svg b/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.svg index 89b23ada0..c338cd4fa 100644 --- a/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.svg +++ b/docs/design/low-overhead-deployment-for-server-mode/sequence_diagram.svg @@ -42,17 +42,17 @@ Lightspeed core service - Llama Stack - runner + OGX + runner - Llama Stack - runner + OGX + runner - Llama Stack - service + OGX + service - Llama Stack - service + OGX + service alt @@ -76,14 +76,13 @@ Generate - Llama - Stack - configuration + OGX + configuration Start - Llama Stack + OGX @@ -127,7 +126,7 @@ - Stop Llama Stack + Stop OGX diff --git a/docs/design/ogx-config-merge/ogx-config-merge-spike.md b/docs/design/ogx-config-merge/ogx-config-merge-spike.md index 0733fea1f..cea25facb 100644 --- a/docs/design/ogx-config-merge/ogx-config-merge-spike.md +++ b/docs/design/ogx-config-merge/ogx-config-merge-spike.md @@ -9,6 +9,11 @@ This split increases the chance of misconfiguration, makes downstream deployment templates larger, and forces every Lightspeed team to understand OGX's internal schema. LCORE-836 asks for a single source of truth. +**Naming note:** Early drafts and the PoC used `llama_stack` / +`ogx.config`. The shipped implementation uses `ogx` / +`ogx.config`; `llama_stack` remains accepted as a deprecated YAML-section +alias (`accept_llama_stack_section_alias` in `src/models/config.py`). + **The recommendation**: A layered approach — Option C (high-level keys + `native_override` escape hatch) as the base structure, with Option D (profiles) enabled as an optional layer on top. See @@ -16,7 +21,7 @@ OGX's internal schema. LCORE-836 asks for a single source of truth. option and [Design alternatives considered](#design-alternatives-considered) for the scoring. -- **High-level keys** in `lightspeed-stack.yaml` under a new `llama_stack.config` +- **High-level keys** in `lightspeed-stack.yaml` under a new `ogx.config` section (inference, later storage/safety/...). Most downstream teams write only these. - **`native_override`** escape hatch under the same section — raw OGX @@ -27,15 +32,15 @@ for the scoring. beyond one or two reference examples under `examples/profiles/`. - **`baseline: default | empty`** selects whether the synthesis starts from LCORE's built-in baseline or a blank slate. -- **Legacy mode preserved**: existing `llama_stack.library_client_config_path` +- **Legacy mode preserved**: existing `ogx.library_client_config_path` works unchanged through a deprecation window. Mutual exclusion with the new - `llama_stack.config` block is enforced at load time. + `ogx.config` block is enforced at load time. - **Migration tool**: `lightspeed-stack --migrate-config` produces a unified single-file config from an existing (`run.yaml` + `lightspeed-stack.yaml`) pair, lossless round-trip. **PoC validation**: A library-mode PoC proves the mechanism end-to-end. -A unified `lightspeed-stack.yaml` containing only `llama_stack.config` +A unified `lightspeed-stack.yaml` containing only `ogx.config` (no external `run.yaml`) successfully drives LCORE: liveness/readiness green, `/v1/query` returns a real model response, `native_override` demonstrably takes effect. Full unit-test suite passes @@ -49,10 +54,10 @@ library-mode PoC and unit tests. ## Design options A–E -- **A (Embedded native)** — `llama_stack.config` is the raw OGX +- **A (Embedded native)** — `ogx.config` is the raw OGX schema, verbatim. Same surface area downstream teams see today, just moved into one file. No abstraction win. -- **B (High-level only)** — `llama_stack.config` exposes only LCORE-defined +- **B (High-level only)** — `ogx.config` exposes only LCORE-defined high-level keys (e.g. `inference.providers`). Best UX when every operator intent maps cleanly; painful at the edges where the high-level schema doesn't yet cover a need (no escape hatch). @@ -60,7 +65,7 @@ library-mode PoC and unit tests. a raw-LS `native_override` block deep-merged last as an escape hatch. Combines B's UX with A's flexibility. **Recommended (Decision S1).** - **D (Profiles)** — a user-authored YAML file pointed to by - `llama_stack.config.profile: `, used as the synthesis baseline + `ogx.config.profile: `, used as the synthesis baseline instead of LCORE's built-in default. A composable *layer* on top of A/B/C, not a standalone shape. LCORE ships the mechanism; downstream teams (or operators) author the YAML. @@ -83,7 +88,7 @@ for the scoring. | Option | Standalone shape | |---|---| -| A (Embedded native) | `llama_stack.config` is raw LS schema, verbatim | +| A (Embedded native) | `ogx.config` is raw LS schema, verbatim | | B (High-level only) | LCORE-defined high-level keys; no escape hatch | | **C (B + `native_override`)** | High-level keys + raw-LS escape hatch | | E (Kustomize-style patches) | Default baseline + JSON-Patch-like overlays | @@ -140,24 +145,24 @@ above pulled in, this spike's JIRAs grow accordingly. **Context**: S1 places the unified config's high-level keys (`inference.providers` today; later `rag.providers`, etc.) inside the -LS-specific subtree at `llama_stack.config.inference`. LCORE will migrate +OGX-specific subtree at `ogx.config.inference`. LCORE will migrate from OGX to Pydantic AI over time. Under S1's layout, that transition would force every downstream team to relearn the config schema — -the `llama_stack` subtree name becomes a lie, and high-level keys would +the nested backend subtree would become misleading, and high-level keys would have to move. **Recommendation**: lift the backend-agnostic keys to the top level of -`lightspeed-stack.yaml` now. Leave LS-specific knobs under -`llama_stack.config`. Extends S1; does not replace it (Option C + optional +`lightspeed-stack.yaml` now. Leave OGX-specific knobs under +`ogx.config`. Extends S1; does not replace it (Option C + optional D recommendation stands). | Today (per S1) | Proposed | |---|---| -| `llama_stack.config.inference.providers: …` | `inference.providers: …` | -| `llama_stack.config.native_override: …` | unchanged — LS-specific | -| `llama_stack.config.profile: …` | unchanged — LS-specific (points at LS run.yaml shape) | -| `llama_stack.config.baseline: …` | unchanged — LS-specific | -| Future RAG / safety / vector_io / shield high-level keys | stay under `llama_stack.config` — Pydantic AI has no equivalent abstraction (see "Pydantic AI research findings" below) | +| `ogx.config.inference.providers: …` | `inference.providers: …` | +| `ogx.config.native_override: …` | unchanged — OGX-specific | +| `ogx.config.profile: …` | unchanged — OGX-specific (points at OGX run.yaml shape) | +| `ogx.config.baseline: …` | unchanged — OGX-specific | +| Future RAG / safety / vector_io / shield high-level keys | stay under `ogx.config` — Pydantic AI has no equivalent abstraction (see "Pydantic AI research findings" below) | The synthesizer reads `inference.providers` from the top level and emits LS provider entries exactly as today — only the input node moves. When the @@ -173,22 +178,22 @@ for query-time routing). Rather than add a competing top-level key, S5 **extends that existing section** with a `providers:` list. So `inference.providers` is the high-level synthesis input, while `inference.default_model` / `default_provider` keep their current -meaning. (The PoC had this list under `llama_stack.config.inference`; +meaning. (The PoC had this list under `ogx.config.inference`; this decision is what moves it.) **Mode-detection knock-on.** Because the `inference:` section always exists (it carries defaults), unified mode is signalled by `inference.providers` being **non-empty** — or by the presence of -`llama_stack.config` — not by the section merely existing. This expands -Decision T1's shape rule from "`llama_stack.config` present" to "any +`ogx.config` — not by the section merely existing. This expands +Decision T1's shape rule from "`ogx.config` present" to "any *synthesis input* present"; see the spec doc's "Mode detection" table. -**Scope discipline — what stays under `llama_stack.config`**: anything +**Scope discipline — what stays under `ogx.config`**: anything whose vocabulary is genuinely LS-specific and unlikely to translate across backends. Today that's `native_override`, `profile`, `baseline`. The research pass (see below) confirms that RAG, safety/shields, vector storage, and the `apis` / `registered_resources` / `storage` blocks should -also stay under `llama_stack.config` whenever they ship as high-level keys +also stay under `ogx.config` whenever they ship as high-level keys — Pydantic AI has **no equivalent built-in abstraction** for any of these. **On the `inference.providers[].type` vocabulary**: keep LCORE's existing @@ -200,7 +205,7 @@ backend-specific synthesizer translates the canonical LCORE vocabulary to its target shape; we do not adopt either backend's surface verbatim. **Pydantic AI research findings** (full report: -[`pydantic-ai-research.md`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/pydantic-ai-research.md), +[`pydantic-ai-research.md`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/pydantic-ai-research.md), pass dated 2026-05-20 against `pydantic-ai 1.98.0`): - Pydantic AI's per-Agent `:` string + `Provider(...)` @@ -212,7 +217,7 @@ pass dated 2026-05-20 against `pydantic-ai 1.98.0`): OpenAI SDK for embeddings; there is no `pydantic_ai.vector_store` module and no public roadmap signal one is coming in the next 6–12 months. **Do not preemptively abstract `rag.*`** — keep any future - high-level RAG keys under `llama_stack.config`. Researcher confidence + high-level RAG keys under `ogx.config`. Researcher confidence it would survive a cutover today: ~25%. - Pydantic AI ships **no built-in safety / shield abstraction**. `pydantic-ai` Issue #1197 ("Guardrails") is open with no merge @@ -220,7 +225,7 @@ pass dated 2026-05-20 against `pydantic-ai 1.98.0`): (`pydantic-ai-shields`, `pydantic-ai-guardrails`) exist but have incompatible vocabularies with each other and with Llama Guard. **Do not preemptively abstract `safety.*`** — keep any future high-level - safety keys under `llama_stack.config`. Researcher confidence on + safety keys under `ogx.config`. Researcher confidence on survival: ~20%. - MCP endpoints are the one tool-runtime concept worth abstracting later (~60% confidence): both backends support MCP natively and the @@ -239,7 +244,7 @@ the inherent risk that the per-Agent model the researcher described forces LCORE's synthesizer to do more work than expected. Both manageable. **Implementation impact**: if adopted, this changes the scope of the -existing **Unified `llama_stack.config` schema + synthesizer** JIRA — it +existing **Unified `ogx.config` schema + synthesizer** JIRA — it ships the top-level shape from day one. No new JIRA is needed. --- @@ -255,7 +260,7 @@ How does LCORE tell unified-mode configs from legacy-mode configs? | Option | Works by | |---|---| -| Shape only | Presence of `llama_stack.config` → unified; else legacy | +| Shape only | Presence of `ogx.config` → unified; else legacy | | Version field only | Explicit `config_format_version: 2` required | | **Both (soft-coupled)** | Shape decides; version field optional but must agree when present | @@ -266,13 +271,13 @@ version field today. Confidence: 75%. **S5 knock-on**: since Decision S5 lifts the high-level `inference` section to the top level, the detected "shape" is the presence of any *synthesis input* — top-level `inference.providers` (non-empty) **or** -`llama_stack.config` — not just `llama_stack.config`. The soft-coupled +`ogx.config` — not just `ogx.config`. The soft-coupled version-field stance is unchanged. See the spec doc's "Mode detection" table for the full combination matrix. ### Decision T2: Override precedence (inside Option C) -When `llama_stack.config.native_override` overlaps with a high-level key, +When `ogx.config.native_override` overlaps with a high-level key, what semantics? | Strategy | Example: `safety: {excluded_categories: [a, b]}` vs override `{excluded_categories: [c]}` | @@ -308,7 +313,7 @@ Where the synthesized `run.yaml` goes at runtime: | Option | Path | |---|---| -| Temp file | `$TMPDIR/llama_stack_synthesized_config.yaml` | +| Temp file | `./.generated/run.yaml (`DEFAULT_SYNTHESIZED_CONFIG_PATH`)` | | **Persistent known path** | Local: `./.generated/run.yaml` or `~/.local/state/lightspeed-stack/run.yaml`; Container: `/app-root/.generated/run.yaml`. Overwrite on each boot. | **Recommendation**: **persistent known path, overwrite on boot**. Debuggable, @@ -360,7 +365,7 @@ caller pick the synthesis starting point. that `native_override` is the only thing the synthesizer sees. **Recommendation**: **accept this field, with `default` as the default value**. -That preserves the zero-config "fresh user authors `llama_stack.config` and +That preserves the zero-config "fresh user authors `ogx.config` and gets a working LS baseline" UX; the migration tool sets `baseline: empty` explicitly so the migrate-then-synthesize loop above matches the original `run.yaml`. Alternatives (`inherit_defaults: bool`, `starting_point: ...`) @@ -399,7 +404,7 @@ server-mode wiring, and the legacy deprecation warning. **Goals**: - A unified `lightspeed-stack.yaml` (a top-level `inference.providers` - section and/or a `llama_stack.config` block) drives LCORE in both + section and/or a `ogx.config` block) drives LCORE in both library and server modes. - A lossless dumb-mode migration tool converts the legacy two-file pair. - Legacy mode keeps working with a startup deprecation WARN through the @@ -408,7 +413,7 @@ server-mode wiring, and the legacy deprecation warning. **Scope**: - In: `UnifiedInferenceProvider` + `InferenceConfiguration.providers`, - `UnifiedLlamaStackConfig`, the synthesizer, `--migrate-config`, the LS + `UnifiedOgxConfig`, the synthesizer, `--migrate-config`, the LS container entrypoint + deployment artifacts, the deprecation WARN. - Out: smart migration factoring; high-level sections beyond `inference` (`rag` / `safety` stay backend-specific for now); LS process @@ -417,13 +422,13 @@ server-mode wiring, and the legacy deprecation warning. -#### LCORE-2336: Unified `llama_stack.config` schema + synthesizer +#### LCORE-2336: Unified `ogx.config` schema + synthesizer **Description**: Implement the unified-mode config schema and the synthesizer that produces a full OGX `run.yaml` from it. The high-level `providers` list lives on the existing top-level `InferenceConfiguration` (`inference.providers`) — backend-agnostic, so -it survives a future backend change — and `UnifiedLlamaStackConfig` +it survives a future backend change — and `UnifiedOgxConfig` holds only the backend-specific knobs (`baseline` / `profile` / `native_override`). Wire library mode to the synthesizer. Preserve legacy mode through mutual-exclusion validation on the root @@ -437,21 +442,21 @@ configuration model. (Full design: the spec doc.) `providers: list[UnifiedInferenceProvider]` (default empty) — this is the high-level synthesis input; `default_model` / `default_provider` keep their current query-routing meaning. - - Add `UnifiedLlamaStackConfig` (`baseline` / `profile` / - `native_override`) and a `config` field on `LlamaStackConfiguration`. + - Add `UnifiedOgxConfig` (`baseline` / `profile` / + `native_override`) and a `config` field on `OgxConfiguration`. - Add the unified-vs-legacy `@model_validator` to the **root** `Configuration` model (it spans top-level `inference.providers` and - `llama_stack.*`). + `ogx.*`). - New functions in `src/ogx_configuration.py`: `synthesize_configuration`, `deep_merge_list_replace`, `apply_high_level_inference`, `load_default_baseline`, `synthesize_to_file`. - A shipped default baseline at `src/data/default_run.yaml`. -- Library-mode wiring in `src/client.py`: detect unified vs legacy +- Library-mode wiring in `src/client/ogx.py`: detect unified vs legacy (synthesis input present vs `library_client_config_path`), write synthesized file, pass path to library client. - Cross-field validation: reject a synthesis input (`inference.providers` non-empty, or `config`) set together with `library_client_config_path`. -- Legacy behavior (`llama_stack.library_client_config_path` path) unchanged. +- Legacy behavior (`ogx.library_client_config_path` path) unchanged. **Acceptance criteria**: @@ -467,10 +472,10 @@ configuration model. (Full design: the spec doc.) Read the "Architecture" and "Implementation Suggestions" sections of docs/design/ogx-config-merge/ogx-config-merge.md. Key files to create or modify: - src/models/config.py (new classes; modify LlamaStackConfiguration) + src/models/config.py (new classes; modify OgxConfiguration) src/ogx_configuration.py (synthesize_configuration + helpers) src/data/default_run.yaml (new) - src/client.py (library-mode wiring) + src/client/ogx.py (library-mode wiring) To verify: run a unified-mode config end-to-end via `uv run lightspeed-stack -c ` and confirm /v1/query succeeds. ``` @@ -481,7 +486,7 @@ To verify: run a unified-mode config end-to-end via `uv run lightspeed-stack -c **Description**: Implement `--migrate-config` on the `lightspeed-stack` CLI that produces a unified single-file config from an existing (`run.yaml` + `lightspeed-stack.yaml`) pair. Dumb mode places the entire -`run.yaml` body under `llama_stack.config.native_override` with +`run.yaml` body under `ogx.config.native_override` with `baseline: empty`, removes `library_client_config_path`. **Scope**: @@ -505,7 +510,7 @@ that produces a unified single-file config from an existing ```text Read "Migration / backwards compatibility" and "Appendix A — Worked example: legacy → unified migration" in docs/design/ogx-config-merge/ogx-config-merge.md. Key files: src/lightspeed_stack.py, src/ogx_configuration.py, -tests/unit/test_llama_stack_synthesize.py. +tests/unit/test_ogx_synthesize.py. To verify: migrate the repo's root run.yaml + lightspeed-stack.yaml, then start LCORE with the output; confirm /v1/query works. ``` @@ -555,8 +560,8 @@ to the migration doc. Legacy mode continues to fully function. **Scope**: -- Warning emission point: on load in `LlamaStackConfiguration` - `check_llama_stack_model` validator, or at LCORE startup. +- Warning emission point: on load in `OgxConfiguration` + `check_ogx_model` validator, or at LCORE startup. - Log line format includes a stable URL fragment to the migration doc. **Acceptance criteria**: @@ -822,7 +827,7 @@ To verify: rendered docs present the unified mode first; legacy mode is visibly files — one remote-provider (OpenAI) and one inline-provider (sentence- transformers + FAISS) — purely as reference material. Document how operators write and reference their own profiles via -`llama_stack.config.profile: `. +`ogx.config.profile: `. **Scope**: @@ -884,13 +889,13 @@ removed from the tree after merge (PoC validation results aren't kept on `main`); the links below are permalinks to the files at the merge commit, where they remain in history: -- [`lightspeed-stack-unified-library.yaml`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/lightspeed-stack-unified-library.yaml) +- [`lightspeed-stack-unified-library.yaml`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/lightspeed-stack-unified-library.yaml) — the unified-mode config used. -- [`library-mode/synthesized-run.yaml`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/library-mode/synthesized-run.yaml) +- [`library-mode/synthesized-run.yaml`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/library-mode/synthesized-run.yaml) — what LCORE produced (3.7 KB). -- [`library-mode/query-response.json`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/library-mode/query-response.json) +- [`library-mode/query-response.json`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/library-mode/query-response.json) — a real `/v1/query` round-trip. -- [`library-mode/README.md`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/library-mode/README.md) +- [`library-mode/README.md`](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/library-mode/README.md) — walkthrough. Summary of validation: @@ -943,7 +948,7 @@ Summary of validation: query. The implementation JIRAs' e2e coverage must exercise a real Llama Guard model (e.g. `meta-llama/Llama-Guard-3-8B`) end-to-end. Caught by CodeRabbit on the PoC artifact at - [`synthesized-run.yaml` L110](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/library-mode/synthesized-run.yaml#L110). + [`synthesized-run.yaml` L110](https://github.com/lightspeed-core/lightspeed-stack/blob/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/library-mode/synthesized-run.yaml#L110). --- @@ -955,8 +960,8 @@ Two files: - **`lightspeed-stack.yaml`** — LCORE settings: service host/port, auth, conversation cache, user data collection, MCP servers, authentication, - authorization, quota, etc. Also contains `llama_stack:` with - connection-to-LS settings (URL/api_key or library-client mode with a path + authorization, quota, etc. Also contains `ogx:` with + connection-to-OGX settings (URL/api_key or library-client mode with a path to an external `run.yaml`). - **`run.yaml`** — OGX operational config: `apis`, `providers` (inference, safety, tool_runtime, vector_io, agents, ...), `storage`, @@ -969,7 +974,7 @@ Two files: BYOK RAG entries, Solr/OKP provider/store/model registration. Output is an enriched `run.yaml`. - Called in two places: `scripts/ogx-entrypoint.sh` at LS container - boot (server mode) and `src/client.py:_enrich_library_config()` (library + boot (server mode) and `src/client/ogx.py:_enrich_library_config()` (library mode). - LCORE-779 made this automatic; LCORE-518 (closed spike) proved (re)generation feasibility. Both are the groundwork the current spike builds on. @@ -1107,7 +1112,7 @@ initializes, serves. One process. **Server mode**: OGX runs as a separate process (container). LCORE connects to it over HTTP. Under unified mode, the LS container's entrypoint reads the mounted `lightspeed-stack.yaml`, the Python CLI auto-detects -unified mode, synthesizes `run.yaml`, then `exec llama stack run` with it. +unified mode, synthesizes `run.yaml`, then `exec ogx stack run` with it. LCORE container reads the same `lightspeed-stack.yaml`, ignores the `config` sub-block (server mode — only connection fields matter), connects. Two processes. LCORE does **not** start, monitor, or supervise the LS @@ -1130,8 +1135,8 @@ Detection rule at load time: | `lightspeed-stack.yaml` shape | Interpretation | |---|---| -| `llama_stack.library_client_config_path` set, no `llama_stack.config` | **Legacy** — today's behavior | -| `llama_stack.config.*` present | **Unified** — new path | +| `ogx.library_client_config_path` set, no `ogx.config` | **Legacy** — today's behavior | +| `ogx.config.*` present | **Unified** — new path | | Both present | Error at load time — clear message | | Neither (remote URL only, no config) | Existing remote mode — unchanged | @@ -1151,14 +1156,14 @@ Relative to `upstream/main`: | File | Purpose | |---|---| -| `src/models/config.py` | New classes: `UnifiedInferenceProvider`, `UnifiedInferenceSection`, `UnifiedLlamaStackConfig`; modified `LlamaStackConfiguration` (adds `config` field + mutual-exclusion validator). _PoC layout; the implementation follows Decision S5 — `inference.providers` on the top-level `InferenceConfiguration`, validator on the root `Configuration` model, no `UnifiedInferenceSection` (see the schema JIRA)._ | +| `src/models/config.py` | New classes: `UnifiedInferenceProvider`, `UnifiedInferenceSection`, `UnifiedOgxConfig`; modified `OgxConfiguration` (adds `config` field + mutual-exclusion validator). _PoC layout; the implementation follows Decision S5 — `inference.providers` on the top-level `InferenceConfiguration`, validator on the root `Configuration` model, no `UnifiedInferenceSection` (see the schema JIRA)._ | | `src/ogx_configuration.py` | New: `synthesize_configuration`, `deep_merge_list_replace`, `apply_high_level_inference`, `load_default_baseline`, `synthesize_to_file`, `migrate_config_dumb`. CLI `main()` auto-detects unified vs legacy. | | `src/data/default_run.yaml` | Built-in default baseline (copied from repo root `run.yaml` for the PoC — implementation JIRA should slim it down; see PoC surprise about `EXTERNAL_PROVIDERS_DIR`) | -| `src/client.py` | Library-mode path picks synthesis for unified configs, enrichment for legacy | +| `src/client/ogx.py` | Library-mode path picks synthesis for unified configs, enrichment for legacy | | `src/lightspeed_stack.py` | `--migrate-config`, `--run-yaml`, `--migrate-output` flags | | `scripts/ogx-entrypoint.sh` | Comment updated — script itself needs no change (Python CLI auto-detects) | | `test.containerfile` | Copies `src/data/` into the LS container | -| `tests/unit/test_llama_stack_synthesize.py` | 22 new tests: merge semantics, high-level inference, synthesize pipeline, migration round-trip | +| `tests/unit/test_ogx_synthesize.py` | 22 new tests: merge semantics, high-level inference, synthesize pipeline, migration round-trip | | `tests/unit/models/config/test_ogx_configuration.py` | 3 new tests: unified/legacy mutual exclusion | | `tests/unit/models/config/test_dump_configuration.py` | 5 expected-dict updates (new `config: None` field appears in dumps) | | `tests/unit/test_client.py` | Error-message regex updated | @@ -1175,7 +1180,7 @@ adjust it for your environment before running.) # 0. Fetch the PoC config from the merge commit (removed from the tree post-merge) mkdir -p /tmp/lcore-836-poc curl -sSL -o /tmp/lcore-836-poc/lightspeed-stack-unified-library.yaml \ - https://raw.githubusercontent.com/lightspeed-core/lightspeed-stack/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/ogx-config-merge/poc-results/lightspeed-stack-unified-library.yaml + https://raw.githubusercontent.com/lightspeed-core/lightspeed-stack/42844d068b488cc7d72928068b5606a7941f8c15/docs/design/llama-stack-config-merge/poc-results/lightspeed-stack-unified-library.yaml # 1. Start LCORE in library mode with the unified config export OPENAI_API_KEY= @@ -1191,5 +1196,5 @@ curl -s -X POST http://localhost:8080/v1/query \ -d '{"query": "Name three primary colors. One sentence."}' # 3. Inspect what was synthesized -cat /tmp/llama_stack_synthesized_config.yaml +cat ./.generated/run.yaml ``` diff --git a/docs/design/ogx-config-merge/ogx-config-merge.md b/docs/design/ogx-config-merge/ogx-config-merge.md index 77d27ef59..b0e7b2d8c 100644 --- a/docs/design/ogx-config-merge/ogx-config-merge.md +++ b/docs/design/ogx-config-merge/ogx-config-merge.md @@ -3,7 +3,7 @@ | | | |--------------------|----------------------------------------------------------------------------------| | **Date** | 2026-04-23 | -| **Component** | Lightspeed Core Stack (src/models/config.py, src/ogx_configuration.py, src/client.py, src/lightspeed_stack.py, scripts/ogx-entrypoint.sh) | +| **Component** | Lightspeed Core Stack (src/models/config.py, src/ogx_configuration.py, src/client/ogx.py, src/lightspeed_stack.py, scripts/ogx-entrypoint.sh) | | **Authors** | Maxim Svistunov | | **Feature** | [LCORE-836](https://redhat.atlassian.net/browse/LCORE-836) | | **Spike** | [ogx-config-merge-spike.md](ogx-config-merge-spike.md) | @@ -16,7 +16,7 @@ This feature collapses the two Lightspeed Core configuration files — operational config) — into a single `lightspeed-stack.yaml`. At runtime, LCORE synthesizes a full OGX `run.yaml` from high-level operator-facing inputs (a top-level `inference.providers` list, plus a -`llama_stack.config` sub-section) and hands it to OGX (library +`ogx.config` sub-section) and hands it to OGX (library client or subprocess, mode-dependent). Key shape: @@ -24,23 +24,24 @@ Key shape: - **Top-level high-level sections** for the common path. v1 ships `inference.providers` — added to the *existing* top-level `inference:` section (alongside its `default_model` / `default_provider`). These - sit at the root of `lightspeed-stack.yaml`, not under `llama_stack`, + sit at the root of `lightspeed-stack.yaml`, not under `ogx`, so they survive a future backend change (Decision S5 in the spike). Future high-level sections (`rag`, `safety`, …) stay under - `llama_stack.config` until proven backend-agnostic. -- `llama_stack.config.native_override` escape hatch — raw OGX + `ogx.config` until proven backend-agnostic. +- `ogx.config.native_override` escape hatch — raw OGX schema, deep-merged with list replacement. Covers anything the high-level sections don't express. -- `llama_stack.config.profile` — path to a user-authored YAML that serves +- `ogx.config.profile` — path to a user-authored YAML that serves as the synthesis baseline. -- `llama_stack.config.baseline: default | byo-llm | empty` — pick +- `ogx.config.baseline: default | byo-llm | empty` — pick LCORE's built-in baseline (includes a conditional OpenAI provider), the same baseline without that OpenAI row, or an empty dict (used by the migration tool for exact round-trip). -- Legacy two-file mode (`llama_stack.library_client_config_path` + +- Legacy two-file mode (`ogx.library_client_config_path`; the deprecated + `llama_stack` YAML-section alias is still accepted) + external `run.yaml`) is preserved during a deprecation window; mutually exclusive with the unified *synthesis inputs* (a non-empty - `inference.providers` or a `llama_stack.config` block). + `inference.providers` or a `ogx.config` block). ## Why @@ -64,16 +65,16 @@ detail that LCORE owns, not an operator-facing artifact. ## Requirements - **R1:** `lightspeed-stack.yaml` using the unified schema (a non-empty - top-level `inference.providers`, and/or a `llama_stack.config` + top-level `inference.providers`, and/or a `ogx.config` sub-section) and no external `run.yaml` boots LCORE in both library and server modes and serves `/v1/query` successfully. -- **R2:** Legacy mode (`llama_stack.library_client_config_path` + +- **R2:** Legacy mode (`ogx.library_client_config_path` + external `run.yaml`) works unchanged through the deprecation window: fully functional with a startup deprecation WARN in 0.6 and 0.7, removed in 0.8 (Decision S2, confirmed 2026-05-20, schedule revised 2026-08-24). -- **R3:** Setting both `llama_stack.config` and - `llama_stack.library_client_config_path` in the same file fails at +- **R3:** Setting both `ogx.config` and + `ogx.library_client_config_path` in the same file fails at configuration load time with a clear error message pointing to the migration tool. - **R4:** `lightspeed-stack --migrate-config --run-yaml X -c Y @@ -81,7 +82,7 @@ detail that LCORE owns, not an operator-facing artifact. two-file pair. Running the migrated file drives OGX to byte-identical behavior as the original pair (dumb-mode lossless round-trip). -- **R5:** When `llama_stack.config.native_override` overlaps a key set +- **R5:** When `ogx.config.native_override` overlaps a key set by the high-level section or by the baseline, deep-merge semantics apply with list replacement (maps merge recursively; lists are replaced wholesale; scalars are replaced). The override wins over the @@ -103,8 +104,8 @@ detail that LCORE owns, not an operator-facing artifact. - **R9:** The unified schema (a) extends the existing top-level `InferenceConfiguration` with a `providers: list[UnifiedInferenceProvider]` field and (b) adds a - `config: Optional[UnifiedLlamaStackConfig]` field to - `LlamaStackConfiguration` (holding `baseline` / `profile` / + `config: Optional[UnifiedOgxConfig]` field to + `OgxConfiguration` (holding `baseline` / `profile` / `native_override`). Cross-field validation on the **root** `Configuration` model enforces mutual exclusion between the unified synthesis inputs and legacy mode, and all unified-mode models reject @@ -118,7 +119,7 @@ detail that LCORE owns, not an operator-facing artifact. location for debugging. - **R11:** Shape detection determines mode. Unified mode is signalled by the presence of any *synthesis input* — a non-empty top-level - `inference.providers` or a `llama_stack.config` block; legacy mode by + `inference.providers` or a `ogx.config` block; legacy mode by `library_client_config_path`. An optional `config_format_version` field is accepted but must agree with the detected shape when present. See the "Mode detection" table under Architecture for the full matrix. @@ -150,7 +151,7 @@ files — authors read it to write Gherkin scenarios. | Req | Observable behavior | Verified by | |---|---|---| -| R1 | Unified config (top-level `inference.providers` and/or `llama_stack.config`, no external `run.yaml`) boots LCORE in library and server mode; `/liveness`, `/readiness`, `/v1/query` succeed | e2e | +| R1 | Unified config (top-level `inference.providers` and/or `ogx.config`, no external `run.yaml`) boots LCORE in library and server mode; `/liveness`, `/readiness`, `/v1/query` succeed | e2e | | R2 | Legacy two-file config still boots and serves; one startup deprecation WARN in 0.6; no WARN in unified mode | e2e | | R3 | A config with a synthesis input *and* `library_client_config_path` fails at load with an error naming `--migrate-config` (cover both the `inference.providers` and the `config` case) | e2e + unit | | R4 | `--migrate-config` on a legacy pair yields a unified file driving byte-identical LS behavior; migrate→synthesize round-trips to the original `run.yaml` | e2e + unit (round-trip) | @@ -180,7 +181,7 @@ lightspeed-stack.yaml (unified mode) ┌────────────────────────────┐ Baseline selection (profile / │ Synthesizer │ default / empty) + enrichment │ synthesize_configuration │ (BYOK RAG, Solr/OKP) + high-level - │ (llama_stack_config…) │ sections + native_override deep-merge. + │ (ogx_config…) │ sections + native_override deep-merge. └────────────┬───────────────┘ │ synthesized run.yaml (dict) ▼ @@ -189,14 +190,14 @@ lightspeed-stack.yaml (unified mode) Write to deterministic path. Written by LS container's entrypoint AsyncOGXAsLibraryClient script (same synthesizer, same CLI, reads the path and initializes. auto-detects unified via Python). - `llama stack run ` starts OGX. + `ogx stack run ` starts OGX. LCORE connects by URL. ``` ### Trigger mechanism At LCORE startup (library mode): if any synthesis input is present (a -non-empty top-level `inference.providers`, or a `llama_stack.config` +non-empty top-level `inference.providers`, or a `ogx.config` block), the synthesizer produces a `run.yaml` dict, writes it to disk, and passes the path to the library client. @@ -211,7 +212,7 @@ before. ### Mode detection *Synthesis inputs* are the top-level high-level sections (v1: a non-empty -`inference.providers`; future `rag`, …) and the `llama_stack.config` +`inference.providers`; future `rag`, …) and the `ogx.config` block. The loaded `lightspeed-stack.yaml` maps to a mode as follows: | Shape | Mode | @@ -248,12 +249,12 @@ config matches the previous unconditional openai provider. ### Configuration Top-level high-level sections plus a sub-section under the existing -`llama_stack` block: +`ogx` block: ```yaml # Top-level inference config — the existing `inference:` section, extended # with a `providers:` list (Decision S5). Backend-agnostic: it stays at the -# root, not under `llama_stack`, so it survives a future backend change. +# root, not under `ogx`, so it survives a future backend change. inference: default_model: gpt-4o-mini # existing — query-time default routing default_provider: openai # existing — query-time default routing @@ -263,7 +264,7 @@ inference: allowed_models: [gpt-4o-mini] - type: sentence_transformers -llama_stack: +ogx: use_as_library_client: true # NOTE: library_client_config_path intentionally OMITTED in unified mode. # Setting a synthesis input (`inference.providers` or `config`) together @@ -303,7 +304,7 @@ class InferenceConfiguration(ConfigurationBase): providers: list[UnifiedInferenceProvider] = Field(default_factory=list) -class UnifiedLlamaStackConfig(ConfigurationBase): +class UnifiedOgxConfig(ConfigurationBase): # Backend-specific knobs only. Per Decision S5, the backend-agnostic # high-level sections (inference, ...) live at the root, NOT here. baseline: Literal["default", "empty", "byo-llm"] = "default" @@ -311,28 +312,28 @@ class UnifiedLlamaStackConfig(ConfigurationBase): native_override: dict[str, Any] = Field(default_factory=dict) -class LlamaStackConfiguration(ConfigurationBase): +class OgxConfiguration(ConfigurationBase): # existing fields unchanged (url, api_key, use_as_library_client, # library_client_config_path, timeout) - config: Optional[UnifiedLlamaStackConfig] = None + config: Optional[UnifiedOgxConfig] = None class Configuration(ConfigurationBase): # The root lightspeed-stack.yaml model (existing). Relevant fields: inference: InferenceConfiguration = Field(default_factory=InferenceConfiguration) - llama_stack: LlamaStackConfiguration + ogx: OgxConfiguration # ... other existing fields (name, service, ...) ... @model_validator(mode="after") def check_unified_vs_legacy(self) -> Self: # Synthesis inputs span the root (inference.providers) and the - # nested llama_stack.config, so the check lives here, not on - # LlamaStackConfiguration. + # nested ogx.config, so the check lives here, not on + # OgxConfiguration. synthesis_input = ( bool(self.inference.providers) - or self.llama_stack.config is not None + or self.ogx.config is not None ) - legacy_input = self.llama_stack.library_client_config_path is not None + legacy_input = self.ogx.library_client_config_path is not None if synthesis_input and legacy_input: raise ValueError("... mutually exclusive ... use --migrate-config") # ...legacy / remote checks preserved... @@ -351,7 +352,7 @@ None at the REST API surface. Internal API additions in - `migrate_config_dumb(run_yaml_path, lightspeed_yaml_path, output_path) -> None` — dumb-mode migration (lossless round-trip). - `deep_merge_list_replace(base, overlay) -> dict` — merge helper. -- `apply_high_level_inference(ls_config, inference)` — high-level expansion. +- `apply_high_level_inference(ogx_config, inference)` — high-level expansion. - `load_default_baseline() -> dict` — loads `src/data/default_run.yaml`. CLI additions in `src/lightspeed_stack.py`: @@ -373,7 +374,7 @@ removed `-g/-i/-o` flags is cleaned up as part of the docs JIRA. - **Library mode with no synthesis input and no `library_client_config_path`**: raised during the same root validator. Error identifies the valid paths (populate `inference.providers` or a - `llama_stack.config` block, or set `library_client_config_path`). + `ogx.config` block, or set `library_client_config_path`). - **`profile:` path does not exist**: surfaced as `FileNotFoundError` from `open(profile_path)` during synthesis. The implementation JIRA should wrap this with context about where the path was resolved. @@ -420,7 +421,7 @@ removed `-g/-i/-o` flags is cleaned up as part of the docs JIRA. ### Migration / backwards compatibility Coexistence mechanism: shape detection (see R11). Legacy configs with -`llama_stack.library_client_config_path` continue through the +`ogx.library_client_config_path` continue through the configured deprecation window. Three operator-facing migration paths (choose per deployment): @@ -449,10 +450,10 @@ not have had a full release with a working migration path. Releases: | File | What to do | |---|---| -| `src/models/config.py` | Add `UnifiedInferenceProvider`. Extend the existing `InferenceConfiguration` with `providers: list[UnifiedInferenceProvider]`. Add `UnifiedLlamaStackConfig` (`baseline`/`profile`/`native_override`) and a `config` field on `LlamaStackConfiguration`. Put the unified-vs-legacy `model_validator` on the **root** `Configuration` model (spans `inference.providers` + `llama_stack.*`). | +| `src/models/config.py` | Add `UnifiedInferenceProvider`. Extend the existing `InferenceConfiguration` with `providers: list[UnifiedInferenceProvider]`. Add `UnifiedOgxConfig` (`baseline`/`profile`/`native_override`) and a `config` field on `OgxConfiguration`. Put the unified-vs-legacy `model_validator` on the **root** `Configuration` model (spans `inference.providers` + `ogx.*`). | | `src/ogx_configuration.py` | Add `synthesize_configuration`, `deep_merge_list_replace`, `apply_high_level_inference`, `load_default_baseline`, `synthesize_to_file`, `migrate_config_dumb`, `PROVIDER_TYPE_MAP`, `DEFAULT_BASELINE_RESOURCE`. Update `main()` to auto-detect unified vs legacy. | | `src/data/default_run.yaml` | New file — a thinner baseline than today's repo-root `run.yaml`. Notably do **not** reference `${env.EXTERNAL_PROVIDERS_DIR}` without a default (see "Findings discovered during PoC" in the spike doc). OpenAI is conditional on `OPENAI_API_KEY` (`${env.OPENAI_API_KEY:+openai}` / `${env.OPENAI_API_KEY:=}`). | -| `src/client.py` | In `_load_library_client`: branch on `config.config` presence. Add `_synthesize_library_config()` that calls the synthesizer and writes to the deterministic path (R10). Keep `_enrich_library_config` for legacy. | +| `src/client/ogx.py` | In `_load_library_client`: branch on synthesis input (`bool(app_config.inference.providers) or config.config is not None`); use `_synthesize_library_config()` for unified mode (R10) and `_enrich_library_config` for legacy (`library_client_config_path`). | | `src/lightspeed_stack.py` | Add `--migrate-config`, `--run-yaml`, `--migrate-output`, `--synthesized-config-output` flags. Add an early-exit branch in `main()` that dispatches to `migrate_config_dumb` when `--migrate-config` is set. Clean up stale docstring. | | `scripts/ogx-entrypoint.sh` | No functional change — the Python CLI already auto-detects. Update the comment to document both modes. | | `test.containerfile` | Copy `src/data/` into `/opt/app-root/data/` so `load_default_baseline()` resolves inside the LS container. | @@ -463,7 +464,7 @@ not have had a full release with a working migration path. Releases: **`synthesize_configuration` pipeline** (the core new function): 1. Resolve the backend-specific block `unified = - lcs_config["llama_stack"].get("config")` — may be `None` when the + lcs_config["ogx"].get("config")` — may be `None` when the operator set only top-level `inference.providers` (then baseline defaults to `default`, no profile, no `native_override`). 2. Baseline: if `unified` and `unified.profile` set → load that file. @@ -474,21 +475,21 @@ not have had a full release with a working migration path. Releases: 3. Run `dedupe_providers_vector_io` on the baseline. 4. Apply existing enrichment: `enrich_byok_rag`, `enrich_solr` (Azure Entra ID intentionally stays separate because it's a `.env` - side-effect, not an `ls_config` mutation). + side-effect, not an `ogx_config` mutation). 5. If top-level `inference.providers` is non-empty → - `apply_high_level_inference(ls_config, lcs_config["inference"])`. + `apply_high_level_inference(ogx_config, lcs_config["inference"])`. 6. If `unified` and `unified.native_override` non-empty → - `deep_merge_list_replace(ls_config, native_override)`. + `deep_merge_list_replace(ogx_config, native_override)`. 7. `dedupe_providers_vector_io` again for good measure. 8. Return the final dict. -**`_load_library_client` fork point** (in `src/client.py`). The check is +**`_load_library_client` fork point** (in `src/client/ogx.py`). The check is "is there a synthesis input?", which spans the root `inference.providers` -and `llama_stack.config`, so the client needs the root config (or a -precomputed flag) rather than only the `llama_stack` block: +and `ogx.config`, so the client needs the root config (or a +precomputed flag) rather than only the `ogx` block: ```python -# app_config is the root Configuration; ls = app_config.llama_stack +# app_config is the root Configuration; ls = app_config.ogx synthesis_input = bool(app_config.inference.providers) or ls.config is not None if synthesis_input: self._config_path = self._synthesize_library_config() @@ -504,10 +505,10 @@ All new config classes extend `ConfigurationBase` (`extra="forbid"`). Use `Field()` with defaults, title, and description for every attribute. The unified-vs-legacy mutual-exclusion check is cross-field and spans the root model's top-level `inference.providers` and the nested -`llama_stack.config` / `library_client_config_path`, so it lives as a +`ogx.config` / `library_client_config_path`, so it lives as a `@model_validator` on the **root** `Configuration` model (not on -`UnifiedLlamaStackConfig` or `LlamaStackConfiguration`). Within -`UnifiedLlamaStackConfig` no cross-field validation is needed — +`UnifiedOgxConfig` or `OgxConfiguration`). Within +`UnifiedOgxConfig` no cross-field validation is needed — synthesis precedence is ordered and handled by the synthesizer. Example config files live in `examples/profiles/` (two reference @@ -518,7 +519,7 @@ reference. ### Test patterns - Framework: pytest + pytest-mock. Unit tests live in - `tests/unit/test_llama_stack_synthesize.py` (synthesizer + migration) + `tests/unit/test_ogx_synthesize.py` (synthesizer + migration) and `tests/unit/models/config/test_ogx_configuration.py` (schema validation). - Merge semantics: parametric tests over scalar / map / list / @@ -544,7 +545,7 @@ reference. - **Additional high-level sections** beyond `inference` — `rag`, `safety`, `storage`, `tools`, `vector_stores`, etc. Add as real demand appears, not speculatively. Per Decision S5 and the Pydantic AI - research, these stay under `llama_stack.config` (not lifted to the top + research, these stay under `ogx.config` (not lifted to the top level like `inference`) until proven backend-agnostic. - **User-supplied profile directory**: `profile_dir: /etc/lcore/profiles/` with name-based lookup. Deferred to v2. @@ -594,7 +595,7 @@ providers: ```yaml # lightspeed-stack.yaml name: LCS -llama_stack: +ogx: use_as_library_client: true library_client_config_path: ./run.yaml # ... rest ... @@ -614,7 +615,7 @@ Produces: ```yaml # lightspeed-stack-unified.yaml name: LCS -llama_stack: +ogx: use_as_library_client: true # library_client_config_path is REMOVED config: @@ -642,7 +643,7 @@ high-level sections) is optional and per-deployment. ```yaml # examples/profiles/openai-remote.yaml # A minimal profile for an OpenAI-backed remote OGX. -# Referenced via `llama_stack.config.profile: examples/profiles/openai-remote.yaml`. +# Referenced via `ogx.config.profile: examples/profiles/openai-remote.yaml`. version: 2 apis: [agents, inference, safety, tool_runtime, vector_io] providers: diff --git a/docs/design/prompt-guardrails/poc-results/README.md b/docs/design/prompt-guardrails/poc-results/README.md index 149b00ddf..e02c17db2 100644 --- a/docs/design/prompt-guardrails/poc-results/README.md +++ b/docs/design/prompt-guardrails/poc-results/README.md @@ -53,7 +53,7 @@ ship, and thresholds are not the remedy (`06`). Measured on the 2B model; `guardrails:` config section (keeps the throwaway out of `Configuration` / OpenAPI). - One detector backend (`granite_guardian`); no `openai_moderations` / - `llama_stack_shields` backends. + `ogx_shields` backends. - Output check is non-streaming only; no streaming checkpoints (Decision T4). - tool_content is a post-hoc check on collected tool results, not the diff --git a/docs/design/prompt-guardrails/poc-results/lcs-poc-config.yaml b/docs/design/prompt-guardrails/poc-results/lcs-poc-config.yaml index c7424d022..e61411c38 100644 --- a/docs/design/prompt-guardrails/poc-results/lcs-poc-config.yaml +++ b/docs/design/prompt-guardrails/poc-results/lcs-poc-config.yaml @@ -7,7 +7,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: use_as_library_client: false url: http://localhost:8321 api_key: xyzzy diff --git a/docs/design/prompt-guardrails/prompt-guardrails-spike.md b/docs/design/prompt-guardrails/prompt-guardrails-spike.md index e7171525e..5b4cb91e9 100644 --- a/docs/design/prompt-guardrails/prompt-guardrails-spike.md +++ b/docs/design/prompt-guardrails/prompt-guardrails-spike.md @@ -99,7 +99,7 @@ backends. Ship three backends: `granite_guardian` (chat-template invocation, custom risks), `openai_moderations` (any OpenAI-compatible `/v1/moderations` endpoint — this also covers OGX 1.x's `moderation_endpoint` services and TrustyAI gateways, making D a *deployment choice*, not an architecture), and -`llama_stack_shields` (transitional bridge wrapping today's behavior). +`ogx_shields` (transitional bridge wrapping today's behavior). The existing input-moderation path keeps working unchanged during the transition (see [S5](#decision-s5-fate-of-the-existing-shields-moderation-path)). @@ -250,7 +250,7 @@ with `shield_ids` request-override semantics documented in | B — Replace immediately | Migrate the input path onto the new layer in this epic; remove the shields code. | **Recommendation**: **A** — additive now, deprecation decision deferred to -the LCORE-1099 work. The `llama_stack_shields` detector backend gives +the LCORE-1099 work. The `ogx_shields` detector backend gives deployments a config-level migration path in the meantime. No behavior change for existing deployments. @@ -307,12 +307,12 @@ _No answer needed — this will be implemented as recommended unless you object. | Option | Description | |--------|-------------| -| A — Protocol + per-type adapters | `DetectorBackend` protocol (`async check(content, rule) -> DetectionResult`); adapters: `granite_guardian`, `openai_moderations`, `llama_stack_shields`. | +| A — Protocol + per-type adapters | `DetectorBackend` protocol (`async check(content, rule) -> DetectionResult`); adapters: `granite_guardian`, `openai_moderations`, `ogx_shields`. | | B — Single Guardian-only implementation | Simpler; closes the door on OGX 1.x moderation endpoints and TrustyAI gateways. | **Recommendation**: **A**. Guardian invocation is OpenAI chat-completions with the risk selected via the guardian chat template (system slot); -`openai_moderations` maps categories to rules; `llama_stack_shields` wraps +`openai_moderations` maps categories to rules; `ogx_shields` wraps the existing `run_shield_moderation` behavior. **Confidence**: 85% @@ -404,7 +404,7 @@ refusals. Every hard part of LCORE-230 is precisely what it does not do, and LCS's own capabilities are better fitted on all five axes (real LLM detector, refusal-string semantics, true redaction, careful multimodal handling, Pydantic config). This also matches the precedent already set -in `docs/design/ogx-config-merge/ogx-config-merge-spike.md:220` +in `docs/design/ogx-config-merge/ogx-config-merge-spike.md#decision-s5-where-do-backend-agnostic-high-level-keys-sit` ("Do not preemptively abstract `safety.*`"). **Already taken from it**: the `AsyncGuardrail` blocking/concurrent/ @@ -812,7 +812,7 @@ runner executing a point's rules in parallel, and a hook in - Activation via `LCS_GUARDRAILS_POC_CONFIG` env var, not the `guardrails:` config section (avoids touching `Configuration`/OpenAPI in a throwaway). - Single detector (Granite Guardian via OpenAI-compatible endpoint); no - `openai_moderations` / `llama_stack_shields` backends. + `openai_moderations` / `ogx_shields` backends. - Tool-content check is post-hoc on collected tool results (Decision T5 option B), not the gating capability hook (option A). - Output check is non-streaming `/v1/query` only; no streaming checkpoints. diff --git a/docs/design/prompt-guardrails/prompt-guardrails.md b/docs/design/prompt-guardrails/prompt-guardrails.md index 7b70d49e9..3cfee9181 100644 --- a/docs/design/prompt-guardrails/prompt-guardrails.md +++ b/docs/design/prompt-guardrails/prompt-guardrails.md @@ -2,361 +2,366 @@ | | | |--------------------|-------------------------------------------| -| **Date** | 2026-07-20 | +| **Date** | 2026-07-20 (revised 2026-09-10) | | **Component** | lightspeed-stack | | **Authors** | Maxim Svistunov | | **Feature** | [LCORE-230](https://redhat.atlassian.net/browse/LCORE-230) | +| **Epic** | [LCORE-3386](https://redhat.atlassian.net/browse/LCORE-3386) | | **Spike** | [LCORE-2657](https://redhat.atlassian.net/browse/LCORE-2657) | -| **Links** | [Spike doc](prompt-guardrails-spike.md), [OWASP LLM01](https://genai.owasp.org/llmrisk/llm01-prompt-injection/) | +| **Links** | [Spike doc](prompt-guardrails-spike.md), [Shields guide](../../user_doc/shields_guide.md), [OWASP LLM01](https://genai.owasp.org/llmrisk/llm01-prompt-injection/) | + +> **Revision note (2026-09-10).** The first version of this document proposed +> a standalone `guardrails:` configuration section and a `src/guardrails/` +> package with pluggable detector backends. The configuration that shipped +> under LCORE-3389 (PR #2580) instead builds guardrails as a **shield type** +> inside the existing shields framework. This revision describes that +> architecture. Capabilities of the original design that the shipped +> configuration does not provide are listed in +> [Deferred from the original design](#deferred-from-the-original-design); +> requirements that are still open name the ticket that covers them. ## What -An optional, config-driven guardrails layer owned by lightspeed-stack. -Deployers declare **detectors** (guardian-model endpoints reachable through -OpenAI-compatible APIs — Granite Guardian on vLLM/RHAIIS, any -`/v1/moderations` service, or, transitionally, OGX shields) and -**rules** (an out-of-the-box risk id or a custom risk definition, bound to -one or more guardrail **points**: `input`, `output`, `tool_content`, with a -blocking or advisory posture). The layer runs the applicable rules in -parallel at each point of the request lifecycle and blocks (or annotates) -requests whose content is flagged. +An optional, config-driven guardrails layer owned by lightspeed-stack and +built on its shields framework. A deployer adds a `granite_guardian` entry to +the top-level `shields:` list in `lightspeed-stack.yaml`. The entry names an +IBM Granite Guardian model reachable through an OpenAI-compatible API and +declares a list of **risks**. Each risk carries a custom risk definition, a +score threshold, a violation message, and the guardrail **points** where it +applies: `input`, `output`, or `tool`. At each point the applicable risks are +evaluated, and content that is flagged is blocked with that risk's violation +message. ## Why -Prompt injection is OWASP's #1 LLM risk. lightspeed-stack today moderates -only *input*, only through OGX shields — an OGX API surface upstream -has deleted in OGX 1.x — with no lightspeed-stack-side configuration, no -output or tool-content coverage, no Granite Guardian support, and no custom -risk definitions. Ask Red Hat's migration to Lightspeed Core -([LCORE-2253](https://redhat.atlassian.net/browse/LCORE-2253)) is blocked -on exactly those capabilities (they run parallel multi-risk Granite -Guardian screening with custom risks in production today). This feature -provides them generically, in a form that survives the planned OGX -phase-out. +Prompt injection is OWASP's #1 LLM risk. Before this feature, lightspeed-stack +moderated only *input*, and only through OGX shields -- an API surface that +OGX 1.x removed -- with no lightspeed-stack-side configuration, no output or +tool-content coverage, no Granite Guardian support, and no custom risk +definitions. Ask Red Hat's migration to Lightspeed Core +([LCORE-2253](https://redhat.atlassian.net/browse/LCORE-2253)) depends on +exactly those capabilities: they run parallel multi-risk Granite Guardian +screening with custom risks in production today. This feature provides them +generically, in a form that does not depend on OGX. ## Requirements +Requirements that are not yet met by merged code name the ticket that covers +them. + - **R1:** Guardrails are configured exclusively in the lightspeed-stack - config file under a top-level `guardrails:` section; absent config means - fully inert (no behavior change, no latency). -- **R2:** A rule can reference an out-of-the-box guardian risk (e.g. - `harm`, `jailbreak`, `answer_relevance`) or carry a custom risk - definition (bring-your-own-criteria text). Custom definitions must - express **safety-adjacent concepts** (obfuscation, roleplay jailbreak, - policy violation), not arbitrary string/format predicates — a guardian - is a safety classifier, not a keyword matcher (PoC Finding A). Arbitrary - predicates are the regex-redaction mechanism's job, not a guardrail's. -- **R3:** A rule binds to one or more guardrail points: `input` (user - prompt before the LLM call), `output` (generated answer before the - client sees it), `tool_content` (tool/MCP/RAG content before it enters - the model context). -- **R4:** All rules applicable at a point run concurrently (the existing - OGX shields path is a sequential loop — `src/utils/shields.py:152` - — which the Ask Red Hat gap analysis flags as a performance gap); a - request is blocked iff at least one *blocking* rule flags it. Advisory - (`blocking: false`) rules record their outcome without altering the - response. -- **R4a:** A rule may carry an optional `threshold` (0..1). When set, the - detector's confidence score decides the verdict (Granite Guardian via - `logprobs` on the verdict token; gateways via their native confidence - score); when unset, the boolean verdict decides. This reproduces Ask - Red Hat's per-risk tuning (0.65 leetspeak, 0.80 CVE). -- **R4b:** A rule may carry its own `violation_message`, overriding the - global default, so deployers can explain which policy fired. -- **R4c:** Recommended/default rule sets shipped in documentation must be - validated against a corpus of legitimate product questions and must not - fire on it. Out-of-the-box guardian risk ids (notably `jailbreak`) flag - legitimate technical questions — "You are now a cluster admin, how do I - drain a node?" scores 0.98 — at levels no threshold separates from real - attacks, so **domain-tuned custom definitions are the shipping default** - and OOTB ids are opt-in. -- **R4d:** A deployment may select the input-guardrail execution mode: - `blocking` (default — the model never sees unscreened input) or - `concurrent` (guardian runs alongside the LLM call, result discarded on - violation; lower latency, but the model processes unsafe input). -- **R5:** A blocked request returns HTTP 200 with the configured violation - message (consistent with existing OGX shields refusals): non-streaming - responses carry it as the answer; streaming responses emit it as the - terminal content. The `llm_calls_validation_errors_total` metric is - incremented and the blocked turn is persisted to the conversation. -- **R6:** Input-blocked requests skip RAG retrieval and the main LLM call. -- **R7:** The Granite Guardian detector invokes the model through an - OpenAI-compatible chat-completions endpoint, selecting the risk (or - custom definition) via the guardian chat template; the OpenAI-moderations - detector invokes any OpenAI-compatible `/v1/moderations` endpoint. -- **R7a:** Output-relevance rules (`answer_relevance`, `context_relevance`, - `groundedness`) receive the turn's retrieved context (and question) - paired with the answer; an answer-only check is insufficient and noisy - (PoC Finding B). -- **R8:** Output rules on streaming endpoints check accumulated text at - configurable checkpoints; content past a failed checkpoint is never - emitted. -- **R9:** Detector errors (unreachable endpoint, timeout) block the request - by default (`on_detector_error: block`), overridable to `allow` per - deployment. -- **R10:** Per-rule detection outcomes and latencies are logged and - exposed as metrics. -- **R11:** The existing OGX shields input-moderation path continues - to work unchanged when `guardrails:` is not configured; both may run - side by side during migration. + config file, as a `granite_guardian` entry in the top-level `shields:` + list. Without such an entry the feature is fully inert: no behavior change + and no added latency. +- **R2:** A risk is defined by custom criteria text (`description`) that is + passed to Granite Guardian. Definitions must express **safety-adjacent + concepts** (obfuscation, roleplay jailbreak, policy violation), not + arbitrary string or format predicates: a guardian is a safety classifier, + not a keyword matcher (PoC Finding A). Arbitrary predicates are the job of + the redaction shield. +- **R3:** A risk binds to one or more guardrail points: `input` (the user + prompt before the LLM call), `output` (the generated answer before the + client sees it), and `tool` (tool, MCP, or RAG content before it enters the + model context). +- **R4:** Risks applicable at a point are evaluated concurrently, in batches + of `batch_size` (default 3) so that guardian endpoints with rate limits are + not overloaded, and per-risk latency is logged. A request is blocked if at + least one risk flags it; once a batch contains a flagged risk, the remaining + batches are skipped. +- **R4a:** Each risk has a `threshold` between 0 and 1 (default 0.65). The + verdict is decided by Granite Guardian's confidence score, derived from the + logprobs of its verdict token, which reproduces Ask Red Hat's per-risk + tuning (0.65 leetspeak, 0.80 CVE). +- **R4b:** Each risk carries its own `violation_message`, so deployers can + explain which policy fired. +- **R4c:** Recommended rule sets shipped in documentation must be validated + against a corpus of legitimate product questions and must not fire on it. + Generic phrasings of risks such as "jailbreak" flag legitimate technical + questions -- "You are now a cluster admin, how do I drain a node?" scores + 0.98 -- at levels no threshold separates from real attacks, so + **domain-tuned custom definitions are the shipping default** (LCORE-3394). +- **R5:** A blocked request returns HTTP 200 with the violation message: + non-streaming responses carry it as the answer, streaming responses emit + it as the terminal content. The blocked turn is persisted to the + conversation and the `llm_calls_validation_errors_total` metric is + incremented. (The metric currently has no callers anywhere: LCORE-4089.) +- **R6:** A request blocked at the `input` point performs no RAG retrieval, + no main LLM call and no topic-summary call, and its response carries no RAG + chunks or referenced documents. (Met on `/v1/responses` and `/rlsapi`; not + yet on `/v1/query` and `/v1/streaming_query`: LCORE-4090.) +- **R7:** Granite Guardian is invoked through an OpenAI-compatible + chat-completions endpoint, using its client-side judge prompt with the + risk's criteria. +- **R7a:** Output relevance risks (answer relevance, context relevance, + groundedness) receive the turn's retrieved context and question alongside + the answer; an answer-only check is insufficient and noisy (PoC Finding B). + The shield's evaluation interface currently takes plain text, so the + payload for relevance checks is to be designed in LCORE-3391. +- **R8:** Output risks on streaming endpoints check accumulated text at + checkpoints; content past a failed checkpoint is never emitted + (LCORE-3391). +- **R9:** Guardian errors (unreachable endpoint, timeout, unparseable + verdict) fail closed: the request is not served. +- **R10:** Per-risk outcomes and latencies are logged and exposed as metrics. + Per-risk latency is currently logged only; metrics are not yet exposed + (LCORE-3390 for input, LCORE-3391 for output). +- **R11:** The other shields (`question_validity`, `redaction`) continue to + work unchanged, and may be configured alongside `granite_guardian`. ## Use Cases - **U1:** As a Lightspeed product team (e.g. Ask Red Hat), I want to - declare my guardian model endpoint and my product's risk set (OOTB + - custom definitions) in the LCS config file, so that my product is - protected without custom code. -- **U2:** As a deployer, I want prompts that attempt jailbreak/injection + declare my guardian model endpoint and my product's risk definitions in + the LCS config file, so that my product is protected without custom code. +- **U2:** As a deployer, I want prompts that attempt jailbreak or injection blocked before they reach the LLM, so that the assistant cannot be subverted. -- **U3:** As a deployer, I want generated answers checked (e.g. harm, - answer relevance) before delivery, so that unsafe or off-context output - never reaches users. +- **U3:** As a deployer, I want generated answers checked (e.g. harm, answer + relevance) before delivery, so that unsafe or off-context output never + reaches users. - **U4:** As a deployer of an MCP-enabled assistant, I want tool and RAG content screened before the model consumes it, so that indirect prompt injection via third-party content is caught. -- **U5:** As an SRE, I want per-rule outcomes and latencies in metrics, so - that I can observe block rates and tune thresholds/rules. -- **U6:** As a security engineer, I want the service to fail closed when - the guardian endpoint is down, so that protection cannot silently lapse. +- **U5:** As an SRE, I want per-risk outcomes and latencies in logs and + metrics, so that I can observe block rates and tune thresholds. +- **U6:** As a security engineer, I want the service to fail closed when the + guardian endpoint is down, so that protection cannot silently lapse. ## Architecture ### Overview ```text - ┌────────────────────────────── lightspeed-stack ──────────────────────────────┐ - │ │ - user query ─┼─► input rules ──blocked──► 200 refusal (skip RAG + LLM; persist turn) │ - │ (parallel) │ - │ │ passed │ - │ ▼ │ - │ RAG retrieval ─► LLM call (Responses API) │ - │ │ ▲ │ - │ tool results │ tool_content rules gate each result │ - │ └────────┘ (flagged content never enters context) │ - │ ▼ │ - │ output rules ──blocked──► refusal replaces/terminates answer │ - │ (checkpointed when streaming) │ - │ │ passed │ - └──────┼───────────────────────────────────────────────────────────────────────┘ - ▼ - response - all rule checks ──► DetectorBackend ──► guardian model - (Guardian chat template (vLLM / RHAIIS / - or /v1/moderations) Ollama / gateway) + ┌─────────────────────────── lightspeed-stack ────────────────────────────┐ + │ │ + user query ─┼─► input sanitization ─► input risks ──blocked──► 200 refusal │ + │ (shield) (skip RAG + LLM; persist turn) │ + │ │ passed │ + │ ▼ │ + │ RAG retrieval ─► agent / LLM call │ + │ │ ▲ │ + │ tool results │ tool risks gate each │ + │ └─────────┘ result (capability) │ + │ ▼ │ + │ output risks ──blocked──► refusal replaces answer │ + │ (checkpointed when streaming) │ + │ │ passed │ + └────────────────────────────┼─────────────────────────────────────────────┘ + ▼ + response + + all risk checks ──► GraniteGuardian shield ──► Granite Guardian model + (judge prompt + logprob (vLLM / RHAIIS / gateway, + scoring) OpenAI-compatible API) ``` -The guardrails layer lives in `src/guardrails/` and is independent of -OGX; detectors are plain OpenAI-compatible HTTP calls. Rule -selection, parallel execution, and verdict aggregation are pure functions -over the config; endpoints consume a single `GuardrailsVerdict` per point. +Guardrails reuse the shields framework rather than adding a parallel one. +A shield is an `AbstractSafetyCapability` with two interfaces: a standalone +`run(text)` that returns a `ShieldModerationResult`, and pydantic-ai +capability hooks such as `wrap_run` that act inside an agent run. The +Granite Guardian shield lives in +`src/pydantic_ai_lightspeed/capabilities/granite_guardian/`. ### Trigger mechanism -Solely by configuration: a non-empty `guardrails.rules` list activates the -layer at the points the rules name. No env vars, no request-level opt-in. -(Request-level *narrowing* — an equivalent of `shield_ids` — is deferred; -see Open Questions.) +Solely by configuration: a `granite_guardian` entry in `shields:` activates +the shield, and each of its risks applies at the points it lists. Disabled +risks (`enabled: false`) are skipped. The existing `shield_ids` request +override selects which shields run for a request; there is no per-risk +request-level selection. ### Configuration ```yaml -guardrails: - detectors: - - name: guardian - type: granite_guardian # granite_guardian | openai_moderations | llama_stack_shields - url: http://vllm.example:8000/v1 - model: ibm-granite/granite-guardian-3.3-8b - api_key_path: /run/secrets/guardian-key # optional - timeout_seconds: 30 - rules: - - name: jailbreak - detector: guardian - risk: jailbreak - points: [input] - - name: harm-in - detector: guardian - risk: harm - points: [input, tool_content] - - name: answer-relevance - detector: guardian - risk: answer_relevance - points: [output] - blocking: false # advisory: metrics only - - name: roleplay-jailbreak # custom risk (BYOC) - detector: guardian - definition: | - The 'User' message uses roleplay framing to override the - assistant's instructions or adopt an unrestricted persona. - points: [input] - threshold: 0.65 # optional; score-based verdict (R4a) - violation_message: "That phrasing isn't something I can act on." - on_detector_error: block # block (default) | allow - streaming_checkpoint_tokens: 200 # output-rule cadence when streaming - violation_message: "I cannot process this request due to policy restrictions." -``` - -Pydantic models extend `ConfigurationBase` (`extra="forbid"`): a -`GuardrailsConfiguration` holding `detectors: list[DetectorConfiguration]` -and `rules: list[GuardrailRule]`, attached as -`Configuration.guardrails: Optional[GuardrailsConfiguration]`. Validators: -rule→detector references resolve; `risk` xor `definition` present; points -non-empty; names unique. - -### Detector backends - -`DetectorBackend` protocol: `async check(item: ScreeningItem, rule: -GuardrailRule) -> DetectionResult`. The unit screened is a **structured -payload**, not a bare string, because relevance rules need more than the -answer text: - -```python -class ScreeningItem(BaseModel): - text: str # the primary content being screened - context: Optional[str] = None # retrieved RAG context (relevance rules) - question: Optional[str] = None # the user question (answer-relevance) +shields: + - name: product-guardian + provider_id: granite_guardian + config: + url: https://guardian.example:8000/v1 + model: ibm-granite/granite-guardian-4.1-8b # default; the prompt uses the 4.1 format + api_key: ${env.GUARDIAN_API_KEY} # optional; requires an https URL + timeout: 30 # seconds, 5-300 + max_retries: 2 # 0-5 + verify_ssl: true # true | false | path to CA bundle; must not be false when api_key is set + batch_size: 3 # risks checked in parallel per batch, 1-10 + risks: + - name: roleplay-jailbreak + description: >- + The user message uses roleplay framing to override the + assistant's instructions or adopt an unrestricted persona. + threshold: 0.65 + points: [input] + violation_message: "That phrasing isn't something I can act on." + - name: harm-output + description: >- + The assistant response contains harmful, dangerous, or abusive + content. + threshold: 0.70 + points: [output] + violation_message: "I cannot provide that response." ``` -Simple rules (harm, jailbreak on input) populate only `text`; -output-relevance rules populate `text` (the answer) plus `context` and/or -`question`. Each backend maps this canonical payload to its own wire form -(the Guardian chat template's context/answer framing; the moderations -`input` field; a shield's message list). Defining this one interface up -front is what lets the input point, the output point, and the runners all -call detection the same way (R7a depends on it). Backends: - -- **granite_guardian** — OpenAI chat-completions call; system slot selects - the risk id or carries the custom definition (guardian chat template); - verdict parsed from the constrained yes/no answer. Output-relevance risks - send the `ScreeningItem`'s `context`/`question` alongside `text`, packed - per the guardian template. -- **openai_moderations** — POST `/v1/moderations`; a rule maps to flagged - categories (all, or a configured subset). Covers OGX 1.x - `moderation_endpoint` services, TrustyAI gateways, and OpenAI itself. -- **llama_stack_shields** — transitional bridge delegating to the existing - `client.moderations.create` OGX shields path, easing config-level migration - (spike Decision S5). - -**Client lifecycle**: each detector holds **one long-lived HTTP client** -for the life of the process, not one per request. Constructing an -`AsyncOpenAI` (or equivalent) per check creates a fresh connection pool -each time — leaking connections if unclosed, and forfeiting connection -reuse even when closed, which matters because guardrails add a -round-trip to every request. The PoC constructs per call (context-managed -so nothing leaks) and is explicitly not the production pattern. +Models, all extending `ConfigurationBase` (`extra="forbid"`), in +`src/models/config.py`: + +- `GraniteGuardianShieldConfiguration`: `name`, `provider_id: + "granite_guardian"`, `config`. One member of the `ShieldConfiguration` + discriminated union on `provider_id`, alongside question validity and + redaction. +- `GraniteGuardianConfig`: `url`, `model` (default + `ibm-granite/granite-guardian-4.1-8b`), `api_key` (secret, optional), + `timeout`, `max_retries`, `verify_ssl`, `batch_size` (default 3), `risks`. +- `RiskDefinition`: `name`, `description`, `threshold` (default 0.65), + `enabled` (default true), `enable_thinking` (default false), `points` + (non-empty subset of `input`, `output`, `tool`), `violation_message`. + +The model docstring and field description for `enable_thinking` refer to a +`ModerationConfig.thinking_enabled` setting that does not exist; until that +is resolved, `enable_thinking` is the only control for think mode. + +### Granite Guardian shield + +- **Risk selection:** for a given point, the shield evaluates the enabled + risks whose `points` include that point. +- **Judge prompt:** each risk is sent as a chat-completions request that + combines the text under evaluation with Granite Guardian 4.1's client-side + judge block: the risk's criteria text, a yes/no scoring schema, and either + a no-think or a think preamble (`enable_thinking`). +- **Scoring:** the request asks for logprobs. The shield parses the response + through its `` and `` tags, takes the top logprobs of the + verdict token, and computes `p_risky = p(yes) / (p(yes) + p(no))`. The risk + is flagged when `p_risky >= threshold`. A response without logprobs or + without a parseable verdict raises an error, which is handled per R9. +- **Model:** `model` sets the model name sent to the inference server + (default `ibm-granite/granite-guardian-4.1-8b`), for example to match an + Ollama tag. The judge prompt is built for the Granite Guardian 4.1 format, + so other Guardian versions need a version-specific prompt (see Open + Questions). +- **Concurrency:** the enabled risks for a point are checked in parallel + batches of `batch_size`; once a batch contains a flagged risk, the remaining + batches are skipped. Batching bounds the load on guardian endpoints that + rate-limit, such as internal model gateways. +- **Client lifecycle:** the shield holds **one long-lived HTTP client** for + the life of the process, not one per request. Constructing a client per + check creates a fresh connection pool each time -- leaking connections if + it is never closed, and forfeiting connection reuse even when it is -- on a + path that runs on every request. The implementation caches the model and + its client per configuration, so the client is created once and reused. ### Request lifecycle integration -- **Input**: next to the existing `run_shield_moderation` call in - `src/app/endpoints/query.py`, `streaming_query.py`, `responses.py`, - `rlsapi_v1.py` — the guardrails verdict feeds the same - `ShieldModerationResult` seam, so the blocked path (RAG skip, refusal, - turn persistence, metrics) is reused as-is. -- **Output**: non-streaming — single check between response retrieval and - `QueryResponse` assembly; streaming — checkpointed buffer-and-release in - the SSE generators (`src/utils/agents/streaming.py`, +- **Input, Responses-based endpoints (`/v1/responses`, `/rlsapi`):** + `run_shield_moderation_v2` runs before RAG retrieval. It sanitizes the + input, then calls each selected shield's `run()`; the first block returns + a `ShieldModerationBlocked`, and the endpoint's existing blocked path + handles the refusal, persistence and RAG skip. +- **Input, agent-based endpoints (`/v1/query`, `/v1/streaming_query`):** + shields are currently attached to the agent as capabilities and evaluated + in `wrap_run`, inside the agent run and therefore after RAG retrieval; the + pre-agent `run_shield_moderation` call on these endpoints is a stub that + always passes. LCORE-4090 moves input shields before RAG on these + endpoints, through the same `run_shield_moderation_v2` path, so that R6 + holds everywhere and each shield runs once per request. +- **Output (LCORE-3391):** non-streaming -- a single check between response + retrieval and response assembly; streaming -- checkpointed + buffer-and-release in the SSE generators (`src/utils/agents/streaming.py`, `src/utils/streaming_sse.py`). -- **Tool content**: a pydantic-ai capability (same mechanism as the - existing inert safety capabilities in - `src/pydantic_ai_lightspeed/capabilities/`) intercepts each tool result - before it re-enters the agent loop; flagged content is replaced by a - policy notice or aborts the turn per the rule's blocking flag. - -The guardrails module itself stays a thin, framework-agnostic library -(`content + rule → verdict`). Per reviewer note, the agent runners in -`src/runners/` are the natural place to invoke it — calling into -`src/guardrails/` from a runner is only a few lines, and keeps the -detection logic decoupled from any one execution path (query endpoint, -runner, or streaming generator). +- **Tool (LCORE-3392):** a capability hook intercepts each tool result before + it re-enters the agent loop; flagged content is replaced by a policy notice + or aborts the turn; which of the two is decided in LCORE-3392. + +See [How shields apply at runtime](../../user_doc/shields_guide.md) for the +per-endpoint behavior of shields in general. ### API changes -None to request models in the core epic. Response behavior on block is the -established refusal shape. (A `guardrail_ids` request-narrowing field -analogous to `shield_ids` is an open question.) +None to request models. The response on block is the established refusal +shape. `GET /v1/shields` lists `granite_guardian` shields like any other. ### Error handling -Detector connectivity/timeout errors follow `on_detector_error`: -`block` (default) returns the refusal shape with a distinct log line and -metric label; `allow` logs a warning and proceeds. Config errors -(unresolvable detector reference, bad risk spec) fail startup validation. +Guardian connectivity errors, timeouts, and unparseable verdicts fail +closed. On the `run_shield_moderation_v2` path the error is mapped to an +HTTP error response; inside an agent run it propagates out of the run and is +returned as an HTTP error (non-streaming) or an `error` SSE event +(streaming). A configurable fail-open posture and a refusal-shaped response +for detector failures are deferred (see below). Configuration errors fail +startup validation. ### Security considerations -- Guardian endpoints and API keys are deployment secrets — keys are read - from files (`api_key_path`) per project convention, never inline. +- The guardian endpoint and its API key are deployment secrets. The API key + is a secret string in configuration; when an API key is set the endpoint + URL must use HTTPS, so the key is never sent in clear text. - Detection is risk reduction, not a security boundary: published bypasses - exist for classifier-based defenses. Layered posture (all three points + - least-privilege MCP config) is the mitigation; thresholds/risks are - deployment policy. -- Moderated content is sent to the guardian endpoint: deployers must place - detectors within the same trust boundary as the serving LLM. + exist for classifier-based defenses. A layered posture -- all three points + plus least-privilege MCP configuration -- is the mitigation; risk + definitions and thresholds are deployment policy. +- Moderated content is sent to the guardian endpoint, so deployers must + place it within the same trust boundary as the serving LLM. ### Migration / backwards compatibility -No `guardrails:` section ⇒ byte-identical behavior to today (R11). The -OGX shields path is untouched; its deprecation is deferred to the -OGX 1.x migration (LCORE-1099). The `llama_stack_shields` backend lets -deployments move their config to the new schema before OGX migrates. +No `granite_guardian` shield configured means behavior is unchanged (R1). +Existing `question_validity` and `redaction` shields are unaffected (R11). ## Acceptance test surface | Req | Observable behavior | Verified by | |-----|---------------------|-------------| -| R1 | No `guardrails:` config ⇒ responses and latency unchanged | e2e | -| R2 | OOTB risk blocks a matching prompt; custom definition blocks its target phrasing | e2e | -| R7a | Relevance rule receives context+answer; answer-only run flagged as misconfiguration in review | integration | -| R3 | A rule with `points: [output]` never fires on input, and vice versa | integration | -| R4 | Two input rules ⇒ both detector calls observed concurrently; advisory rule never alters response | integration | -| R4a | Same content flips verdict across a threshold boundary (e.g. 0.6 vs 0.9); unset threshold falls back to boolean verdict | integration | -| R4b | Rule with its own `violation_message` returns that text, not the global default | e2e | -| R4c | Documented recommended rule set produces zero blocks on the legitimate-question corpus | e2e / tuning fixture | -| R4d | `concurrent` mode returns the same verdict as `blocking` for the same input, with lower wall-clock | integration | +| R1 | No `granite_guardian` shield ⇒ responses and latency unchanged | e2e | +| R2 | A custom risk definition blocks its target phrasing and passes a benign one | e2e | +| R3 | A risk with `points: [output]` never fires on input, and vice versa | integration | +| R4 | Risks at one point ⇒ guardian calls run concurrently up to `batch_size`; later batches skipped after a flag; per-risk latency logged | integration | +| R4a | Same content flips verdict across a threshold boundary (e.g. 0.6 vs 0.9) | integration | +| R4b | A risk's own `violation_message` is returned when it fires | e2e | +| R4c | Documented recommended risk set produces zero blocks on the legitimate-question corpus | e2e / tuning fixture | | R5 | Blocked query ⇒ HTTP 200, violation message as answer, metric incremented, turn persisted | e2e | -| R6 | Input-blocked query produces no RAG retrieval and no main-LLM call | integration | -| R7 | Guardian receives risk id / definition in the system slot; moderations backend hits `/v1/moderations` | integration | +| R6 | Input-blocked query ⇒ no RAG retrieval, no main-LLM or topic-summary call, no RAG documents in the response | integration | +| R7 | Guardian request carries the judge block with the risk's criteria and requests logprobs | integration | | R8 | Streaming: flagged checkpoint ⇒ refusal emitted, withheld text never sent | e2e | -| R9 | Detector down ⇒ refusal (default) / pass-through (`allow`) | e2e | -| R10 | Per-rule outcome + latency present in logs and metrics | integration | -| R11 | OGX shields-only deployment behaves exactly as before the feature | e2e | +| R9 | Guardian down or unparseable ⇒ request not served | integration / e2e | +| R10 | Per-risk outcome and latency present in logs and metrics | integration | +| R11 | Question-validity and redaction shields behave as before when a Guardian shield is added | e2e | ## Aspect-specific concerns ### Latency and Cost -Each blocking rule adds one guardian inference to the critical path; -parallel execution makes the per-point cost ≈ the slowest single check -(Guardian-8B on GPU: high tens to low hundreds of ms; small CPU models: -lower). Input and output points each add at most one such round; -`tool_content` multiplies by tool-call count — deployers control exposure -via rule→point bindings, and per-rule latency metrics (R10) make the cost -observable. PoC latency measurements: see the spike doc's PoC results. +Each risk adds one guardian inference on its point's critical path. With +batched concurrent evaluation (R4) the cost per point is roughly the slowest +check in each batch, summed over the number of batches (Guardian 8B on GPU: +high tens to low hundreds of ms per check); a larger `batch_size` lowers +latency at the cost of more simultaneous load on the guardian endpoint. The `tool` point +multiplies by the number of tool calls; deployers control exposure through +point bindings, and per-risk latency (R10) makes the cost observable. +Guardian token usage is not counted against user quota or reported token +counts; it should at least be logged per risk. PoC latency measurements: +see the spike doc's PoC results. ### Observability -Per-rule structured logs (rule, point, verdict, latency, raw verdict -text at debug); metrics: existing `llm_calls_validation_errors_total` on -block, plus per-rule outcome/latency counters and histograms. Detector -errors get a distinct metric label to drive alerting (fail-closed events -are page-worthy). +Per-risk structured logs (risk, point, verdict, score, latency; raw verdict +text at debug level). Metrics: `llm_calls_validation_errors_total` on block +(LCORE-4089), plus per-risk outcome and latency counters and histograms. +Guardian errors get a distinct log line and metric label, because fail-closed +events are page-worthy. ### Failure modes -- Guardian endpoint down ⇒ R9 posture (default: block; alert fires). -- Guardian misbehaving (non-yes/no output) ⇒ treated as not-flagged for - advisory rules and per `on_detector_error` for blocking rules - (unparseable verdict ≈ detector error). -- Slow detector ⇒ per-detector timeout bounds the stall; timeout ⇒ R9. -- Config drift (rule names a removed detector) ⇒ startup validation error. +- Guardian endpoint down or timing out ⇒ fail closed (R9); the configured + `timeout` and `max_retries` bound the stall. +- Guardian output without the expected ``/`` structure or + without logprobs ⇒ treated as a guardian error (R9). +- Configuration drift (for example an unknown point name) ⇒ startup + validation error. -### Runbook / oncall implications +### Runbook / on-call implications -New alert: detector-error rate (fail-closed blocks). Recovery: restore the -guardian endpoint or temporarily set `on_detector_error: allow` /remove -rules (explicit, logged policy change). Block-rate dashboards distinguish -policy blocks (working as intended) from error blocks. +New alert: guardian error rate (fail-closed requests). Recovery: restore the +guardian endpoint, or remove or disable the affected risks (`enabled: +false`) as an explicit, logged policy change. Block-rate dashboards should +distinguish policy blocks (working as intended) from error-driven failures. ## Implementation Suggestions @@ -364,54 +369,86 @@ policy blocks (working as intended) from error blocks. | File | What to do | |------|------------| -| `src/models/config.py` | Add `GuardrailsConfiguration` + sub-models; attach to `Configuration` | -| `src/guardrails/` (new) | Models, `DetectorBackend` protocol, backends, parallel runner | -| `src/app/endpoints/query.py` (+streaming, responses, rlsapi) | Input-point call feeding the `ShieldModerationResult` seam | -| `src/utils/agents/streaming.py`, `src/utils/streaming_sse.py` | Output checkpoints in SSE generators | -| `src/pydantic_ai_lightspeed/capabilities/` | Tool-content gating capability; wire via `_agent_capabilities()` | -| `src/metrics/` | Per-rule outcome/latency instruments | -| `docs/user_doc/`, `examples/` | Deployer guide + validated config example | +| `src/models/config.py` | `GraniteGuardianShieldConfiguration`, `GraniteGuardianConfig`, `RiskDefinition` (shipped) | +| `src/pydantic_ai_lightspeed/capabilities/granite_guardian/` | The shield: risk selection, judge prompt, logprob scoring, batched risk checks, cached client, `run()` and capability hooks | +| `src/utils/shields.py` | `run_shield_moderation_v2` and `build_shield` | +| `src/app/endpoints/query.py`, `streaming_query.py` | Input shields before RAG via `run_shield_moderation_v2` (LCORE-4090) | +| `src/utils/pydantic_ai_helpers.py` | Capabilities attached to agents; keep the tool point here, move input out (LCORE-4090) | +| `src/utils/agents/streaming.py`, `src/utils/streaming_sse.py` | Output checkpoints in the SSE generators (LCORE-3391) | +| `src/metrics/` | Per-risk outcome and latency instruments; call the validation-error metric (LCORE-4089) | +| `docs/user_doc/`, `examples/` | Deployer guide and validated config example (LCORE-3394) | ### Insertion point detail -The input hook mirrors the PoC: after `run_shield_moderation(...)` in each -endpoint, when the verdict blocks, construct `ShieldModerationBlocked` -(message, synthetic moderation id, refusal response) — every downstream -branch already handles it. The tool-content capability follows the -`QuestionValidity` capability's interception pattern -(`src/pydantic_ai_lightspeed/capabilities/question_validity/_capability.py`) +The input point uses the shields path that Responses-based endpoints already +use: `run_shield_moderation_v2` before `build_rag_context`, returning a +`ShieldModerationBlocked` that every downstream branch already handles. The +tool point follows the question-validity capability's interception pattern +(`src/pydantic_ai_lightspeed/capabilities/question_validity/_capability.py`), applied to tool results rather than the user prompt. ### Config pattern -Follow the project's Configuration conventions (see -[CLAUDE.md](../../../CLAUDE.md) — Configuration section); schema and YAML -example above. Regenerate `docs/openapi.json` and config docs after -attaching the section. +Follow the project's configuration conventions (see +[CLAUDE.md](../../../CLAUDE.md), Configuration section). Regenerate +`docs/devel_doc/openapi.json` and the config docs after changing the models. ### Test patterns -- Unit/integration tests need **no real guardian**: a scripted - OpenAI-compatible mock (respond yes/no per marker phrases) exercises - every layer behavior deterministically. -- e2e needs a guardian stand-in the CI environment can run: either the - mock detector as a service, or a small real model where resources allow - — decide in the step-definitions ticket against CI constraints. -- Concurrency: assert parallelism (not sequencing) of multi-rule points - via call-timestamp capture in the mock. +- Unit and integration tests need **no real guardian**: a scripted + OpenAI-compatible mock that returns a `` verdict with logprobs per + marker phrase exercises every shield behavior deterministically. +- e2e needs a guardian stand-in the CI environment can run: either the mock + as a service or a small real model where resources allow; decide in the + step-definitions ticket (LCORE-3388) against CI constraints. +- Concurrency: assert that risks at one point run in parallel within a batch + and that later batches are skipped after a flag, by capturing call + timestamps in the mock. +- Failure posture: pin the fail-closed behavior on both the + `run_shield_moderation_v2` path and the in-agent path. + +## Deferred from the original design + +The first version of this document proposed the following. The shipped +configuration (LCORE-3389) does not provide them; each is deferred until a +product need justifies it. + +- A dedicated `guardrails:` configuration section with separate `detectors` + and `rules`, a `src/guardrails/` package, a `DetectorBackend` protocol and + a structured `ScreeningItem` payload. +- The `openai_moderations` backend (any `/v1/moderations` service, TrustyAI + gateways) and an `ogx_shields` transitional backend. +- Selecting out-of-the-box Granite Guardian risk ids; risks are custom + criteria text only. +- A boolean verdict when no threshold is set; every risk has a threshold. +- `on_detector_error: allow` (fail-open) and a refusal-shaped response for + detector failures. +- `api_key_path` (reading the key from a file); the shipped config takes the + key as a secret string. +- An input execution mode that runs the guardian concurrently with the main + LLM call (former R4d). +- A global `violation_message` default; each risk carries its own. +- Advisory (non-blocking) risks that record their outcome without altering + the response, originally intended for output relevance checks. Ask Red Hat + runs blocking-only screening and no current consumer needs advisory risks; + a `blocking` flag on `RiskDefinition` (default true) is enough to add them + when one does. ## Open Questions for Future Work -- Request-level rule narrowing (a `guardrail_ids` analog of `shield_ids`) - — deferred from spike Decision T1; wait for a product ask. -- Unifying question-validity and PII redaction under the same - policy/config umbrella — deferred from spike Decision T7. -- Streaming checkpoint sizing defaults — spike Decision T4 (70% - confidence); tune during implementation with real latency data. -- Cheap classifier tier for `tool_content` (Prompt Guard 2-class) and its - licensing posture — deferred from spike Decisions S2/S3. -- Deprecation timeline for the OGX shields path — owned by - LCORE-1099 (spike Decision S5). +- **Other Guardian versions:** `model` accepts any model name, but the judge + prompt is built for the 4.1 format. The spike benchmarked 3.3-8B (spike + Decision S3), which uses a different prompt format; supporting it needs a + version-specific prompt. +- **Guardian token usage:** whether guardian calls should count against user + quota or be tracked as service overhead. Compaction's summarization calls + raise the same question. +- **Streaming checkpoint sizing:** defaults for LCORE-3391 (spike Decision + T4, 70% confidence); tune with real latency data. +- **Cheap classifier tier for `tool`:** Prompt Guard 2-class, and its + licensing posture (spike Decisions S2 and S3). +- **Per-risk request narrowing:** `shield_ids` selects shields, not + individual risks; wait for a product ask. ## Changelog @@ -421,3 +458,5 @@ attaching the section. | 2026-08-03 | Added R4a (per-rule thresholds), R4b (per-rule violation messages), R4d (input execution mode), R7a (output-relevance context pairing); `ScreeningItem` detector payload; client-lifecycle and `src/runners` integration notes | Decisions T8–T10 and PoC finding B | | 2026-08-03 | Added R4c (recommended rule sets validated against a legitimate-question corpus) | PoC finding D — OOTB `jailbreak` false-positives on legitimate OpenShift questions at ~0.98 | | 2026-08-03 | PR #2182 review: `DetectorBackend` takes a structured payload; recommended-model rec split (3.3-8B benchmarked, 4.1-8B extrapolated) | @sbunciak / @tisnik review + CodeRabbit | +| 2026-09-10 | Architecture rewritten to the shield-based design that shipped (Granite Guardian as a `shields:` entry with `RiskDefinition`s); R6 extended to topic-summary calls and RAG documents; open requirements linked to LCORE-3390, 3391, 4089 and 4090; original-design capabilities, including advisory risks, moved to "Deferred from the original design" | LCORE-3389 shipped as a shield type (PR #2580); implementation review of LCORE-3390 (PR #2646) | +| 2026-09-11 | Aligned with the LCORE-3390 implementation (PR #2646): configurable `model`, risks checked in parallel batches (`batch_size`), one cached client per configuration, per-risk latency logged | Implementation review of LCORE-3390 | diff --git a/docs/devel_doc/ARCHITECTURE.md b/docs/devel_doc/ARCHITECTURE.md index c20752f08..a1e2815aa 100644 --- a/docs/devel_doc/ARCHITECTURE.md +++ b/docs/devel_doc/ARCHITECTURE.md @@ -137,7 +137,7 @@ LCore requires two main configuration files: 2. **OGX Configuration** (`run.yaml`): - Required for both library and server modes - Defines LLM providers, models, RAG stores, shields - - See [OGX documentation](https://llama-stack.readthedocs.io/) for details + - See [OGX documentation](https://ogx-ai.github.io/) for details **Configuration Validation:** - Pydantic models validate configuration structure at startup diff --git a/docs/devel_doc/architecture.svg b/docs/devel_doc/architecture.svg index 6d9b53acd..e67e10268 100644 --- a/docs/devel_doc/architecture.svg +++ b/docs/devel_doc/architecture.svg @@ -1,7 +1,7 @@ - + @@ -285,14 +285,14 @@
-
Llama Stack
+
OGX
(library or
service mode)
- Llama Stack... + OGX @@ -722,11 +722,11 @@
-
Llama Stack
Configuration
+
OGX
Configuration
- Llama Stack... + OGX Configuration diff --git a/docs/devel_doc/container_orchestration.md b/docs/devel_doc/container_orchestration.md index 66a2b5fa8..30f2a09c6 100644 --- a/docs/devel_doc/container_orchestration.md +++ b/docs/devel_doc/container_orchestration.md @@ -207,7 +207,7 @@ Override any of these variables when running `make`: ```bash make run OGX_PORT=9321 ``` -*Note: Also update `llama_stack.url` in `lightspeed-stack.yaml` to `http://localhost:9321`* +*Note: Also update `ogx.url` in `lightspeed-stack.yaml` to `http://localhost:9321`* **Use custom config files:** ```bash @@ -216,7 +216,7 @@ make run CONFIG=my-config.yaml OGX_CONFIG=my-run.yaml **Use custom container name:** ```bash -make run OGX_CONTAINER_NAME=my-llama-stack +make run OGX_CONTAINER_NAME=my-ogx ``` **Force Docker instead of Podman:** @@ -245,7 +245,7 @@ This file configures the Lightspeed Core Stack service. **OGX connection settings:** ```yaml -llama_stack: +ogx: use_as_library_client: false url: http://localhost:8321 # api_key: custom-key # Optional authentication @@ -500,7 +500,7 @@ Error: cannot listen on the TCP port: listen tcp4 0.0.0.0:8321: bind: address al Don't forget to update `lightspeed-stack.yaml`: ```yaml - llama_stack: + ogx: url: http://localhost:9321 ``` @@ -582,7 +582,7 @@ curl -fsSL https://get.docker.com | sh 1. **Check OGX URL in config:** ```yaml # lightspeed-stack.yaml - llama_stack: + ogx: url: http://localhost:8321 # Must match OGX_PORT ``` @@ -737,52 +737,52 @@ If you need more control than the Makefile provides, you can manage the containe #### Build the Image ```bash -podman build -f deploy/ogx/test.containerfile -t my-llama-stack:custom . +podman build -f deploy/ogx/test.containerfile -t my-ogx:custom . ``` #### Run the Container ```bash podman run -d \ - --name my-llama-stack \ + --name my-ogx \ -p 9000:8321 \ -v $(pwd)/run.yaml:/opt/app-root/run.yaml:z \ -v $(pwd)/lightspeed-stack.yaml:/opt/app-root/lightspeed-stack.yaml:ro,z \ -e OPENAI_API_KEY \ - my-llama-stack:custom + my-ogx:custom ``` #### Monitor the Container ```bash # Follow logs -podman logs -f my-llama-stack +podman logs -f my-ogx # Check health -podman inspect --format='{{.State.Health.Status}}' my-llama-stack +podman inspect --format='{{.State.Health.Status}}' my-ogx # Execute commands inside container -podman exec my-llama-stack curl http://localhost:8321/v1/health +podman exec my-ogx curl http://localhost:8321/v1/health # View container stats (CPU, memory) -podman stats my-llama-stack +podman stats my-ogx ``` #### Stop and Remove ```bash # Stop gracefully -podman stop -t 10 my-llama-stack +podman stop -t 10 my-ogx # Remove container -podman rm my-llama-stack +podman rm my-ogx # Remove image -podman rmi my-llama-stack:custom +podman rmi my-ogx:custom ``` #### Connect LCORE to Manual Container Update `lightspeed-stack.yaml`: ```yaml -llama_stack: +ogx: use_as_library_client: false url: http://localhost:9000 # Use your custom port ``` diff --git a/docs/devel_doc/conversation_history.svg b/docs/devel_doc/conversation_history.svg index f339b3408..48c2d589a 100644 --- a/docs/devel_doc/conversation_history.svg +++ b/docs/devel_doc/conversation_history.svg @@ -1,7 +1,7 @@ - + @@ -191,11 +191,11 @@
-
llama-stack
+
ogx
- llama-stack + ogx
@@ -544,11 +544,11 @@
-
Llama Stack
Configuration
+
OGX
Configuration
- Llama Stack... + OGX Configuration diff --git a/docs/devel_doc/conversations_api.md b/docs/devel_doc/conversations_api.md index 23f94ab6b..424f2f01d 100644 --- a/docs/devel_doc/conversations_api.md +++ b/docs/devel_doc/conversations_api.md @@ -115,9 +115,9 @@ When a user makes a query **without** providing a `conversation_id`: ```python # No conversation_id provided - create a new conversation first conversation = await client.conversations.create(metadata={}) -llama_stack_conv_id = conversation.id +ogx_conv_id = conversation.id # Store the normalized version -conversation_id = normalize_conversation_id(llama_stack_conv_id) +conversation_id = normalize_conversation_id(ogx_conv_id) # Use the conversation in responses.create() response = await client.responses.create( @@ -125,7 +125,7 @@ response = await client.responses.create( model=model_id, instructions=system_prompt, store=True, - conversation=llama_stack_conv_id, # Use OGX format + conversation=ogx_conv_id, # Use OGX format # ... other parameters ) ``` @@ -151,7 +151,7 @@ ogx_conv_id = to_ogx_conversation_id(conversation_id) response = await client.responses.create( input=input_text, model=model_id, - conversation=llama_stack_conv_id, # Existing conversation + conversation=ogx_conv_id, # Existing conversation # ... other parameters ) ``` @@ -166,9 +166,16 @@ Conversations are stored in **two databases**: - `openai_conversations`: Stores conversation metadata - `conversation_items`: Stores individual messages/turns in conversations -**Configuration (in `config/llama_stack_client_config.yaml`):** +**Configuration (in OGX `run.yaml` / library client config; see `examples/run.yaml`):** ```yaml +apis: +- conversations +# ... storage: + backends: + sql_default: + type: sql_sqlite + db_path: ${env.SQL_STORE_PATH:=~/.llama/storage/sql_store.db} stores: conversations: table_name: openai_conversations @@ -509,6 +516,6 @@ Calling `/v3/conversations/{conversation_id}` returns empty `chat_history`. ## References - [OpenAI Responses API Documentation](https://platform.openai.com/docs/api-reference/responses) -- [OGX Documentation](https://github.com/meta-llama/llama-stack) +- [OGX Documentation](https://github.com/ogx-ai/ogx) - [LCS Configuration Guide](./config.md) - [LCS Getting Started Guide](./getting_started.md) diff --git a/docs/devel_doc/core2llama-stack_interface.png b/docs/devel_doc/core2ogx_interface.png similarity index 100% rename from docs/devel_doc/core2llama-stack_interface.png rename to docs/devel_doc/core2ogx_interface.png diff --git a/docs/devel_doc/hexagonal_architecture.svg b/docs/devel_doc/hexagonal_architecture.svg new file mode 100644 index 000000000..eaadbd9c5 --- /dev/null +++ b/docs/devel_doc/hexagonal_architecture.svg @@ -0,0 +1,1344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Service + Domain + + + Port + + Port + + Port + + Port + + Port + + Port + + Adapter + + Adapter + + Port + + Adapter + + Adapter + + Adapter + + Adapter + + Adapter + + Request flow + + + REST API + Database + LLMs + Telemetry + Metrics + OKP MCP + BYOK + Agents + + Port + + Adapter + + Primary actors + Secondary actors + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Feedback + + + + + + + + + + Transcripts + Port + Adapter + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/devel_doc/openapi.json b/docs/devel_doc/openapi.json index dbd72110c..80a3c1e11 100644 --- a/docs/devel_doc/openapi.json +++ b/docs/devel_doc/openapi.json @@ -13,7 +13,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "0.7.0rc2" + "version": "0.7.0rc3" }, "servers": [ { @@ -3844,7 +3844,8 @@ } } } - } + }, + "deprecated": true }, "post": { "tags": [ @@ -4063,7 +4064,8 @@ } } } - } + }, + "deprecated": true } }, "/v1/vector-stores/{vector_store_id}": { @@ -4074,6 +4076,7 @@ "summary": "Get Vector Store", "description": "Retrieve a vector store by ID.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store to retrieve.\n auth: Authentication tuple from the auth dependency.\n\nReturns:\n VectorStoreResponse: The vector store object.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 404: Vector store not found\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX", "operationId": "get_vector_store_v1_vector_stores__vector_store_id__get", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -4294,6 +4297,7 @@ "summary": "Update Vector Store", "description": "Update a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store to update.\n auth: Authentication tuple from the auth dependency.\n body: Vector store update parameters.\n\nReturns:\n VectorStoreResponse: The updated vector store object.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 404: Vector store not found\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX", "operationId": "update_vector_store_v1_vector_stores__vector_store_id__put", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -4524,6 +4528,7 @@ "summary": "Delete Vector Store", "description": "Delete a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store to delete.\n auth: Authentication tuple from the auth dependency.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX\n\nReturns:\n VectorStoreDeleteResponse: Delete outcome for the requested vector store.", "operationId": "delete_vector_store_v1_vector_stores__vector_store_id__delete", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -4958,7 +4963,8 @@ } } } - } + }, + "deprecated": true } }, "/v1/vector-stores/{vector_store_id}/files": { @@ -4969,6 +4975,7 @@ "summary": "Add File To Vector Store", "description": "Add a file to a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store.\n auth: Authentication tuple from the auth dependency.\n body: File addition parameters.\n\nReturns:\n VectorStoreFileResponse: The vector store file object.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 404: Vector store or file not found\n - 429: Too many concurrent vector store file attachments\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX", "operationId": "add_file_to_vector_store_v1_vector_stores__vector_store_id__files_post", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -5214,6 +5221,7 @@ "summary": "List Vector Store Files", "description": "List files in a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store.\n auth: Authentication tuple from the auth dependency.\n\nReturns:\n VectorStoreFilesListResponse: List of files in the vector store.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 404: Vector store not found\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX", "operationId": "list_vector_store_files_v1_vector_stores__vector_store_id__files_get", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -5441,6 +5449,7 @@ "summary": "Get Vector Store File", "description": "Retrieve a file from a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store.\n file_id: ID of the file.\n auth: Authentication tuple from the auth dependency.\n\nReturns:\n VectorStoreFileResponse: The vector store file object.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 404: File not found in vector store\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX", "operationId": "get_vector_store_file_v1_vector_stores__vector_store_id__files__file_id__get", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -5685,6 +5694,7 @@ "summary": "Delete Vector Store File", "description": "Delete a file from a vector store.\n\nParameters:\n request: The incoming HTTP request.\n vector_store_id: ID of the vector store.\n file_id: ID of the file to delete.\n auth: Authentication tuple from the auth dependency.\n\nRaises:\n HTTPException:\n - 401: Authentication failed\n - 403: Authorization failed\n - 500: Lightspeed Stack configuration not loaded\n - 503: Unable to connect to OGX\n\nReturns:\n VectorStoreFileDeleteResponse: Delete outcome for the requested file.", "operationId": "delete_vector_store_file_v1_vector_stores__vector_store_id__files__file_id__delete", + "deprecated": true, "parameters": [ { "name": "vector_store_id", @@ -13295,11 +13305,15 @@ }, { "$ref": "#/components/schemas/RedactionShieldConfiguration" + }, + { + "$ref": "#/components/schemas/GraniteGuardianShieldConfiguration" } ], "discriminator": { "propertyName": "provider_id", "mapping": { + "granite_guardian": "#/components/schemas/GraniteGuardianShieldConfiguration", "question_validity": "#/components/schemas/QuestionValidityShieldConfiguration", "redaction": "#/components/schemas/RedactionShieldConfiguration" } @@ -14630,6 +14644,105 @@ } ] }, + "GraniteGuardianConfig": { + "properties": { + "url": { + "type": "string", + "title": "Base URL", + "description": "The model_id to use for the guard" + }, + "api_key": { + "anyOf": [ + { + "type": "string", + "format": "password", + "writeOnly": true + }, + { + "type": "null" + } + ], + "title": "Granite Guardian API key", + "description": "API key for the inference" + }, + "max_retries": { + "type": "integer", + "maximum": 5.0, + "minimum": 0.0, + "exclusiveMinimum": 0.0, + "title": "Max retries", + "description": "Maximun number of retries", + "default": 2 + }, + "timeout": { + "type": "integer", + "maximum": 300.0, + "minimum": 5.0, + "exclusiveMinimum": 0.0, + "title": "Timeout", + "description": "Request timeout in seconds", + "default": 30 + }, + "verify_ssl": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ], + "title": "Verify SSL", + "description": "SSL certificate verification. Can be:\n - True: Verify using system CA bundle (default, recommended)\n - False: Disable verification (insecure, for dev only)\n - str: Path to custom CA bundle file (for internal PKI)", + "default": true + }, + "risks": { + "items": { + "$ref": "#/components/schemas/RiskDefinition" + }, + "type": "array", + "title": "Defined risks", + "description": "Risks to be considered while applying this guardrail" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "url", + "risks" + ], + "title": "GraniteGuardianConfig", + "description": "Configuration for the Granite Guardian moderation guardrail." + }, + "GraniteGuardianShieldConfiguration": { + "properties": { + "name": { + "type": "string", + "title": "Shield name", + "description": "Unique, user-facing name identifying this shield instance." + }, + "provider_id": { + "type": "string", + "const": "granite_guardian", + "title": "Shield provider id", + "description": "Discriminator identifying this as a granite-guardian shield." + }, + "config": { + "$ref": "#/components/schemas/GraniteGuardianConfig", + "title": "Shield configuration", + "description": "Granite-guardian-specific configuration for this shield" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "provider_id", + "config" + ], + "title": "GraniteGuardianShieldConfiguration", + "description": "Configuration for a named Granite Guardian guardrail shield.\n\nAttributes:\n name: Unique, user-facing name identifying this shield instance.\n provider_id: Discriminator identifying this as a granite-guardian shield.\n config: Granite-guardian-specific configuration." + }, "HTTPAuthSecurityScheme": { "properties": { "bearerFormat": { @@ -16076,7 +16189,7 @@ "additionalProperties": false, "type": "object", "title": "OgxConfiguration", - "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/)" + "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications)" }, "OkpConfiguration": { "properties": { @@ -20777,6 +20890,69 @@ "title": "RetrievalStrategyConfiguration", "description": "Configuration for a single retrieval strategy (inline or tool)." }, + "RiskDefinition": { + "properties": { + "name": { + "type": "string", + "title": "Risk name", + "description": "Unique identifier for this risk (e.g., 'liability', 'competitor_mention')" + }, + "description": { + "type": "string", + "title": "Rist description", + "description": "Risk definition text passed to Granite Guardian as custom_criteria" + }, + "threshold": { + "type": "number", + "maximum": 1.0, + "minimum": 0.0, + "title": "Risk threshold", + "description": "Score threshold for flagging (lower = more sensitive)", + "default": 0.65 + }, + "enabled": { + "type": "boolean", + "title": "Risk enabled", + "description": "Whether to run this check", + "default": true + }, + "enable_thinking": { + "type": "boolean", + "title": "Risk enable thinking", + "description": "Internal field - set via ModerationConfig.thinking_enabled list, not directly. When True, Granite Guardian provides detailed reasoning before scoring.", + "default": false + }, + "points": { + "items": { + "type": "string", + "enum": [ + "input", + "output", + "tool" + ] + }, + "type": "array", + "minItems": 1, + "title": "Guardrail points", + "description": "Where this risk is evaluated: `input` (user message), `output` (model response), or `tool` (tool/MCP content)." + }, + "violation_message": { + "type": "string", + "title": "Violation message", + "description": "Message to be displayed when this risk is violated" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "name", + "description", + "points", + "violation_message" + ], + "title": "RiskDefinition", + "description": "Definition for a custom risk category.\n\nCustom risks allow applications to add use-case-specific safety checks\nbeyond the standard harm, jailbreak, leetspeak, amnesia, and\nhistory_politics checks.\nExample:\n liability_risk = RiskDefinition(\n name=\"liability\",\n description=\"Content requesting legal, medical, or financial advice\",\n threshold=0.55,\n points=[\"input\"],\n )\n pii_risk = RiskDefinition(\n name=\"pii_request\",\n description=\"User is asking the AI to reveal personal information\",\n threshold=0.50,\n points=[\"input\", \"tool\"],\n )\nNote:\n To enable think mode (detailed reasoning) for a risk, add the risk name\n to the `thinking_enabled` list in `ModerationConfig`. Do not set\n `enable_thinking` directly - it is managed internally." + }, "RlsapiV1Attachment": { "properties": { "contents": { @@ -23605,7 +23781,7 @@ }, { "name": "vector-stores", - "description": "Vector stores and files." + "description": "Vector stores and files (OGX proxy). Deprecated: will be removed in the next LCS release." } ] } \ No newline at end of file diff --git a/docs/devel_doc/openapi.md b/docs/devel_doc/openapi.md index 7ed7626ab..f1116d4db 100644 --- a/docs/devel_doc/openapi.md +++ b/docs/devel_doc/openapi.md @@ -260,7 +260,7 @@ Lightspeed Core Stack (LCS) service API specification. * [JwtConfiguration](#jwtconfiguration) * [JwtRoleRule](#jwtrolerule) * [LivenessResponse](#livenessresponse) - * [LlamaStackConfiguration](#llamastackconfiguration) + * [OgxConfiguration](#ogxconfiguration) * [MCPClientAuthOptionsResponse](#mcpclientauthoptionsresponse) * [MCPListToolsTool](#mcplisttoolstool) * [MCPServerAuthInfo](#mcpserverauthinfo) @@ -6098,7 +6098,7 @@ Global service configuration. |-------|------|-------------| | name | string | Name of the service. That value will be used in REST API endpoints. | | service | | This section contains Lightspeed Core Stack service configuration. | -| llama_stack | | This section contains OGX configuration. Lightspeed Core Stack service can call OGX in library mode or in server mode. | +| ogx | | This section contains OGX configuration. Lightspeed Core Stack service can call OGX in library mode or in server mode. | | user_data_collection | | This section contains configuration for subsystem that collects user data(transcription history and feedbacks). | | database | | Configuration for database to store conversation IDs and other runtime data | | mcp_servers | array | MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured in this section. Only MCP servers defined in the lightspeed-stack.yaml configuration are available to the agents. Tools configured in the OGX run.yaml are not accessible to lightspeed-core agents. | @@ -6728,7 +6728,7 @@ Attributes: | alive | boolean | Flag indicating that the app is alive | -## LlamaStackConfiguration +## OgxConfiguration OGX configuration. @@ -6742,7 +6742,7 @@ Useful resources: - [OGX](https://ogx-ai.github.io/) - [Python OGX client](https://github.com/ogx-ai/ogx-client-python) - - [Build AI Applications with OGX](https://ogx-ai.github.io/) + - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications) | Field | Type | Description | diff --git a/docs/devel_doc/persistent_storage.svg b/docs/devel_doc/persistent_storage.svg index 5332f3ad6..753c86a57 100644 --- a/docs/devel_doc/persistent_storage.svg +++ b/docs/devel_doc/persistent_storage.svg @@ -1,7 +1,7 @@ - + @@ -43,11 +43,11 @@
-
llama-stack
+
ogx
- llama-stack + ogx
@@ -285,11 +285,11 @@
-
Llama Stack
Configuration
+
OGX
Configuration
- Llama Stack... + OGX Configuration @@ -336,11 +336,11 @@
-
llama-stack
+
ogx
- llama-stack + ogx @@ -578,11 +578,11 @@
-
Llama Stack
Configuration
+
OGX
Configuration
- Llama Stack... + OGX Configuration diff --git a/docs/devel_doc/providers.md b/docs/devel_doc/providers.md index 8fae59996..9df629460 100644 --- a/docs/devel_doc/providers.md +++ b/docs/devel_doc/providers.md @@ -106,14 +106,14 @@ inference: model_validation: false # added automatically by Lightspeed enrichment ``` -**How it works:** OGX defers Azure authentication to inference time. Lightspeed acquires Entra ID tokens at runtime and passes them via the `X-LlamaStack-Provider-Data` header (`azure_api_key`, `azure_api_base`). +**How it works:** OGX defers Azure authentication to inference time. Lightspeed acquires Entra ID tokens at runtime and passes them via the `X-OGX-Provider-Data` header (`azure_api_key`, `azure_api_base`). #### Access Token Lifecycle and Management **Lightspeed startup (library and service mode):** 1. Lightspeed reads your Entra ID configuration 2. Does not acquire or cache access tokens at startup—authentication is deferred until request time -3. Initializes the OGX client without Azure credentials; credentials are supplied later via `X-LlamaStack-Provider-Data` when an Azure model is used +3. Initializes the OGX client without Azure credentials; credentials are supplied later via `X-OGX-Provider-Data` when an Azure model is used **OGX service startup (container mode):** 1. Config enrichment sets `model_validation: false` on the Azure provider @@ -123,12 +123,12 @@ inference: **During inference requests:** 1. Before each request, Lightspeed checks if the token has expired 2. If expired, a new token is automatically acquired and cached in memory -3. The token is passed via `X-LlamaStack-Provider-Data` (library and service mode) +3. The token is passed via `X-OGX-Provider-Data` (library and service mode) **Token security:** - Access tokens are wrapped in `SecretStr` to prevent accidental logging - Tokens are cached in `AzureEntraIDManager` singleton class -- Inference uses `X-LlamaStack-Provider-Data` headers +- Inference uses `X-OGX-Provider-Data` headers - Each Uvicorn worker maintains its own token lifecycle independently **Token validity:** @@ -157,15 +157,15 @@ make run CONFIG=examples/lightspeed-stack-azure-entraid-lib.yaml ```bash # Terminal 1: Start OGX service with Azure Entra ID config -make run-llama-stack CONFIG=examples/lightspeed-stack-azure-entraid-service.yaml LLAMA_STACK_CONFIG=examples/azure-run.yaml +make run-ogx-local CONFIG=examples/lightspeed-stack-azure-entraid-service.yaml OGX_CONFIG=examples/azure-run.yaml # Terminal 2: Start Lightspeed (after OGX is ready) make run CONFIG=examples/lightspeed-stack-azure-entraid-service.yaml ``` -**Note:** The `make run-llama-stack` command accepts two variables: +**Note:** The `make run-ogx-local` command accepts two variables: - `CONFIG` - Lightspeed configuration file (default: `lightspeed-stack.yaml`) -- `LLAMA_STACK_CONFIG` - OGX configuration file to enrich and run (default: `run.yaml`) +- `OGX_CONFIG` - OGX configuration file to enrich and run (default: `run.yaml`) --- @@ -288,7 +288,7 @@ Shields are owned by LCORE (configured under `shields:` block), not as OGX `prov Run the following command to find out required dependencies for the desired provider (or check the tables above): ```bash - uv run llama stack list-providers + uv run ogx stack list-providers ``` Edit your `pyproject.toml` and add the required pip packages for the provider into `ogxlibdev` section: ```toml @@ -370,7 +370,7 @@ Shields are owned by LCORE (configured under `shields:` block), not as OGX `prov If you are running OGX as a standalone service, restart it with: ```bash - uv run llama stack run run.yaml + uv run ogx stack run run.yaml ``` If you are running it within Lightspeed Core, use: ```bash diff --git a/docs/devel_doc/query_endpoint.svg b/docs/devel_doc/query_endpoint.svg index a025f12d4..d96a4b102 100644 --- a/docs/devel_doc/query_endpoint.svg +++ b/docs/devel_doc/query_endpoint.svg @@ -23,10 +23,10 @@ Auth Auth - - Llama Stack Client - - Llama Stack Client + + OGX Client + + OGX Client Cache diff --git a/docs/devel_doc/streaming_query_endpoint.svg b/docs/devel_doc/streaming_query_endpoint.svg index ed35f7439..054c0ca4c 100644 --- a/docs/devel_doc/streaming_query_endpoint.svg +++ b/docs/devel_doc/streaming_query_endpoint.svg @@ -22,10 +22,10 @@ Auth Auth - - Llama Stack Client - - Llama Stack Client + + OGX Client + + OGX Client Stream build event @@ -65,7 +65,7 @@ loop - [For each chunk from LlamaStack] + [For each chunk from OGX] diff --git a/docs/maintenance/versions_supported.md b/docs/maintenance/versions_supported.md index 7dc1c69f9..e73000716 100644 --- a/docs/maintenance/versions_supported.md +++ b/docs/maintenance/versions_supported.md @@ -10,4 +10,5 @@ Dates shown in italic are scheduled and can be adjusted. | 0.6.0 | 2026-06-01 | Current | 2026-07-01 | 2026-11-30 | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | | 0.7.0rc1 | 2026-08-01 | Current | 2026-08-07 | to be decided | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | | 0.7.0rc2 | 2026-09-01 | Current | 2026-09-03 | to be decided | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | +| 0.7.0rc3 | 2026-09-01 | Current | 2026-09-16 | to be decided | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | | 0.7.0 | 2026-07-01 | Future | | 2027-01-31 | Pavel Tišnovský, Erin Bournival, Štefan Bunčiak | diff --git a/docs/migrations/v0.7.0.md b/docs/migrations/v0.7.0.md index 3b7e7807d..59e5b4f04 100644 --- a/docs/migrations/v0.7.0.md +++ b/docs/migrations/v0.7.0.md @@ -7,14 +7,16 @@ * [RAG Configuration](#rag-configuration) * [OGX naming (`llama_stack` → `ogx`)](#ogx-naming-llama_stack--ogx) +* [Vector stores API deprecation](#vector-stores-api-deprecation) --- ## OGX naming (`llama_stack` → `ogx`) In v0.7.0, Lightspeed Core Stack adopts `ogx` as the canonical name for the -OGX configuration section and related API fields. The old `llama_stack` names -remain available as **deprecated aliases** with a startup warning. +OGX configuration section in `lightspeed-stack.yaml`. The deprecated +`llama_stack:` top-level YAML key remains accepted with a startup warning. +API response fields were renamed without backward-compatible aliases. ### Configuration YAML @@ -54,10 +56,14 @@ Migrate configs to `ogx:` when convenient. ### `/info` API +The `/info` response field was renamed; the old name is not returned: + | v0.6.x | v0.7.0 | |---|---| | `llama_stack_version` | `ogx_version` | +Update clients that read `/info` to use `ogx_version`. + --- ## RAG Configuration @@ -180,3 +186,14 @@ Chunk limits, currently hardcoded as constants, will be configurable fields in ` | `OKP_RAG_MAX_CHUNKS` | `rag.okp.max_chunks` | 5 | | `INLINE_RAG_MAX_CHUNKS` | `rag.retrieval.inline.max_chunks` | 10 | | `TOOL_RAG_MAX_CHUNKS` | `rag.retrieval.tool.max_chunks` | 10 | + +--- + +## Vector stores API deprecation + +The `/v1/vector-stores` and `/v1/files` routes proxy OGX vector-store and file +APIs. They are **deprecated in v0.7.0** and will be **removed in v0.8.0** when +OGX is dropped from Lightspeed Core Stack. + +Migrate to [BYOK RAG](../user_doc/byok_guide.md) for document retrieval. The +OpenAPI schema marks these operations as `deprecated`. diff --git a/docs/models/common.json b/docs/models/common.json index e05889c53..0bbad2b17 100644 --- a/docs/models/common.json +++ b/docs/models/common.json @@ -1326,7 +1326,7 @@ "type": "object" }, "OpenAIResponseOutputMessageWebSearchToolCall": { - "description": "Web search tool call output message for OpenAI responses.\n\n:param id: Unique identifier for this tool call\n:param status: Current status of the web search operation\n:param type: Tool call type identifier, always \"web_search_call\"", + "description": "Web search tool call output message for OpenAI responses.", "properties": { "id": { "title": "Id", @@ -1341,6 +1341,24 @@ "default": "web_search_call", "title": "Type", "type": "string" + }, + "action": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchActionSearch" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionOpenPage" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionFind" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Action" } }, "required": [ @@ -2020,6 +2038,102 @@ }, "title": "TurnSummary", "type": "object" + }, + "WebSearchActionFind": { + "description": "Web search action: searches for a pattern within a loaded page.", + "properties": { + "type": { + "const": "find_in_page", + "default": "find_in_page", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + }, + "pattern": { + "title": "Pattern", + "type": "string" + } + }, + "required": [ + "url", + "pattern" + ], + "title": "WebSearchActionFind", + "type": "object" + }, + "WebSearchActionOpenPage": { + "description": "Web search action: opens a specific URL from search results.", + "properties": { + "type": { + "const": "open_page", + "default": "open_page", + "title": "Type", + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "default": null, + "title": "Url" + } + }, + "title": "WebSearchActionOpenPage", + "type": "object" + }, + "WebSearchActionSearch": { + "description": "Web search action: performs a search query.", + "properties": { + "type": { + "const": "search", + "default": "search", + "title": "Type", + "type": "string" + }, + "query": { + "title": "Query", + "type": "string" + }, + "queries": { + "type": "array", + "nullable": true, + "default": null, + "title": "Queries" + }, + "sources": { + "type": "array", + "nullable": true, + "default": null, + "title": "Sources" + } + }, + "required": [ + "query" + ], + "title": "WebSearchActionSearch", + "type": "object" + }, + "WebSearchSource": { + "description": "A source URL returned by a web search action.", + "properties": { + "type": { + "const": "url", + "default": "url", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "WebSearchSource", + "type": "object" } } }, diff --git a/docs/models/common.md b/docs/models/common.md index 7a282a985..49bf3113c 100644 --- a/docs/models/common.md +++ b/docs/models/common.md @@ -623,16 +623,13 @@ A summary of reasoning output from the model. Web search tool call output message for OpenAI responses. -:param id: Unique identifier for this tool call -:param status: Current status of the web search operation -:param type: Tool call type identifier, always "web_search_call" - | Field | Type | Description | |-------|------|-------------| | id | string | | | status | string | | | type | string | | +| action | | | ## OpenAITokenLogProb @@ -906,3 +903,54 @@ Summary of a turn in OGX. | output_items | array | Structured response output items, captured for compacted-mode turn persistence (LCORE-1572). Empty on the non-compacted path. | | partial_tokens | array | Accumulated text deltas during streaming, used to reconstruct partial content on interruption. | | next_chunk_id | integer | Next monotonic SSE chunk index, kept in sync with the inner generator so the interrupt handler can emit a sequentially valid id. | + + +## WebSearchActionFind + + +Web search action: searches for a pattern within a loaded page. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | +| pattern | string | | + + +## WebSearchActionOpenPage + + +Web search action: opens a specific URL from search results. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | + + +## WebSearchActionSearch + + +Web search action: performs a search query. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| query | string | | +| queries | array | | +| sources | array | | + + +## WebSearchSource + + +A source URL returned by a web search action. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | diff --git a/docs/models/common.puml b/docs/models/common.puml index 39aaeccb8..2c424e48d 100644 --- a/docs/models/common.puml +++ b/docs/models/common.puml @@ -10,31 +10,31 @@ class "AgentTurnAccumulator" as src.models.common.agents.turn_accumulator.AgentT seen_docs : set[tuple[str, str]] text_parts : list[str] tool_round : int - turn_summary : TurnSummary + turn_summary vector_store_ids : Final[list[str]] increment_round_if_pending() -> None } class "Attachment" as src.models.common.query.Attachment { - attachment_type : Optional[str] - content : Optional[str] - content_type : Optional[str] + attachment_type : str + content : str + content_type : str model_config : dict validate_image_attachment() -> Self } class "CatalogModel" as src.models.common.models.CatalogModel { - api_model_type : Optional[str] - identifier : Optional[str] - metadata : Optional[dict[str, Any]] - model_type : Optional[str] - provider_id : Optional[str] - provider_resource_id : Optional[str] - type : Optional[str] + api_model_type : str + identifier : str + metadata : dict[str, Any] + model_type : str + provider_id : str + provider_resource_id : str + type : str } class "CatalogShield" as src.models.common.shields.CatalogShield { - config : Optional[dict[str, Any]] - name : Optional[str] - provider_id : Optional[Literal['question_validity', 'redaction']] - type : Optional[Literal['shield']] + config : dict[str, Any] + name : str + provider_id : Literal['question_validity', 'redaction'] + type : Literal['shield'] } class "CatalogTool" as src.models.common.tools.CatalogTool { description : str @@ -58,7 +58,7 @@ class "ConversationData" as src.models.common.conversation.ConversationData { topic_summary : Optional[str] } class "ConversationDetails" as src.models.common.conversation.ConversationDetails { - conversation_id : Optional[str] + conversation_id : str created_at : Optional[str] last_message_at : Optional[str] last_used_model : Optional[str] @@ -67,16 +67,16 @@ class "ConversationDetails" as src.models.common.conversation.ConversationDetail topic_summary : Optional[str] } class "ConversationTurn" as src.models.common.conversation.ConversationTurn { - completed_at : Optional[str] - messages : Optional[list[Message]] - model : Optional[str] - provider : Optional[str] - started_at : Optional[str] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] + completed_at : str + messages : list[Message] + model : str + provider : str + started_at : str + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] } class "EndEventData" as src.models.common.agents.stream_payloads.EndEventData { - context_status : Literal + context_status input_tokens : int output_tokens : int referenced_documents : list[ReferencedDocument] @@ -124,39 +124,39 @@ class "ListedMcpTool" as src.models.common.tools.ListedMcpTool { name : str } class "MCPListToolsSummary" as src.models.common.turn_summary.MCPListToolsSummary { - server_label : Optional[str] - tools : Optional[list[ToolInfoSummary]] + server_label : str + tools : list[ToolInfoSummary] } class "MCPServerAuthInfo" as src.models.common.mcp.MCPServerAuthInfo { - client_auth_headers : Optional[list[str]] - name : Optional[str] + client_auth_headers : list[str] + name : str } class "MCPServerInfo" as src.models.common.mcp.MCPServerInfo { - name : Optional[str] - provider_id : Optional[str] - source : Optional[str] - url : Optional[str] + name : str + provider_id : str + source : str + url : str } class "Message" as src.models.common.conversation.Message { - content : Optional[str] + content : str referenced_documents : Optional[list[ReferencedDocument]] - type : Optional[Literal['user', 'assistant', 'system', 'developer']] + type : Literal['user', 'assistant', 'system', 'developer'] } class "ProviderHealthStatus" as src.models.common.health.ProviderHealthStatus { message : Optional[str] - provider_id : Optional[str] - status : Optional[str] + provider_id : str + status : str } class "RAGChunk" as src.models.common.turn_summary.RAGChunk { attributes : Optional[dict[str, Any]] - content : Optional[str] + content : str score : Optional[float] source : Optional[str] } class "RAGContext" as src.models.common.turn_summary.RAGContext { - context_text : Optional[str] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] + context_text : str + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] } class "ReferencedDocument" as src.models.common.turn_summary.ReferencedDocument { doc_title : Optional[str] @@ -165,12 +165,12 @@ class "ReferencedDocument" as src.models.common.turn_summary.ReferencedDocument source : Optional[str] } class "ResponseGeneratorContext" as src.models.common.responses.contexts.ResponseGeneratorContext { - client : AsyncOgxClient + client conversation_id : str - inline_rag_context : RAGContext + inline_rag_context model_id : str moderation_result - query_request : QueryRequest + query_request rag_id_mapping : dict[str, str] request_id : str skip_userid_check : bool @@ -179,24 +179,24 @@ class "ResponseGeneratorContext" as src.models.common.responses.contexts.Respons vector_store_ids : list[str] } class "ResponsesApiParams" as src.models.common.responses.responses_api_params.ResponsesApiParams { - conversation : Optional[str] + conversation : str extra_headers : Optional[dict[str, str]] include : Optional[list[IncludeParameter]] - input : Optional[ResponseInput] + input instructions : Optional[str] max_infer_iters : Optional[int] max_output_tokens : Optional[int] max_tool_calls : Optional[int] metadata : Optional[dict[str, str]] - model : Optional[str] - omit_conversation : Optional[bool] + model : str + omit_conversation : bool parallel_tool_calls : Optional[bool] previous_response_id : Optional[str] prompt : Optional[Prompt] reasoning : Optional[Reasoning] safety_identifier : Optional[str] - store : Optional[bool] - stream : Optional[bool] + store : bool + stream : bool temperature : Optional[float] text : Optional[Text] tool_choice : Optional[ToolChoice] @@ -205,45 +205,45 @@ class "ResponsesApiParams" as src.models.common.responses.responses_api_params.R model_dump() -> dict[str, Any] } class "ResponsesContext" as src.models.common.responses.contexts.ResponsesContext { - auth : Optional[tuple[str, str, bool, str]] + auth : tuple[str, str, bool, str] background_tasks : Optional[BackgroundTasks] - client : Optional[AsyncOgxClient] + client compacted_original_input : Optional[ResponseInput] - endpoint_path : Optional[str] - filter_server_tools : Optional[bool] - generate_topic_summary : Optional[bool] - inline_rag_context : Optional[RAGContext] - input_text : Optional[str] - model_config : ConfigDict - moderation_result : Optional[ShieldModerationResult] - rh_identity_context : Optional[tuple[str, str]] - root_span : Span - started_at : Optional[datetime] + endpoint_path : str + filter_server_tools : bool + generate_topic_summary : bool + inline_rag_context + input_text : str + model_config + moderation_result + rh_identity_context : tuple[str, str] + root_span + started_at : datetime user_agent : Optional[str] } class "ResponsesConversationContext" as src.models.common.responses.responses_conversation_context.ResponsesConversationContext { - conversation : Optional[str] - generate_topic_summary : Optional[bool] - model_config : ConfigDict + conversation : str + generate_topic_summary : bool + model_config user_conversation : Optional[UserConversation] } class "ShieldModerationBlocked" as src.models.common.moderation.ShieldModerationBlocked { decision : Literal['blocked'] message : str moderation_id : str - refusal_response : ResponseMessage + refusal_response } class "ShieldModerationPassed" as src.models.common.moderation.ShieldModerationPassed { decision : Literal['passed'] } class "SkillMetadata" as src.models.common.skills.SkillMetadata { - description : Optional[str] - name : Optional[str] + description : str + name : str } class "SolrVectorSearchRequest" as src.models.common.query.SolrVectorSearchRequest { filters : Optional[dict[str, Any]] mode : Optional[Literal['semantic', 'hybrid', 'lexical', 'keyword']] - model_config : ConfigDict + model_config coerce_legacy_plain_dict(data: Any) -> Any } class "StartEventData" as src.models.common.agents.stream_payloads.StartEventData { @@ -256,7 +256,7 @@ class "StartStreamPayload" as src.models.common.agents.stream_payloads.StartStre create() -> Self } class "StreamPayloadBase" as src.models.common.agents.stream_payloads.StreamPayloadBase { - model_config : ConfigDict + model_config serialize_json() -> str serialize_text() -> str } @@ -271,42 +271,42 @@ class "TokenStreamPayload" as src.models.common.agents.stream_payloads.TokenStre serialize_text() -> str } class "ToolCallStreamPayload" as src.models.common.agents.stream_payloads.ToolCallStreamPayload { - data : ToolCallSummary + data event : Literal['tool_call'] serialize_text() -> str } class "ToolCallSummary" as src.models.common.turn_summary.ToolCallSummary { - args : Optional[dict[str, Any]] - id : Optional[str] - name : Optional[str] - type : Optional[str] + args : dict[str, Any] + id : str + name : str + type : str } class "ToolInfoSummary" as src.models.common.turn_summary.ToolInfoSummary { description : Optional[str] input_schema : Optional[dict[str, Any]] - name : Optional[str] + name : str } class "ToolResultStreamPayload" as src.models.common.agents.stream_payloads.ToolResultStreamPayload { - data : ToolResultSummary + data event : Literal['tool_result'] serialize_text() -> str } class "ToolResultSummary" as src.models.common.turn_summary.ToolResultSummary { - content : Optional[str] - id : Optional[str] - round : Optional[int] - status : Optional[str] - type : Optional[str] + content : str + id : str + round : int + status : str + type : str } class "Transcript" as src.models.common.transcripts.Transcript { - attachments : Optional[list[dict[str, Any]]] + attachments : list[dict[str, Any]] llm_response : str metadata query_is_valid : bool - rag_chunks : Optional[list[dict[str, Any]]] + rag_chunks : list[dict[str, Any]] redacted_query : str - tool_calls : Optional[list[dict[str, Any]]] - tool_results : Optional[list[dict[str, Any]]] + tool_calls : list[dict[str, Any]] + tool_results : list[dict[str, Any]] truncated : bool } class "TranscriptMetadata" as src.models.common.transcripts.TranscriptMetadata { @@ -324,16 +324,16 @@ class "TurnCompleteStreamPayload" as src.models.common.agents.stream_payloads.Tu create() -> Self } class "TurnSummary" as src.models.common.turn_summary.TurnSummary { - id : Optional[str] + id : str llm_response : str - next_chunk_id : Optional[int] - output_items : Optional[list[OpenAIResponseOutput]] - partial_tokens : Optional[list[str]] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - token_usage : Optional[TokenCounter] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] + next_chunk_id : int + output_items : list[OpenAIResponseOutput] + partial_tokens : list[str] + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] + token_usage + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] } src.models.common.agents.stream_payloads.EndStreamPayload --|> src.models.common.agents.stream_payloads.StreamPayloadBase src.models.common.agents.stream_payloads.ErrorStreamPayload --|> src.models.common.agents.stream_payloads.StreamPayloadBase @@ -343,11 +343,11 @@ src.models.common.agents.stream_payloads.TokenStreamPayload --|> src.models.comm src.models.common.agents.stream_payloads.ToolCallStreamPayload --|> src.models.common.agents.stream_payloads.StreamPayloadBase src.models.common.agents.stream_payloads.ToolResultStreamPayload --|> src.models.common.agents.stream_payloads.StreamPayloadBase src.models.common.agents.stream_payloads.TurnCompleteStreamPayload --|> src.models.common.agents.stream_payloads.StreamPayloadBase -src.models.common.agents.stream_payloads.EndStreamPayload --> src.models.common.agents.stream_payloads.EndEventData : data -src.models.common.agents.stream_payloads.ErrorStreamPayload --> src.models.common.agents.stream_payloads.ErrorEventData : data -src.models.common.agents.stream_payloads.InterruptedStreamPayload --> src.models.common.agents.stream_payloads.InterruptedEventData : data -src.models.common.agents.stream_payloads.StartStreamPayload --> src.models.common.agents.stream_payloads.StartEventData : data -src.models.common.agents.stream_payloads.TokenStreamPayload --> src.models.common.agents.stream_payloads.TokenChunkData : data -src.models.common.agents.stream_payloads.TurnCompleteStreamPayload --> src.models.common.agents.stream_payloads.TokenChunkData : data -src.models.common.transcripts.Transcript --> src.models.common.transcripts.TranscriptMetadata : metadata +src.models.common.agents.stream_payloads.EndEventData --* src.models.common.agents.stream_payloads.EndStreamPayload : data +src.models.common.agents.stream_payloads.ErrorEventData --* src.models.common.agents.stream_payloads.ErrorStreamPayload : data +src.models.common.agents.stream_payloads.InterruptedEventData --* src.models.common.agents.stream_payloads.InterruptedStreamPayload : data +src.models.common.agents.stream_payloads.StartEventData --* src.models.common.agents.stream_payloads.StartStreamPayload : data +src.models.common.agents.stream_payloads.TokenChunkData --* src.models.common.agents.stream_payloads.TokenStreamPayload : data +src.models.common.agents.stream_payloads.TokenChunkData --* src.models.common.agents.stream_payloads.TurnCompleteStreamPayload : data +src.models.common.transcripts.TranscriptMetadata --* src.models.common.transcripts.Transcript : metadata @enduml diff --git a/docs/models/common.svg b/docs/models/common.svg index b4b5e2587..823c08fd1 100644 --- a/docs/models/common.svg +++ b/docs/models/common.svg @@ -1,5 +1,5 @@ - + @@ -18,750 +18,750 @@ seen_docs : set[tuple[str, str]] text_parts : list[str] tool_round : int - turn_summary : TurnSummary + turn_summary vector_store_ids : Final[list[str]] increment_round_if_pending() -> None - - - - Attachment - - attachment_type : Optional[str] - content : Optional[str] - content_type : Optional[str] - model_config : dict - - validate_image_attachment() -> Self + + + + Attachment + + attachment_type : str + content : str + content_type : str + model_config : dict + + validate_image_attachment() -> Self - - - - CatalogModel - - api_model_type : Optional[str] - identifier : Optional[str] - metadata : Optional[dict[str, Any]] - model_type : Optional[str] - provider_id : Optional[str] - provider_resource_id : Optional[str] - type : Optional[str] - + + + + CatalogModel + + api_model_type : str + identifier : str + metadata : dict[str, Any] + model_type : str + provider_id : str + provider_resource_id : str + type : str + - - - - CatalogShield - - config : Optional[dict[str, Any]] - name : Optional[str] - provider_id : Optional[Literal['question_validity', 'redaction']] - type : Optional[Literal['shield']] - + + + + CatalogShield + + config : dict[str, Any] + name : str + provider_id : Literal['question_validity', 'redaction'] + type : Literal['shield'] + - - - - CatalogTool - - description : str - identifier : str - parameters : list[CatalogToolParameter] - provider_id : str - server_source : str - toolgroup_id : str - type : str - + + + + CatalogTool + + description : str + identifier : str + parameters : list[CatalogToolParameter] + provider_id : str + server_source : str + toolgroup_id : str + type : str + - - - - CatalogToolParameter - - default : Optional[Any] - description : str - name : str - parameter_type : str - required : bool - + + + + CatalogToolParameter + + default : Optional[Any] + description : str + name : str + parameter_type : str + required : bool + - - - - ConversationData - - conversation_id : str - last_message_timestamp : float - topic_summary : Optional[str] - + + + + ConversationData + + conversation_id : str + last_message_timestamp : float + topic_summary : Optional[str] + - - - - ConversationDetails - - conversation_id : Optional[str] - created_at : Optional[str] - last_message_at : Optional[str] - last_used_model : Optional[str] - last_used_provider : Optional[str] - message_count : Optional[int] - topic_summary : Optional[str] - + + + + ConversationDetails + + conversation_id : str + created_at : Optional[str] + last_message_at : Optional[str] + last_used_model : Optional[str] + last_used_provider : Optional[str] + message_count : Optional[int] + topic_summary : Optional[str] + - - - - ConversationTurn - - completed_at : Optional[str] - messages : Optional[list[Message]] - model : Optional[str] - provider : Optional[str] - started_at : Optional[str] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] - + + + + ConversationTurn + + completed_at : str + messages : list[Message] + model : str + provider : str + started_at : str + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] + - - - - EndEventData - - context_status : Literal - input_tokens : int - output_tokens : int - referenced_documents : list[ReferencedDocument] - truncated : Optional[bool] - + + + + EndEventData + + context_status + input_tokens : int + output_tokens : int + referenced_documents : list[ReferencedDocument] + truncated : Optional[bool] + - - - - EndStreamPayload - - available_quotas : dict[str, int] - data - event : Literal['end'] - - create() -> Self - serialize_text() -> str + + + + EndStreamPayload + + available_quotas : dict[str, int] + data + event : Literal['end'] + + create() -> Self + serialize_text() -> str - - - - ErrorEventData - - cause : str - response : str - status_code : int - + + + + ErrorEventData + + cause : str + response : str + status_code : int + - - - - ErrorStreamPayload - - data - event : Literal['error'] - - create() -> Self - from_error_response(error_response: AbstractErrorResponse) -> Self - serialize_text() -> str + + + + ErrorStreamPayload + + data + event : Literal['error'] + + create() -> Self + from_error_response(error_response: AbstractErrorResponse) -> Self + serialize_text() -> str - - - - FeedbackCategory - - name - + + + + FeedbackCategory + + name + - - - - HealthStatus - - name - + + + + HealthStatus + + name + - - - - InputToolMCP - - authorization : Optional[str] - + + + + InputToolMCP + + authorization : Optional[str] + - - - - InterruptedEventData - - request_id : str - + + + + InterruptedEventData + + request_id : str + - - - - InterruptedStreamPayload - - data - event : Literal['interrupted'] - - create() -> Self + + + + InterruptedStreamPayload + + data + event : Literal['interrupted'] + + create() -> Self - - - - ListedMcpTool - - description : Optional[str] - input_schema : Optional[dict[str, Any]] - name : str - + + + + ListedMcpTool + + description : Optional[str] + input_schema : Optional[dict[str, Any]] + name : str + - - - - MCPListToolsSummary - - server_label : Optional[str] - tools : Optional[list[ToolInfoSummary]] - + + + + MCPListToolsSummary + + server_label : str + tools : list[ToolInfoSummary] + - - - - MCPServerAuthInfo - - client_auth_headers : Optional[list[str]] - name : Optional[str] - + + + + MCPServerAuthInfo + + client_auth_headers : list[str] + name : str + - - - - MCPServerInfo - - name : Optional[str] - provider_id : Optional[str] - source : Optional[str] - url : Optional[str] - + + + + MCPServerInfo + + name : str + provider_id : str + source : str + url : str + - - - - Message - - content : Optional[str] - referenced_documents : Optional[list[ReferencedDocument]] - type : Optional[Literal['user', 'assistant', 'system', 'developer']] - + + + + Message + + content : str + referenced_documents : Optional[list[ReferencedDocument]] + type : Literal['user', 'assistant', 'system', 'developer'] + - - - - ProviderHealthStatus - - message : Optional[str] - provider_id : Optional[str] - status : Optional[str] - + + + + ProviderHealthStatus + + message : Optional[str] + provider_id : str + status : str + - - - - RAGChunk - - attributes : Optional[dict[str, Any]] - content : Optional[str] - score : Optional[float] - source : Optional[str] - + + + + RAGChunk + + attributes : Optional[dict[str, Any]] + content : str + score : Optional[float] + source : Optional[str] + - - - - RAGContext - - context_text : Optional[str] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - + + + + RAGContext + + context_text : str + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] + - - - - ReferencedDocument - - doc_title : Optional[str] - doc_url : Optional[AnyUrl] - document_id : Optional[str] - source : Optional[str] - + + + + ReferencedDocument + + doc_title : Optional[str] + doc_url : Optional[AnyUrl] + document_id : Optional[str] + source : Optional[str] + - - - - ResponseGeneratorContext - - client : AsyncOgxClient - conversation_id : str - inline_rag_context : RAGContext - model_id : str - moderation_result - query_request : QueryRequest - rag_id_mapping : dict[str, str] - request_id : str - skip_userid_check : bool - started_at : str - user_id : str - vector_store_ids : list[str] - + + + + ResponseGeneratorContext + + client + conversation_id : str + inline_rag_context + model_id : str + moderation_result + query_request + rag_id_mapping : dict[str, str] + request_id : str + skip_userid_check : bool + started_at : str + user_id : str + vector_store_ids : list[str] + - - - - ResponsesApiParams - - conversation : Optional[str] - extra_headers : Optional[dict[str, str]] - include : Optional[list[IncludeParameter]] - input : Optional[ResponseInput] - instructions : Optional[str] - max_infer_iters : Optional[int] - max_output_tokens : Optional[int] - max_tool_calls : Optional[int] - metadata : Optional[dict[str, str]] - model : Optional[str] - omit_conversation : Optional[bool] - parallel_tool_calls : Optional[bool] - previous_response_id : Optional[str] - prompt : Optional[Prompt] - reasoning : Optional[Reasoning] - safety_identifier : Optional[str] - store : Optional[bool] - stream : Optional[bool] - temperature : Optional[float] - text : Optional[Text] - tool_choice : Optional[ToolChoice] - tools : Optional[list[InputTool]] - - echoed_params(rag_id_mapping: Mapping[str, str]) -> dict[str, Any] - model_dump() -> dict[str, Any] + + + + ResponsesApiParams + + conversation : str + extra_headers : Optional[dict[str, str]] + include : Optional[list[IncludeParameter]] + input + instructions : Optional[str] + max_infer_iters : Optional[int] + max_output_tokens : Optional[int] + max_tool_calls : Optional[int] + metadata : Optional[dict[str, str]] + model : str + omit_conversation : bool + parallel_tool_calls : Optional[bool] + previous_response_id : Optional[str] + prompt : Optional[Prompt] + reasoning : Optional[Reasoning] + safety_identifier : Optional[str] + store : bool + stream : bool + temperature : Optional[float] + text : Optional[Text] + tool_choice : Optional[ToolChoice] + tools : Optional[list[InputTool]] + + echoed_params(rag_id_mapping: Mapping[str, str]) -> dict[str, Any] + model_dump() -> dict[str, Any] - - - - ResponsesContext - - auth : Optional[tuple[str, str, bool, str]] - background_tasks : Optional[BackgroundTasks] - client : Optional[AsyncOgxClient] - compacted_original_input : Optional[ResponseInput] - endpoint_path : Optional[str] - filter_server_tools : Optional[bool] - generate_topic_summary : Optional[bool] - inline_rag_context : Optional[RAGContext] - input_text : Optional[str] - model_config : ConfigDict - moderation_result : Optional[ShieldModerationResult] - rh_identity_context : Optional[tuple[str, str]] - root_span : Span - started_at : Optional[datetime] - user_agent : Optional[str] - + + + + ResponsesContext + + auth : tuple[str, str, bool, str] + background_tasks : Optional[BackgroundTasks] + client + compacted_original_input : Optional[ResponseInput] + endpoint_path : str + filter_server_tools : bool + generate_topic_summary : bool + inline_rag_context + input_text : str + model_config + moderation_result + rh_identity_context : tuple[str, str] + root_span + started_at : datetime + user_agent : Optional[str] + - - - - ResponsesConversationContext - - conversation : Optional[str] - generate_topic_summary : Optional[bool] - model_config : ConfigDict - user_conversation : Optional[UserConversation] - + + + + ResponsesConversationContext + + conversation : str + generate_topic_summary : bool + model_config + user_conversation : Optional[UserConversation] + - - - - ShieldModerationBlocked - - decision : Literal['blocked'] - message : str - moderation_id : str - refusal_response : ResponseMessage - + + + + ShieldModerationBlocked + + decision : Literal['blocked'] + message : str + moderation_id : str + refusal_response + - - - - ShieldModerationPassed - - decision : Literal['passed'] - + + + + ShieldModerationPassed + + decision : Literal['passed'] + - - - - SkillMetadata - - description : Optional[str] - name : Optional[str] - + + + + SkillMetadata + + description : str + name : str + - - - - SolrVectorSearchRequest - - filters : Optional[dict[str, Any]] - mode : Optional[Literal['semantic', 'hybrid', 'lexical', 'keyword']] - model_config : ConfigDict - - coerce_legacy_plain_dict(data: Any) -> Any + + + + SolrVectorSearchRequest + + filters : Optional[dict[str, Any]] + mode : Optional[Literal['semantic', 'hybrid', 'lexical', 'keyword']] + model_config + + coerce_legacy_plain_dict(data: Any) -> Any - - - - StartEventData - - conversation_id : str - request_id : str - + + + + StartEventData + + conversation_id : str + request_id : str + - - - - StartStreamPayload - - data - event : Literal['start'] - - create() -> Self + + + + StartStreamPayload + + data + event : Literal['start'] + + create() -> Self - - - - StreamPayloadBase - - model_config : ConfigDict - - serialize_json() -> str - serialize_text() -> str + + + + StreamPayloadBase + + model_config + + serialize_json() -> str + serialize_text() -> str - - - - TokenChunkData - - id : int - token : str - + + + + TokenChunkData + + id : int + token : str + - - - - TokenStreamPayload - - data - event : Literal['token'] - - create() -> Self - serialize_text() -> str + + + + TokenStreamPayload + + data + event : Literal['token'] + + create() -> Self + serialize_text() -> str - - - - ToolCallStreamPayload - - data : ToolCallSummary - event : Literal['tool_call'] - - serialize_text() -> str + + + + ToolCallStreamPayload + + data + event : Literal['tool_call'] + + serialize_text() -> str - - - - ToolCallSummary - - args : Optional[dict[str, Any]] - id : Optional[str] - name : Optional[str] - type : Optional[str] - + + + + ToolCallSummary + + args : dict[str, Any] + id : str + name : str + type : str + - - - - ToolInfoSummary - - description : Optional[str] - input_schema : Optional[dict[str, Any]] - name : Optional[str] - + + + + ToolInfoSummary + + description : Optional[str] + input_schema : Optional[dict[str, Any]] + name : str + - - - - ToolResultStreamPayload - - data : ToolResultSummary - event : Literal['tool_result'] - - serialize_text() -> str + + + + ToolResultStreamPayload + + data + event : Literal['tool_result'] + + serialize_text() -> str - - - - ToolResultSummary - - content : Optional[str] - id : Optional[str] - round : Optional[int] - status : Optional[str] - type : Optional[str] - + + + + ToolResultSummary + + content : str + id : str + round : int + status : str + type : str + - - - - Transcript - - attachments : Optional[list[dict[str, Any]]] - llm_response : str - metadata - query_is_valid : bool - rag_chunks : Optional[list[dict[str, Any]]] - redacted_query : str - tool_calls : Optional[list[dict[str, Any]]] - tool_results : Optional[list[dict[str, Any]]] - truncated : bool - + + + + Transcript + + attachments : list[dict[str, Any]] + llm_response : str + metadata + query_is_valid : bool + rag_chunks : list[dict[str, Any]] + redacted_query : str + tool_calls : list[dict[str, Any]] + tool_results : list[dict[str, Any]] + truncated : bool + - - - - TranscriptMetadata - - conversation_id : str - model : str - provider : Optional[str] - query_model : Optional[str] - query_provider : Optional[str] - timestamp : str - user_id : str - + + + + TranscriptMetadata + + conversation_id : str + model : str + provider : Optional[str] + query_model : Optional[str] + query_provider : Optional[str] + timestamp : str + user_id : str + - - - - TurnCompleteStreamPayload - - data - event : Literal['turn_complete'] - - create() -> Self + + + + TurnCompleteStreamPayload + + data + event : Literal['turn_complete'] + + create() -> Self - - - - TurnSummary - - id : Optional[str] - llm_response : str - next_chunk_id : Optional[int] - output_items : Optional[list[OpenAIResponseOutput]] - partial_tokens : Optional[list[str]] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - token_usage : Optional[TokenCounter] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] - + + + + TurnSummary + + id : str + llm_response : str + next_chunk_id : int + output_items : list[OpenAIResponseOutput] + partial_tokens : list[str] + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] + token_usage + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - - - - - - - data - - - - - - data - - - - - - data - - - - - - data - - - - - - data - - - - - - data - - - - - - metadata - - + + + + + + + + data + + + + + + data + + + + + + data + + + + + + data + + + + + + data + + + + + + data + + + + + + metadata + + diff --git a/docs/models/requests.json b/docs/models/requests.json index e16154aaa..4a56726b3 100644 --- a/docs/models/requests.json +++ b/docs/models/requests.json @@ -1052,7 +1052,7 @@ "type": "object" }, "OpenAIResponseInputToolWebSearch": { - "description": "Web search tool configuration for OpenAI response inputs.\n\n:param type: Web search tool type variant to use\n:param search_context_size: (Optional) Size of search context, must be \"low\", \"medium\", or \"high\"", + "description": "Web search tool configuration for OpenAI response inputs.", "properties": { "type": { "anyOf": [ @@ -1079,8 +1079,30 @@ "search_context_size": { "type": "string", "nullable": true, - "default": "medium", + "default": null, "title": "Search Context Size" + }, + "filters": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchFilters" + }, + { + "type": "null" + } + ], + "default": null + }, + "user_location": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchUserLocation" + }, + { + "type": "null" + } + ], + "default": null } }, "title": "OpenAIResponseInputToolWebSearch", @@ -1601,7 +1623,7 @@ "type": "object" }, "OpenAIResponseOutputMessageWebSearchToolCall": { - "description": "Web search tool call output message for OpenAI responses.\n\n:param id: Unique identifier for this tool call\n:param status: Current status of the web search operation\n:param type: Tool call type identifier, always \"web_search_call\"", + "description": "Web search tool call output message for OpenAI responses.", "properties": { "id": { "title": "Id", @@ -1616,6 +1638,24 @@ "default": "web_search_call", "title": "Type", "type": "string" + }, + "action": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchActionSearch" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionOpenPage" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionFind" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Action" } }, "required": [ @@ -1660,6 +1700,14 @@ "default": null, "title": "Effort" }, + "generate_summary": { + "type": "string", + "nullable": true, + "default": null, + "deprecated": true, + "description": "Deprecated: use 'summary' instead.", + "title": "Generate Summary" + }, "summary": { "type": "string", "nullable": true, @@ -1688,7 +1736,7 @@ "verbosity": { "type": "string", "nullable": true, - "default": null, + "default": "medium", "title": "Verbosity" } }, @@ -2610,7 +2658,7 @@ "type": "object" }, "SearchRankingOptions": { - "description": "Options for ranking and filtering search results.\n\nThis class configures how search results are ranked and filtered. You can use algorithm-based\nrerankers (weighted, RRF) or neural rerankers. Defaults from VectorStoresConfig are\nused when parameters are not provided.\n\nExamples:\n # Weighted ranker with custom alpha\n SearchRankingOptions(ranker=\"weighted\", alpha=0.7)\n\n # RRF ranker with custom impact factor\n SearchRankingOptions(ranker=\"rrf\", impact_factor=50.0)\n\n # Use config defaults (just specify ranker type)\n SearchRankingOptions(ranker=\"weighted\") # Uses alpha from VectorStoresConfig\n\n # Score threshold filtering\n SearchRankingOptions(ranker=\"weighted\", score_threshold=0.5)\n\n:param ranker: (Optional) Name of the ranking algorithm to use. Supported values:\n - \"weighted\": Weighted combination of vector and keyword scores\n - \"rrf\": Reciprocal Rank Fusion algorithm\n - \"neural\": Neural reranking model (requires model parameter)\n Note: For OpenAI API compatibility, any string value is accepted, but only the above values are supported.\n:param score_threshold: (Optional) Minimum relevance score threshold for results. Default: 0.0\n:param alpha: (Optional) Weight factor for weighted ranker (0-1).\n - 0.0 = keyword only\n - 0.5 = equal weight (default)\n - 1.0 = vector only\n Only used when ranker=\"weighted\" and weights is not provided.\n Falls back to VectorStoresConfig.chunk_retrieval_params.weighted_search_alpha if not provided.\n:param impact_factor: (Optional) Impact factor (k) for RRF algorithm.\n Lower values emphasize higher-ranked results. Default: 60.0 (optimal from research).\n Only used when ranker=\"rrf\".\n Falls back to VectorStoresConfig.chunk_retrieval_params.rrf_impact_factor if not provided.\n:param weights: (Optional) Dictionary of weights for combining different signal types.\n Keys can be \"vector\", \"keyword\", \"neural\". Values should sum to 1.0.\n Used when combining algorithm-based reranking with neural reranking.\n Example: {\"vector\": 0.3, \"keyword\": 0.3, \"neural\": 0.4}\n:param model: (Optional) Model identifier for neural reranker (e.g., \"transformers/Qwen/Qwen3-Reranker-0.6B\").\n Required when ranker=\"neural\" or when weights contains \"neural\".", + "description": "Options for ranking and filtering search results.\n\nThis class configures how search results are ranked and filtered. You can use algorithm-based\nrerankers (weighted, RRF) or neural rerankers. Defaults from VectorStoresConfig are\nused when parameters are not provided.\n\nExamples:\n # Weighted ranker with custom alpha\n SearchRankingOptions(ranker=\"weighted\", alpha=0.7)\n\n # RRF ranker with custom impact factor\n SearchRankingOptions(ranker=\"rrf\", impact_factor=50.0)\n\n # Use config defaults (just specify ranker type)\n SearchRankingOptions(ranker=\"weighted\") # Uses alpha from VectorStoresConfig\n\n # Score threshold filtering\n SearchRankingOptions(ranker=\"weighted\", score_threshold=0.5)\n\n:param ranker: (Optional) Name of the ranking algorithm to use. Supported values:\n - \"weighted\": Weighted combination of vector and keyword scores\n - \"rrf\": Reciprocal Rank Fusion algorithm\n - \"neural\": Neural reranking model (requires model parameter)\n Note: For OpenAI API compatibility, any string value is accepted, but only the above values are supported.\n:param score_threshold: (Optional) Minimum relevance score threshold for results. Default: 0.0\n:param alpha: (Optional) Weight factor for weighted ranker (0-1).\n - 0.0 = keyword only\n - 0.5 = equal weight (default)\n - 1.0 = vector only\n Only used when ranker=\"weighted\" and weights is not provided.\n Falls back to VectorStoresConfig.chunk_retrieval_params.weighted_search_alpha if not provided.\n:param impact_factor: (Optional) Impact factor (k) for RRF algorithm.\n Lower values emphasize higher-ranked results. Default: 60.0 (optimal from research).\n Only used when ranker=\"rrf\".\n Falls back to VectorStoresConfig.chunk_retrieval_params.rrf_impact_factor if not provided.\n:param weights: (Optional) Dictionary of weights for combining different signal types.\n Keys can be \"vector\", \"keyword\", \"neural\". Values should sum to 1.0.\n Used when combining algorithm-based reranking with neural reranking.\n Example: {\"vector\": 0.3, \"keyword\": 0.3, \"neural\": 0.4}\n:param model: (Optional) Model identifier for neural reranker\n (e.g., \"sentence-transformers/Qwen/Qwen3-Reranker-0.6B\"). Required when ranker=\"neural\" or when\n weights contains \"neural\".", "properties": { "ranker": { "type": "string", @@ -2940,6 +2988,152 @@ }, "title": "VectorStoreUpdateRequest", "type": "object" + }, + "WebSearchActionFind": { + "description": "Web search action: searches for a pattern within a loaded page.", + "properties": { + "type": { + "const": "find_in_page", + "default": "find_in_page", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + }, + "pattern": { + "title": "Pattern", + "type": "string" + } + }, + "required": [ + "url", + "pattern" + ], + "title": "WebSearchActionFind", + "type": "object" + }, + "WebSearchActionOpenPage": { + "description": "Web search action: opens a specific URL from search results.", + "properties": { + "type": { + "const": "open_page", + "default": "open_page", + "title": "Type", + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "default": null, + "title": "Url" + } + }, + "title": "WebSearchActionOpenPage", + "type": "object" + }, + "WebSearchActionSearch": { + "description": "Web search action: performs a search query.", + "properties": { + "type": { + "const": "search", + "default": "search", + "title": "Type", + "type": "string" + }, + "query": { + "title": "Query", + "type": "string" + }, + "queries": { + "type": "array", + "nullable": true, + "default": null, + "title": "Queries" + }, + "sources": { + "type": "array", + "nullable": true, + "default": null, + "title": "Sources" + } + }, + "required": [ + "query" + ], + "title": "WebSearchActionSearch", + "type": "object" + }, + "WebSearchFilters": { + "description": "Domain filters for web search results.", + "properties": { + "allowed_domains": { + "type": "array", + "nullable": true, + "default": null, + "title": "Allowed Domains" + } + }, + "title": "WebSearchFilters", + "type": "object" + }, + "WebSearchSource": { + "description": "A source URL returned by a web search action.", + "properties": { + "type": { + "const": "url", + "default": "url", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "WebSearchSource", + "type": "object" + }, + "WebSearchUserLocation": { + "description": "Approximate user location to refine web search results.", + "properties": { + "type": { + "const": "approximate", + "default": "approximate", + "title": "Type", + "type": "string" + }, + "city": { + "type": "string", + "nullable": true, + "default": null, + "title": "City" + }, + "country": { + "type": "string", + "nullable": true, + "default": null, + "title": "Country" + }, + "region": { + "type": "string", + "nullable": true, + "default": null, + "title": "Region" + }, + "timezone": { + "type": "string", + "nullable": true, + "default": null, + "title": "Timezone" + } + }, + "title": "WebSearchUserLocation", + "type": "object" } } }, diff --git a/docs/models/requests.md b/docs/models/requests.md index 3579a66bc..baf8562e7 100644 --- a/docs/models/requests.md +++ b/docs/models/requests.md @@ -507,14 +507,13 @@ Function tool configuration for OpenAI response inputs. Web search tool configuration for OpenAI response inputs. -:param type: Web search tool type variant to use -:param search_context_size: (Optional) Size of search context, must be "low", "medium", or "high" - | Field | Type | Description | |-------|------|-------------| | type | | | | search_context_size | string | | +| filters | | | +| user_location | | | ## OpenAIResponseMCPApprovalRequest @@ -732,16 +731,13 @@ A summary of reasoning output from the model. Web search tool call output message for OpenAI responses. -:param id: Unique identifier for this tool call -:param status: Current status of the web search operation -:param type: Tool call type identifier, always "web_search_call" - | Field | Type | Description | |-------|------|-------------| | id | string | | | status | string | | | type | string | | +| action | | | ## OpenAIResponsePrompt @@ -776,6 +772,7 @@ Controls how much reasoning the model performs before generating a response. | Field | Type | Description | |-------|------|-------------| | effort | string | | +| generate_summary | string | Deprecated: use 'summary' instead. | | summary | string | Summary mode for reasoning output. One of 'auto', 'concise', or 'detailed'. | @@ -1174,8 +1171,9 @@ Examples: Keys can be "vector", "keyword", "neural". Values should sum to 1.0. Used when combining algorithm-based reranking with neural reranking. Example: {"vector": 0.3, "keyword": 0.3, "neural": 0.4} -:param model: (Optional) Model identifier for neural reranker (e.g., "transformers/Qwen/Qwen3-Reranker-0.6B"). - Required when ranker="neural" or when weights contains "neural". +:param model: (Optional) Model identifier for neural reranker + (e.g., "sentence-transformers/Qwen/Qwen3-Reranker-0.6B"). Required when ranker="neural" or when + weights contains "neural". | Field | Type | Description | @@ -1279,3 +1277,80 @@ Attributes: | name | string | New name for the vector store | | expires_at | integer | Unix timestamp when the vector store should expire | | metadata | object | Metadata dictionary for storing session information | + + +## WebSearchActionFind + + +Web search action: searches for a pattern within a loaded page. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | +| pattern | string | | + + +## WebSearchActionOpenPage + + +Web search action: opens a specific URL from search results. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | + + +## WebSearchActionSearch + + +Web search action: performs a search query. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| query | string | | +| queries | array | | +| sources | array | | + + +## WebSearchFilters + + +Domain filters for web search results. + + +| Field | Type | Description | +|-------|------|-------------| +| allowed_domains | array | | + + +## WebSearchSource + + +A source URL returned by a web search action. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | + + +## WebSearchUserLocation + + +Approximate user location to refine web search results. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| city | string | | +| country | string | | +| region | string | | +| timezone | string | | diff --git a/docs/models/requests.puml b/docs/models/requests.puml index 99f8b7012..904a8e6f4 100644 --- a/docs/models/requests.puml +++ b/docs/models/requests.puml @@ -2,16 +2,16 @@ set namespaceSeparator none class "ConversationUpdateRequest" as src.models.api.requests.conversations.ConversationUpdateRequest { model_config : dict - topic_summary : Optional[str] + topic_summary : str } class "FeedbackRequest" as src.models.api.requests.feedback.FeedbackRequest { categories : Optional[list[FeedbackCategory]] - conversation_id : Optional[str] - llm_response : Optional[str] + conversation_id : str + llm_response : str model_config : dict sentiment : Optional[int] user_feedback : Optional[str] - user_question : Optional[str] + user_question : str check_feedback_provided() -> Self check_sentiment(value: Optional[int]) -> Optional[int] check_uuid(value: str) -> str @@ -19,17 +19,17 @@ class "FeedbackRequest" as src.models.api.requests.feedback.FeedbackRequest { } class "FeedbackStatusUpdateRequest" as src.models.api.requests.feedback.FeedbackStatusUpdateRequest { model_config : dict - status : Optional[bool] + status : bool get_value() -> bool } class "MCPServerRegistrationRequest" as src.models.api.requests.mcp_servers.MCPServerRegistrationRequest { authorization_headers : Optional[dict[str, str]] headers : Optional[list[str]] model_config : dict - name : Optional[str] - provider_id : Optional[str] + name : str + provider_id : str timeout : Optional[int] - url : Optional[str] + url : str validate_authorization_header_values(value: Optional[dict[str, str]]) -> Optional[dict[str, str]] validate_url(value: str) -> str } @@ -39,15 +39,15 @@ class "ModelFilter" as src.models.api.requests.catalog.ModelFilter { } class "PromptCreateRequest" as src.models.api.requests.prompts.PromptCreateRequest { model_config : dict - prompt : Optional[str] + prompt : str variables : Optional[list[str]] } class "PromptUpdateRequest" as src.models.api.requests.prompts.PromptUpdateRequest { model_config : dict - prompt : Optional[str] + prompt : str set_as_default : Optional[bool] variables : Optional[list[str]] - version : Optional[int] + version : int } class "QueryRequest" as src.models.api.requests.query.QueryRequest { attachments : Optional[list[Attachment]] @@ -58,7 +58,7 @@ class "QueryRequest" as src.models.api.requests.query.QueryRequest { model_config : dict no_tools : Optional[bool] provider : Optional[str] - query : Optional[str] + query : str shield_ids : Optional[list[str]] solr : Optional[SolrVectorSearchRequest] system_prompt : Optional[str] @@ -98,46 +98,46 @@ class "ResponsesRequest" as src.models.api.requests.responses_openai.ResponsesRe validate_conversation_and_previous_response_id_mutually_exclusive() -> Self } class "RlsapiV1Attachment" as src.models.api.requests.rlsapi.RlsapiV1Attachment { - contents : Optional[str] - mimetype : Optional[str] + contents : str + mimetype : str } class "RlsapiV1CLA" as src.models.api.requests.rlsapi.RlsapiV1CLA { - nevra : Optional[str] - version : Optional[str] + nevra : str + version : str } class "RlsapiV1Context" as src.models.api.requests.rlsapi.RlsapiV1Context { - attachments : Optional[RlsapiV1Attachment] - cla : Optional[RlsapiV1CLA] - stdin : Optional[str] - systeminfo : Optional[RlsapiV1SystemInfo] - terminal : Optional[RlsapiV1Terminal] + attachments + cla + stdin : str + systeminfo + terminal } class "RlsapiV1InferRequest" as src.models.api.requests.rlsapi.RlsapiV1InferRequest { - context : Optional[RlsapiV1Context] - include_metadata : Optional[bool] - question : Optional[str] - skip_rag : Optional[bool] + context + include_metadata : bool + question : str + skip_rag : bool get_input_source() -> str validate_question(value: str) -> str } class "RlsapiV1SystemInfo" as src.models.api.requests.rlsapi.RlsapiV1SystemInfo { - arch : Optional[str] + arch : str model_config : dict - os : Optional[str] - system_id : Optional[str] - version : Optional[str] + os : str + system_id : str + version : str } class "RlsapiV1Terminal" as src.models.api.requests.rlsapi.RlsapiV1Terminal { - output : Optional[str] + output : str } class "SavedPromptCreateRequest" as src.models.api.requests.saved_prompts.SavedPromptCreateRequest { - content : Optional[str] + content : str model_config : dict - name : Optional[str] + name : str } class "StreamingInterruptRequest" as src.models.api.requests.query.StreamingInterruptRequest { model_config : dict - request_id : Optional[str] + request_id : str check_request_id(value: str) -> str } class "VectorStoreCreateRequest" as src.models.api.requests.vector_stores.VectorStoreCreateRequest { @@ -146,13 +146,13 @@ class "VectorStoreCreateRequest" as src.models.api.requests.vector_stores.Vector embedding_model : Optional[str] metadata : Optional[dict[str, Any]] model_config : dict - name : Optional[str] + name : str provider_id : Optional[str] } class "VectorStoreFileCreateRequest" as src.models.api.requests.vector_stores.VectorStoreFileCreateRequest { attributes : Optional[dict[str, str | float | bool]] chunking_strategy : Optional[dict[str, Any]] - file_id : Optional[str] + file_id : str model_config : dict validate_attributes(value: Optional[dict[str, str | float | bool]]) -> Optional[dict[str, str | float | bool]] } @@ -163,4 +163,9 @@ class "VectorStoreUpdateRequest" as src.models.api.requests.vector_stores.Vector name : Optional[str] check_at_least_one_field() -> Self } +src.models.api.requests.rlsapi.RlsapiV1Attachment --* src.models.api.requests.rlsapi.RlsapiV1Context : attachments +src.models.api.requests.rlsapi.RlsapiV1CLA --* src.models.api.requests.rlsapi.RlsapiV1Context : cla +src.models.api.requests.rlsapi.RlsapiV1Context --* src.models.api.requests.rlsapi.RlsapiV1InferRequest : context +src.models.api.requests.rlsapi.RlsapiV1SystemInfo --* src.models.api.requests.rlsapi.RlsapiV1Context : systeminfo +src.models.api.requests.rlsapi.RlsapiV1Terminal --* src.models.api.requests.rlsapi.RlsapiV1Context : terminal @enduml diff --git a/docs/models/requests.svg b/docs/models/requests.svg index d11285e1d..9ae9f16f1 100644 --- a/docs/models/requests.svg +++ b/docs/models/requests.svg @@ -1,310 +1,340 @@ - + - - - - ConversationUpdateRequest - - model_config : dict - topic_summary : Optional[str] - + + + + ConversationUpdateRequest + + model_config : dict + topic_summary : str + - - - - FeedbackRequest - - categories : Optional[list[FeedbackCategory]] - conversation_id : Optional[str] - llm_response : Optional[str] - model_config : dict - sentiment : Optional[int] - user_feedback : Optional[str] - user_question : Optional[str] - - check_feedback_provided() -> Self - check_sentiment(value: Optional[int]) -> Optional[int] - check_uuid(value: str) -> str - validate_categories(value: Optional[list[FeedbackCategory]]) -> Optional[list[FeedbackCategory]] + + + + FeedbackRequest + + categories : Optional[list[FeedbackCategory]] + conversation_id : str + llm_response : str + model_config : dict + sentiment : Optional[int] + user_feedback : Optional[str] + user_question : str + + check_feedback_provided() -> Self + check_sentiment(value: Optional[int]) -> Optional[int] + check_uuid(value: str) -> str + validate_categories(value: Optional[list[FeedbackCategory]]) -> Optional[list[FeedbackCategory]] - - - - FeedbackStatusUpdateRequest - - model_config : dict - status : Optional[bool] - - get_value() -> bool + + + + FeedbackStatusUpdateRequest + + model_config : dict + status : bool + + get_value() -> bool - - - - MCPServerRegistrationRequest - - authorization_headers : Optional[dict[str, str]] - headers : Optional[list[str]] - model_config : dict - name : Optional[str] - provider_id : Optional[str] - timeout : Optional[int] - url : Optional[str] - - validate_authorization_header_values(value: Optional[dict[str, str]]) -> Optional[dict[str, str]] - validate_url(value: str) -> str + + + + MCPServerRegistrationRequest + + authorization_headers : Optional[dict[str, str]] + headers : Optional[list[str]] + model_config : dict + name : str + provider_id : str + timeout : Optional[int] + url : str + + validate_authorization_header_values(value: Optional[dict[str, str]]) -> Optional[dict[str, str]] + validate_url(value: str) -> str - - - - ModelFilter - - model_config : dict - model_type : Optional[str] - + + + + ModelFilter + + model_config : dict + model_type : Optional[str] + - - - - PromptCreateRequest - - model_config : dict - prompt : Optional[str] - variables : Optional[list[str]] - + + + + PromptCreateRequest + + model_config : dict + prompt : str + variables : Optional[list[str]] + - - - - PromptUpdateRequest - - model_config : dict - prompt : Optional[str] - set_as_default : Optional[bool] - variables : Optional[list[str]] - version : Optional[int] - + + + + PromptUpdateRequest + + model_config : dict + prompt : str + set_as_default : Optional[bool] + variables : Optional[list[str]] + version : int + - - - - QueryRequest - - attachments : Optional[list[Attachment]] - conversation_id : Optional[str] - generate_topic_summary : Optional[bool] - media_type : Optional[str] - model : Optional[str] - model_config : dict - no_tools : Optional[bool] - provider : Optional[str] - query : Optional[str] - shield_ids : Optional[list[str]] - solr : Optional[SolrVectorSearchRequest] - system_prompt : Optional[str] - vector_store_ids : Optional[list[str]] - - check_uuid(value: Optional[str]) -> Optional[str] - validate_media_type() -> Self - validate_provider_and_model() -> Self + + + + QueryRequest + + attachments : Optional[list[Attachment]] + conversation_id : Optional[str] + generate_topic_summary : Optional[bool] + media_type : Optional[str] + model : Optional[str] + model_config : dict + no_tools : Optional[bool] + provider : Optional[str] + query : str + shield_ids : Optional[list[str]] + solr : Optional[SolrVectorSearchRequest] + system_prompt : Optional[str] + vector_store_ids : Optional[list[str]] + + check_uuid(value: Optional[str]) -> Optional[str] + validate_media_type() -> Self + validate_provider_and_model() -> Self - - - - ResponsesRequest - - conversation : Optional[str] - generate_topic_summary : Optional[bool] - include : Optional[list[IncludeParameter]] - input - instructions : Optional[str] - max_infer_iters : Optional[int] - max_output_tokens : Optional[int] - max_tool_calls : Optional[int] - metadata : Optional[dict[str, str]] - model : Optional[str] - model_config : dict - parallel_tool_calls : Optional[bool] - previous_response_id : Optional[str] - prompt : Optional[Prompt] - reasoning : Optional[Reasoning] - safety_identifier : Optional[str] - shield_ids : Optional[list[str]] - solr : Optional[SolrVectorSearchRequest] - store : bool - stream : bool - temperature : Optional[float] - text : Optional[Text] - tool_choice : Optional[ToolChoice] - tools : Optional[list[InputTool]] - - check_previous_response_id(value: Optional[str]) -> Optional[str] - check_suid(value: Optional[str]) -> Optional[str] - validate_body_size(values: Any) -> Any - validate_conversation_and_previous_response_id_mutually_exclusive() -> Self + + + + ResponsesRequest + + conversation : Optional[str] + generate_topic_summary : Optional[bool] + include : Optional[list[IncludeParameter]] + input + instructions : Optional[str] + max_infer_iters : Optional[int] + max_output_tokens : Optional[int] + max_tool_calls : Optional[int] + metadata : Optional[dict[str, str]] + model : Optional[str] + model_config : dict + parallel_tool_calls : Optional[bool] + previous_response_id : Optional[str] + prompt : Optional[Prompt] + reasoning : Optional[Reasoning] + safety_identifier : Optional[str] + shield_ids : Optional[list[str]] + solr : Optional[SolrVectorSearchRequest] + store : bool + stream : bool + temperature : Optional[float] + text : Optional[Text] + tool_choice : Optional[ToolChoice] + tools : Optional[list[InputTool]] + + check_previous_response_id(value: Optional[str]) -> Optional[str] + check_suid(value: Optional[str]) -> Optional[str] + validate_body_size(values: Any) -> Any + validate_conversation_and_previous_response_id_mutually_exclusive() -> Self - - - - RlsapiV1Attachment - - contents : Optional[str] - mimetype : Optional[str] - + + + + RlsapiV1Attachment + + contents : str + mimetype : str + - - - - RlsapiV1CLA - - nevra : Optional[str] - version : Optional[str] - + + + + RlsapiV1CLA + + nevra : str + version : str + - - - - RlsapiV1Context - - attachments : Optional[RlsapiV1Attachment] - cla : Optional[RlsapiV1CLA] - stdin : Optional[str] - systeminfo : Optional[RlsapiV1SystemInfo] - terminal : Optional[RlsapiV1Terminal] - + + + + RlsapiV1Context + + attachments + cla + stdin : str + systeminfo + terminal + - - - - RlsapiV1InferRequest - - context : Optional[RlsapiV1Context] - include_metadata : Optional[bool] - question : Optional[str] - skip_rag : Optional[bool] - - get_input_source() -> str - validate_question(value: str) -> str + + + + RlsapiV1InferRequest + + context + include_metadata : bool + question : str + skip_rag : bool + + get_input_source() -> str + validate_question(value: str) -> str - - - - RlsapiV1SystemInfo - - arch : Optional[str] - model_config : dict - os : Optional[str] - system_id : Optional[str] - version : Optional[str] - + + + + RlsapiV1SystemInfo + + arch : str + model_config : dict + os : str + system_id : str + version : str + - - - - RlsapiV1Terminal - - output : Optional[str] - + + + + RlsapiV1Terminal + + output : str + - - - - SavedPromptCreateRequest - - content : Optional[str] - model_config : dict - name : Optional[str] - + + + + SavedPromptCreateRequest + + content : str + model_config : dict + name : str + - - - - StreamingInterruptRequest - - model_config : dict - request_id : Optional[str] - - check_request_id(value: str) -> str + + + + StreamingInterruptRequest + + model_config : dict + request_id : str + + check_request_id(value: str) -> str - - - - VectorStoreCreateRequest - - chunking_strategy : Optional[dict[str, Any]] - embedding_dimension : Optional[int] - embedding_model : Optional[str] - metadata : Optional[dict[str, Any]] - model_config : dict - name : Optional[str] - provider_id : Optional[str] - + + + + VectorStoreCreateRequest + + chunking_strategy : Optional[dict[str, Any]] + embedding_dimension : Optional[int] + embedding_model : Optional[str] + metadata : Optional[dict[str, Any]] + model_config : dict + name : str + provider_id : Optional[str] + - - - - VectorStoreFileCreateRequest - - attributes : Optional[dict[str, str | float | bool]] - chunking_strategy : Optional[dict[str, Any]] - file_id : Optional[str] - model_config : dict - - validate_attributes(value: Optional[dict[str, str | float | bool]]) -> Optional[dict[str, str | float | bool]] + + + + VectorStoreFileCreateRequest + + attributes : Optional[dict[str, str | float | bool]] + chunking_strategy : Optional[dict[str, Any]] + file_id : str + model_config : dict + + validate_attributes(value: Optional[dict[str, str | float | bool]]) -> Optional[dict[str, str | float | bool]] - - - - VectorStoreUpdateRequest - - expires_at : Optional[int] - metadata : Optional[dict[str, Any]] - model_config : dict - name : Optional[str] - - check_at_least_one_field() -> Self + + + + VectorStoreUpdateRequest + + expires_at : Optional[int] + metadata : Optional[dict[str, Any]] + model_config : dict + name : Optional[str] + + check_at_least_one_field() -> Self - + + + + + attachments + + + + + + cla + + + + + + context + + + + + + systeminfo + + + + + + terminal + + diff --git a/docs/models/responses.puml b/docs/models/responses.puml index eb5f3eac6..0eef6a549 100644 --- a/docs/models/responses.puml +++ b/docs/models/responses.puml @@ -1,14 +1,14 @@ @startuml classes set namespaceSeparator none class "AbstractDeleteResponse" as src.models.api.responses.successful.bases.AbstractDeleteResponse { - deleted : Optional[bool] + deleted : bool resource_name : ClassVar[str] openapi_response() -> dict[str, Any] response() -> str } class "AbstractErrorResponse" as src.models.api.responses.error.bases.AbstractErrorResponse { - detail : Optional[DetailModel] - status_code : Optional[int] + detail + status_code : int get_description() -> str openapi_response(examples: Optional[list[str]]) -> dict[str, Any] } @@ -17,16 +17,16 @@ class "AbstractSuccessfulResponse" as src.models.api.responses.successful.bases. } class "AuthorizedResponse" as src.models.api.responses.successful.probes.AuthorizedResponse { model_config : dict - skip_userid_check : Optional[bool] - user_id : Optional[str] - username : Optional[str] + skip_userid_check : bool + user_id : str + username : str } class "BadRequestResponse" as src.models.api.responses.error.bad_request.BadRequestResponse { description : ClassVar[str] model_config : dict } class "ConfigurationResponse" as src.models.api.responses.successful.configuration.ConfigurationResponse { - configuration : Configuration + configuration model_config : ConfigDict } class "ConflictResponse" as src.models.api.responses.error.conflict.ConflictResponse { @@ -36,21 +36,21 @@ class "ConflictResponse" as src.models.api.responses.error.conflict.ConflictResp mcp_tool(server_label: str) -> Self } class "ConversationDeleteResponse" as src.models.api.responses.successful.conversations.ConversationDeleteResponse { - conversation_id : Optional[str] + conversation_id : str model_config : dict resource_name : ClassVar[str] success() -> bool } class "ConversationResponse" as src.models.api.responses.successful.conversations.ConversationResponse { - chat_history : Optional[list[ConversationTurn]] - conversation_id : Optional[str] + chat_history : list[ConversationTurn] + conversation_id : str model_config : dict } class "ConversationUpdateResponse" as src.models.api.responses.successful.conversations.ConversationUpdateResponse { - conversation_id : Optional[str] - message : Optional[str] + conversation_id : str + message : str model_config : dict - success : Optional[bool] + success : bool } class "ConversationsListResponse" as src.models.api.responses.successful.conversations.ConversationsListResponse { conversations : list[ConversationDetails] @@ -61,25 +61,25 @@ class "ConversationsListResponseV2" as src.models.api.responses.successful.conve model_config : dict } class "DetailModel" as src.models.api.responses.error.bases.DetailModel { - cause : Optional[str] - response : Optional[str] + cause : str + response : str } class "FeedbackResponse" as src.models.api.responses.successful.feedback.FeedbackResponse { model_config : dict - response : Optional[str] + response : str } class "FeedbackStatusUpdateResponse" as src.models.api.responses.successful.feedback.FeedbackStatusUpdateResponse { model_config : dict status : dict[str, Any] } class "FileResponse" as src.models.api.responses.successful.vector_stores.FileResponse { - bytes : Optional[int] - created_at : Optional[int] - filename : Optional[str] - id : Optional[str] + bytes : int + created_at : int + filename : str + id : str model_config : dict - object : Optional[str] - purpose : Optional[str] + object : str + purpose : str } class "FileTooLargeResponse" as src.models.api.responses.error.content_too_large.FileTooLargeResponse { description : ClassVar[str] @@ -98,10 +98,10 @@ class "ForbiddenResponse" as src.models.api.responses.error.forbidden.ForbiddenR saved_prompt(action: str, resource_id: str, user_id: str) -> Self } class "InfoResponse" as src.models.api.responses.successful.probes.InfoResponse { - ogx_version : Optional[str] model_config : dict - name : Optional[str] - service_version : Optional[str] + name : str + ogx_version : str + service_version : str } class "InternalServerErrorResponse" as src.models.api.responses.error.internal.InternalServerErrorResponse { description : ClassVar[str] @@ -115,32 +115,32 @@ class "InternalServerErrorResponse" as src.models.api.responses.error.internal.I query_failed(cause: str) -> Self } class "LivenessResponse" as src.models.api.responses.successful.probes.LivenessResponse { - alive : Optional[bool] + alive : bool model_config : dict } class "MCPClientAuthOptionsResponse" as src.models.api.responses.successful.mcp_servers.MCPClientAuthOptionsResponse { model_config : dict - servers : Optional[list[MCPServerAuthInfo]] + servers : list[MCPServerAuthInfo] } class "MCPServerDeleteResponse" as src.models.api.responses.successful.mcp_servers.MCPServerDeleteResponse { model_config : dict - name : Optional[str] + name : str resource_name : ClassVar[str] } class "MCPServerListResponse" as src.models.api.responses.successful.mcp_servers.MCPServerListResponse { model_config : dict - servers : Optional[list[MCPServerInfo]] + servers : list[MCPServerInfo] } class "MCPServerRegistrationResponse" as src.models.api.responses.successful.mcp_servers.MCPServerRegistrationResponse { - message : Optional[str] + message : str model_config : dict - name : Optional[str] - provider_id : Optional[str] - url : Optional[str] + name : str + provider_id : str + url : str } class "ModelsResponse" as src.models.api.responses.successful.catalog.ModelsResponse { model_config : dict - models : Optional[list[CatalogModel]] + models : list[CatalogModel] } class "NotFoundResponse" as src.models.api.responses.error.not_found.NotFoundResponse { description : ClassVar[str] @@ -148,50 +148,50 @@ class "NotFoundResponse" as src.models.api.responses.error.not_found.NotFoundRes } class "PromptDeleteResponse" as src.models.api.responses.successful.prompts.PromptDeleteResponse { model_config : dict - prompt_id : Optional[str] + prompt_id : str resource_name : ClassVar[str] } class "PromptResourceResponse" as src.models.api.responses.successful.prompts.PromptResourceResponse { is_default : Optional[bool] model_config : dict prompt : Optional[str] - prompt_id : Optional[str] + prompt_id : str variables : Optional[list[str]] - version : Optional[int] + version : int } class "PromptTooLongResponse" as src.models.api.responses.error.content_too_large.PromptTooLongResponse { description : ClassVar[str] model_config : dict } class "PromptsListResponse" as src.models.api.responses.successful.prompts.PromptsListResponse { - data : Optional[list[PromptResourceResponse]] + data : list[PromptResourceResponse] model_config : dict } class "ProviderResponse" as src.models.api.responses.successful.catalog.ProviderResponse { - api : Optional[str] - config : Optional[dict[str, Any]] - health : Optional[dict[str, Any]] + api : str + config : dict[str, Any] + health : dict[str, Any] model_config : dict - provider_id : Optional[str] - provider_type : Optional[str] + provider_id : str + provider_type : str } class "ProvidersListResponse" as src.models.api.responses.successful.catalog.ProvidersListResponse { model_config : dict - providers : Optional[dict[str, list[dict[str, Any]]]] + providers : dict[str, list[dict[str, Any]]] } class "QueryResponse" as src.models.api.responses.successful.query.QueryResponse { - available_quotas : Optional[dict[str, int]] - context_status : Optional[ContextStatus] + available_quotas : dict[str, int] + context_status conversation_id : Optional[str] - input_tokens : Optional[int] + input_tokens : int model_config : dict - output_tokens : Optional[int] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - response : Optional[str] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] - truncated : Optional[bool] + output_tokens : int + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] + response : str + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] + truncated : bool } class "QuotaExceededResponse" as src.models.api.responses.error.too_many_requests.QuotaExceededResponse { description : ClassVar[str] @@ -200,27 +200,27 @@ class "QuotaExceededResponse" as src.models.api.responses.error.too_many_request model(model_name: str) -> Self } class "RAGInfoResponse" as src.models.api.responses.successful.catalog.RAGInfoResponse { - created_at : Optional[int] + created_at : int expires_at : Optional[int] - id : Optional[str] + id : str last_active_at : Optional[int] model_config : dict name : Optional[str] - object : Optional[str] - status : Optional[str] - usage_bytes : Optional[int] + object : str + status : str + usage_bytes : int } class "RAGListResponse" as src.models.api.responses.successful.catalog.RAGListResponse { model_config : dict - rags : Optional[list[str]] + rags : list[str] } class "ReadinessResponse" as src.models.api.responses.successful.probes.ReadinessResponse { impacts : Optional[list[str]] model_config : dict - overall_status : Optional[HealthStatus] - providers : Optional[list[ProviderHealthStatus]] - ready : Optional[bool] - reason : Optional[str] + overall_status + providers : list[ProviderHealthStatus] + ready : bool + reason : str } class "ResponsesResponse" as src.models.api.responses.successful.responses_openai.ResponsesResponse { available_quotas : dict[str, int] @@ -260,36 +260,36 @@ class "RlsapiV1InferData" as src.models.api.responses.successful.rlsapi.RlsapiV1 rag_chunks : Optional[list[RAGChunk]] referenced_documents : Optional[list[ReferencedDocument]] request_id : Optional[str] - text : Optional[str] + text : str tool_calls : Optional[list[ToolCallSummary]] tool_results : Optional[list[ToolResultSummary]] } class "RlsapiV1InferResponse" as src.models.api.responses.successful.rlsapi.RlsapiV1InferResponse { - data : Optional[RlsapiV1InferData] + data model_config : dict } class "SavedPromptDeleteResponse" as src.models.api.responses.successful.saved_prompts.SavedPromptDeleteResponse { model_config : dict - prompt_id : Optional[str] + prompt_id : str resource_name : ClassVar[str] } class "SavedPromptResponse" as src.models.api.responses.successful.saved_prompts.SavedPromptResponse { - content : Optional[str] - created_at : Optional[datetime] - id : Optional[str] + content : str + created_at : datetime + id : str model_config : dict - name : Optional[str] - updated_at : Optional[datetime] + name : str + updated_at : datetime } class "SavedPromptsConfigResponse" as src.models.api.responses.successful.saved_prompts.SavedPromptsConfigResponse { - max_content_length : Optional[int] - max_display_name_length : Optional[int] - max_prompts_per_user : Optional[int] + max_content_length : int + max_display_name_length : int + max_prompts_per_user : int model_config : dict } class "SavedPromptsListResponse" as src.models.api.responses.successful.saved_prompts.SavedPromptsListResponse { model_config : dict - prompts : Optional[list[SavedPromptResponse]] + prompts : list[SavedPromptResponse] } class "ServiceUnavailableResponse" as src.models.api.responses.error.service_unavailable.ServiceUnavailableResponse { description : ClassVar[str] @@ -297,22 +297,22 @@ class "ServiceUnavailableResponse" as src.models.api.responses.error.service_una } class "ShieldsResponse" as src.models.api.responses.successful.catalog.ShieldsResponse { model_config : dict - shields : Optional[list[CatalogShield]] + shields : list[CatalogShield] } class "SkillsResponse" as src.models.api.responses.successful.catalog.SkillsResponse { model_config : dict - skills : Optional[list[SkillMetadata]] + skills : list[SkillMetadata] } class "StatusResponse" as src.models.api.responses.successful.probes.StatusResponse { - functionality : Optional[str] + functionality : str model_config : dict - status : Optional[dict[str, Any]] + status : dict[str, Any] } class "StreamingInterruptResponse" as src.models.api.responses.successful.query.StreamingInterruptResponse { - interrupted : Optional[bool] - message : Optional[str] + interrupted : bool + message : str model_config : dict - request_id : Optional[str] + request_id : str } class "StreamingQueryResponse" as src.models.api.responses.successful.query.StreamingQueryResponse { model_config : dict @@ -326,7 +326,7 @@ class "TooManyConcurrentRequestsResponse" as src.models.api.responses.error.too_ } class "ToolsResponse" as src.models.api.responses.successful.catalog.ToolsResponse { model_config : dict - tools : Optional[list[CatalogTool]] + tools : list[CatalogTool] } class "UnauthorizedResponse" as src.models.api.responses.error.unauthorized.UnauthorizedResponse { description : ClassVar[str] @@ -339,41 +339,43 @@ class "UnprocessableEntityResponse" as src.models.api.responses.error.unprocessa class "VectorStoreDeleteResponse" as src.models.api.responses.successful.vector_stores.VectorStoreDeleteResponse { model_config : dict resource_name : ClassVar[str] - vector_store_id : Optional[str] + vector_store_id : str } class "VectorStoreFileDeleteResponse" as src.models.api.responses.successful.vector_stores.VectorStoreFileDeleteResponse { - file_id : Optional[str] + file_id : str model_config : dict resource_name : ClassVar[str] } class "VectorStoreFileResponse" as src.models.api.responses.successful.vector_stores.VectorStoreFileResponse { - attributes : Optional[dict[str, str | float | bool]] - id : Optional[str] + attributes : Optional[Mapping[str, Any]] + id : str last_error : Optional[str] model_config : dict - object : Optional[str] - status : Optional[str] - vector_store_id : Optional[str] + object : str + status : str + vector_store_id : str } class "VectorStoreFilesListResponse" as src.models.api.responses.successful.vector_stores.VectorStoreFilesListResponse { - data : Optional[list[VectorStoreFileResponse]] + data : list[VectorStoreFileResponse] model_config : dict - object : Optional[str] + object : str } class "VectorStoreResponse" as src.models.api.responses.successful.vector_stores.VectorStoreResponse { - created_at : Optional[int] + created_at : int expires_at : Optional[int] - id : Optional[str] + id : str last_active_at : Optional[int] metadata : Optional[dict[str, Any]] model_config : dict - name : Optional[str] - status : Optional[str] - usage_bytes : Optional[int] + name : str + status : str + usage_bytes : int } class "VectorStoresListResponse" as src.models.api.responses.successful.vector_stores.VectorStoresListResponse { - data : Optional[list[VectorStoreResponse]] + data : list[VectorStoreResponse] model_config : dict - object : Optional[str] + object : str } +src.models.api.responses.error.bases.DetailModel --* src.models.api.responses.error.bases.AbstractErrorResponse : detail +src.models.api.responses.successful.rlsapi.RlsapiV1InferData --* src.models.api.responses.successful.rlsapi.RlsapiV1InferResponse : data @enduml diff --git a/docs/models/responses.svg b/docs/models/responses.svg index eba0ae5fb..83c8ae562 100644 --- a/docs/models/responses.svg +++ b/docs/models/responses.svg @@ -1,810 +1,822 @@ - + - - - - AbstractDeleteResponse - - deleted : Optional[bool] - resource_name : ClassVar[str] - - openapi_response() -> dict[str, Any] - response() -> str + + + + AbstractDeleteResponse + + deleted : bool + resource_name : ClassVar[str] + + openapi_response() -> dict[str, Any] + response() -> str - - - - AbstractErrorResponse - - detail : Optional[DetailModel] - status_code : Optional[int] - - get_description() -> str - openapi_response(examples: Optional[list[str]]) -> dict[str, Any] + + + + AbstractErrorResponse + + detail + status_code : int + + get_description() -> str + openapi_response(examples: Optional[list[str]]) -> dict[str, Any] - - - - AbstractSuccessfulResponse - - - openapi_response() -> dict[str, Any] + + + + AbstractSuccessfulResponse + + + openapi_response() -> dict[str, Any] - - - - AuthorizedResponse - - model_config : dict - skip_userid_check : Optional[bool] - user_id : Optional[str] - username : Optional[str] - + + + + AuthorizedResponse + + model_config : dict + skip_userid_check : bool + user_id : str + username : str + - - - - BadRequestResponse - - description : ClassVar[str] - model_config : dict - + + + + BadRequestResponse + + description : ClassVar[str] + model_config : dict + - - - - ConfigurationResponse - - configuration : Configuration - model_config : ConfigDict - + + + + ConfigurationResponse + + configuration + model_config : ConfigDict + - - - - ConflictResponse - - description : ClassVar[str] - model_config : dict - - file_search() -> Self - mcp_tool(server_label: str) -> Self + + + + ConflictResponse + + description : ClassVar[str] + model_config : dict + + file_search() -> Self + mcp_tool(server_label: str) -> Self - - - - ConversationDeleteResponse - - conversation_id : Optional[str] - model_config : dict - resource_name : ClassVar[str] - - success() -> bool + + + + ConversationDeleteResponse + + conversation_id : str + model_config : dict + resource_name : ClassVar[str] + + success() -> bool - - - - ConversationResponse - - chat_history : Optional[list[ConversationTurn]] - conversation_id : Optional[str] - model_config : dict - + + + + ConversationResponse + + chat_history : list[ConversationTurn] + conversation_id : str + model_config : dict + - - - - ConversationUpdateResponse - - conversation_id : Optional[str] - message : Optional[str] - model_config : dict - success : Optional[bool] - + + + + ConversationUpdateResponse + + conversation_id : str + message : str + model_config : dict + success : bool + - - - - ConversationsListResponse - - conversations : list[ConversationDetails] - model_config : dict - + + + + ConversationsListResponse + + conversations : list[ConversationDetails] + model_config : dict + - - - - ConversationsListResponseV2 - - conversations : list[ConversationData] - model_config : dict - + + + + ConversationsListResponseV2 + + conversations : list[ConversationData] + model_config : dict + - - - - DetailModel - - cause : Optional[str] - response : Optional[str] - + + + + DetailModel + + cause : str + response : str + - - - - FeedbackResponse - - model_config : dict - response : Optional[str] - + + + + FeedbackResponse + + model_config : dict + response : str + - - - - FeedbackStatusUpdateResponse - - model_config : dict - status : dict[str, Any] - + + + + FeedbackStatusUpdateResponse + + model_config : dict + status : dict[str, Any] + - - - - FileResponse - - bytes : Optional[int] - created_at : Optional[int] - filename : Optional[str] - id : Optional[str] - model_config : dict - object : Optional[str] - purpose : Optional[str] - + + + + FileResponse + + bytes : int + created_at : int + filename : str + id : str + model_config : dict + object : str + purpose : str + - - - - FileTooLargeResponse - - description : ClassVar[str] - model_config : dict - - exceeds_local_limit() -> Self - from_backend_rejection() -> Self + + + + FileTooLargeResponse + + description : ClassVar[str] + model_config : dict + + exceeds_local_limit() -> Self + from_backend_rejection() -> Self - - - - ForbiddenResponse - - description : ClassVar[str] - model_config : dict - - conversation(action: str, resource_id: str, user_id: str) -> Self - endpoint(user_id: str) -> Self - feedback_disabled() -> Self - mcp_server_static_config(server_name: str) -> Self - model_override() -> Self - saved_prompt(action: str, resource_id: str, user_id: str) -> Self + + + + ForbiddenResponse + + description : ClassVar[str] + model_config : dict + + conversation(action: str, resource_id: str, user_id: str) -> Self + endpoint(user_id: str) -> Self + feedback_disabled() -> Self + mcp_server_static_config(server_name: str) -> Self + model_override() -> Self + saved_prompt(action: str, resource_id: str, user_id: str) -> Self - - - - InfoResponse - - ogx_version : Optional[str] - model_config : dict - name : Optional[str] - service_version : Optional[str] - + + + + InfoResponse + + model_config : dict + name : str + ogx_version : str + service_version : str + - - - - InternalServerErrorResponse - - description : ClassVar[str] - model_config : dict - - cache_unavailable() -> Self - configuration_not_loaded() -> Self - database_error() -> Self - feedback_path_invalid(path: str) -> Self - generic() -> Self - mcp_server_registration_failed() -> Self - query_failed(cause: str) -> Self + + + + InternalServerErrorResponse + + description : ClassVar[str] + model_config : dict + + cache_unavailable() -> Self + configuration_not_loaded() -> Self + database_error() -> Self + feedback_path_invalid(path: str) -> Self + generic() -> Self + mcp_server_registration_failed() -> Self + query_failed(cause: str) -> Self - - - - LivenessResponse - - alive : Optional[bool] - model_config : dict - + + + + LivenessResponse + + alive : bool + model_config : dict + - - - - MCPClientAuthOptionsResponse - - model_config : dict - servers : Optional[list[MCPServerAuthInfo]] - + + + + MCPClientAuthOptionsResponse + + model_config : dict + servers : list[MCPServerAuthInfo] + - - - - MCPServerDeleteResponse - - model_config : dict - name : Optional[str] - resource_name : ClassVar[str] - + + + + MCPServerDeleteResponse + + model_config : dict + name : str + resource_name : ClassVar[str] + - - - - MCPServerListResponse - - model_config : dict - servers : Optional[list[MCPServerInfo]] - + + + + MCPServerListResponse + + model_config : dict + servers : list[MCPServerInfo] + - - - - MCPServerRegistrationResponse - - message : Optional[str] - model_config : dict - name : Optional[str] - provider_id : Optional[str] - url : Optional[str] - + + + + MCPServerRegistrationResponse + + message : str + model_config : dict + name : str + provider_id : str + url : str + - - - - ModelsResponse - - model_config : dict - models : Optional[list[CatalogModel]] - + + + + ModelsResponse + + model_config : dict + models : list[CatalogModel] + - - - - NotFoundResponse - - description : ClassVar[str] - model_config : dict - + + + + NotFoundResponse + + description : ClassVar[str] + model_config : dict + - - - - PromptDeleteResponse - - model_config : dict - prompt_id : Optional[str] - resource_name : ClassVar[str] - + + + + PromptDeleteResponse + + model_config : dict + prompt_id : str + resource_name : ClassVar[str] + - - - - PromptResourceResponse - - is_default : Optional[bool] - model_config : dict - prompt : Optional[str] - prompt_id : Optional[str] - variables : Optional[list[str]] - version : Optional[int] - + + + + PromptResourceResponse + + is_default : Optional[bool] + model_config : dict + prompt : Optional[str] + prompt_id : str + variables : Optional[list[str]] + version : int + - - - - PromptTooLongResponse - - description : ClassVar[str] - model_config : dict - + + + + PromptTooLongResponse + + description : ClassVar[str] + model_config : dict + - - - - PromptsListResponse - - data : Optional[list[PromptResourceResponse]] - model_config : dict - + + + + PromptsListResponse + + data : list[PromptResourceResponse] + model_config : dict + - - - - ProviderResponse - - api : Optional[str] - config : Optional[dict[str, Any]] - health : Optional[dict[str, Any]] - model_config : dict - provider_id : Optional[str] - provider_type : Optional[str] - + + + + ProviderResponse + + api : str + config : dict[str, Any] + health : dict[str, Any] + model_config : dict + provider_id : str + provider_type : str + - - - - ProvidersListResponse - - model_config : dict - providers : Optional[dict[str, list[dict[str, Any]]]] - + + + + ProvidersListResponse + + model_config : dict + providers : dict[str, list[dict[str, Any]]] + - - - - QueryResponse - - available_quotas : Optional[dict[str, int]] - context_status : Optional[ContextStatus] - conversation_id : Optional[str] - input_tokens : Optional[int] - model_config : dict - output_tokens : Optional[int] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - response : Optional[str] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] - truncated : Optional[bool] - + + + + QueryResponse + + available_quotas : dict[str, int] + context_status + conversation_id : Optional[str] + input_tokens : int + model_config : dict + output_tokens : int + rag_chunks : list[RAGChunk] + referenced_documents : list[ReferencedDocument] + response : str + tool_calls : list[ToolCallSummary] + tool_results : list[ToolResultSummary] + truncated : bool + - - - - QuotaExceededResponse - - description : ClassVar[str] - model_config : dict - - from_exception(exc: QuotaExceedError) -> Self - model(model_name: str) -> Self + + + + QuotaExceededResponse + + description : ClassVar[str] + model_config : dict + + from_exception(exc: QuotaExceedError) -> Self + model(model_name: str) -> Self - - - - RAGInfoResponse - - created_at : Optional[int] - expires_at : Optional[int] - id : Optional[str] - last_active_at : Optional[int] - model_config : dict - name : Optional[str] - object : Optional[str] - status : Optional[str] - usage_bytes : Optional[int] - + + + + RAGInfoResponse + + created_at : int + expires_at : Optional[int] + id : str + last_active_at : Optional[int] + model_config : dict + name : Optional[str] + object : str + status : str + usage_bytes : int + - - - - RAGListResponse - - model_config : dict - rags : Optional[list[str]] - + + + + RAGListResponse + + model_config : dict + rags : list[str] + - - - - ReadinessResponse - - impacts : Optional[list[str]] - model_config : dict - overall_status : Optional[HealthStatus] - providers : Optional[list[ProviderHealthStatus]] - ready : Optional[bool] - reason : Optional[str] - + + + + ReadinessResponse + + impacts : Optional[list[str]] + model_config : dict + overall_status + providers : list[ProviderHealthStatus] + ready : bool + reason : str + - - - - ResponsesResponse - - available_quotas : dict[str, int] - completed_at : Optional[int] - conversation : Optional[str] - created_at : int - error : Optional[Error] - id : str - instructions : Optional[str] - max_output_tokens : Optional[int] - max_tool_calls : Optional[int] - metadata : Optional[dict[str, str]] - model : str - model_config : dict - object : Literal['response'] - output : list[Output] - output_text : str - parallel_tool_calls : bool - previous_response_id : Optional[str] - prompt : Optional[Prompt] - reasoning : Optional[Reasoning] - safety_identifier : Optional[str] - status : str - store : Optional[bool] - temperature : Optional[float] - text : Optional[Text] - tool_choice : Optional[ToolChoice] - tools : Optional[list[OutputTool]] - top_p : Optional[float] - truncation : Optional[str] - usage : Optional[Usage] - - openapi_response() -> dict[str, Any] + + + + ResponsesResponse + + available_quotas : dict[str, int] + completed_at : Optional[int] + conversation : Optional[str] + created_at : int + error : Optional[Error] + id : str + instructions : Optional[str] + max_output_tokens : Optional[int] + max_tool_calls : Optional[int] + metadata : Optional[dict[str, str]] + model : str + model_config : dict + object : Literal['response'] + output : list[Output] + output_text : str + parallel_tool_calls : bool + previous_response_id : Optional[str] + prompt : Optional[Prompt] + reasoning : Optional[Reasoning] + safety_identifier : Optional[str] + status : str + store : Optional[bool] + temperature : Optional[float] + text : Optional[Text] + tool_choice : Optional[ToolChoice] + tools : Optional[list[OutputTool]] + top_p : Optional[float] + truncation : Optional[str] + usage : Optional[Usage] + + openapi_response() -> dict[str, Any] - - - - RlsapiV1InferData - - input_tokens : Optional[int] - output_tokens : Optional[int] - rag_chunks : Optional[list[RAGChunk]] - referenced_documents : Optional[list[ReferencedDocument]] - request_id : Optional[str] - text : Optional[str] - tool_calls : Optional[list[ToolCallSummary]] - tool_results : Optional[list[ToolResultSummary]] - + + + + RlsapiV1InferData + + input_tokens : Optional[int] + output_tokens : Optional[int] + rag_chunks : Optional[list[RAGChunk]] + referenced_documents : Optional[list[ReferencedDocument]] + request_id : Optional[str] + text : str + tool_calls : Optional[list[ToolCallSummary]] + tool_results : Optional[list[ToolResultSummary]] + - - - - RlsapiV1InferResponse - - data : Optional[RlsapiV1InferData] - model_config : dict - + + + + RlsapiV1InferResponse + + data + model_config : dict + - - - - SavedPromptDeleteResponse - - model_config : dict - prompt_id : Optional[str] - resource_name : ClassVar[str] - + + + + SavedPromptDeleteResponse + + model_config : dict + prompt_id : str + resource_name : ClassVar[str] + - - - - SavedPromptResponse - - content : Optional[str] - created_at : Optional[datetime] - id : Optional[str] - model_config : dict - name : Optional[str] - updated_at : Optional[datetime] - + + + + SavedPromptResponse + + content : str + created_at : datetime + id : str + model_config : dict + name : str + updated_at : datetime + - - - - SavedPromptsConfigResponse - - max_content_length : Optional[int] - max_display_name_length : Optional[int] - max_prompts_per_user : Optional[int] - model_config : dict - + + + + SavedPromptsConfigResponse + + max_content_length : int + max_display_name_length : int + max_prompts_per_user : int + model_config : dict + - - - - SavedPromptsListResponse - - model_config : dict - prompts : Optional[list[SavedPromptResponse]] - + + + + SavedPromptsListResponse + + model_config : dict + prompts : list[SavedPromptResponse] + - - - - ServiceUnavailableResponse - - description : ClassVar[str] - model_config : dict - + + + + ServiceUnavailableResponse + + description : ClassVar[str] + model_config : dict + - - - - ShieldsResponse - - model_config : dict - shields : Optional[list[CatalogShield]] - + + + + ShieldsResponse + + model_config : dict + shields : list[CatalogShield] + - - - - SkillsResponse - - model_config : dict - skills : Optional[list[SkillMetadata]] - + + + + SkillsResponse + + model_config : dict + skills : list[SkillMetadata] + - - - - StatusResponse - - functionality : Optional[str] - model_config : dict - status : Optional[dict[str, Any]] - + + + + StatusResponse + + functionality : str + model_config : dict + status : dict[str, Any] + - - - - StreamingInterruptResponse - - interrupted : Optional[bool] - message : Optional[str] - model_config : dict - request_id : Optional[str] - + + + + StreamingInterruptResponse + + interrupted : bool + message : str + model_config : dict + request_id : str + - - - - StreamingQueryResponse - - model_config : dict - - openapi_response() -> dict[str, Any] + + + + StreamingQueryResponse + + model_config : dict + + openapi_response() -> dict[str, Any] - - - - TooManyConcurrentRequestsResponse - - description : ClassVar[str] - model_config : dict - - file_upload() -> Self - vector_store_attach() -> Self + + + + TooManyConcurrentRequestsResponse + + description : ClassVar[str] + model_config : dict + + file_upload() -> Self + vector_store_attach() -> Self - - - - ToolsResponse - - model_config : dict - tools : Optional[list[CatalogTool]] - + + + + ToolsResponse + + model_config : dict + tools : list[CatalogTool] + - - - - UnauthorizedResponse - - description : ClassVar[str] - model_config : dict - + + + + UnauthorizedResponse + + description : ClassVar[str] + model_config : dict + - - - - UnprocessableEntityResponse - - description : ClassVar[str] - model_config : dict - + + + + UnprocessableEntityResponse + + description : ClassVar[str] + model_config : dict + - - - - VectorStoreDeleteResponse - - model_config : dict - resource_name : ClassVar[str] - vector_store_id : Optional[str] - + + + + VectorStoreDeleteResponse + + model_config : dict + resource_name : ClassVar[str] + vector_store_id : str + - - - - VectorStoreFileDeleteResponse - - file_id : Optional[str] - model_config : dict - resource_name : ClassVar[str] - + + + + VectorStoreFileDeleteResponse + + file_id : str + model_config : dict + resource_name : ClassVar[str] + - - - - VectorStoreFileResponse - - attributes : Optional[dict[str, str | float | bool]] - id : Optional[str] - last_error : Optional[str] - model_config : dict - object : Optional[str] - status : Optional[str] - vector_store_id : Optional[str] - + + + + VectorStoreFileResponse + + attributes : Optional[Mapping[str, Any]] + id : str + last_error : Optional[str] + model_config : dict + object : str + status : str + vector_store_id : str + - - - - VectorStoreFilesListResponse - - data : Optional[list[VectorStoreFileResponse]] - model_config : dict - object : Optional[str] - + + + + VectorStoreFilesListResponse + + data : list[VectorStoreFileResponse] + model_config : dict + object : str + - - - - VectorStoreResponse - - created_at : Optional[int] - expires_at : Optional[int] - id : Optional[str] - last_active_at : Optional[int] - metadata : Optional[dict[str, Any]] - model_config : dict - name : Optional[str] - status : Optional[str] - usage_bytes : Optional[int] - + + + + VectorStoreResponse + + created_at : int + expires_at : Optional[int] + id : str + last_active_at : Optional[int] + metadata : Optional[dict[str, Any]] + model_config : dict + name : str + status : str + usage_bytes : int + - - - - VectorStoresListResponse - - data : Optional[list[VectorStoreResponse]] - model_config : dict - object : Optional[str] - - - + + + + VectorStoresListResponse + + data : list[VectorStoreResponse] + model_config : dict + object : str + + + + + + + detail + + + + + + data + + diff --git a/docs/models/successful_responses.json b/docs/models/successful_responses.json index 8fd4b10f3..ed6aa8e46 100644 --- a/docs/models/successful_responses.json +++ b/docs/models/successful_responses.json @@ -863,6 +863,7 @@ "items": { "discriminator": { "mapping": { + "granite_guardian": "`#/components/schemas/`GraniteGuardianShieldConfiguration", "question_validity": "`#/components/schemas/`QuestionValidityShieldConfiguration", "redaction": "`#/components/schemas/`RedactionShieldConfiguration" }, @@ -874,6 +875,9 @@ }, { "$ref": "`#/components/schemas/`RedactionShieldConfiguration" + }, + { + "$ref": "`#/components/schemas/`GraniteGuardianShieldConfiguration" } ] }, @@ -1769,6 +1773,96 @@ "title": "FileResponse", "type": "object" }, + "GraniteGuardianConfig": { + "additionalProperties": false, + "description": "Configuration for the Granite Guardian moderation guardrail.", + "properties": { + "url": { + "description": "The model_id to use for the guard", + "title": "Base URL", + "type": "string" + }, + "api_key": { + "type": "string", + "nullable": true, + "default": null, + "description": "API key for the inference", + "title": "Granite Guardian API key" + }, + "max_retries": { + "default": 2, + "description": "Maximun number of retries", + "minimum": 0, + "maximum": 5, + "title": "Max retries", + "type": "integer" + }, + "timeout": { + "default": 30, + "description": "Request timeout in seconds", + "minimum": 5, + "maximum": 300, + "title": "Timeout", + "type": "integer" + }, + "verify_ssl": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ], + "default": true, + "description": "SSL certificate verification. Can be:\n - True: Verify using system CA bundle (default, recommended)\n - False: Disable verification (insecure, for dev only)\n - str: Path to custom CA bundle file (for internal PKI)", + "title": "Verify SSL" + }, + "risks": { + "description": "Risks to be considered while applying this guardrail", + "items": { + "$ref": "`#/components/schemas/`RiskDefinition" + }, + "title": "Defined risks", + "type": "array" + } + }, + "required": [ + "url", + "risks" + ], + "title": "GraniteGuardianConfig", + "type": "object" + }, + "GraniteGuardianShieldConfiguration": { + "additionalProperties": false, + "description": "Configuration for a named Granite Guardian guardrail shield.\n\nAttributes:\n name: Unique, user-facing name identifying this shield instance.\n provider_id: Discriminator identifying this as a granite-guardian shield.\n config: Granite-guardian-specific configuration.", + "properties": { + "name": { + "description": "Unique, user-facing name identifying this shield instance.", + "title": "Shield name", + "type": "string" + }, + "provider_id": { + "const": "granite_guardian", + "description": "Discriminator identifying this as a granite-guardian shield.", + "title": "Shield provider id", + "type": "string" + }, + "config": { + "$ref": "`#/components/schemas/`GraniteGuardianConfig", + "description": "Granite-guardian-specific configuration for this shield", + "title": "Shield configuration" + } + }, + "required": [ + "name", + "provider_id", + "config" + ], + "title": "GraniteGuardianShieldConfiguration", + "type": "object" + }, "HealthStatus": { "description": "Health status enum for provider and service health checks.\n\nThis enum serves two purposes:\n\n1. Provider-level health (returned by OGX providers):\n - OK: Provider is healthy and operational\n - ERROR: Provider is unhealthy or failed health check\n - NOT_IMPLEMENTED: Provider does not implement health checks\n - UNKNOWN: Fallback when provider status cannot be determined\n\n2. Service-level health (overall LCORE status):\n - HEALTHY: All systems operational, LLS connected, all providers healthy\n - DEGRADED: Service running with reduced functionality (e.g., LLS unavailable)\n - UNHEALTHY: Service connected but one or more providers are unhealthy", "enum": [ @@ -2433,7 +2527,7 @@ }, "OgxConfiguration": { "additionalProperties": false, - "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/)", + "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications)", "properties": { "url": { "type": "string", @@ -3057,7 +3151,7 @@ "type": "object" }, "OpenAIResponseInputToolWebSearch": { - "description": "Web search tool configuration for OpenAI response inputs.\n\n:param type: Web search tool type variant to use\n:param search_context_size: (Optional) Size of search context, must be \"low\", \"medium\", or \"high\"", + "description": "Web search tool configuration for OpenAI response inputs.", "properties": { "type": { "anyOf": [ @@ -3084,8 +3178,30 @@ "search_context_size": { "type": "string", "nullable": true, - "default": "medium", + "default": null, "title": "Search Context Size" + }, + "filters": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchFilters" + }, + { + "type": "null" + } + ], + "default": null + }, + "user_location": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchUserLocation" + }, + { + "type": "null" + } + ], + "default": null } }, "title": "OpenAIResponseInputToolWebSearch", @@ -3569,7 +3685,7 @@ "type": "object" }, "OpenAIResponseOutputMessageWebSearchToolCall": { - "description": "Web search tool call output message for OpenAI responses.\n\n:param id: Unique identifier for this tool call\n:param status: Current status of the web search operation\n:param type: Tool call type identifier, always \"web_search_call\"", + "description": "Web search tool call output message for OpenAI responses.", "properties": { "id": { "title": "Id", @@ -3584,6 +3700,24 @@ "default": "web_search_call", "title": "Type", "type": "string" + }, + "action": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`WebSearchActionSearch" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionOpenPage" + }, + { + "$ref": "`#/components/schemas/`WebSearchActionFind" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Action" } }, "required": [ @@ -3628,6 +3762,14 @@ "default": null, "title": "Effort" }, + "generate_summary": { + "type": "string", + "nullable": true, + "default": null, + "deprecated": true, + "description": "Deprecated: use 'summary' instead.", + "title": "Generate Summary" + }, "summary": { "type": "string", "nullable": true, @@ -3656,7 +3798,7 @@ "verbosity": { "type": "string", "nullable": true, - "default": null, + "default": "medium", "title": "Verbosity" } }, @@ -5576,6 +5718,69 @@ "title": "RetrievalStrategyConfiguration", "type": "object" }, + "RiskDefinition": { + "additionalProperties": false, + "description": "Definition for a custom risk category.\n\nCustom risks allow applications to add use-case-specific safety checks\nbeyond the standard harm, jailbreak, leetspeak, amnesia, and\nhistory_politics checks.\nExample:\n liability_risk = RiskDefinition(\n name=\"liability\",\n description=\"Content requesting legal, medical, or financial advice\",\n threshold=0.55,\n points=[\"input\"],\n )\n pii_risk = RiskDefinition(\n name=\"pii_request\",\n description=\"User is asking the AI to reveal personal information\",\n threshold=0.50,\n points=[\"input\", \"tool\"],\n )\nNote:\n To enable think mode (detailed reasoning) for a risk, add the risk name\n to the `thinking_enabled` list in `ModerationConfig`. Do not set\n `enable_thinking` directly - it is managed internally.", + "properties": { + "name": { + "description": "Unique identifier for this risk (e.g., 'liability', 'competitor_mention')", + "title": "Risk name", + "type": "string" + }, + "description": { + "description": "Risk definition text passed to Granite Guardian as custom_criteria", + "title": "Rist description", + "type": "string" + }, + "threshold": { + "default": 0.65, + "description": "Score threshold for flagging (lower = more sensitive)", + "maximum": 1.0, + "minimum": 0.0, + "title": "Risk threshold", + "type": "number" + }, + "enabled": { + "default": true, + "description": "Whether to run this check", + "title": "Risk enabled", + "type": "boolean" + }, + "enable_thinking": { + "default": false, + "description": "Internal field - set via ModerationConfig.thinking_enabled list, not directly. When True, Granite Guardian provides detailed reasoning before scoring.", + "title": "Risk enable thinking", + "type": "boolean" + }, + "points": { + "description": "Where this risk is evaluated: `input` (user message), `output` (model response), or `tool` (tool/MCP content).", + "items": { + "enum": [ + "input", + "output", + "tool" + ], + "type": "string" + }, + "minItems": 1, + "title": "Guardrail points", + "type": "array" + }, + "violation_message": { + "description": "Message to be displayed when this risk is violated", + "title": "Violation message", + "type": "string" + } + }, + "required": [ + "name", + "description", + "points", + "violation_message" + ], + "title": "RiskDefinition", + "type": "object" + }, "RlsapiV1Configuration": { "additionalProperties": false, "description": "Configuration for the rlsapi v1 /infer endpoint.\n\nSettings specific to the RHEL Lightspeed Command Line Assistant (CLA)\nstateless inference endpoint. Kept separate from shared configuration\nsections so that CLA-specific options do not affect other endpoints.", @@ -5931,7 +6136,7 @@ "type": "object" }, "SearchRankingOptions": { - "description": "Options for ranking and filtering search results.\n\nThis class configures how search results are ranked and filtered. You can use algorithm-based\nrerankers (weighted, RRF) or neural rerankers. Defaults from VectorStoresConfig are\nused when parameters are not provided.\n\nExamples:\n # Weighted ranker with custom alpha\n SearchRankingOptions(ranker=\"weighted\", alpha=0.7)\n\n # RRF ranker with custom impact factor\n SearchRankingOptions(ranker=\"rrf\", impact_factor=50.0)\n\n # Use config defaults (just specify ranker type)\n SearchRankingOptions(ranker=\"weighted\") # Uses alpha from VectorStoresConfig\n\n # Score threshold filtering\n SearchRankingOptions(ranker=\"weighted\", score_threshold=0.5)\n\n:param ranker: (Optional) Name of the ranking algorithm to use. Supported values:\n - \"weighted\": Weighted combination of vector and keyword scores\n - \"rrf\": Reciprocal Rank Fusion algorithm\n - \"neural\": Neural reranking model (requires model parameter)\n Note: For OpenAI API compatibility, any string value is accepted, but only the above values are supported.\n:param score_threshold: (Optional) Minimum relevance score threshold for results. Default: 0.0\n:param alpha: (Optional) Weight factor for weighted ranker (0-1).\n - 0.0 = keyword only\n - 0.5 = equal weight (default)\n - 1.0 = vector only\n Only used when ranker=\"weighted\" and weights is not provided.\n Falls back to VectorStoresConfig.chunk_retrieval_params.weighted_search_alpha if not provided.\n:param impact_factor: (Optional) Impact factor (k) for RRF algorithm.\n Lower values emphasize higher-ranked results. Default: 60.0 (optimal from research).\n Only used when ranker=\"rrf\".\n Falls back to VectorStoresConfig.chunk_retrieval_params.rrf_impact_factor if not provided.\n:param weights: (Optional) Dictionary of weights for combining different signal types.\n Keys can be \"vector\", \"keyword\", \"neural\". Values should sum to 1.0.\n Used when combining algorithm-based reranking with neural reranking.\n Example: {\"vector\": 0.3, \"keyword\": 0.3, \"neural\": 0.4}\n:param model: (Optional) Model identifier for neural reranker (e.g., \"transformers/Qwen/Qwen3-Reranker-0.6B\").\n Required when ranker=\"neural\" or when weights contains \"neural\".", + "description": "Options for ranking and filtering search results.\n\nThis class configures how search results are ranked and filtered. You can use algorithm-based\nrerankers (weighted, RRF) or neural rerankers. Defaults from VectorStoresConfig are\nused when parameters are not provided.\n\nExamples:\n # Weighted ranker with custom alpha\n SearchRankingOptions(ranker=\"weighted\", alpha=0.7)\n\n # RRF ranker with custom impact factor\n SearchRankingOptions(ranker=\"rrf\", impact_factor=50.0)\n\n # Use config defaults (just specify ranker type)\n SearchRankingOptions(ranker=\"weighted\") # Uses alpha from VectorStoresConfig\n\n # Score threshold filtering\n SearchRankingOptions(ranker=\"weighted\", score_threshold=0.5)\n\n:param ranker: (Optional) Name of the ranking algorithm to use. Supported values:\n - \"weighted\": Weighted combination of vector and keyword scores\n - \"rrf\": Reciprocal Rank Fusion algorithm\n - \"neural\": Neural reranking model (requires model parameter)\n Note: For OpenAI API compatibility, any string value is accepted, but only the above values are supported.\n:param score_threshold: (Optional) Minimum relevance score threshold for results. Default: 0.0\n:param alpha: (Optional) Weight factor for weighted ranker (0-1).\n - 0.0 = keyword only\n - 0.5 = equal weight (default)\n - 1.0 = vector only\n Only used when ranker=\"weighted\" and weights is not provided.\n Falls back to VectorStoresConfig.chunk_retrieval_params.weighted_search_alpha if not provided.\n:param impact_factor: (Optional) Impact factor (k) for RRF algorithm.\n Lower values emphasize higher-ranked results. Default: 60.0 (optimal from research).\n Only used when ranker=\"rrf\".\n Falls back to VectorStoresConfig.chunk_retrieval_params.rrf_impact_factor if not provided.\n:param weights: (Optional) Dictionary of weights for combining different signal types.\n Keys can be \"vector\", \"keyword\", \"neural\". Values should sum to 1.0.\n Used when combining algorithm-based reranking with neural reranking.\n Example: {\"vector\": 0.3, \"keyword\": 0.3, \"neural\": 0.4}\n:param model: (Optional) Model identifier for neural reranker\n (e.g., \"sentence-transformers/Qwen/Qwen3-Reranker-0.6B\"). Required when ranker=\"neural\" or when\n weights contains \"neural\".", "properties": { "ranker": { "type": "string", @@ -7009,6 +7214,152 @@ }, "title": "VectorStoresListResponse", "type": "object" + }, + "WebSearchActionFind": { + "description": "Web search action: searches for a pattern within a loaded page.", + "properties": { + "type": { + "const": "find_in_page", + "default": "find_in_page", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + }, + "pattern": { + "title": "Pattern", + "type": "string" + } + }, + "required": [ + "url", + "pattern" + ], + "title": "WebSearchActionFind", + "type": "object" + }, + "WebSearchActionOpenPage": { + "description": "Web search action: opens a specific URL from search results.", + "properties": { + "type": { + "const": "open_page", + "default": "open_page", + "title": "Type", + "type": "string" + }, + "url": { + "type": "string", + "nullable": true, + "default": null, + "title": "Url" + } + }, + "title": "WebSearchActionOpenPage", + "type": "object" + }, + "WebSearchActionSearch": { + "description": "Web search action: performs a search query.", + "properties": { + "type": { + "const": "search", + "default": "search", + "title": "Type", + "type": "string" + }, + "query": { + "title": "Query", + "type": "string" + }, + "queries": { + "type": "array", + "nullable": true, + "default": null, + "title": "Queries" + }, + "sources": { + "type": "array", + "nullable": true, + "default": null, + "title": "Sources" + } + }, + "required": [ + "query" + ], + "title": "WebSearchActionSearch", + "type": "object" + }, + "WebSearchFilters": { + "description": "Domain filters for web search results.", + "properties": { + "allowed_domains": { + "type": "array", + "nullable": true, + "default": null, + "title": "Allowed Domains" + } + }, + "title": "WebSearchFilters", + "type": "object" + }, + "WebSearchSource": { + "description": "A source URL returned by a web search action.", + "properties": { + "type": { + "const": "url", + "default": "url", + "title": "Type", + "type": "string" + }, + "url": { + "title": "Url", + "type": "string" + } + }, + "required": [ + "url" + ], + "title": "WebSearchSource", + "type": "object" + }, + "WebSearchUserLocation": { + "description": "Approximate user location to refine web search results.", + "properties": { + "type": { + "const": "approximate", + "default": "approximate", + "title": "Type", + "type": "string" + }, + "city": { + "type": "string", + "nullable": true, + "default": null, + "title": "City" + }, + "country": { + "type": "string", + "nullable": true, + "default": null, + "title": "Country" + }, + "region": { + "type": "string", + "nullable": true, + "default": null, + "title": "Region" + }, + "timezone": { + "type": "string", + "nullable": true, + "default": null, + "title": "Timezone" + } + }, + "title": "WebSearchUserLocation", + "type": "object" } } }, diff --git a/docs/models/successful_responses.md b/docs/models/successful_responses.md index 649b0c527..c6d1888cf 100644 --- a/docs/models/successful_responses.md +++ b/docs/models/successful_responses.md @@ -696,6 +696,43 @@ Attributes: | object | string | Object type | +## GraniteGuardianConfig + + +Configuration for the Granite Guardian moderation guardrail. + + +| Field | Type | Description | +|-------|------|-------------| +| url | string | The model_id to use for the guard | +| api_key | string | API key for the inference | +| max_retries | integer | Maximun number of retries | +| timeout | integer | Request timeout in seconds | +| verify_ssl | | SSL certificate verification. Can be: + - True: Verify using system CA bundle (default, recommended) + - False: Disable verification (insecure, for dev only) + - str: Path to custom CA bundle file (for internal PKI) | +| risks | array | Risks to be considered while applying this guardrail | + + +## GraniteGuardianShieldConfiguration + + +Configuration for a named Granite Guardian guardrail shield. + +Attributes: + name: Unique, user-facing name identifying this shield instance. + provider_id: Discriminator identifying this as a granite-guardian shield. + config: Granite-guardian-specific configuration. + + +| Field | Type | Description | +|-------|------|-------------| +| name | string | Unique, user-facing name identifying this shield instance. | +| provider_id | string | Discriminator identifying this as a granite-guardian shield. | +| config | | Granite-guardian-specific configuration for this shield | + + ## HealthStatus @@ -1051,7 +1088,7 @@ Useful resources: - [OGX](https://ogx-ai.github.io/) - [Python OGX client](https://github.com/ogx-ai/ogx-client-python) - - [Build AI Applications with OGX](https://ogx-ai.github.io/) + - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications) | Field | Type | Description | @@ -1385,14 +1422,13 @@ Function tool configuration for OpenAI response inputs. Web search tool configuration for OpenAI response inputs. -:param type: Web search tool type variant to use -:param search_context_size: (Optional) Size of search context, must be "low", "medium", or "high" - | Field | Type | Description | |-------|------|-------------| | type | | | | search_context_size | string | | +| filters | | | +| user_location | | | ## OpenAIResponseMCPApprovalRequest @@ -1595,16 +1631,13 @@ A summary of reasoning output from the model. Web search tool call output message for OpenAI responses. -:param id: Unique identifier for this tool call -:param status: Current status of the web search operation -:param type: Tool call type identifier, always "web_search_call" - | Field | Type | Description | |-------|------|-------------| | id | string | | | status | string | | | type | string | | +| action | | | ## OpenAIResponsePrompt @@ -1639,6 +1672,7 @@ Controls how much reasoning the model performs before generating a response. | Field | Type | Description | |-------|------|-------------| | effort | string | | +| generate_summary | string | Deprecated: use 'summary' instead. | | summary | string | Summary mode for reasoning output. One of 'auto', 'concise', or 'detailed'. | @@ -2362,6 +2396,44 @@ Configuration for a single retrieval strategy (inline or tool). | reranker | | Neural reranking of RAG chunks using cross-encoder. Only applicable to inline retrieval. | +## RiskDefinition + + +Definition for a custom risk category. + +Custom risks allow applications to add use-case-specific safety checks +beyond the standard harm, jailbreak, leetspeak, amnesia, and +history_politics checks. +Example: + liability_risk = RiskDefinition( + name="liability", + description="Content requesting legal, medical, or financial advice", + threshold=0.55, + points=["input"], + ) + pii_risk = RiskDefinition( + name="pii_request", + description="User is asking the AI to reveal personal information", + threshold=0.50, + points=["input", "tool"], + ) +Note: + To enable think mode (detailed reasoning) for a risk, add the risk name + to the `thinking_enabled` list in `ModerationConfig`. Do not set + `enable_thinking` directly - it is managed internally. + + +| Field | Type | Description | +|-------|------|-------------| +| name | string | Unique identifier for this risk (e.g., 'liability', 'competitor_mention') | +| description | string | Risk definition text passed to Granite Guardian as custom_criteria | +| threshold | number | Score threshold for flagging (lower = more sensitive) | +| enabled | boolean | Whether to run this check | +| enable_thinking | boolean | Internal field - set via ModerationConfig.thinking_enabled list, not directly. When True, Granite Guardian provides detailed reasoning before scoring. | +| points | array | Where this risk is evaluated: `input` (user message), `output` (model response), or `tool` (tool/MCP content). | +| violation_message | string | Message to be displayed when this risk is violated | + + ## RlsapiV1Configuration @@ -2566,8 +2638,9 @@ Examples: Keys can be "vector", "keyword", "neural". Values should sum to 1.0. Used when combining algorithm-based reranking with neural reranking. Example: {"vector": 0.3, "keyword": 0.3, "neural": 0.4} -:param model: (Optional) Model identifier for neural reranker (e.g., "transformers/Qwen/Qwen3-Reranker-0.6B"). - Required when ranker="neural" or when weights contains "neural". +:param model: (Optional) Model identifier for neural reranker + (e.g., "sentence-transformers/Qwen/Qwen3-Reranker-0.6B"). Required when ranker="neural" or when + weights contains "neural". | Field | Type | Description | @@ -3040,3 +3113,80 @@ Attributes: |-------|------|-------------| | data | array | List of vector stores | | object | string | Object type | + + +## WebSearchActionFind + + +Web search action: searches for a pattern within a loaded page. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | +| pattern | string | | + + +## WebSearchActionOpenPage + + +Web search action: opens a specific URL from search results. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | + + +## WebSearchActionSearch + + +Web search action: performs a search query. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| query | string | | +| queries | array | | +| sources | array | | + + +## WebSearchFilters + + +Domain filters for web search results. + + +| Field | Type | Description | +|-------|------|-------------| +| allowed_domains | array | | + + +## WebSearchSource + + +A source URL returned by a web search action. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| url | string | | + + +## WebSearchUserLocation + + +Approximate user location to refine web search results. + + +| Field | Type | Description | +|-------|------|-------------| +| type | string | | +| city | string | | +| country | string | | +| region | string | | +| timezone | string | | diff --git a/docs/testing/e2e_testing.md b/docs/testing/e2e_testing.md index f2666134c..0305b8dd4 100644 --- a/docs/testing/e2e_testing.md +++ b/docs/testing/e2e_testing.md @@ -15,8 +15,9 @@ This guide describes how to run, extend, and understand the Lightspeed Core Stac 7. [Configuration Files](#configuration-files) 8. [Feature Files and Steps](#feature-files-and-steps) 9. [Gherkin Keywords in Feature Files](#gherkin-keywords-in-feature-files) -10. [Writing New Scenarios](#writing-new-scenarios) -11. [Troubleshooting](#troubleshooting) +10. [Choosing the Test Layer: E2E or Integration?](#choosing-the-test-layer-e2e-or-integration) +11. [Writing New Scenarios](#writing-new-scenarios) +12. [Troubleshooting](#troubleshooting) --- @@ -142,10 +143,10 @@ uv run behave tests/e2e/features/health.feature --tags=-skip-in-library-mode | `E2E_DEPLOYMENT_MODE` | `server` | `server` or `library`. Drives config paths and which scenarios run (e.g. `@skip-in-library-mode`). | | `E2E_LSC_HOSTNAME` | `localhost` | Host of the Lightspeed Core Stack API. | | `E2E_LSC_PORT` | `8080` | Port of the Lightspeed Core Stack API. | -| `E2E_LLAMA_HOSTNAME` | `localhost` | Host of the OGX service (server mode). | -| `E2E_LLAMA_PORT` | `8321` | Port of the OGX service. | -| `E2E_LLAMA_STACK_URL` | — | Full base URL for OGX (overrides host/port if set). Used by shield helpers. | -| `E2E_LLAMA_STACK_API_KEY` | `xyzzy` | API key for OGX client (e.g. shield API). | +| `E2E_OGX_HOSTNAME` | `localhost` | Host of the OGX service (server mode). GitHub Actions repo variable: `E2E_LLAMA_HOSTNAME` (compose maps it here). | +| `E2E_OGX_PORT` | `8321` | Port of the OGX service. | +| `E2E_OGX_STACK_URL` | — | Full base URL for OGX (overrides host/port if set). Used by shield helpers. | +| `E2E_OGX_STACK_API_KEY` | `xyzzy` | API key for OGX client (e.g. shield API). | | `E2E_DEFAULT_MODEL_OVERRIDE` | — | Override default LLM model id (e.g. `gpt-4o-mini`). | | `E2E_DEFAULT_PROVIDER_OVERRIDE` | — | Override default provider id (e.g. `openai`). | | `FAISS_VECTOR_STORE_ID` | — | Vector store id for FAISS-related scenarios. | @@ -336,9 +337,50 @@ Here, **Given** sets state, **When** performs the HTTP call, **Then** and **And* --- +## Choosing the Test Layer: E2E or Integration? + +Before writing a scenario, decide whether it belongs here at all. The suite has +three layers, and the boundary between the top two is strict: + +| Layer | Location | Talks to | May touch `src/`? | +|---|---|---|---| +| Unit | `tests/unit/` | one function or class, everything else mocked | yes | +| Integration | `tests/integration/` (pytest) | real configuration loading, real database, real pipelines in-process; external services (OGX, LLM providers) mocked; repo CLIs as subprocesses | yes | +| E2E | `tests/e2e/` (behave) | a deployed stack, through its public surfaces only: the HTTP API, container lifecycle and logs, configuration files the harness applies | **never** | + +The rule for e2e: **a step definition must not import from, invoke, or shell out +to anything under `src/`.** The moment it does, the scenario stops proving what a +deployed stack does and starts proving what a checked-out source tree does — that +is an integration test, and it belongs in `tests/integration/` as pytest, where it +runs in seconds without Docker. + +A quick test: *could this scenario run unchanged against a container image, with +no source checkout on the machine?* If yes, it is e2e. If it needs +`src/lightspeed_stack.py`, `src/ogx_configuration.py`, a Python import from the +service, or a subprocess of a repo entrypoint, it is integration. + +Typical consequences: + +- Configuration **validation**, **migration** (`--migrate-config`) and run.yaml + **synthesis** are integration concerns: they exercise CLIs and the config + pipeline, not a running service. See `tests/integration/test_unified_synthesis.py` + and `tests/integration/test_unified_mode_cli.py`. +- **Boot** scenarios (apply a config, restart, hit `readiness` and `query`) and + **log-evidence** scenarios (`docker logs `) are e2e: they observe the + deployed stack from outside. +- If a scenario needs a generated artifact as its starting point (for example a + migrated configuration), commit the artifact as a fixture and add an + integration test that guards it against drift, rather than generating it inside + the e2e step. + +The integration side of this boundary is described in +[tests/integration/README.md](../../tests/integration/README.md#what-to-test). + +--- + ## Writing New Scenarios -1. **Choose or add a feature file** under `tests/e2e/features/` and use existing steps where possible. If you add a new file, **add it to `tests/e2e/test_list.txt`** so the suite runs it. +1. **Confirm the scenario is e2e at all** — see [Choosing the Test Layer](#choosing-the-test-layer-e2e-or-integration). Then **choose or add a feature file** under `tests/e2e/features/` and use existing steps where possible. If you add a new file, **add it to `tests/e2e/test_list.txt`** so the suite runs it. 2. **Use tags** for mode-dependent or config-dependent behavior (`@skip-in-library-mode`, `@Authorized`, etc.). **Adding a tag that switches configuration** (e.g. a new feature-level or scenario-level config) usually means you must also add or change a **Lightspeed Stack config** file under `configuration/server-mode/` or `library-mode/` and wire the tag in `environment.py` (e.g. in `before_feature` / `after_feature` or `before_scenario` / `after_scenario`) so the config is applied and the container restarted when the tag is active. 3. **Use placeholders** `{MODEL}` and `{PROVIDER}` in request bodies so the same scenario works with different backends. 4. **Add step definitions** in the appropriate `features/steps/*.py` if you need new steps; reuse `context` for host, port, auth, and responses. diff --git a/docs/testing/testing.md b/docs/testing/testing.md index 4331f1dd8..cee759c36 100644 --- a/docs/testing/testing.md +++ b/docs/testing/testing.md @@ -132,6 +132,7 @@ As specified in Definition of Done, new changes need to be covered by tests. Integration tests are based on the [Pytest framework](https://docs.pytest.org/en/) and code coverage is measured by the plugin [pytest-cov](https://github.com/pytest-dev/pytest-cov). For mocking and patching, the [unittest framework](https://docs.python.org/3/library/unittest.html) is used. * Defined in [tests/integration](https://github.com/lightspeed-core/lightspeed-stack/tree/main/tests/integration) +* **Integration or e2e?** Integration tests may touch `src/` (in-process pipelines, repo CLIs as subprocesses); e2e tests never do. See [Choosing the Test Layer](e2e_testing.md#choosing-the-test-layer-e2e-or-integration). diff --git a/docs/user_doc/a2a_protocol.md b/docs/user_doc/a2a_protocol.md index 41125f314..728d26045 100644 --- a/docs/user_doc/a2a_protocol.md +++ b/docs/user_doc/a2a_protocol.md @@ -789,5 +789,5 @@ The protocol version is included in the agent card response and indicates which ## References - [A2A Protocol Specification](https://github.com/google/A2A) -- [OGX Documentation](https://llama-stack.readthedocs.io/) +- [OGX Documentation](https://ogx-ai.github.io/) - [FastAPI Documentation](https://fastapi.tiangolo.com/) diff --git a/docs/user_doc/both_services_in_container.svg b/docs/user_doc/both_services_in_container.svg index 8f20cce84..c01084d88 100644 --- a/docs/user_doc/both_services_in_container.svg +++ b/docs/user_doc/both_services_in_container.svg @@ -1,7 +1,7 @@ - + @@ -151,7 +151,7 @@
-
llama-stack as separate server
+
OGX as separate server
diff --git a/docs/user_doc/byok_guide.md b/docs/user_doc/byok_guide.md index 16de6160b..d18fe99c0 100644 --- a/docs/user_doc/byok_guide.md +++ b/docs/user_doc/byok_guide.md @@ -327,7 +327,7 @@ Both modes can be enabled simultaneously. Choose based on your latency and contr > [!TIP] > A ready-to-use example combining BYOK and OKP is available at -> [`examples/lightspeed-stack-byok-okp-rag.yaml`](../examples/lightspeed-stack-byok-okp-rag.yaml). +> [`examples/lightspeed-stack-byok-okp-rag.yaml`](../../examples/lightspeed-stack-byok-okp-rag.yaml). --- @@ -463,7 +463,7 @@ rag: > [!TIP] > A complete working example combining BYOK and OKP is available at -> [`examples/lightspeed-stack-byok-okp-rag.yaml`](../examples/lightspeed-stack-byok-okp-rag.yaml). +> [`examples/lightspeed-stack-byok-okp-rag.yaml`](../../examples/lightspeed-stack-byok-okp-rag.yaml). --- diff --git a/docs/user_doc/config.html b/docs/user_doc/config.html index d6990e9e3..1fa6c84f9 100644 --- a/docs/user_doc/config.html +++ b/docs/user_doc/config.html @@ -629,9 +629,9 @@

Configuration

Optional explicit marker of the configuration format. When set, it must agree with the shape detected from the configuration body: ‘unified’ requires a synthesis input (a non-empty inference.providers, a -non-empty vector_store.providers, or a ogx.config block), -‘legacy’ requires no synthesis input. Reserved as the lever for a future -breaking change of the unified schema (R11). +non-empty vector_store.providers, or an ogx.config block), ‘legacy’ +requires no synthesis input. Reserved as the lever for a future breaking +change of the unified schema (R11). service @@ -642,9 +642,8 @@

Configuration

ogx - This section contains OGX configuration. Lightspeed Core -Stack service can call OGX in library mode or in server -mode. + This section contains OGX configuration. Lightspeed Core Stack +service can call OGX in library mode or in server mode. user_data_collection @@ -664,8 +663,8 @@

Configuration

MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured in this section. Only MCP servers defined in the lightspeed-stack.yaml configuration are available to the -agents. Tools configured in the OGX run.yaml are not accessible -to lightspeed-core agents. +agents. Tools configured in the OGX run.yaml are not accessible to +lightspeed-core agents. authentication @@ -945,8 +944,8 @@

FaissVectorStoreProvider

id string - OGX vector_io provider_id. Surrounding whitespace is -stripped before validation and emission. + OGX vector_io provider_id. Surrounding whitespace is stripped before +validation and emission. embedding_model @@ -989,6 +988,87 @@

FaissVectorStoreProviderConfig

+

GraniteGuardianConfig

+

Configuration for the Granite Guardian moderation guardrail.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
urlstringThe model_id to use for the guard
api_keystringAPI key for the inference
max_retriesintegerMaximun number of retries
timeoutintegerRequest timeout in seconds
verify_ssl + SSL certificate verification. Can be:
+
    +
  • True: Verify using system CA bundle (default, recommended)
  • +
  • False: Disable verification (insecure, for dev only)
  • +
  • str: Path to custom CA bundle file (for internal PKI) | | risks | +array | Risks to be considered while applying this guardrail |
  • +
+

GraniteGuardianShieldConfiguration

+

Configuration for a named Granite Guardian guardrail shield.

+

Attributes: name: Unique, user-facing name identifying this shield +instance. provider_id: Discriminator identifying this as a +granite-guardian shield. config: Granite-guardian-specific +configuration.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
namestringUnique, user-facing name identifying this shield instance.
provider_idstringDiscriminator identifying this as a granite-guardian shield.
config + Granite-guardian-specific configuration for this shield

InMemoryCacheConfig

In-memory cache configuration.

@@ -1220,117 +1300,13 @@

JwtRoleRule

-

LlamaStackConfiguration

-

OGX configuration.

-

OGX is a comprehensive system that provides a uniform set of -tools for building, scaling, and deploying generative AI applications, -enabling developers to create, integrate, and orchestrate multiple AI -services and capabilities into an adaptable setup.

-

Useful resources:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldTypeDescription
urlstringURL to OGX service; used when library mode is disabled. Must -be a valid HTTP or HTTPS URL.
api_keystringAPI key to access OGX service
use_as_library_clientbooleanWhen set to true OGX will be used in library mode, not in -server mode (default)
library_client_config_pathstringPath to configuration file used when OGX is run in library -mode. DEPRECATED legacy two-file setup: logs a startup warning since 0.6 -and is removed in 0.8 — use unified mode instead (the config block -below, and/or the root-level inference.providers section); migrate with -lightspeed-stack –migrate-config.
timeoutintegerTimeout in seconds for requests to OGX service. Default is -180 seconds (3 minutes) to accommodate long-running RAG queries.
max_retriesintegerMaximum number of connection attempts before giving up. Used on -startup to connect to OGX and retrieve its version. Connection -attempts are retried with a fixed delay to handle the case where Llama -Stack is still starting up (e.g., when running as a sidecar in the same -pod).
retry_delayintegerDelay in seconds between retry attempts. Used on startup to connect -to OGX and retrieve its version. Connection attempts are retried -with a fixed delay to handle the case where OGX is still -starting up (e.g., when running as a sidecar in the same pod).
allow_degraded_modebooleanIf enabled, Lightspeed Core can be started even when OGX is -not accessible (valid for server mode only)
config - Backend-specific knobs for unified mode, where LCORE synthesizes the -OGX run.yaml instead of reading an external file. Holds the -baseline selector, an optional profile path, and a raw native_override -escape hatch. Backend-agnostic high-level sections -(e.g. inference.providers) live at the configuration root, not here. -Mutually exclusive with library_client_config_path; that cross-field -check lives on the root Configuration model. When set in library mode, -library_client_config_path is not required.

ModelContextProtocolServer

Model context protocol server configuration.

MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured by this structure. Only MCP servers defined in the lightspeed-stack.yaml configuration are available -to the agents. Tools configured in the OGX run.yaml are not -accessible to lightspeed-core agents.

+to the agents. Tools configured in the OGX run.yaml are not accessible +to lightspeed-core agents.

Useful resources:

  • @@ -1408,8 +1384,8 @@

    ModelContextProtocolServer

    timeout integer Timeout in seconds for requests to the MCP server. If not specified, -the default timeout from OGX will be used. Note: This field is -reserved for future use when OGX adds timeout support. +the default timeout from OGX will be used. Note: This field is reserved +for future use when OGX adds timeout support. @@ -1441,6 +1417,108 @@

    ObservabilityConfiguration

    +

    OgxConfiguration

    +

    OGX configuration.

    +

    OGX is a comprehensive system that provides a uniform set of tools +for building, scaling, and deploying generative AI applications, +enabling developers to create, integrate, and orchestrate multiple AI +services and capabilities into an adaptable setup.

    +

    Useful resources:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldTypeDescription
    urlstringURL to OGX service; used when library mode is disabled. Must be a +valid HTTP or HTTPS URL.
    api_keystringAPI key to access OGX service
    use_as_library_clientbooleanWhen set to true OGX will be used in library mode, not in server +mode (default)
    library_client_config_pathstringPath to configuration file used when OGX is run in library mode. +DEPRECATED legacy two-file setup: logs a startup warning since 0.6 and +is removed in 0.8 — use unified mode instead (the config block below, +and/or the root-level inference.providers section); migrate with +lightspeed-stack –migrate-config.
    timeoutintegerTimeout in seconds for requests to OGX service. Default is 180 +seconds (3 minutes) to accommodate long-running RAG queries.
    max_retriesintegerMaximum number of connection attempts before giving up. Used on +startup to connect to OGX and retrieve its version. Connection attempts +are retried with a fixed delay to handle the case where OGX is still +starting up (e.g., when running as a sidecar in the same pod).
    retry_delayintegerDelay in seconds between retry attempts. Used on startup to connect +to OGX and retrieve its version. Connection attempts are retried with a +fixed delay to handle the case where OGX is still starting up (e.g., +when running as a sidecar in the same pod).
    allow_degraded_modebooleanIf enabled, Lightspeed Core can be started even when OGX is not +accessible (valid for server mode only)
    config + Backend-specific knobs for unified mode, where LCORE synthesizes the +OGX run.yaml instead of reading an external file. Holds the baseline +selector, an optional profile path, and a raw native_override escape +hatch. Backend-agnostic high-level sections (e.g. inference.providers) +live at the configuration root, not here. Mutually exclusive with +library_client_config_path; that cross-field check lives on the root +Configuration model. When set in library mode, +library_client_config_path is not required.

    OkpConfiguration

    OKP (Offline Knowledge Portal) provider configuration.

    Controls provider-specific behaviour for the OKP vector store. Only @@ -1515,8 +1593,8 @@

    PgvectorVectorStoreProvider

    id string - OGX vector_io provider_id. Surrounding whitespace is -stripped before validation and emission. + OGX vector_io provider_id. Surrounding whitespace is stripped before +validation and emission. embedding_model @@ -2238,6 +2316,76 @@

    RetrievalStrategyConfiguration

    +

    RiskDefinition

    +

    Definition for a custom risk category.

    +

    Custom risks allow applications to add use-case-specific safety +checks beyond the standard harm, jailbreak, leetspeak, amnesia, and +history_politics checks. Example: liability_risk = RiskDefinition( +name=“liability”, description=“Content requesting legal, medical, or +financial advice”, threshold=0.55, points=[“input”], ) pii_risk = +RiskDefinition( name=“pii_request”, description=“User is asking the AI +to reveal personal information”, threshold=0.50, points=[“input”, +“tool”], ) Note: To enable think mode (detailed reasoning) for a risk, +add the risk name to the thinking_enabled list in +ModerationConfig. Do not set enable_thinking +directly - it is managed internally.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    FieldTypeDescription
    namestringUnique identifier for this risk (e.g., ‘liability’, +‘competitor_mention’)
    descriptionstringRisk definition text passed to Granite Guardian as +custom_criteria
    thresholdnumberScore threshold for flagging (lower = more sensitive)
    enabledbooleanWhether to run this check
    enable_thinkingbooleanInternal field - set via ModerationConfig.thinking_enabled list, not +directly. When True, Granite Guardian provides detailed reasoning before +scoring.
    pointsarrayWhere this risk is evaluated: input (user message), +output (model response), or tool (tool/MCP +content).
    violation_messagestringMessage to be displayed when this risk is violated

    RlsapiV1Configuration

    Configuration for the rlsapi v1 /infer endpoint.

    Settings specific to the RHEL Lightspeed Command Line Assistant (CLA) @@ -2385,26 +2533,55 @@

    ServiceConfiguration

    Number of Uvicorn worker processes to start + max_concurrent_file_uploads + integer + Maximum number of file uploads (POST /v1/files) processed +concurrently per worker. Each in-flight upload can hold up to the +configured maximum file size in memory, so this bounds worst-case memory +usage from concurrent uploads. Additional uploads are rejected with 429 +until a slot frees up. + + + max_concurrent_vector_store_attaches + integer + Maximum number of vector store file attachments (POST +/v1/vector-stores/{id}/files) processed concurrently per worker. Each +in-flight attachment re-reads and chunks the source file, so this bounds +worst-case memory usage independently of max_concurrent_file_uploads. +Additional attachments are rejected with 429 until a slot frees up. + + + delete_file_after_vector_store_attach + boolean + When true, deletes a file (POST /v1/files) once it has been +successfully attached to a vector store, since the vector store keeps +its own chunked/embedded copy of the content. Defaults to false to match +the OpenAI Files API, where a file remains reusable across multiple +vector stores until the caller explicitly deletes it - enabling this +makes attached files single-use: re-attaching the same file_id to +another vector store will fail once it has been deleted. + + color_log boolean Enables colorized logging - + access_log boolean Enables logging of all access information - + tls_config Transport Layer Security configuration for HTTPS support - + root_path string ASGI root path for serving behind a reverse proxy on a subpath - + cors Cross-Origin Resource Sharing configuration for cross-domain @@ -2622,26 +2799,25 @@

    TrustedProxyServiceAccount

    UnifiedInferenceProvider

    A high-level inference provider entry for unified-mode synthesis.

    Operators describe inference providers at this high level -(backend-agnostic vocabulary) instead of authoring raw OGX -provider blocks. The synthesizer -(apply_high_level_inference) expands each entry into a -OGX providers.inference entry, mapping -type to a provider_type and emitting +(backend-agnostic vocabulary) instead of authoring raw OGX provider +blocks. The synthesizer (apply_high_level_inference) +expands each entry into an OGX providers.inference entry, +mapping type to a provider_type and emitting ${env.<VAR>} references for secrets (never literal values).

    Attributes: type: Canonical provider identifier. Vendor-neutral so it survives a future backend change; each backend-specific synthesizer maps it to its own provider vocabulary. id: Optional identifier emitted as -the OGX provider_id. When omitted, synthesized as type with -underscores hyphenated. If set, must be non-empty after stripping -whitespace and may contain only lowercase letters, digits, underscores, -and hyphens. api_key_env: Name of the environment variable holding the -provider API key. Emitted verbatim as ${env.<name>} -so the secret never lands on disk resolved. allowed_models: Optional -allow-list of model identifiers passed through to the synthesized -provider config. extra: Additional provider-config keys merged verbatim -into the synthesized provider’s config block — an escape -hatch for provider-specific knobs not modeled here.

    +the OGX provider_id. When omitted, synthesized as type with underscores +hyphenated. If set, must be non-empty after stripping whitespace and may +contain only lowercase letters, digits, underscores, and hyphens. +api_key_env: Name of the environment variable holding the provider API +key. Emitted verbatim as ${env.<name>} so the secret +never lands on disk resolved. allowed_models: Optional allow-list of +model identifiers passed through to the synthesized provider config. +extra: Additional provider-config keys merged verbatim into the +synthesized provider’s config block — an escape hatch for +provider-specific knobs not modeled here.

    @@ -2659,15 +2835,15 @@

    UnifiedInferenceProvider

    - + - @@ -2690,23 +2866,24 @@

    UnifiedInferenceProvider

    type stringCanonical, backend-agnostic provider identifier mapped to a Llama -Stack provider_type by the synthesizer.Canonical, backend-agnostic provider identifier mapped to an OGX +provider_type by the synthesizer.
    id stringOptional identifier emitted as the OGX provider_id. When -omitted, synthesized as type with underscores hyphenated. If set, must -be non-empty after stripping whitespace and may contain only lowercase + Optional identifier emitted as the OGX provider_id. When omitted, +synthesized as type with underscores hyphenated. If set, must be +non-empty after stripping whitespace and may contain only lowercase letters, digits, underscores, and hyphens.
    -

    UnifiedLlamaStackConfig

    +

    UnifiedOgxConfig

    Backend-specific knobs for unified-mode OGX synthesis.

    Per Decision S5 of the design spike, backend-agnostic high-level sections (inference, …) live at the configuration root, not here. This -block holds only the Llama-Stack-specific synthesis controls: which -baseline to start from, an optional profile file, and a raw -native_override escape hatch.

    +block holds only the OGX-specific synthesis controls: which baseline to +start from, an optional profile file, and a raw native_override escape +hatch.

    Attributes: baseline: Synthesis starting point. “default” begins from -LCORE’s built-in baseline (src/data/default_run.yaml); “empty” begins -from an empty dict (used by the migration tool for an exact round-trip). -Ignored when profile is set. profile: Optional path to a -user-authored run.yaml-shaped file used as the synthesis baseline. -Relative paths resolve against the directory of the loaded -lightspeed-stack.yaml. native_override: Raw OGX schema -deep-merged last (maps merge recursively, lists and scalars replace). -The escape hatch for anything the high-level sections do not -express.

    +LCORE’s built-in baseline (src/data/default_run.yaml) including the +conditional OpenAI inference provider. “byo-llm” begins from the same +file with that OpenAI row removed. “empty” begins from an empty dict +(used by the migration tool for an exact round-trip). Ignored when +profile is set. profile: Optional path to a user-authored +run.yaml-shaped file used as the synthesis baseline. Relative paths +resolve against the directory of the loaded lightspeed-stack.yaml. +native_override: Raw OGX schema deep-merged last (maps merge +recursively, lists and scalars replace). The escape hatch for anything +the high-level sections do not express.

    @@ -2724,8 +2901,10 @@

    UnifiedLlamaStackConfig

    - + @@ -2736,8 +2915,8 @@

    UnifiedLlamaStackConfig

    - +
    baseline stringSynthesis starting point: ‘default’ uses LCORE’s built-in baseline, -‘empty’ starts from {}. Ignored when ‘profile’ is set.Synthesis starting point: ‘default’ uses LCORE’s built-in baseline +including the conditional OpenAI provider, ‘byo-llm’ uses the same +baseline without that OpenAI row, ‘empty’ starts from {}. Ignored when +‘profile’ is set.
    profile
    native_override objectRaw OGX schema deep-merged last (maps merge recursively; -lists and scalars replace).Raw OGX schema deep-merged last (maps merge recursively; lists and +scalars replace).
    @@ -2789,8 +2968,8 @@

    VectorStoreConfiguration

    sibling default_provider pointer, rather than a per-entry default flag.

    Attributes: default_provider: Provider id used for -vector_stores.default_* in the synthesized OGX config. Required -when providers is non-empty; must match one of providers[].id. Must be +vector_stores.default_* in the synthesized OGX config. Required when +providers is non-empty; must match one of providers[].id. Must be omitted when providers is empty. providers: Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as rag.byok.stores (static registered corpora).

    @@ -2811,9 +2990,9 @@

    VectorStoreConfiguration

    default_provider string - Provider id used for vector_stores.default_* in the synthesized -OGX config. Required when providers is non-empty; must match one -of providers[].id. + Provider id used for vector_stores.default_* in the synthesized OGX +config. Required when providers is non-empty; must match one of +providers[].id. providers diff --git a/docs/user_doc/config.json b/docs/user_doc/config.json index 5d2860547..7b593ec41 100644 --- a/docs/user_doc/config.json +++ b/docs/user_doc/config.json @@ -449,7 +449,7 @@ "title": "Service configuration" }, "ogx": { - "$ref": "`#/components/schemas/`LlamaStackConfiguration", + "$ref": "`#/components/schemas/`OgxConfiguration", "description": "This section contains OGX configuration. Lightspeed Core Stack service can call OGX in library mode or in server mode.", "title": "OGX configuration" }, @@ -604,6 +604,7 @@ "items": { "discriminator": { "mapping": { + "granite_guardian": "`#/components/schemas/`GraniteGuardianShieldConfiguration", "question_validity": "`#/components/schemas/`QuestionValidityShieldConfiguration", "redaction": "`#/components/schemas/`RedactionShieldConfiguration" }, @@ -615,6 +616,9 @@ }, { "$ref": "`#/components/schemas/`RedactionShieldConfiguration" + }, + { + "$ref": "`#/components/schemas/`GraniteGuardianShieldConfiguration" } ] }, @@ -863,6 +867,96 @@ "title": "FaissVectorStoreProviderConfig", "type": "object" }, + "GraniteGuardianConfig": { + "additionalProperties": false, + "description": "Configuration for the Granite Guardian moderation guardrail.", + "properties": { + "url": { + "description": "The model_id to use for the guard", + "title": "Base URL", + "type": "string" + }, + "api_key": { + "type": "string", + "nullable": true, + "default": null, + "description": "API key for the inference", + "title": "Granite Guardian API key" + }, + "max_retries": { + "default": 2, + "description": "Maximun number of retries", + "minimum": 0, + "maximum": 5, + "title": "Max retries", + "type": "integer" + }, + "timeout": { + "default": 30, + "description": "Request timeout in seconds", + "minimum": 5, + "maximum": 300, + "title": "Timeout", + "type": "integer" + }, + "verify_ssl": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ], + "default": true, + "description": "SSL certificate verification. Can be:\n - True: Verify using system CA bundle (default, recommended)\n - False: Disable verification (insecure, for dev only)\n - str: Path to custom CA bundle file (for internal PKI)", + "title": "Verify SSL" + }, + "risks": { + "description": "Risks to be considered while applying this guardrail", + "items": { + "$ref": "`#/components/schemas/`RiskDefinition" + }, + "title": "Defined risks", + "type": "array" + } + }, + "required": [ + "url", + "risks" + ], + "title": "GraniteGuardianConfig", + "type": "object" + }, + "GraniteGuardianShieldConfiguration": { + "additionalProperties": false, + "description": "Configuration for a named Granite Guardian guardrail shield.\n\nAttributes:\n name: Unique, user-facing name identifying this shield instance.\n provider_id: Discriminator identifying this as a granite-guardian shield.\n config: Granite-guardian-specific configuration.", + "properties": { + "name": { + "description": "Unique, user-facing name identifying this shield instance.", + "title": "Shield name", + "type": "string" + }, + "provider_id": { + "const": "granite_guardian", + "description": "Discriminator identifying this as a granite-guardian shield.", + "title": "Shield provider id", + "type": "string" + }, + "config": { + "$ref": "`#/components/schemas/`GraniteGuardianConfig", + "description": "Granite-guardian-specific configuration for this shield", + "title": "Shield configuration" + } + }, + "required": [ + "name", + "provider_id", + "config" + ], + "title": "GraniteGuardianShieldConfiguration", + "type": "object" + }, "InMemoryCacheConfig": { "additionalProperties": false, "description": "In-memory cache configuration.", @@ -1037,83 +1131,6 @@ "title": "JwtRoleRule", "type": "object" }, - "LlamaStackConfiguration": { - "additionalProperties": false, - "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/)", - "properties": { - "url": { - "type": "string", - "nullable": true, - "default": null, - "description": "URL to OGX service; used when library mode is disabled. Must be a valid HTTP or HTTPS URL.", - "title": "OGX URL" - }, - "api_key": { - "type": "string", - "nullable": true, - "default": null, - "description": "API key to access OGX service", - "title": "API key" - }, - "use_as_library_client": { - "type": "boolean", - "nullable": true, - "default": null, - "description": "When set to true OGX will be used in library mode, not in server mode (default)", - "title": "Use as library" - }, - "library_client_config_path": { - "type": "string", - "nullable": true, - "default": null, - "description": "Path to configuration file used when OGX is run in library mode. DEPRECATED legacy two-file setup: logs a startup warning since 0.6 and is removed in 0.8 — use unified mode instead (the config block below, and/or the root-level inference.providers section); migrate with lightspeed-stack --migrate-config.", - "title": "OGX configuration path (legacy, deprecated)" - }, - "timeout": { - "default": 180, - "description": "Timeout in seconds for requests to OGX service. Default is 180 seconds (3 minutes) to accommodate long-running RAG queries.", - "minimum": 0, - "title": "Request timeout", - "type": "integer" - }, - "max_retries": { - "default": 5, - "description": "Maximum number of connection attempts before giving up. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod).", - "minimum": 0, - "title": "Maximum number of connection attempts before giving up", - "type": "integer" - }, - "retry_delay": { - "default": 2, - "description": "Delay in seconds between retry attempts. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod).", - "minimum": 0, - "title": "Delay in seconds between retry attempts", - "type": "integer" - }, - "allow_degraded_mode": { - "type": "boolean", - "nullable": true, - "default": false, - "description": "If enabled, Lightspeed Core can be started even when OGX is not accessible (valid for server mode only)", - "title": "Allow degraded mode" - }, - "config": { - "anyOf": [ - { - "$ref": "`#/components/schemas/`UnifiedLlamaStackConfig" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Backend-specific knobs for unified mode, where LCORE synthesizes the OGX run.yaml instead of reading an external file. Holds the baseline selector, an optional profile path, and a raw native_override escape hatch. Backend-agnostic high-level sections (e.g. inference.providers) live at the configuration root, not here. Mutually exclusive with library_client_config_path; that cross-field check lives on the root Configuration model. When set in library mode, library_client_config_path is not required.", - "title": "Unified OGX configuration" - } - }, - "title": "LlamaStackConfiguration", - "type": "object" - }, "ModelContextProtocolServer": { "additionalProperties": false, "description": "Model context protocol server configuration.\n\nMCP (Model Context Protocol) servers provide tools and capabilities to the\nAI agents. These are configured by this structure. Only MCP servers\ndefined in the lightspeed-stack.yaml configuration are available to the\nagents. Tools configured in the OGX run.yaml are not accessible to\nlightspeed-core agents.\n\nUseful resources:\n\n- [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)\n- [MCP FAQs](https://modelcontextprotocol.io/faqs)\n- [Wikipedia article](https://en.wikipedia.org/wiki/Model_Context_Protocol)", @@ -1198,6 +1215,83 @@ "title": "ObservabilityConfiguration", "type": "object" }, + "OgxConfiguration": { + "additionalProperties": false, + "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications)", + "properties": { + "url": { + "type": "string", + "nullable": true, + "default": null, + "description": "URL to OGX service; used when library mode is disabled. Must be a valid HTTP or HTTPS URL.", + "title": "OGX URL" + }, + "api_key": { + "type": "string", + "nullable": true, + "default": null, + "description": "API key to access OGX service", + "title": "API key" + }, + "use_as_library_client": { + "type": "boolean", + "nullable": true, + "default": null, + "description": "When set to true OGX will be used in library mode, not in server mode (default)", + "title": "Use as library" + }, + "library_client_config_path": { + "type": "string", + "nullable": true, + "default": null, + "description": "Path to configuration file used when OGX is run in library mode. DEPRECATED legacy two-file setup: logs a startup warning since 0.6 and is removed in 0.8 — use unified mode instead (the config block below, and/or the root-level inference.providers section); migrate with lightspeed-stack --migrate-config.", + "title": "OGX configuration path (legacy, deprecated)" + }, + "timeout": { + "default": 180, + "description": "Timeout in seconds for requests to OGX service. Default is 180 seconds (3 minutes) to accommodate long-running RAG queries.", + "minimum": 0, + "title": "Request timeout", + "type": "integer" + }, + "max_retries": { + "default": 5, + "description": "Maximum number of connection attempts before giving up. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod).", + "minimum": 0, + "title": "Maximum number of connection attempts before giving up", + "type": "integer" + }, + "retry_delay": { + "default": 2, + "description": "Delay in seconds between retry attempts. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod).", + "minimum": 0, + "title": "Delay in seconds between retry attempts", + "type": "integer" + }, + "allow_degraded_mode": { + "type": "boolean", + "nullable": true, + "default": false, + "description": "If enabled, Lightspeed Core can be started even when OGX is not accessible (valid for server mode only)", + "title": "Allow degraded mode" + }, + "config": { + "anyOf": [ + { + "$ref": "`#/components/schemas/`UnifiedOgxConfig" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Backend-specific knobs for unified mode, where LCORE synthesizes the OGX run.yaml instead of reading an external file. Holds the baseline selector, an optional profile path, and a raw native_override escape hatch. Backend-agnostic high-level sections (e.g. inference.providers) live at the configuration root, not here. Mutually exclusive with library_client_config_path; that cross-field check lives on the root Configuration model. When set in library mode, library_client_config_path is not required.", + "title": "Unified OGX configuration" + } + }, + "title": "OgxConfiguration", + "type": "object" + }, "OkpConfiguration": { "additionalProperties": false, "description": "OKP (Offline Knowledge Portal) provider configuration.\n\nControls provider-specific behaviour for the OKP vector store.\nOnly relevant when ``\"okp\"`` is listed in ``rag.retrieval.inline.sources``\nor ``rag.retrieval.tool.sources``.", @@ -1428,7 +1522,7 @@ "type": "string" }, "model_prompt": { - "default": "\nInstructions:\n- You are a question classifying tool\n- You are an expert in kubernetes and openshift\n- Your job is to determine where or a user's question is related to kubernetes and/or openshift technologies and to provide a one-word response.\n- If a question appears to be related to kubernetes or openshift technologies, answer with the word ${allowed}, otherwise answer with the word ${rejected}.\n- Do not explain your answer, just provide the one-word response. Do not give any other response.\n- If the given question is an empty string, answer with the word ${rejected}\n\n\nExample Question:\nWhy is the sky blue?\nExample Response:\n${rejected}\n\nExample Question:\nWhy is the grass green?\nExample Response:\n${rejected}\n\nExample Question:\nWhy is sand yellow?\nExample Response:\n${rejected}\n\nExample Question:\nCan you help configure my cluster to automatically scale?\nExample Response:\n${allowed}\n\nQuestion:\n${message}\nResponse:\n", + "default": "\nInstructions:\n- You are a question classifying tool\n- You are an expert in Kubernetes and OpenShift\n- Your job is to determine where or a user's question is related to Kubernetes and/or OpenShift technologies and to provide a one-word response.\n- If a question appears to be related to Kubernetes or OpenShift technologies, answer with the word ${allowed}, otherwise answer with the word ${rejected}.\n- Do not explain your answer, just provide the one-word response. Do not give any other response.\n- If the given question is an empty string, answer with the word ${rejected}\n\n\nExample Question:\nWhy is the sky blue?\nExample Response:\n${rejected}\n\nExample Question:\nWhy is the grass green?\nExample Response:\n${rejected}\n\nExample Question:\nWhy is sand yellow?\nExample Response:\n${rejected}\n\nExample Question:\nCan you help configure my cluster to automatically scale?\nExample Response:\n${allowed}\n\nQuestion:\n${message}\nResponse:\n", "description": "The default prompt sent to the LLM used to validate the Users' question.", "title": "Model prompt", "type": "string" @@ -1912,6 +2006,69 @@ "title": "RetrievalStrategyConfiguration", "type": "object" }, + "RiskDefinition": { + "additionalProperties": false, + "description": "Definition for a custom risk category.\n\nCustom risks allow applications to add use-case-specific safety checks\nbeyond the standard harm, jailbreak, leetspeak, amnesia, and\nhistory_politics checks.\nExample:\n liability_risk = RiskDefinition(\n name=\"liability\",\n description=\"Content requesting legal, medical, or financial advice\",\n threshold=0.55,\n points=[\"input\"],\n )\n pii_risk = RiskDefinition(\n name=\"pii_request\",\n description=\"User is asking the AI to reveal personal information\",\n threshold=0.50,\n points=[\"input\", \"tool\"],\n )\nNote:\n To enable think mode (detailed reasoning) for a risk, add the risk name\n to the `thinking_enabled` list in `ModerationConfig`. Do not set\n `enable_thinking` directly - it is managed internally.", + "properties": { + "name": { + "description": "Unique identifier for this risk (e.g., 'liability', 'competitor_mention')", + "title": "Risk name", + "type": "string" + }, + "description": { + "description": "Risk definition text passed to Granite Guardian as custom_criteria", + "title": "Rist description", + "type": "string" + }, + "threshold": { + "default": 0.65, + "description": "Score threshold for flagging (lower = more sensitive)", + "maximum": 1.0, + "minimum": 0.0, + "title": "Risk threshold", + "type": "number" + }, + "enabled": { + "default": true, + "description": "Whether to run this check", + "title": "Risk enabled", + "type": "boolean" + }, + "enable_thinking": { + "default": false, + "description": "Internal field - set via ModerationConfig.thinking_enabled list, not directly. When True, Granite Guardian provides detailed reasoning before scoring.", + "title": "Risk enable thinking", + "type": "boolean" + }, + "points": { + "description": "Where this risk is evaluated: `input` (user message), `output` (model response), or `tool` (tool/MCP content).", + "items": { + "enum": [ + "input", + "output", + "tool" + ], + "type": "string" + }, + "minItems": 1, + "title": "Guardrail points", + "type": "array" + }, + "violation_message": { + "description": "Message to be displayed when this risk is violated", + "title": "Violation message", + "type": "string" + } + }, + "required": [ + "name", + "description", + "points", + "violation_message" + ], + "title": "RiskDefinition", + "type": "object" + }, "RlsapiV1Configuration": { "additionalProperties": false, "description": "Configuration for the rlsapi v1 /infer endpoint.\n\nSettings specific to the RHEL Lightspeed Command Line Assistant (CLA)\nstateless inference endpoint. Kept separate from shared configuration\nsections so that CLA-specific options do not affect other endpoints.", @@ -2018,6 +2175,26 @@ "title": "Number of workers", "type": "integer" }, + "max_concurrent_file_uploads": { + "default": 5, + "description": "Maximum number of file uploads (POST /v1/files) processed concurrently per worker. Each in-flight upload can hold up to the configured maximum file size in memory, so this bounds worst-case memory usage from concurrent uploads. Additional uploads are rejected with 429 until a slot frees up.", + "minimum": 0, + "title": "Maximum concurrent file uploads", + "type": "integer" + }, + "max_concurrent_vector_store_attaches": { + "default": 5, + "description": "Maximum number of vector store file attachments (POST /v1/vector-stores/{id}/files) processed concurrently per worker. Each in-flight attachment re-reads and chunks the source file, so this bounds worst-case memory usage independently of max_concurrent_file_uploads. Additional attachments are rejected with 429 until a slot frees up.", + "minimum": 0, + "title": "Maximum concurrent vector store file attachments", + "type": "integer" + }, + "delete_file_after_vector_store_attach": { + "default": false, + "description": "When true, deletes a file (POST /v1/files) once it has been successfully attached to a vector store, since the vector store keeps its own chunked/embedded copy of the content. Defaults to false to match the OpenAI Files API, where a file remains reusable across multiple vector stores until the caller explicitly deletes it - enabling this makes attached files single-use: re-attaching the same file_id to another vector store will fail once it has been deleted.", + "title": "Delete file after vector store attach", + "type": "boolean" + }, "color_log": { "default": true, "description": "Enables colorized logging", @@ -2195,10 +2372,10 @@ }, "UnifiedInferenceProvider": { "additionalProperties": false, - "description": "A high-level inference provider entry for unified-mode synthesis.\n\nOperators describe inference providers at this high level (backend-agnostic\nvocabulary) instead of authoring raw OGX provider blocks. The\nsynthesizer (`apply_high_level_inference`) expands each entry into a Llama\nStack `providers.inference` entry, mapping `type` to a `provider_type` and\nemitting `${env.}` references for secrets (never literal values).\n\nAttributes:\n type: Canonical provider identifier. Vendor-neutral so it survives a\n future backend change; each backend-specific synthesizer maps it to\n its own provider vocabulary.\n id: Optional identifier emitted as the OGX provider_id. When\n omitted, synthesized as type with underscores hyphenated. If set,\n must be non-empty after stripping whitespace and may contain only\n lowercase letters, digits, underscores, and hyphens.\n api_key_env: Name of the environment variable holding the provider API\n key. Emitted verbatim as `${env.}` so the secret never lands\n on disk resolved.\n allowed_models: Optional allow-list of model identifiers passed through\n to the synthesized provider config.\n extra: Additional provider-config keys merged verbatim into the\n synthesized provider's `config` block — an escape hatch for\n provider-specific knobs not modeled here.", + "description": "A high-level inference provider entry for unified-mode synthesis.\n\nOperators describe inference providers at this high level (backend-agnostic\nvocabulary) instead of authoring raw OGX provider blocks. The\nsynthesizer (`apply_high_level_inference`) expands each entry into an OGX\n`providers.inference` entry, mapping `type` to a `provider_type` and\nemitting `${env.}` references for secrets (never literal values).\n\nAttributes:\n type: Canonical provider identifier. Vendor-neutral so it survives a\n future backend change; each backend-specific synthesizer maps it to\n its own provider vocabulary.\n id: Optional identifier emitted as the OGX provider_id. When\n omitted, synthesized as type with underscores hyphenated. If set,\n must be non-empty after stripping whitespace and may contain only\n lowercase letters, digits, underscores, and hyphens.\n api_key_env: Name of the environment variable holding the provider API\n key. Emitted verbatim as `${env.}` so the secret never lands\n on disk resolved.\n allowed_models: Optional allow-list of model identifiers passed through\n to the synthesized provider config.\n extra: Additional provider-config keys merged verbatim into the\n synthesized provider's `config` block — an escape hatch for\n provider-specific knobs not modeled here.", "properties": { "type": { - "description": "Canonical, backend-agnostic provider identifier mapped to a OGX provider_type by the synthesizer.", + "description": "Canonical, backend-agnostic provider identifier mapped to an OGX provider_type by the synthesizer.", "enum": [ "openai", "ollama", @@ -2247,9 +2424,9 @@ "title": "UnifiedInferenceProvider", "type": "object" }, - "UnifiedLlamaStackConfig": { + "UnifiedOgxConfig": { "additionalProperties": false, - "description": "Backend-specific knobs for unified-mode OGX synthesis.\n\nPer Decision S5 of the design spike, backend-agnostic high-level sections\n(inference, ...) live at the configuration root, not here. This block holds\nonly the Llama-Stack-specific synthesis controls: which baseline to start\nfrom, an optional profile file, and a raw native_override escape hatch.\n\nAttributes:\n baseline: Synthesis starting point. \"default\" begins from LCORE's\n built-in baseline (src/data/default_run.yaml); \"empty\" begins from\n an empty dict (used by the migration tool for an exact round-trip).\n Ignored when `profile` is set.\n profile: Optional path to a user-authored run.yaml-shaped file used as\n the synthesis baseline. Relative paths resolve against the directory\n of the loaded lightspeed-stack.yaml.\n native_override: Raw OGX schema deep-merged last (maps merge\n recursively, lists and scalars replace). The escape hatch for\n anything the high-level sections do not express.", + "description": "Backend-specific knobs for unified-mode OGX synthesis.\n\nPer Decision S5 of the design spike, backend-agnostic high-level sections\n(inference, ...) live at the configuration root, not here. This block holds\nonly the OGX-specific synthesis controls: which baseline to start\nfrom, an optional profile file, and a raw native_override escape hatch.\n\nAttributes:\n baseline: Synthesis starting point. \"default\" begins from LCORE's\n built-in baseline (src/data/default_run.yaml) including the\n conditional OpenAI inference provider. \"byo-llm\" begins from the\n same file with that OpenAI row removed. \"empty\" begins from an\n empty dict (used by the migration tool for an exact round-trip).\n Ignored when `profile` is set.\n profile: Optional path to a user-authored run.yaml-shaped file used as\n the synthesis baseline. Relative paths resolve against the directory\n of the loaded lightspeed-stack.yaml.\n native_override: Raw OGX schema deep-merged last (maps merge\n recursively, lists and scalars replace). The escape hatch for\n anything the high-level sections do not express.", "properties": { "baseline": { "default": "default", @@ -2276,7 +2453,7 @@ "type": "object" } }, - "title": "UnifiedLlamaStackConfig", + "title": "UnifiedOgxConfig", "type": "object" }, "UserDataCollection": { diff --git a/docs/user_doc/config.md b/docs/user_doc/config.md index 4a1d38888..85a67fb36 100644 --- a/docs/user_doc/config.md +++ b/docs/user_doc/config.md @@ -221,34 +221,34 @@ Attributes: Global service configuration. -| Field | Type | Description | -|------------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| name | string | Name of the service. That value will be used in REST API endpoints. | -| config_format_version | string | Optional explicit marker of the configuration format. When set, it must agree with the shape detected from the configuration body: 'unified' requires a synthesis input (a non-empty inference.providers, a non-empty vector_store.providers, or a ogx.config block), 'legacy' requires no synthesis input. Reserved as the lever for a future breaking change of the unified schema (R11). | -| service | | This section contains Lightspeed Core Stack service configuration. | -| ogx | | This section contains OGX configuration. Lightspeed Core Stack service can call OGX in library mode or in server mode. | -| user_data_collection | | This section contains configuration for subsystem that collects user data(transcription history and feedbacks). | -| database | | Configuration for database to store conversation IDs and other runtime data | -| mcp_servers | array | MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured in this section. Only MCP servers defined in the lightspeed-stack.yaml configuration are available to the agents. Tools configured in the OGX run.yaml are not accessible to lightspeed-core agents. | -| authentication | | Authentication configuration | -| authorization | | Lightspeed Core Stack implements a modular authentication and authorization system with multiple authentication methods. Authorization is configurable through role-based access control. Authentication is handled through selectable modules configured via the module field in the authentication configuration. | -| customization | | It is possible to customize Lightspeed Core Stack via this section. System prompt can be customized and also different parts of the service can be replaced by custom Python modules. | -| inference | | One LLM provider and one its model might be selected as default ones. When no provider+model pair is specified in REST API calls (query endpoints), the default provider and model are used. | -| conversation_cache | | | -| compaction | | Controls when conversation history is summarized to keep the model's input below the context window limit. Disabled by default — when disabled, requests that exceed the window continue to surface as HTTP 413. | -| approvals | | Settings for human-in-the-loop approval of MCP tool invocations | -| vector_store | | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as rag.byok.stores (static registered corpora). When providers is non-empty, default_provider is required and must match one of providers[].id. Applied in unified synthesis only. | -| a2a_state | | Configuration for A2A protocol persistent state storage. | -| quota_handlers | | Quota handlers configuration | -| azure_entra_id | | | -| rlsapi_v1 | | Configuration for the rlsapi v1 /infer endpoint used by the RHEL Lightspeed Command Line Assistant (CLA). | -| splunk | | Splunk HEC configuration for sending telemetry events. | -| observability | | OpenTelemetry and observability configuration collected from OTEL_* environment variables. | -| deployment_environment | string | Deployment environment name (e.g., 'development', 'staging', 'production'). Used in telemetry events. | -| rag | | Unified RAG configuration: BYOK stores, OKP provider, and retrieval strategies (inline and tool-based). | -| skills | | Agent skills configuration. Specifies paths to skill directories. | -| saved_prompts | | Configuration for saved prompts feature limits including maximum prompts per user, display name length, and content length. | -| shields | array | List of pydantic-ai-lightspeed agent guardrail shields (question validity and PII redaction). Each entry has a unique 'name', a 'provider_id' ('question_validity' or 'redaction'), and a type-specific 'config'. | +| Field | Type | Description | +|------------------------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | string | Name of the service. That value will be used in REST API endpoints. | +| config_format_version | string | Optional explicit marker of the configuration format. When set, it must agree with the shape detected from the configuration body: 'unified' requires a synthesis input (a non-empty inference.providers, a non-empty vector_store.providers, or an ogx.config block), 'legacy' requires no synthesis input. Reserved as the lever for a future breaking change of the unified schema (R11). | +| service | | This section contains Lightspeed Core Stack service configuration. | +| ogx | | This section contains OGX configuration. Lightspeed Core Stack service can call OGX in library mode or in server mode. | +| user_data_collection | | This section contains configuration for subsystem that collects user data(transcription history and feedbacks). | +| database | | Configuration for database to store conversation IDs and other runtime data | +| mcp_servers | array | MCP (Model Context Protocol) servers provide tools and capabilities to the AI agents. These are configured in this section. Only MCP servers defined in the lightspeed-stack.yaml configuration are available to the agents. Tools configured in the OGX run.yaml are not accessible to lightspeed-core agents. | +| authentication | | Authentication configuration | +| authorization | | Lightspeed Core Stack implements a modular authentication and authorization system with multiple authentication methods. Authorization is configurable through role-based access control. Authentication is handled through selectable modules configured via the module field in the authentication configuration. | +| customization | | It is possible to customize Lightspeed Core Stack via this section. System prompt can be customized and also different parts of the service can be replaced by custom Python modules. | +| inference | | One LLM provider and one its model might be selected as default ones. When no provider+model pair is specified in REST API calls (query endpoints), the default provider and model are used. | +| conversation_cache | | | +| compaction | | Controls when conversation history is summarized to keep the model's input below the context window limit. Disabled by default — when disabled, requests that exceed the window continue to surface as HTTP 413. | +| approvals | | Settings for human-in-the-loop approval of MCP tool invocations | +| vector_store | | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as rag.byok.stores (static registered corpora). When providers is non-empty, default_provider is required and must match one of providers[].id. Applied in unified synthesis only. | +| a2a_state | | Configuration for A2A protocol persistent state storage. | +| quota_handlers | | Quota handlers configuration | +| azure_entra_id | | | +| rlsapi_v1 | | Configuration for the rlsapi v1 /infer endpoint used by the RHEL Lightspeed Command Line Assistant (CLA). | +| splunk | | Splunk HEC configuration for sending telemetry events. | +| observability | | OpenTelemetry and observability configuration collected from OTEL_* environment variables. | +| deployment_environment | string | Deployment environment name (e.g., 'development', 'staging', 'production'). Used in telemetry events. | +| rag | | Unified RAG configuration: BYOK stores, OKP provider, and retrieval strategies (inline and tool-based). | +| skills | | Agent skills configuration. Specifies paths to skill directories. | +| saved_prompts | | Configuration for saved prompts feature limits including maximum prompts per user, display name length, and content length. | +| shields | array | List of pydantic-ai-lightspeed agent guardrail shields (question validity and PII redaction). Each entry has a unique 'name', a 'provider_id' ('question_validity' or 'redaction'), and a type-specific 'config'. | ## ConversationHistoryConfiguration @@ -313,13 +313,13 @@ Database configuration. Dynamic FAISS vector-store provider (runtime create capacity). -| Field | Type | Description | -|---------------------|---------|-------------------------------------------------------------------------------------------------------| +| Field | Type | Description | +|---------------------|---------|-----------------------------------------------------------------------------------------------| | id | string | OGX vector_io provider_id. Surrounding whitespace is stripped before validation and emission. | -| embedding_model | string | Embedding model identification used for stores created against this provider. | -| embedding_dimension | integer | Dimensionality of embedding vectors for this provider. | -| type | string | Product type for this dynamic vector-store provider. | -| config | | FAISS storage settings for this provider. | +| embedding_model | string | Embedding model identification used for stores created against this provider. | +| embedding_dimension | integer | Dimensionality of embedding vectors for this provider. | +| type | string | Product type for this dynamic vector-store provider. | +| config | | FAISS storage settings for this provider. | ## FaissVectorStoreProviderConfig @@ -333,6 +333,40 @@ Storage config for a FAISS dynamic vector-store provider. | path | string | On-disk FAISS/SQLite path for this provider. | +## GraniteGuardianConfig + + +Configuration for the Granite Guardian moderation guardrail. + + +| Field | Type | Description | +|-------------|---------|------------------------------------------------------| +| url | string | The model_id to use for the guard | +| api_key | string | API key for the inference | +| max_retries | integer | Maximun number of retries | +| timeout | integer | Request timeout in seconds | +| verify_ssl | | SSL certificate verification | +| risks | array | Risks to be considered while applying this guardrail | + + +## GraniteGuardianShieldConfiguration + + +Configuration for a named Granite Guardian guardrail shield. + +Attributes: + name: Unique, user-facing name identifying this shield instance. + provider_id: Discriminator identifying this as a granite-guardian shield. + config: Granite-guardian-specific configuration. + + +| Field | Type | Description | +|-------------|--------|--------------------------------------------------------------| +| name | string | Unique, user-facing name identifying this shield instance. | +| provider_id | string | Discriminator identifying this as a granite-guardian shield. | +| config | | Granite-guardian-specific configuration for this shield | + + ## InMemoryCacheConfig @@ -350,14 +384,14 @@ In-memory cache configuration. Inference configuration. -| Field | Type | Description | -|------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| default_model | string | Identification of default model used when no other model is specified. | -| default_provider | string | Identification of default provider used when no other model is specified. | -| context_windows | object | Map of fully-qualified model identifier (e.g., "openai/gpt-4o-mini") to context window size in tokens. Used by the conversation compaction trigger to decide when older turns must be summarized before the input exceeds the window. Models absent from this map have no registered window — callers fall back to their own default or skip the token-based trigger. | +| Field | Type | Description | +|------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| default_model | string | Identification of default model used when no other model is specified. | +| default_provider | string | Identification of default provider used when no other model is specified. | +| context_windows | object | Map of fully-qualified model identifier (e.g., "openai/gpt-4o-mini") to context window size in tokens. Used by the conversation compaction trigger to decide when older turns must be summarized before the input exceeds the window. Models absent from this map have no registered window — callers fall back to their own default or skip the token-based trigger. | | providers | array | Unified-mode synthesis input (Decision S5): a high-level, backend-agnostic list of inference providers the synthesizer expands into OGX provider entries. Lives at the configuration root so it survives a future backend change. A non-empty list signals unified mode. Empty (the default) leaves legacy/remote modes unaffected. The sibling default_model / default_provider keep their query-time routing meaning and are independent of this list. | -| max_infer_iters | integer | Server-side default for the maximum number of inference iterations a model can perform in a single request. Prevents small models from looping indefinitely on tool calls. Per-request values take precedence over this default. Set to None to disable the limit. | -| max_tool_calls | integer | Server-side default for the maximum number of tool calls allowed in a single response. Prevents small models from exhausting the context window with repeated tool calls. Per-request values take precedence over this default. Set to None to disable the limit. | +| max_infer_iters | integer | Server-side default for the maximum number of inference iterations a model can perform in a single request. Prevents small models from looping indefinitely on tool calls. Per-request values take precedence over this default. Set to None to disable the limit. | +| max_tool_calls | integer | Server-side default for the maximum number of tool calls allowed in a single response. Prevents small models from exhausting the context window with repeated tool calls. Per-request values take precedence over this default. Set to None to disable the limit. | ## JsonPathOperator @@ -432,36 +466,6 @@ Rule for extracting roles from JWT claims. | roles | array | Roles to be assigned if the rule matches | -## LlamaStackConfiguration - - -OGX configuration. - -OGX is a comprehensive system that provides a uniform set of tools -for building, scaling, and deploying generative AI applications, enabling -developers to create, integrate, and orchestrate multiple AI services and -capabilities into an adaptable setup. - -Useful resources: - - - [OGX](https://ogx-ai.github.io/) - - [Python OGX client](https://github.com/ogx-ai/ogx-client-python) - - [Build AI Applications with OGX](https://ogx-ai.github.io/) - - -| Field | Type | Description | -|----------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| url | string | URL to OGX service; used when library mode is disabled. Must be a valid HTTP or HTTPS URL. | -| api_key | string | API key to access OGX service | -| use_as_library_client | boolean | When set to true OGX will be used in library mode, not in server mode (default) | -| library_client_config_path | string | Path to configuration file used when OGX is run in library mode. DEPRECATED legacy two-file setup: logs a startup warning since 0.6 and is removed in 0.8 — use unified mode instead (the config block below, and/or the root-level inference.providers section); migrate with lightspeed-stack --migrate-config. | -| timeout | integer | Timeout in seconds for requests to OGX service. Default is 180 seconds (3 minutes) to accommodate long-running RAG queries. | -| max_retries | integer | Maximum number of connection attempts before giving up. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod). | -| retry_delay | integer | Delay in seconds between retry attempts. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod). | -| allow_degraded_mode | boolean | If enabled, Lightspeed Core can be started even when OGX is not accessible (valid for server mode only) | -| config | | Backend-specific knobs for unified mode, where LCORE synthesizes the OGX run.yaml instead of reading an external file. Holds the baseline selector, an optional profile path, and a raw native_override escape hatch. Backend-agnostic high-level sections (e.g. inference.providers) live at the configuration root, not here. Mutually exclusive with library_client_config_path; that cross-field check lives on the root Configuration model. When set in library mode, library_client_config_path is not required. | - - ## ModelContextProtocolServer @@ -488,7 +492,7 @@ Useful resources: | authorization_headers | object | Headers to send to the MCP server. The map contains the header name and the path to a file containing the header value (secret). There are 3 special cases: 1. Usage of the kubernetes token in the header. To specify this use a string 'kubernetes' instead of the file path. 2. Usage of the client-provided token in the header. To specify this use a string 'client' instead of the file path. 3. Usage of the oauth token in the header. To specify this use a string 'oauth' instead of the file path. | | headers | array | List of HTTP header names to automatically forward from the incoming request to this MCP server. Headers listed here are extracted from the original client request and included when calling the MCP server. This is useful when infrastructure components (e.g. API gateways) inject headers that MCP servers need, such as x-rh-identity in HCC. Header matching is case-insensitive. These headers are additive with authorization_headers and MCP-HEADERS. | | require_approval | | When to require human approval for tool invocations. 'always' requires approval for all tools, 'never' auto-approves, or use ApprovalFilter for granular control. | -| timeout | integer | Timeout in seconds for requests to the MCP server. If not specified, the default timeout from OGX will be used. Note: This field is reserved for future use when OGX adds timeout support. | +| timeout | integer | Timeout in seconds for requests to the MCP server. If not specified, the default timeout from OGX will be used. Note: This field is reserved for future use when OGX adds timeout support. | ## ObservabilityConfiguration @@ -508,6 +512,36 @@ Attributes: | otel | object | Active OpenTelemetry configuration from OTEL_* environment variables | +## OgxConfiguration + + +OGX configuration. + +OGX is a comprehensive system that provides a uniform set of tools +for building, scaling, and deploying generative AI applications, enabling +developers to create, integrate, and orchestrate multiple AI services and +capabilities into an adaptable setup. + +Useful resources: + + - [OGX](https://ogx-ai.github.io/) + - [Python OGX client](https://github.com/ogx-ai/ogx-client-python) + - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications) + + +| Field | Type | Description | +|----------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| url | string | URL to OGX service; used when library mode is disabled. Must be a valid HTTP or HTTPS URL. | +| api_key | string | API key to access OGX service | +| use_as_library_client | boolean | When set to true OGX will be used in library mode, not in server mode (default) | +| library_client_config_path | string | Path to configuration file used when OGX is run in library mode. DEPRECATED legacy two-file setup: logs a startup warning since 0.6 and is removed in 0.8 — use unified mode instead (the config block below, and/or the root-level inference.providers section); migrate with lightspeed-stack --migrate-config. | +| timeout | integer | Timeout in seconds for requests to OGX service. Default is 180 seconds (3 minutes) to accommodate long-running RAG queries. | +| max_retries | integer | Maximum number of connection attempts before giving up. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod). | +| retry_delay | integer | Delay in seconds between retry attempts. Used on startup to connect to OGX and retrieve its version. Connection attempts are retried with a fixed delay to handle the case where OGX is still starting up (e.g., when running as a sidecar in the same pod). | +| allow_degraded_mode | boolean | If enabled, Lightspeed Core can be started even when OGX is not accessible (valid for server mode only) | +| config | | Backend-specific knobs for unified mode, where LCORE synthesizes the OGX run.yaml instead of reading an external file. Holds the baseline selector, an optional profile path, and a raw native_override escape hatch. Backend-agnostic high-level sections (e.g. inference.providers) live at the configuration root, not here. Mutually exclusive with library_client_config_path; that cross-field check lives on the root Configuration model. When set in library mode, library_client_config_path is not required. | + + ## OkpConfiguration @@ -533,13 +567,13 @@ or ``rag.retrieval.tool.sources``. Dynamic pgvector vector-store provider (runtime create capacity). -| Field | Type | Description | -|---------------------|---------|-------------------------------------------------------------------------------------------------------| +| Field | Type | Description | +|---------------------|---------|-----------------------------------------------------------------------------------------------| | id | string | OGX vector_io provider_id. Surrounding whitespace is stripped before validation and emission. | -| embedding_model | string | Embedding model identification used for stores created against this provider. | -| embedding_dimension | integer | Dimensionality of embedding vectors for this provider. | -| type | string | Product type for this dynamic vector-store provider. | -| config | | pgvector connection settings for this provider. | +| embedding_model | string | Embedding model identification used for stores created against this provider. | +| embedding_dimension | integer | Dimensionality of embedding vectors for this provider. | +| type | string | Product type for this dynamic vector-store provider. | +| config | | pgvector connection settings for this provider. | ## PgvectorVectorStoreProviderConfig @@ -725,7 +759,7 @@ BYOK (Bring Your Own Knowledge) RAG store configuration. | embedding_dimension | integer | Dimensionality of embedding vectors. | | vector_db_id | string | Vector database identification. | | db_path | string | Path to RAG database. Required for faiss backend. | -| score_multiplier | number | Multiplier applied to relevance scores from this vector store. Used to weight results when querying multiple knowledge sources. Values > 1 boost this store's results; values < 1 reduce them. | +| score_multiplier | number | Multiplier applied to relevance scores from this vector store. Used to weight results when querying multiple knowledge sources. Values > 1 boost this store's results; values < 1 reduce them. | | relevance_cutoff_score | number | Minimum raw similarity score to consider a result relevant. Results with a similarity score below this threshold are not returned. | | host | string | PostgreSQL host for pgvector backend. Defaults to ${env.POSTGRES_HOST} when backend is pgvector. | | port | | PostgreSQL port for pgvector backend. Defaults to ${env.POSTGRES_PORT} when backend is pgvector. | @@ -829,6 +863,44 @@ Configuration for a single retrieval strategy (inline or tool). | reranker | | Neural reranking of RAG chunks using cross-encoder. Only applicable to inline retrieval. | +## RiskDefinition + + +Definition for a custom risk category. + +Custom risks allow applications to add use-case-specific safety checks +beyond the standard harm, jailbreak, leetspeak, amnesia, and +history_politics checks. +Example: + liability_risk = RiskDefinition( + name="liability", + description="Content requesting legal, medical, or financial advice", + threshold=0.55, + points=["input"], + ) + pii_risk = RiskDefinition( + name="pii_request", + description="User is asking the AI to reveal personal information", + threshold=0.50, + points=["input", "tool"], + ) +Note: + To enable think mode (detailed reasoning) for a risk, add the risk name + to the `thinking_enabled` list in `ModerationConfig`. Do not set + `enable_thinking` directly - it is managed internally. + + +| Field | Type | Description | +|-------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | string | Unique identifier for this risk (e.g., 'liability', 'competitor_mention') | +| description | string | Risk definition text passed to Granite Guardian as custom_criteria | +| threshold | number | Score threshold for flagging (lower = more sensitive) | +| enabled | boolean | Whether to run this check | +| enable_thinking | boolean | Internal field - set via ModerationConfig.thinking_enabled list, not directly. When True, Granite Guardian provides detailed reasoning before scoring. | +| points | array | Where this risk is evaluated: `input` (user message), `output` (model response), or `tool` (tool/MCP content). | +| violation_message | string | Message to be displayed when this risk is violated | + + ## RlsapiV1Configuration @@ -889,18 +961,21 @@ and specify the number of Uvicorn workers. When more workers are specified, the service can handle requests concurrently. -| Field | Type | Description | -|--------------|---------|------------------------------------------------------------------------| -| host | string | Service hostname | -| port | integer | Service port | -| base_url | string | Externally reachable base URL for the service; needed for A2A support. | -| auth_enabled | boolean | Enables the authentication subsystem | -| workers | integer | Number of Uvicorn worker processes to start | -| color_log | boolean | Enables colorized logging | -| access_log | boolean | Enables logging of all access information | -| tls_config | | Transport Layer Security configuration for HTTPS support | -| root_path | string | ASGI root path for serving behind a reverse proxy on a subpath | -| cors | | Cross-Origin Resource Sharing configuration for cross-domain requests | +| Field | Type | Description | +|---------------------------------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| host | string | Service hostname | +| port | integer | Service port | +| base_url | string | Externally reachable base URL for the service; needed for A2A support. | +| auth_enabled | boolean | Enables the authentication subsystem | +| workers | integer | Number of Uvicorn worker processes to start | +| max_concurrent_file_uploads | integer | Maximum number of file uploads (POST /v1/files) processed concurrently per worker. Each in-flight upload can hold up to the configured maximum file size in memory, so this bounds worst-case memory usage from concurrent uploads. Additional uploads are rejected with 429 until a slot frees up. | +| max_concurrent_vector_store_attaches | integer | Maximum number of vector store file attachments (POST /v1/vector-stores/{id}/files) processed concurrently per worker. Each in-flight attachment re-reads and chunks the source file, so this bounds worst-case memory usage independently of max_concurrent_file_uploads. Additional attachments are rejected with 429 until a slot frees up. | +| delete_file_after_vector_store_attach | boolean | When true, deletes a file (POST /v1/files) once it has been successfully attached to a vector store, since the vector store keeps its own chunked/embedded copy of the content. Defaults to false to match the OpenAI Files API, where a file remains reusable across multiple vector stores until the caller explicitly deletes it - enabling this makes attached files single-use: re-attaching the same file_id to another vector store will fail once it has been deleted. | +| color_log | boolean | Enables colorized logging | +| access_log | boolean | Enables logging of all access information | +| tls_config | | Transport Layer Security configuration for HTTPS support | +| root_path | string | ASGI root path for serving behind a reverse proxy on a subpath | +| cors | | Cross-Origin Resource Sharing configuration for cross-domain requests | ## SkillsConfiguration @@ -1027,32 +1102,25 @@ Attributes: provider-specific knobs not modeled here. -| Field | Type | Description | -|----------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| type | string | Canonical, backend-agnostic provider identifier mapped to a OGX provider_type by the synthesizer. | +| Field | Type | Description | +|----------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| type | string | Canonical, backend-agnostic provider identifier mapped to an OGX provider_type by the synthesizer. | | id | string | Optional identifier emitted as the OGX provider_id. When omitted, synthesized as type with underscores hyphenated. If set, must be non-empty after stripping whitespace and may contain only lowercase letters, digits, underscores, and hyphens. | -| api_key_env | string | Name of the environment variable holding the provider API key. Emitted as a ${env.} reference so the secret is never written to disk in resolved form. | -| allowed_models | array | Optional allow-list of model identifiers for this provider. | -| extra | object | Additional provider-config keys merged verbatim into the synthesized provider's config block. | +| api_key_env | string | Name of the environment variable holding the provider API key. Emitted as a ${env.} reference so the secret is never written to disk in resolved form. | +| allowed_models | array | Optional allow-list of model identifiers for this provider. | +| extra | object | Additional provider-config keys merged verbatim into the synthesized provider's config block. | -## UnifiedLlamaStackConfig +## UnifiedOgxConfig Backend-specific knobs for unified-mode OGX synthesis. Per Decision S5 of the design spike, backend-agnostic high-level sections (inference, ...) live at the configuration root, not here. This block holds -only the Llama-Stack-specific synthesis controls: which baseline to start +only the OGX-specific synthesis controls: which baseline to start from, an optional profile file, and a raw native_override escape hatch. -During synthesis from the default baseline or a profile, LCORE ensures the -OGX MCP tool_runtime provider (`provider_id: model-context-protocol`, -`provider_type: remote::model-context-protocol`) is present so static -`mcp_servers` and dynamic MCP registration work. That ensure is skipped when -`baseline: empty` (migration / blank-slate); supply MCP via `native_override` -in that case. - Attributes: baseline: Synthesis starting point. "default" begins from LCORE's built-in baseline (src/data/default_run.yaml) including the @@ -1068,11 +1136,11 @@ Attributes: anything the high-level sections do not express. -| Field | Type | Description | -|-----------------|--------|----------------------------------------------------------------------------------------------------------------------------| +| Field | Type | Description | +|-----------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | baseline | string | Synthesis starting point: 'default' uses LCORE's built-in baseline including the conditional OpenAI provider, 'byo-llm' uses the same baseline without that OpenAI row, 'empty' starts from {}. Ignored when 'profile' is set. | -| profile | string | Path to a run.yaml-shaped baseline file. Relative paths resolve against the directory of the loaded lightspeed-stack.yaml. | -| native_override | object | Raw OGX schema deep-merged last (maps merge recursively; lists and scalars replace). | +| profile | string | Path to a run.yaml-shaped baseline file. Relative paths resolve against the directory of the loaded lightspeed-stack.yaml. | +| native_override | object | Raw OGX schema deep-merged last (maps merge recursively; lists and scalars replace). | ## UserDataCollection @@ -1107,7 +1175,7 @@ Attributes: registered corpora). -| Field | Type | Description | -|------------------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Field | Type | Description | +|------------------|--------|-----------------------------------------------------------------------------------------------------------------------------------------------------| | default_provider | string | Provider id used for vector_stores.default_* in the synthesized OGX config. Required when providers is non-empty; must match one of providers[].id. | -| providers | array | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as rag.byok.stores (static registered corpora). | +| providers | array | Dynamic vector-store provider capacity for runtime POST /v1/vector-stores creates. Not the same as rag.byok.stores (static registered corpora). | diff --git a/docs/user_doc/config.puml b/docs/user_doc/config.puml index 22ff4d458..9cebc782d 100644 --- a/docs/user_doc/config.puml +++ b/docs/user_doc/config.puml @@ -179,10 +179,10 @@ class "JwtRoleRule" as src.models.config.JwtRoleRule { check_regex_pattern() -> Self check_roles() -> Self } -class "LlamaStackConfiguration" as src.models.config.LlamaStackConfiguration { +class "OgxConfiguration" as src.models.config.OgxConfiguration { allow_degraded_mode : Optional[bool] api_key : Optional[SecretStr] - config : Optional['UnifiedLlamaStackConfig'] + config : Optional['UnifiedOgxConfig'] library_client_config_path : Optional[str] max_retries retry_delay @@ -307,7 +307,7 @@ class "UnifiedInferenceProvider" as src.models.config.UnifiedInferenceProvider { id : str | None type : Literal['openai', 'ollama', 'vllm', 'sentence_transformers', 'azure', 'vertexai', 'watsonx', 'vllm_rhaiis', 'vllm_rhel_ai'] } -class "UnifiedLlamaStackConfig" as src.models.config.UnifiedLlamaStackConfig { +class "UnifiedOgxConfig" as src.models.config.UnifiedOgxConfig { baseline : Literal['default', 'empty'] native_override : dict[str, object] profile : Optional[str] @@ -339,7 +339,7 @@ src.models.config.InferenceConfiguration --|> src.models.config.ConfigurationBas src.models.config.JwkConfiguration --|> src.models.config.ConfigurationBase src.models.config.JwtConfiguration --|> src.models.config.ConfigurationBase src.models.config.JwtRoleRule --|> src.models.config.ConfigurationBase -src.models.config.LlamaStackConfiguration --|> src.models.config.ConfigurationBase +src.models.config.OgxConfiguration --|> src.models.config.ConfigurationBase src.models.config.ModelContextProtocolServer --|> src.models.config.ConfigurationBase src.models.config.OkpConfiguration --|> src.models.config.ConfigurationBase src.models.config.PostgreSQLDatabaseConfiguration --|> src.models.config.ConfigurationBase @@ -358,7 +358,7 @@ src.models.config.TLSConfiguration --|> src.models.config.ConfigurationBase src.models.config.TrustedProxyConfiguration --|> src.models.config.ConfigurationBase src.models.config.TrustedProxyServiceAccount --|> src.models.config.ConfigurationBase src.models.config.UnifiedInferenceProvider --|> src.models.config.ConfigurationBase -src.models.config.UnifiedLlamaStackConfig --|> src.models.config.ConfigurationBase +src.models.config.UnifiedOgxConfig --|> src.models.config.ConfigurationBase src.models.config.UserDataCollection --|> src.models.config.ConfigurationBase src.models.config.A2AStateConfiguration --* src.models.config.Configuration : a2a_state src.models.config.ApprovalsConfiguration --* src.models.config.Configuration : approvals @@ -371,7 +371,7 @@ src.models.config.DatabaseConfiguration --* src.models.config.Configuration : da src.models.config.InferenceConfiguration --* src.models.config.Configuration : inference src.models.config.JsonPathOperator --* src.models.config.JwtRoleRule : operator src.models.config.JwtConfiguration --* src.models.config.JwkConfiguration : jwt_configuration -src.models.config.LlamaStackConfiguration --* src.models.config.Configuration : ogx +src.models.config.OgxConfiguration --* src.models.config.Configuration : ogx src.models.config.OkpConfiguration --* src.models.config.Configuration : okp src.models.config.QuotaHandlersConfiguration --* src.models.config.Configuration : quota_handlers src.models.config.QuotaSchedulerConfiguration --* src.models.config.QuotaHandlersConfiguration : scheduler diff --git a/docs/user_doc/config.svg b/docs/user_doc/config.svg index 9b0884399..b1270e7d6 100644 --- a/docs/user_doc/config.svg +++ b/docs/user_doc/config.svg @@ -186,7 +186,7 @@ database deployment_environment : str inference - ogx + ogx mcp_servers : list[ModelContextProtocolServer] name : str okp @@ -348,16 +348,16 @@ check_regex_pattern() -> Self check_roles() -> Self - - - + + + - LlamaStackConfiguration + OgxConfiguration allow_degraded_mode : Optional[bool] api_key : Optional[SecretStr] - config : Optional['UnifiedLlamaStackConfig'] + config : Optional['UnifiedOgxConfig'] library_client_config_path : Optional[str] max_retries retry_delay @@ -608,12 +608,12 @@ type : Literal['openai', 'sentence_transformers', 'azure', 'vertexai', 'watsonx', 'vllm_rhaiis', 'vllm_rhel_ai'] - - - + + + - UnifiedLlamaStackConfig + UnifiedOgxConfig baseline : Literal['default', 'empty'] native_override : dict[str, object] @@ -734,9 +734,9 @@ - - - + + + @@ -829,9 +829,9 @@ - - - + + + @@ -905,11 +905,11 @@ jwt_configuration - - - + + + - ogx + ogx diff --git a/docs/user_doc/deployment_guide.md b/docs/user_doc/deployment_guide.md index 42aa10f01..3309ab876 100644 --- a/docs/user_doc/deployment_guide.md +++ b/docs/user_doc/deployment_guide.md @@ -90,12 +90,19 @@ ways it can drive the underlying OGX: optional [profile](#profiles) you author, the high-level `inference.providers` section, and a raw `native_override` escape hatch. All examples in this guide show unified mode first. -2. **Legacy two-file mode (deprecated).** `llama_stack.library_client_config_path` +2. **Legacy two-file mode (deprecated).** `ogx.library_client_config_path` + (the deprecated `llama_stack` YAML-section alias is still accepted) points at an external, hand-maintained `run.yaml`. This path is deprecated: since release 0.6 it logs a startup warning, and it is **removed in release 0.7**. See [Migrating from the legacy two-file configuration](#migrating-from-the-legacy-two-file-configuration). +> [!NOTE] +> The deprecated `llama_stack` YAML key remains accepted with a startup +> warning; use `ogx:` for new configuration. See +> [Migrating from the legacy two-file configuration](#migrating-from-the-legacy-two-file-configuration) +> and [v0.7.0 migration notes](../migrations/v0.7.0.md). + The two modes are mutually exclusive in one file — configuration loading fails if a unified synthesis input and `library_client_config_path` are both present. @@ -112,7 +119,7 @@ The OGX framework can be run as a standalone server and accessed via its the RES When this mode is selected, OGX is used as a regular Python library. This means that the library must be installed in the system Python environment, a user-level environment, or a virtual environment. All calls to OGX are performed via standard function or method calls: -![OGX as library](./llama_stack_as_library.svg) +![OGX as library](./ogx_as_library.svg) > [!NOTE] > Even when OGX is used as a library, it still requires a `run.yaml` @@ -150,14 +157,14 @@ for `baseline: empty` (use `native_override` there if you need MCP). Keep secrets out of the file: write `${env.MY_KEY}` environment references, which OGX resolves at startup. -**Referencing a profile.** Point `llama_stack.config.profile` at the file: +**Referencing a profile.** Point `ogx.config.profile` at the file: ```yaml name: Lightspeed Core Service (LCS) service: host: 0.0.0.0 port: 8080 -llama_stack: +ogx: use_as_library_client: true config: profile: ./profiles/openai-remote.yaml @@ -179,7 +186,7 @@ synthesizer evolves. When this mode is selected, OGX is started as a separate REST API service. All communication with OGX is performed via REST API calls, which means that OGX can run on a separate machine if needed. -![OGX as service](./llama_stack_as_service.svg) +![OGX as service](./ogx_as_service.svg) > [!NOTE] > The REST API schema and semantics can change at any time, especially before version 1.0.0 is released. By using *Lightspeed Core Service*, developers, users, and customers stay isolated from these incompatibilities. @@ -227,7 +234,7 @@ llama_stack: ```yaml # lightspeed-stack-unified.yaml name: LCS - llama_stack: + ogx: use_as_library_client: true config: baseline: empty @@ -289,17 +296,17 @@ The easiest option is to run OGX in a separate process. This means that there wi 1. Create a new directory outside of the lightspeed-stack project directory ```bash - mkdir /tmp/llama-stack-server + mkdir /tmp/ogx-server ``` -1. Copy the project file named `pyproject.llamastack.toml` into the new directory, renaming it to `pyproject.toml': +1. Copy the project file named `pyproject.ogx.toml` into the new directory, renaming it to `pyproject.toml`: ```bash - cp examples/pyproject.llamastack.toml /tmp/llama-stack-server/pyproject.toml + cp examples/pyproject.ogx.toml /tmp/ogx-server/pyproject.toml ``` 1. Run the following command to install all OGX dependencies in a new venv located in your new directory: ```bash - cd /tmp/llama-stack-server + cd /tmp/ogx-server uv sync ``` @@ -338,7 +345,7 @@ The easiest option is to run OGX in a separate process. This means that there wi 1. In the next step, we need to verify that it is possible to run a tool called `ogx`. It was installed into a Python virtual environment and therefore we have to run it via `uv run` command: ```bash - uv run llama + uv run ogx ``` 1. If the installation was successful, the following messages should be displayed on the terminal: ``` @@ -359,7 +366,7 @@ The easiest option is to run OGX in a separate process. This means that there wi ``` 1. If we try to run the OGX without configuring it, only the exception information is displayed (which is not very user-friendly): ```bash - uv run llama stack run + uv run ogx stack run ``` Output: ``` @@ -368,13 +375,13 @@ The easiest option is to run OGX in a separate process. This means that there wi File "/tmp/ramdisk/ogx-runner/.venv/bin/ogx", line 10, in sys.exit(main()) ^^^^^^ - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 53, in main + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 53, in main parser.run(args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 47, in run + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 47, in run args.func(args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/stack/run.py", line 164, in _run_stack_run_cmd + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/stack/run.py", line 164, in _run_stack_run_cmd server_main(server_args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/distribution/server/server.py", line 414, in main + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/distribution/server/server.py", line 414, in main elif args.template: ^^^^^^^^^^^^^ AttributeError: 'Namespace' object has no attribute 'template' @@ -387,7 +394,7 @@ The easiest option is to run OGX in a separate process. This means that there wi OGX needs to be configured properly. For using the default runnable OGX a file named `run.yaml` needs to be created. Copy the example `examples/run.yaml` from the lightspeed-stack project directory into your OGX directory. ```bash -cp examples/run.yaml /tmp/llama-stack-server +cp examples/run.yaml /tmp/ogx-server ``` @@ -399,7 +406,7 @@ cp examples/run.yaml /tmp/llama-stack-server ``` 1. Run the following command: ```bash - uv run llama stack run run.yaml + uv run ogx stack run run.yaml ``` 1. Check the output on terminal, it should look like: ``` @@ -648,7 +655,7 @@ cp examples/lightspeed-stack-lls-library.yaml lightspeed-stack.yaml The example is a unified-mode configuration: the `run.yaml` you created above is consumed as the synthesis [profile](#profiles) via -`llama_stack.config.profile` — there is no deprecated +`ogx.config.profile` — there is no deprecated `library_client_config_path` in it. @@ -831,12 +838,12 @@ a4982f43195537b9eb1cec510fe6655f245d6d4b7236a4759808115d5d719972 1. Create project file named `pyproject.toml` in this directory. This file should have the following content: ```toml [project] - name = "llama-stack-demo" + name = "ogx-demo" version = "0.1.0" description = "Default template for PDM package" authors = [] dependencies = [ - "llama-stack==0.2.22", + "ogx==1.2.5", "fastapi>=0.115.12", "opentelemetry-sdk>=1.34.0", "opentelemetry-exporter-otlp>=1.34.0", @@ -903,7 +910,7 @@ a4982f43195537b9eb1cec510fe6655f245d6d4b7236a4759808115d5d719972 1. In the next step, we need to verify that it is possible to run a tool called `ogx`. It was installed into a Python virtual environment and therefore we have to run it via `uv run` command: ```bash - uv run llama + uv run ogx ``` 1. If the installation was successful, the following messages should be displayed on the terminal: ```text @@ -924,7 +931,7 @@ a4982f43195537b9eb1cec510fe6655f245d6d4b7236a4759808115d5d719972 ``` 1. If we try to run the OGX without configuring it, only the exception information is displayed (which is not very user-friendly): ```bash - uv run llama stack run + uv run ogx stack run ``` Output: ``` @@ -933,13 +940,13 @@ a4982f43195537b9eb1cec510fe6655f245d6d4b7236a4759808115d5d719972 File "/tmp/ramdisk/ogx-runner/.venv/bin/ogx", line 10, in sys.exit(main()) ^^^^^^ - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 53, in main + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 53, in main parser.run(args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 47, in run + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/llama.py", line 47, in run args.func(args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/stack/run.py", line 164, in _run_stack_run_cmd + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/cli/stack/run.py", line 164, in _run_stack_run_cmd server_main(server_args) - File "/tmp/ramdisk/llama-stack-runner/.venv/lib64/python3.12/site-packages/llama_stack/distribution/server/server.py", line 414, in main + File "/tmp/ramdisk/ogx-runner/.venv/lib64/python3.12/site-packages/llama_stack/distribution/server/server.py", line 414, in main elif args.template: ^^^^^^^^^^^^^ AttributeError: 'Namespace' object has no attribute 'template' @@ -949,7 +956,7 @@ a4982f43195537b9eb1cec510fe6655f245d6d4b7236a4759808115d5d719972 #### OGX configuration -OGX needs to be configured properly. For using the default runnable OGX a file named `run.yaml` needs to be created. Use the example configuration from [examples/run.yaml](../examples/run.yaml). +OGX needs to be configured properly. For using the default runnable OGX a file named `run.yaml` needs to be created. Use the example configuration from [examples/run.yaml](../../examples/run.yaml). @@ -961,7 +968,7 @@ OGX needs to be configured properly. For using the default runnable OGX a file n ``` 1. Run the following command: ```bash - uv run llama stack run run.yaml + uv run ogx stack run run.yaml ``` 1. Check the output on terminal, it should look like: ```text @@ -1126,7 +1133,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: use_as_library_client: false url: http://localhost:8321 api_key: xyzzy @@ -1173,7 +1180,7 @@ export OPENAI_API_KEY="sk-foo-bar-baz-my-key" #### OGX configuration -Create a file named `run.yaml`. Use the example configuration from [examples/run.yaml](../examples/run.yaml). +Create a file named `run.yaml`. Use the example configuration from [examples/run.yaml](../../examples/run.yaml). ### LCS configuration @@ -1190,7 +1197,7 @@ service: workers: 1 color_log: true access_log: true -llama_stack: +ogx: use_as_library_client: true config: profile: ./run.yaml diff --git a/docs/user_doc/lcs_in_container.svg b/docs/user_doc/lcs_in_container.svg index 6232cf444..c9fb42c5a 100644 --- a/docs/user_doc/lcs_in_container.svg +++ b/docs/user_doc/lcs_in_container.svg @@ -1,7 +1,7 @@ - + @@ -151,7 +151,7 @@
    -
    llama-stack as separate server
    +
    OGX as separate server
    diff --git a/docs/user_doc/llama_stack_as_library.svg b/docs/user_doc/ogx_as_library.svg similarity index 99% rename from docs/user_doc/llama_stack_as_library.svg rename to docs/user_doc/ogx_as_library.svg index bfa8645b6..3d458f0f7 100644 --- a/docs/user_doc/llama_stack_as_library.svg +++ b/docs/user_doc/ogx_as_library.svg @@ -102,12 +102,12 @@
    -
    llama-stack as library (direct Python dependency)
    +
    OGX as library (direct Python dependency)
    - llama-stack as libra... + OGX as library...
    diff --git a/docs/user_doc/llama_stack_as_service.svg b/docs/user_doc/ogx_as_service.svg similarity index 99% rename from docs/user_doc/llama_stack_as_service.svg rename to docs/user_doc/ogx_as_service.svg index 9062ed96a..f98538d1e 100644 --- a/docs/user_doc/llama_stack_as_service.svg +++ b/docs/user_doc/ogx_as_service.svg @@ -126,12 +126,12 @@
    -
    llama-stack as separate server
    +
    OGX as separate server
    - llama-stack as separa... + OGX as separate server
    diff --git a/docs/user_doc/opentelemetry.md b/docs/user_doc/opentelemetry.md index 6cfac2c94..d3b90a265 100644 --- a/docs/user_doc/opentelemetry.md +++ b/docs/user_doc/opentelemetry.md @@ -84,7 +84,7 @@ The `otel` object is one section inside the full `configuration` payload returne "configuration": { "name": "lightspeed-stack", "service": { "..." }, - "llama_stack": { "..." }, + "ogx": { "..." }, "authentication": { "..." }, "authorization": { "..." }, "inference": { "..." }, diff --git a/docs/user_doc/rag_guide.md b/docs/user_doc/rag_guide.md index 84ae9bea4..49a8cb964 100644 --- a/docs/user_doc/rag_guide.md +++ b/docs/user_doc/rag_guide.md @@ -102,7 +102,7 @@ Download a local embedding model such as `sentence-transformers/all-mpnet-base-v -BYOK knowledge sources are configured in the `rag.byok.stores` section of `lightspeed-stack.yaml`. The required configuration is automatically generated at startup when using `make run`, `make run-stack`, `docker-compose`, or library mode — no manual enrichment is needed. +BYOK knowledge sources are configured in the `rag.byok.stores` section of `lightspeed-stack.yaml`. The required configuration is automatically generated at startup when using `make run`, `make run-ogx`, `docker-compose`, or library mode — no manual enrichment is needed. ### FAISS example @@ -123,7 +123,7 @@ Where: - `db_path` is the path to the vector index (.db file in this case) - `vector_db_id` is the ID generated by rag-content during index creation (e.g. `vs_8c94967b-81cc-4028-a294-9cfac6fd9ae2`) -See the full working [config example](../examples/lightspeed-stack-byok-okp-rag.yaml) for more details. +See the full working [config example](../../examples/lightspeed-stack-byok-okp-rag.yaml) for more details. ### pgvector example @@ -181,8 +181,8 @@ Requirements: - When `providers` is empty, `default_provider` must be omitted - Provider `id` must match `[a-z0-9_-]+` and must not start with `byok_` - Applied in **unified** OGX synthesis only - (`llama_stack.use_as_library_client: true` with a synthesis input such as - `llama_stack.config`, `inference.providers`, or `vector_store.providers`) + (`ogx.use_as_library_client: true` with a synthesis input such as + `ogx.config`, `inference.providers`, or `vector_store.providers`) `default_provider` becomes `vector_stores.default_provider_id` and that provider's embedding model becomes `default_embedding_model` in the @@ -527,7 +527,7 @@ You are a helpful assistant with access to a 'knowledge_search' tool. When users --- # RAG annotations -The top-level `vector_stores` block in [`run.yaml`](../examples/run.yaml) may include `annotation_prompt_params` to control whether extra RAG annotation instructions are injected into the model prompt (for example, citation-style markers). The default configuration sets `enable_annotations: false` under that block to avoid unwanted annotations. +The top-level `vector_stores` block in [`run.yaml`](../../examples/run.yaml) may include `annotation_prompt_params` to control whether extra RAG annotation instructions are injected into the model prompt (for example, citation-style markers). The default configuration sets `enable_annotations: false` under that block to avoid unwanted annotations. When `vector_store` is configured, `default_provider` overwrites `vector_stores.default_provider_id` and `default_embedding_model` diff --git a/docs/user_doc/splunk.md b/docs/user_doc/splunk.md index a23d07dfa..c09867cfc 100644 --- a/docs/user_doc/splunk.md +++ b/docs/user_doc/splunk.md @@ -85,7 +85,7 @@ Events follow the rlsapi telemetry format for consistency with existing analytic "system_id": "abc-def-123", "total_llm_tokens": 0, "request_id": "req_xyz789", - "cla_version": "CLA/0.7.0rc2", + "cla_version": "CLA/0.7.0rc3", "system_os": "RHEL", "system_version": "9.3", "system_arch": "x86_64" @@ -163,4 +163,4 @@ splunk: ## Extending to Other Endpoints -See [src/observability/README.md](../src/observability/README.md) for developer documentation on adding Splunk telemetry to additional endpoints. +See [src/observability/README.md](../../src/observability/README.md) for developer documentation on adding Splunk telemetry to additional endpoints. diff --git a/examples/lightspeed-stack-azure-entraid-service.yaml b/examples/lightspeed-stack-azure-entraid-service.yaml index f3f1a3186..eb77e4f48 100644 --- a/examples/lightspeed-stack-azure-entraid-service.yaml +++ b/examples/lightspeed-stack-azure-entraid-service.yaml @@ -12,7 +12,7 @@ ogx: use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://localhost:8321 api_key: xyzzy user_data_collection: diff --git a/examples/profiles/inline-faiss.yaml b/examples/profiles/inline-faiss.yaml index 1e1004ac1..a88db319a 100644 --- a/examples/profiles/inline-faiss.yaml +++ b/examples/profiles/inline-faiss.yaml @@ -18,7 +18,7 @@ # # - type: vllm # # id: vllm-staging # # api_key_env: VLLM_STAGING_KEY -# llama_stack: +# ogx: # use_as_library_client: true # config: # profile: examples/profiles/inline-faiss.yaml diff --git a/examples/profiles/openai-remote.yaml b/examples/profiles/openai-remote.yaml index a212db622..268fc599d 100644 --- a/examples/profiles/openai-remote.yaml +++ b/examples/profiles/openai-remote.yaml @@ -4,7 +4,7 @@ # the synthesis baseline instead of LCORE's built-in default. Reference it from # lightspeed-stack.yaml: # -# llama_stack: +# ogx: # use_as_library_client: true # config: # profile: examples/profiles/openai-remote.yaml diff --git a/examples/pyproject.llamastack.toml b/examples/pyproject.ogx.toml similarity index 89% rename from examples/pyproject.llamastack.toml rename to examples/pyproject.ogx.toml index 9f873ee09..d75209b53 100644 --- a/examples/pyproject.llamastack.toml +++ b/examples/pyproject.ogx.toml @@ -1,10 +1,12 @@ [project] -name = "llama-stack-demo" +name = "ogx-demo" version = "0.1.0" description = "Default template for PDM package" authors = [] dependencies = [ - "llama-stack==0.2.22", + "ogx==1.2.5", + "ogx-api==1.2.5", + "ogx-client==1.2.5", "fastapi>=0.115.12", "opentelemetry-sdk>=1.34.0", "opentelemetry-exporter-otlp>=1.34.0", diff --git a/examples/quota-limiter-configuration-pg.yaml b/examples/quota-limiter-configuration-pg.yaml index dfe7e8502..9883ebaaf 100644 --- a/examples/quota-limiter-configuration-pg.yaml +++ b/examples/quota-limiter-configuration-pg.yaml @@ -8,11 +8,11 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with an OGX run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://localhost:8321 api_key: xyzzy user_data_collection: diff --git a/examples/quota-limiter-configuration-sqlite.yaml b/examples/quota-limiter-configuration-sqlite.yaml index 757182436..a083ca626 100644 --- a/examples/quota-limiter-configuration-sqlite.yaml +++ b/examples/quota-limiter-configuration-sqlite.yaml @@ -8,11 +8,11 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with an OGX run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://localhost:8321 api_key: xyzzy user_data_collection: diff --git a/lightspeed-stack.yaml b/lightspeed-stack.yaml index 62af987bb..c784d14c1 100644 --- a/lightspeed-stack.yaml +++ b/lightspeed-stack.yaml @@ -7,11 +7,11 @@ service: workers: 1 color_log: true access_log: true -# llama_stack configuration +# ogx configuration # When using 'make run', a container is ALWAYS launched at http://localhost:8321 (hardcoded in Makefile). -# This llama_stack section controls where lightspeed-core connects to OGX. -# To use a different port: override with 'make run LLAMA_STACK_PORT=' and update the url below, -# or run ogx manually and don't use 'make run'. +# This ogx section controls where lightspeed-core connects to OGX. +# To use a different port: override with 'make run OGX_PORT=' and update the url below, +# or run OGX manually and don't use 'make run'. ogx: use_as_library_client: false url: http://localhost:8321 diff --git a/pyproject.toml b/pyproject.toml index aa0152c1e..c9335bed2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ keywords = [ "LLM", "RAG" ] -requires-python = ">=3.12,<3.14" +requires-python = ">=3.12,<3.15" readme = "README.md" license = {file = "LICENSE"} dependencies = [ @@ -152,6 +152,8 @@ dev = [ "pytest-benchmark>=5.2.3", "pyroscope-io>=0.8.7", "memray>=1.13.0", + "pygal>=3.1.3", + "pygaljs>=1.0.2", ] ogxlibdev = [ # To check OGX API provider dependecies: @@ -176,6 +178,7 @@ ogxlibdev = [ "faiss-cpu>=1.11.0", "chardet>=5.2.0", "psycopg2-binary>=2.9.10", + "pgvector>=0.3.6", "pypdf>=6.10.2", # API scoring: inline::basic "requests>=2.33.0", @@ -249,7 +252,7 @@ line-length = 88 extend-exclude = ["tests/profiles/syntax_error.py"] [tool.ruff.lint] -extend-select = ["TID251", "UP006", "UP007", "UP008", "UP010", "UP012", "UP017", "UP024", "UP035", "UP040", "UP041", "RUF100", "B009", "B010", "DTZ005", "D202", "I001", "PLR1733", "RUF022", "PLW1510", "TC004", "PIE790", "PERF402", "FURB129", "RET501"] +extend-select = ["TID251", "UP006", "UP007", "UP008", "UP010", "UP012", "UP017", "UP024", "UP035", "UP040", "UP041", "RUF100", "B009", "B010", "B011", "DTZ005", "D202", "I001", "PLR1733", "RUF022", "PLW1510", "TC004", "PIE790", "PERF402", "FURB129", "RET501"] ignore = ["UP047", "UP045", "BLE", "S", "C", "RUF", "SIM", "B017", "TRY004", "TRY201", "TRY203", "TRY401", "B008", "EXE001", "G201", "ISC004", "LOG014", "PYI034", "PYI064"] [tool.ruff.lint.flake8-tidy-imports.banned-api] diff --git a/scripts/konflux_resolve.py b/scripts/konflux_resolve.py index 195745924..e3a4836af 100644 --- a/scripts/konflux_resolve.py +++ b/scripts/konflux_resolve.py @@ -13,10 +13,10 @@ import os import re import subprocess +import sys import time import tomllib import urllib.request -import sys from collections import deque from collections.abc import Sequence from html.parser import HTMLParser @@ -890,7 +890,7 @@ def write_hashed_requirements( # RHOAI packages store hashes per platform if "platforms" in info: - for _arch, (_, sha) in info["platforms"].items(): + for (_, sha) in info["platforms"].values(): if sha: hashes.add(sha) diff --git a/scripts/ogx-entrypoint.sh b/scripts/ogx-entrypoint.sh index b510ec040..81a409b10 100755 --- a/scripts/ogx-entrypoint.sh +++ b/scripts/ogx-entrypoint.sh @@ -6,12 +6,12 @@ # configuration shape: # - unified mode: the lightspeed config carries a synthesis input (a # non-empty inference.providers or vector_store.providers, or a -# llama_stack.config / ogx.config block). The full run.yaml is synthesized +# ogx.config block (deprecated alias: llama_stack.config). The full run.yaml is synthesized # from it — no external run.yaml mount is needed, and $OGX_CONFIG / -# $LLAMA_STACK_CONFIG is ignored. The shipped default baseline is read from +# $LLAMA_STACK_CONFIG (deprecated) is ignored. The shipped default baseline is read from # /opt/app-root/data/default_run.yaml. -# - legacy mode: the mounted run.yaml ($OGX_CONFIG, falling back to -# $LLAMA_STACK_CONFIG) is enriched with lightspeed dynamic values +# - legacy mode: the mounted run.yaml ($OGX_CONFIG, falling back to the +# deprecated $LLAMA_STACK_CONFIG) is enriched with lightspeed dynamic values # (BYOK RAG, Solr/OKP, Azure Entra ID). set -e diff --git a/scripts/ogx_tutorial.sh b/scripts/ogx_tutorial.sh index d3a3aaa6c..5847995b0 100755 --- a/scripts/ogx_tutorial.sh +++ b/scripts/ogx_tutorial.sh @@ -3,7 +3,7 @@ # OGX Tutorial - Interactive Guide # This tutorial demonstrates key features of the OGX server -LLAMA_STACK_URL="http://localhost:8321" +OGX_URL="http://localhost:8321" GREEN='\033[0;32m' BLUE='\033[0;34m' @@ -26,7 +26,7 @@ print_section() { print_header() { echo "" echo "🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀" - echo " WELCOME TO THE QUICK LLAMA STACK TUTORIAL " + echo " WELCOME TO THE QUICK OGX TUTORIAL " echo "🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀" echo "" } @@ -77,15 +77,15 @@ fi print_header if [ "$INTERACTIVE" = true ]; then - echo "This tutorial will guide you through the Llama Stack API step by step." + echo "This tutorial will guide you through the OGX API step by step." echo "You'll explore models, tools, shields, and see example API calls." wait_for_user fi # Section 0: What is OGX? -print_section "What is Llama Stack?" +print_section "What is OGX?" cat << 'EOF' -Llama Stack serves as the AI INTEGRATION LAYER - it's the middleware that abstracts +OGX serves as the AI INTEGRATION LAYER - it's the middleware that abstracts away the complexity of working with different LLM providers and provides a unified API for AI operations. @@ -113,13 +113,13 @@ KEY FEATURES: • Allows runtime switching between different LLM providers (OpenAI, Azure, etc.) DEPLOYMENT MODES: - 1. Service Mode: Llama Stack runs as a separate service - 2. Library Mode: Llama Stack embedded directly in the app + 1. Service Mode: OGX runs as a separate service + 2. Library Mode: OGX embedded directly in the app BOTTOM LINE: -Think of Llama Stack as a UNIVERSAL ADAPTER for AI operations. Instead of coding -directly against OpenAI's API, Azure's API, etc., Lightspeed Stack uses Llama -Stack's unified interface. This makes it easy to switch providers, add new +Think of OGX as a UNIVERSAL ADAPTER for AI operations. Instead of coding +directly against OpenAI's API, Azure's API, etc., Lightspeed Stack uses OGX's +unified interface. This makes it easy to switch providers, add new capabilities (like agents or RAG), and maintain consistent behavior across different LLM backends. @@ -128,26 +128,26 @@ wait_for_user # Section 1: Health Check print_section "1. Health Check" -echo "Let's verify the Llama Stack server is running..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/health | ${JQ_CMD}" +echo "Let's verify the OGX server is running..." +run_command "curl -s ${OGX_URL}/v1/health | ${JQ_CMD}" wait_for_user # Section 2: Version print_section "2. Server Version" -echo "Checking Llama Stack version..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/version | ${JQ_CMD}" +echo "Checking OGX version..." +run_command "curl -s ${OGX_URL}/v1/version | ${JQ_CMD}" wait_for_user # Section 3: Models print_section "3. Available Models" -echo "Llama Stack supports multiple models from different providers." +echo "OGX supports multiple models from different providers." echo "Let's see what models are available..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/models | ${JQ_CMD}" +run_command "curl -s ${OGX_URL}/v1/models | ${JQ_CMD}" echo "" echo "Let me analyze the models for you..." -MODELS_JSON=$(curl -s ${LLAMA_STACK_URL}/v1/models) +MODELS_JSON=$(curl -s ${OGX_URL}/v1/models) if command -v jq &> /dev/null; then @@ -175,25 +175,25 @@ wait_for_user print_section "4. Safety Shields" echo "Shields provide content filtering and safety mechanisms." echo "Let's see what shields are configured..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/shields | ${JQ_CMD}" +run_command "curl -s ${OGX_URL}/v1/shields | ${JQ_CMD}" wait_for_user # Section 5: Tool Groups print_section "5. Tool Groups" -echo "Llama Stack supports tool groups that organize related tools." +echo "OGX supports tool groups that organize related tools." echo "Let's explore available tool groups..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/toolgroups | ${JQ_CMD}" +run_command "curl -s ${OGX_URL}/v1/toolgroups | ${JQ_CMD}" wait_for_user # Section 6: Tools print_section "6. Available Tools" echo "Tools allow agents to perform specific actions." echo "Let's see what tools are available..." -run_command "curl -s ${LLAMA_STACK_URL}/v1/tools | ${JQ_CMD}" +run_command "curl -s ${OGX_URL}/v1/tools | ${JQ_CMD}" echo "" echo "Let me show you the tool details..." -TOOLS_JSON=$(curl -s ${LLAMA_STACK_URL}/v1/tools) +TOOLS_JSON=$(curl -s ${OGX_URL}/v1/tools) if command -v jq &> /dev/null; then echo "" @@ -219,7 +219,7 @@ curl -X POST http://localhost:8321/v1/inference/chat-completion \ "messages": [ { "role": "user", - "content": "Explain Llama Stack in one sentence." + "content": "Explain OGX in one sentence." } ], "stream": false @@ -235,7 +235,7 @@ curl -X POST http://localhost:8321/v1/inference/embeddings \ -H 'Content-Type: application/json' \ -d '{ "model_id": "openai/text-embedding-3-small", - "contents": ["Llama Stack is awesome!"] + "contents": ["OGX is awesome!"] }' | jq . EOF @@ -268,12 +268,12 @@ EOF wait_for_user # Section 9: Integration -print_section "9. How Lightspeed Stack Uses Llama Stack" +print_section "9. How Lightspeed Stack Uses OGX" cat << 'EOF' -Lightspeed Stack integrates with Llama Stack to provide: +Lightspeed Stack integrates with OGX to provide: 1. 🤖 Multi-Provider LLM Support - - Llama Stack abstracts different providers (OpenAI, Azure, etc.) + - OGX abstracts different providers (OpenAI, Azure, etc.) - Lightspeed Stack uses this to support multiple models seamlessly 2. 🛡️ Safety & Content Filtering @@ -289,33 +289,33 @@ Lightspeed Stack integrates with Llama Stack to provide: - Simplifies AI integration in the Lightspeed Stack codebase Key Integration Points in Lightspeed Stack: -- src/client.py: Llama Stack client wrapper -- src/app/endpoints/: API endpoints using Llama Stack -- src/configuration.py: Configuration for Llama Stack connection +- src/client.py: OGX client wrapper +- src/app/endpoints/: API endpoints using OGX +- src/configuration.py: Configuration for OGX connection EOF wait_for_user # Section 10: Try It Now print_section "10. Try It Yourself!" echo "Let's make a real API call to see all available routes!" -run_command "curl -s ${LLAMA_STACK_URL}/v1/inspect/routes | ${JQ_CMD}" +run_command "curl -s ${OGX_URL}/v1/inspect/routes | ${JQ_CMD}" wait_for_user # Conclusion print_section "🎉 Tutorial Complete!" cat << 'EOF' You've learned about: -✅ Llama Stack server capabilities +✅ OGX server capabilities ✅ Available models (LLMs and embeddings) ✅ Safety shields for content filtering ✅ Tools and tool groups ✅ How to make API calls -✅ How Lightspeed Stack integrates with Llama Stack +✅ How Lightspeed Stack integrates with OGX Next Steps: 1. Explore the OpenAPI docs: http://localhost:8321/docs 2. Try the example commands above -3. Look at how Lightspeed Stack uses Llama Stack in src/client.py +3. Look at how Lightspeed Stack uses OGX in src/client.py 4. Experiment with different models and tools Resources: diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index 861bec02e..e49424239 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -42,7 +42,6 @@ import matplotlib.pyplot as plt from dateutil import parser -from matplotlib.figure import Figure type DependabotAlert = dict[str, Any] type DependabotAlerts = list[DependabotAlert] @@ -429,9 +428,7 @@ def process_dependabot_file(dependabot_file: str) -> dict[str, Any]: return stat -def save_graph( - fig: Figure, prefix: str, postfix: str, svg_output: bool, png_output: bool -) -> None: +def save_graph(prefix: str, postfix: str, svg_output: bool, png_output: bool) -> None: """ Save a figure in SVG and/or PNG formats based on output flags. @@ -459,14 +456,14 @@ def generate_overal_state_graph( svg_output (bool): Whether to save in SVG format. png_output (bool): Whether to save in PNG format. """ - fig, ax = plt.subplots() - D = stat["state"] + _, ax = plt.subplots() + data = stat["state"] ax.bar( - range(len(D)), list(D.values()), align="center", color=["#c00000", "#00c000"] + range(len(data)), list(data.values()), align="center", color=["#c00000", "#00c000"] ) ax.set_ylim(top=400) - ax.set_xticks(range(len(D)), list(D.keys())) - save_graph(fig, prefix, "state", svg_output, png_output) + ax.set_xticks(range(len(data)), list(data.keys())) + save_graph(prefix, "state", svg_output, png_output) def generate_severity_graph( @@ -481,16 +478,16 @@ def generate_severity_graph( svg_output (bool): If true, save the graph as SVG. png_output (bool): If true, save the graph as PNG. """ - fig, ax = plt.subplots() - D = stat["severity"] + _, ax = plt.subplots() + data = stat["severity"] ax.bar( - range(len(D)), - list(D.values()), + range(len(data)), + list(data.values()), align="center", color=["#c00000", "orange", "#e0e000", "#00c000"], ) - ax.set_xticks(range(len(D)), list(D.keys())) - save_graph(fig, prefix, "severity", svg_output, png_output) + ax.set_xticks(range(len(data)), list(data.keys())) + save_graph(prefix, "severity", svg_output, png_output) def generate_vuln_for_days_graph( @@ -505,13 +502,13 @@ def generate_vuln_for_days_graph( svg_output (bool): If true, save the graph as SVG. png_output (bool): If true, save the graph as PNG. """ - fig, ax = plt.subplots() - D = stat["days"]["days"] - ax.hist(D, bins=30, edgecolor="black") + _, ax = plt.subplots() + data = stat["days"]["days"] + ax.hist(data, bins=30, edgecolor="black") ax.set_ylim(top=100) ax.set_xlabel("Days") ax.set_title("Fixed in day(s)") - save_graph(fig, prefix, "days", svg_output, png_output) + save_graph(prefix, "days", svg_output, png_output) def generate_vulnerable_packages_graph( @@ -528,14 +525,14 @@ def generate_vulnerable_packages_graph( png_output (bool): If true, save the graph as PNG. """ fig, ax = plt.subplots() - D = stat["packages"] - names, counts = zip(*D.most_common(10)) + data = stat["packages"] + names, counts = zip(*data.most_common(10)) ax.bar(names, counts, edgecolor="black") ax.set_ylim(top=100) ax.set_title("CVEs per package") ax.tick_params(axis="x", labelrotation=90) fig.tight_layout() - save_graph(fig, prefix, "packages", svg_output, png_output) + save_graph(prefix, "packages", svg_output, png_output) def generate_new_cve_dates_graph( @@ -551,12 +548,12 @@ def generate_new_cve_dates_graph( svg_output (bool): Whether to save the graph as SVG. png_output (bool): Whether to save the graph as PNG. """ - fig, ax = plt.subplots() - D = stat["dates"] - dates, counts = zip(*sorted(D.items(), key=lambda x: x[0])) + _, ax = plt.subplots() + data = stat["dates"] + dates, counts = zip(*sorted(data.items(), key=lambda x: x[0])) ax.plot(dates, counts) ax.set_title("New CVEs timeline") - save_graph(fig, prefix, "timeline", svg_output, png_output) + save_graph(prefix, "timeline", svg_output, png_output) def generate_graphs( diff --git a/src/README.md b/src/README.md index e5b29fee7..7ce0c9b1c 100644 --- a/src/README.md +++ b/src/README.md @@ -16,14 +16,14 @@ Constants used in business logic. Entry point to the Lightspeed Core Stack REST API service. -## [ogx_configuration.py](ogx_configuration.py) - -OGX configuration enrichment and synthesis. - ## [log.py](log.py) Log utilities. +## [ogx_configuration.py](ogx_configuration.py) + +OGX configuration enrichment and synthesis. + ## [version.py](version.py) Service version that is read by project manager tools. diff --git a/src/app/endpoints/a2a.py b/src/app/endpoints/a2a.py index fd7931459..3bd6db1c2 100644 --- a/src/app/endpoints/a2a.py +++ b/src/app/endpoints/a2a.py @@ -68,7 +68,7 @@ apply_compaction_blocking, store_compacted_turn, ) -from utils.mcp_headers import McpHeaders, mcp_headers_dependency +from utils.mcp.mcp_headers import McpHeaders, mcp_headers_dependency from utils.otel_tracing import ( SpanAttributes, SpanEvents, diff --git a/src/app/endpoints/a2a_openapi.py b/src/app/endpoints/a2a_openapi.py index e5990e665..4f4fc925e 100644 --- a/src/app/endpoints/a2a_openapi.py +++ b/src/app/endpoints/a2a_openapi.py @@ -1,11 +1,12 @@ """OpenAPI-only metadata for A2A JSON-RPC routes.""" -from typing import Any, Final +from typing import Final from constants import MEDIA_TYPE_EVENT_STREAM, MEDIA_TYPE_JSON +from utils.types import Responses # 200 may be buffered JSON-RPC (application/json) or SSE (text/event-stream). -a2a_jsonrpc_responses: Final[dict[int | str, dict[str, Any]]] = { +a2a_jsonrpc_responses: Final[Responses] = { 200: { "description": "Successful response", "content": { diff --git a/src/app/endpoints/authorized.py b/src/app/endpoints/authorized.py index 34b882b65..4930273bf 100644 --- a/src/app/endpoints/authorized.py +++ b/src/app/endpoints/authorized.py @@ -1,6 +1,6 @@ """Handler for REST API call to authorized endpoint.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends from opentelemetry import trace @@ -16,12 +16,13 @@ ) from models.api.responses.successful import AuthorizedResponse from utils.otel_tracing import SpanAttributes, anonymize_value, set_span_attributes +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["authorized"]) -authorized_responses: dict[int | str, dict[str, Any]] = { +authorized_responses: Responses = { 200: AuthorizedResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/config.py b/src/app/endpoints/config.py index 8102bb2b1..425790efb 100644 --- a/src/app/endpoints/config.py +++ b/src/app/endpoints/config.py @@ -1,6 +1,6 @@ """Handler for REST API call to retrieve service configuration.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from opentelemetry import trace @@ -20,13 +20,14 @@ from models.api.responses.successful import ConfigurationResponse from models.config import Action from utils.endpoints import check_configuration_loaded +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["config"]) -get_config_responses: dict[int | str, dict[str, Any]] = { +get_config_responses: Responses = { 200: ConfigurationResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/conversations_v1.py b/src/app/endpoints/conversations_v1.py index 4edfaac16..d744db5f4 100644 --- a/src/app/endpoints/conversations_v1.py +++ b/src/app/endpoints/conversations_v1.py @@ -52,12 +52,13 @@ normalize_conversation_id, to_ogx_conversation_id, ) +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["conversations_v1"]) -conversation_get_responses: dict[int | str, dict[str, Any]] = { +conversation_get_responses: Responses = { 200: ConversationResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -71,7 +72,7 @@ ), } -conversation_delete_responses: dict[int | str, dict[str, Any]] = { +conversation_delete_responses: Responses = { 200: ConversationDeleteResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -86,7 +87,7 @@ ), } -conversations_list_responses: dict[int | str, dict[str, Any]] = { +conversations_list_responses: Responses = { 200: ConversationsListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -98,7 +99,7 @@ ), } -conversation_update_responses: dict[int | str, dict[str, Any]] = { +conversation_update_responses: Responses = { 200: ConversationUpdateResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), diff --git a/src/app/endpoints/conversations_v2.py b/src/app/endpoints/conversations_v2.py index 8905303dc..0478caa6d 100644 --- a/src/app/endpoints/conversations_v2.py +++ b/src/app/endpoints/conversations_v2.py @@ -33,13 +33,14 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.suid import check_suid +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["conversations_v2"]) -conversation_get_responses: dict[int | str, dict[str, Any]] = { +conversation_get_responses: Responses = { 200: ConversationResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -51,7 +52,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -conversation_delete_responses: dict[int | str, dict[str, Any]] = { +conversation_delete_responses: Responses = { 200: ConversationDeleteResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -62,7 +63,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -conversations_list_responses: dict[int | str, dict[str, Any]] = { +conversations_list_responses: Responses = { 200: ConversationsListResponseV2.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -72,7 +73,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -conversation_update_responses: dict[int | str, dict[str, Any]] = { +conversation_update_responses: Responses = { 200: ConversationUpdateResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["conversation_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), diff --git a/src/app/endpoints/feedback.py b/src/app/endpoints/feedback.py index d593aa917..77162e4ea 100644 --- a/src/app/endpoints/feedback.py +++ b/src/app/endpoints/feedback.py @@ -38,6 +38,7 @@ set_span_attributes, ) from utils.suid import get_suid +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) @@ -45,7 +46,7 @@ feedback_status_lock = threading.Lock() -feedback_post_response: dict[int | str, dict[str, Any]] = { +feedback_post_response: Responses = { 200: FeedbackResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint", "feedback"]), @@ -56,7 +57,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -feedback_put_response: dict[int | str, dict[str, Any]] = { +feedback_put_response: Responses = { 200: FeedbackStatusUpdateResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -64,7 +65,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -feedback_get_response: dict[int | str, dict[str, Any]] = { +feedback_get_response: Responses = { 200: StatusResponse.openapi_response(), } diff --git a/src/app/endpoints/health.py b/src/app/endpoints/health.py index cce30df60..7e0becbd0 100644 --- a/src/app/endpoints/health.py +++ b/src/app/endpoints/health.py @@ -5,7 +5,7 @@ methods. For HEAD HTTP method, just the HTTP response code is used. """ -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Response, status from ogx_client import ApiException @@ -33,13 +33,14 @@ ) from models.config import Action from utils.degraded_mode import DegradedModeTracker +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["health"]) -get_readiness_responses: dict[int | str, dict[str, Any]] = { +get_readiness_responses: Responses = { 200: ReadinessResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -48,7 +49,7 @@ ), } -get_liveness_responses: dict[int | str, dict[str, Any]] = { +get_liveness_responses: Responses = { 200: LivenessResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/info.py b/src/app/endpoints/info.py index 90b52baf4..5dd12177f 100644 --- a/src/app/endpoints/info.py +++ b/src/app/endpoints/info.py @@ -1,6 +1,6 @@ """Handler for REST API call to provide info.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Request from ogx_client import ApiException @@ -21,6 +21,7 @@ from models.api.responses.successful import InfoResponse from models.config import Action from utils.otel_tracing import set_span_attributes +from utils.types import Responses from version import __version__ logger = get_logger(__name__) @@ -28,7 +29,7 @@ router = APIRouter(tags=["info"]) -get_info_responses: dict[int | str, dict[str, Any]] = { +get_info_responses: Responses = { 200: InfoResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/mcp_auth.py b/src/app/endpoints/mcp_auth.py index 9cd7b326a..6327e7d6e 100644 --- a/src/app/endpoints/mcp_auth.py +++ b/src/app/endpoints/mcp_auth.py @@ -1,6 +1,6 @@ """Handler for REST API calls related to MCP server authentication.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from opentelemetry import trace @@ -23,13 +23,14 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.otel_tracing import SpanAttributes, set_span_attributes +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(prefix="/mcp-auth", tags=["mcp-auth"]) -mcp_auth_responses: dict[int | str, dict[str, Any]] = { +mcp_auth_responses: Responses = { 200: MCPClientAuthOptionsResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/mcp_servers.py b/src/app/endpoints/mcp_servers.py index 449336c85..6ac5bd8a6 100644 --- a/src/app/endpoints/mcp_servers.py +++ b/src/app/endpoints/mcp_servers.py @@ -1,6 +1,6 @@ """Handler for REST API calls to dynamically manage MCP servers.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Request, status from opentelemetry import trace @@ -27,13 +27,14 @@ from models.config import Action, ModelContextProtocolServer from utils.endpoints import check_configuration_loaded from utils.otel_tracing import SpanAttributes, set_span_attributes +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["mcp-servers"]) -register_responses: dict[int | str, dict[str, Any]] = { +register_responses: Responses = { 201: MCPServerRegistrationResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -107,7 +108,7 @@ async def register_mcp_server_handler( ) -list_responses: dict[int | str, dict[str, Any]] = { +list_responses: Responses = { 200: MCPServerListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -161,7 +162,7 @@ async def list_mcp_servers_handler( return MCPServerListResponse(servers=servers) -delete_responses: dict[int | str, dict[str, Any]] = { +delete_responses: Responses = { 200: MCPServerDeleteResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint", "mcp server static"]), diff --git a/src/app/endpoints/metrics.py b/src/app/endpoints/metrics.py index a3a2e5742..22cd4aa12 100644 --- a/src/app/endpoints/metrics.py +++ b/src/app/endpoints/metrics.py @@ -1,6 +1,6 @@ """Handler for REST API call to provide metrics.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from fastapi.responses import PlainTextResponse @@ -21,12 +21,13 @@ UnauthorizedResponse, ) from models.config import Action +from utils.types import Responses tracer = trace.get_tracer(__name__) router = APIRouter(tags=["metrics"]) -metrics_get_responses: dict[int | str, dict[str, Any]] = { +metrics_get_responses: Responses = { 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), 500: InternalServerErrorResponse.openapi_response(examples=["configuration"]), diff --git a/src/app/endpoints/models.py b/src/app/endpoints/models.py index 57c2d4929..bd134594e 100644 --- a/src/app/endpoints/models.py +++ b/src/app/endpoints/models.py @@ -1,6 +1,6 @@ """Handler for REST API call to list available models.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, HTTPException, Query, Request from fastapi.params import Depends @@ -25,13 +25,14 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.model_list import parse_model_list_response +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["models"]) -models_responses: dict[int | str, dict[str, Any]] = { +models_responses: Responses = { 200: ModelsResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/prompts.py b/src/app/endpoints/prompts.py index 31f3c2fbc..9ecaab6f2 100644 --- a/src/app/endpoints/prompts.py +++ b/src/app/endpoints/prompts.py @@ -1,6 +1,6 @@ """Handler for REST API calls to manage OGX stored prompt templates.""" -from typing import Annotated, Any, Optional +from typing import Annotated, Optional from fastapi import APIRouter, Depends, HTTPException, Request from ogx_api import PromptNotFoundError, PromptVersionNotFoundError @@ -32,13 +32,14 @@ from utils.ogx_serialization import dump_ogx_model from utils.query import handle_known_apistatus_errors from utils.suid import check_suid_prompt +from utils.types import Responses logger = get_logger(__name__) router = APIRouter(tags=["prompts"]) # Response schemas for OpenAPI documentation -prompt_create_responses: dict[int | str, dict[str, Any]] = { +prompt_create_responses: Responses = { 200: PromptResourceResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint", "prompt manage"]), @@ -48,7 +49,7 @@ ), } -prompt_list_responses: dict[int | str, dict[str, Any]] = { +prompt_list_responses: Responses = { 200: PromptsListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint", "prompt read"]), @@ -58,7 +59,7 @@ ), } -prompt_get_responses: dict[int | str, dict[str, Any]] = { +prompt_get_responses: Responses = { 200: PromptResourceResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["prompt_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -70,7 +71,7 @@ ), } -prompt_update_responses: dict[int | str, dict[str, Any]] = { +prompt_update_responses: Responses = { 200: PromptResourceResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["prompt_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -82,7 +83,7 @@ ), } -prompt_delete_responses: dict[int | str, dict[str, Any]] = { +prompt_delete_responses: Responses = { 200: PromptDeleteResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["prompt_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), diff --git a/src/app/endpoints/providers.py b/src/app/endpoints/providers.py index 0250ad41d..898c10cab 100644 --- a/src/app/endpoints/providers.py +++ b/src/app/endpoints/providers.py @@ -29,13 +29,14 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.ogx_serialization import dump_ogx_model +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["providers"]) -providers_list_responses: dict[int | str, dict[str, Any]] = { +providers_list_responses: Responses = { 200: ProvidersListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -45,7 +46,7 @@ ), } -provider_get_responses: dict[int | str, dict[str, Any]] = { +provider_get_responses: Responses = { 200: ProviderResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/query.py b/src/app/endpoints/query.py index 8b7f12e93..33865574a 100644 --- a/src/app/endpoints/query.py +++ b/src/app/endpoints/query.py @@ -1,7 +1,7 @@ """Handler for REST API call to provide answer to query using Response API.""" import datetime -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from opentelemetry import trace @@ -37,8 +37,8 @@ check_configuration_loaded, validate_and_retrieve_conversation, ) -from utils.mcp_headers import McpHeaders, mcp_headers_dependency -from utils.mcp_oauth_probe import check_mcp_auth +from utils.mcp.mcp_headers import McpHeaders, mcp_headers_dependency +from utils.mcp.mcp_oauth_probe import check_mcp_auth from utils.otel_tracing import ( SpanAttributes, SpanEvents, @@ -61,13 +61,14 @@ ) from utils.shields import run_shield_moderation, validate_shield_ids_override from utils.suid import normalize_conversation_id +from utils.types import Responses from utils.vector_search import build_rag_context logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["query"]) -query_response: dict[int | str, dict[str, Any]] = { +query_response: Responses = { 200: QueryResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response( examples=UNAUTHORIZED_OPENAPI_EXAMPLES_WITH_MCP_OAUTH diff --git a/src/app/endpoints/rags.py b/src/app/endpoints/rags.py index 6bd49d1d5..106a70701 100644 --- a/src/app/endpoints/rags.py +++ b/src/app/endpoints/rags.py @@ -1,6 +1,6 @@ """Handler for REST API calls to list and retrieve available RAGs.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, HTTPException, Request from fastapi.params import Depends @@ -27,13 +27,14 @@ ) from models.config import Action, RagStore from utils.endpoints import check_configuration_loaded +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["rags"]) -rags_responses: dict[int | str, dict[str, Any]] = { +rags_responses: Responses = { 200: RAGListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -43,7 +44,7 @@ ), } -rag_responses: dict[int | str, dict[str, Any]] = { +rag_responses: Responses = { 200: RAGInfoResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/responses.py b/src/app/endpoints/responses.py index 38110287d..8dff2b596 100644 --- a/src/app/endpoints/responses.py +++ b/src/app/endpoints/responses.py @@ -71,8 +71,8 @@ check_configuration_loaded, resolve_response_context, ) -from utils.mcp_headers import mcp_headers_dependency -from utils.mcp_oauth_probe import check_mcp_auth +from utils.mcp.mcp_headers import mcp_headers_dependency +from utils.mcp.mcp_oauth_probe import check_mcp_auth from utils.ogx_serialization import dump_ogx_model from utils.otel_tracing import ( SpanAttributes, @@ -93,6 +93,7 @@ ) from utils.quota_utils import check_tokens_available, get_available_quotas from utils.responses import ( + apply_reasoning_for_resolved_tools, build_tool_call_summary, build_turn_summary, check_model_configured, @@ -116,6 +117,7 @@ normalize_conversation_id, ) from utils.tool_formatter import translate_vector_store_ids_to_user_facing +from utils.types import Responses from utils.vector_search import ( append_inline_rag_context_to_responses_input, build_rag_context, @@ -266,7 +268,7 @@ def _get_user_agent(request: Request) -> Optional[str]: return sanitized or None -responses_response: dict[int | str, dict[str, Any]] = { +responses_response: Responses = { 200: ResponsesResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response( examples=UNAUTHORIZED_OPENAPI_EXAMPLES_WITH_MCP_OAUTH @@ -561,11 +563,6 @@ async def handle_responses_with_tracing( # pylint: disable=too-many-locals updated_request = responses_request.model_copy(deep=True) _ = responses_request - # Known LLS bug: https://redhat.atlassian.net/browse/LCORE-1583 - if original_request.reasoning is not None: - logger.warning("reasoning is not yet supported in LCORE and will be ignored") - updated_request.reasoning = None - check_configuration_loaded(configuration) started_at = datetime.now(UTC) rh_identity_context = get_rh_identity_context(request) @@ -667,6 +664,12 @@ async def handle_responses_with_tracing( # pylint: disable=too-many-locals mcp_headers, request.headers, ) + # Known LLS bug: https://redhat.atlassian.net/browse/LCORE-1583 + updated_request.reasoning = apply_reasoning_for_resolved_tools( + original_request.reasoning, + updated_request.tools, + updated_request.model, + ) # Extract vector store IDs for Inline RAG context from the original request vector_store_ids: Optional[list[str]] = ( @@ -810,9 +813,7 @@ async def handle_streaming_response( ) try: response = await context.client.responses.create( - **api_params.model_dump( - exclude_none=True, exclude={"safety_identifier"} - ) + **api_params.model_dump(exclude_none=True) ) generator = response_generator( stream=cast(AsyncIterator[OpenAIResponseObjectStream], response), @@ -1135,9 +1136,6 @@ async def response_generator( chunk_dict["response"]["conversation"] = normalize_conversation_id( api_params.conversation ) - chunk_dict["response"][ - "safety_identifier" - ] = api_params.safety_identifier _sanitize_response_dict( chunk_dict["response"], configured_mcp_labels, @@ -1348,9 +1346,7 @@ async def handle_non_streaming_response( api_response = cast( OpenAIResponseObject, await context.client.responses.create( - **api_params.model_dump( - exclude_none=True, exclude={"safety_identifier"} - ) + **api_params.model_dump(exclude_none=True) ), ) _record_response_inference_result( @@ -1457,14 +1453,12 @@ async def handle_non_streaming_response( tools, configuration.rag_id_mapping, ) - response = ResponsesResponse.model_validate( + return ResponsesResponse.model_validate( { **response_dict, - "safety_identifier": api_params.safety_identifier, "available_quotas": available_quotas, "conversation": normalize_conversation_id(api_params.conversation), "completed_at": int(completed_at.timestamp()), "output_text": output_text, } ) - return response diff --git a/src/app/endpoints/rlsapi_v1.py b/src/app/endpoints/rlsapi_v1.py index 81046f5a8..0baefbd69 100644 --- a/src/app/endpoints/rlsapi_v1.py +++ b/src/app/endpoints/rlsapi_v1.py @@ -73,6 +73,7 @@ from utils.rh_identity import AUTH_DISABLED, get_rh_identity_context from utils.shields import run_shield_moderation_v2 from utils.suid import get_suid +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) @@ -94,7 +95,7 @@ class TemplateRenderError(Exception): ) -infer_responses: dict[int | str, dict[str, Any]] = { +infer_responses: Responses = { 200: RlsapiV1InferResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/root.py b/src/app/endpoints/root.py index 87ae816ed..8dcc24a94 100644 --- a/src/app/endpoints/root.py +++ b/src/app/endpoints/root.py @@ -1,6 +1,6 @@ """Handler for the / endpoint.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from fastapi.responses import HTMLResponse @@ -17,6 +17,7 @@ UnauthorizedResponse, ) from models.config import Action +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) @@ -783,7 +784,7 @@ """ -root_responses: dict[int | str, dict[str, Any]] = { +root_responses: Responses = { 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), diff --git a/src/app/endpoints/saved_prompts.py b/src/app/endpoints/saved_prompts.py index 750f6a6b6..f571a58ce 100644 --- a/src/app/endpoints/saved_prompts.py +++ b/src/app/endpoints/saved_prompts.py @@ -1,6 +1,6 @@ """Handler for REST API calls to manage saved prompts.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Request, status from fastapi.concurrency import run_in_threadpool @@ -43,12 +43,13 @@ validate_saved_prompt_name, ) from utils.suid import check_suid +from utils.types import Responses logger = get_logger(__name__) router = APIRouter(tags=["saved-prompts"]) -get_saved_prompts_config_responses: dict[int | str, dict[str, Any]] = { +get_saved_prompts_config_responses: Responses = { 200: SavedPromptsConfigResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -56,7 +57,7 @@ 503: ServiceUnavailableResponse.openapi_response(examples=["kubernetes api"]), } -list_saved_prompts_responses: dict[int | str, dict[str, Any]] = { +list_saved_prompts_responses: Responses = { 200: SavedPromptsListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -65,7 +66,7 @@ ), } -create_saved_prompts_responses: dict[int | str, dict[str, Any]] = { +create_saved_prompts_responses: Responses = { 201: SavedPromptResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -78,7 +79,7 @@ ), } -delete_saved_prompts_responses: dict[int | str, dict[str, Any]] = { +delete_saved_prompts_responses: Responses = { 200: SavedPromptDeleteResponse.openapi_response(), 400: BadRequestResponse.openapi_response(examples=["saved_prompt_id"]), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), diff --git a/src/app/endpoints/shields.py b/src/app/endpoints/shields.py index d432a8760..996fbbecb 100644 --- a/src/app/endpoints/shields.py +++ b/src/app/endpoints/shields.py @@ -1,6 +1,6 @@ """Handler for REST API call to list available shields.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Request from fastapi.params import Depends @@ -21,13 +21,14 @@ from models.common.shields import CatalogShield from models.config import Action from utils.endpoints import check_configuration_loaded +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["shields"]) -shields_responses: dict[int | str, dict[str, Any]] = { +shields_responses: Responses = { 200: ShieldsResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/skills.py b/src/app/endpoints/skills.py index 8fd8abdb8..e95b21e77 100644 --- a/src/app/endpoints/skills.py +++ b/src/app/endpoints/skills.py @@ -1,6 +1,6 @@ """Handler for REST API call to list loaded agent skills.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Request from fastapi.concurrency import run_in_threadpool @@ -21,12 +21,13 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.pydantic_ai_helpers import get_skills_metadata +from utils.types import Responses logger = get_logger(__name__) router = APIRouter(tags=["skills"]) -skills_responses: dict[int | str, dict[str, Any]] = { +skills_responses: Responses = { 200: SkillsResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/stream_interrupt.py b/src/app/endpoints/stream_interrupt.py index efbe97ff1..0c3b8f6cf 100644 --- a/src/app/endpoints/stream_interrupt.py +++ b/src/app/endpoints/stream_interrupt.py @@ -1,8 +1,10 @@ """Endpoint for interrupting in-progress streaming query requests.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, HTTPException +from opentelemetry import trace +from opentelemetry.trace import Status, StatusCode from authentication import get_auth_dependency from authentication.interface import AuthTuple @@ -17,15 +19,18 @@ ) from models.api.responses.successful import StreamingInterruptResponse from models.config import Action +from utils.otel_tracing import SpanAttributes from utils.stream_interrupts import ( CancelStreamResult, StreamInterruptRegistry, get_stream_interrupt_registry, ) +from utils.types import Responses router = APIRouter(tags=["streaming_query_interrupt"]) +tracer = trace.get_tracer(__name__) -stream_interrupt_responses: dict[int | str, dict[str, Any]] = { +stream_interrupt_responses: Responses = { 200: StreamingInterruptResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -62,31 +67,62 @@ async def stream_interrupt_endpoint_handler( """ user_id, _, _, _ = auth request_id = interrupt_request.request_id - cancel_result = registry.cancel_stream(request_id, user_id) - if cancel_result == CancelStreamResult.NOT_FOUND: - response = NotFoundResponse( - resource="streaming request", - resource_id=request_id, - ) - raise HTTPException(**response.model_dump()) - if cancel_result == CancelStreamResult.FORBIDDEN: - response = ForbiddenResponse( - response="User does not have permission to interrupt this streaming request", - cause=( - f"User {user_id} does not own streaming request " - f"with ID {request_id}" - ), - ) - raise HTTPException(**response.model_dump()) - if cancel_result == CancelStreamResult.ALREADY_DONE: + + with tracer.start_as_current_span("stream.interrupt") as span: + span.set_attribute(SpanAttributes.INTERRUPT_REQUEST_ID, request_id) + + # Surface the conversation id when the caller owns the stream. The + # conversation id is looked up before cancellation so it is available + # for the span regardless of the cancel outcome. It is only recorded + # for streams owned by the requesting user to avoid exposing another + # user's conversation identifier. + existing_stream = registry.get_stream(request_id) + if ( + existing_stream is not None + and existing_stream.user_id == user_id + and existing_stream.conversation_id + ): + span.set_attribute( + SpanAttributes.STREAM_CONVERSATION_ID, + existing_stream.conversation_id, + ) + + cancel_result = registry.cancel_stream(request_id, user_id) + span.set_attribute(SpanAttributes.INTERRUPT_RESULT, cancel_result.value) + + if cancel_result == CancelStreamResult.NOT_FOUND: + span.set_status(Status(StatusCode.ERROR, "streaming request not found")) + response = NotFoundResponse( + resource="streaming request", + resource_id=request_id, + ) + raise HTTPException(**response.model_dump()) + if cancel_result == CancelStreamResult.FORBIDDEN: + span.set_status( + Status(StatusCode.ERROR, "caller does not own streaming request") + ) + response = ForbiddenResponse( + response=( + "User does not have permission to interrupt this " + "streaming request" + ), + cause=( + f"User {user_id} does not own streaming request " + f"with ID {request_id}" + ), + ) + raise HTTPException(**response.model_dump()) + if cancel_result == CancelStreamResult.ALREADY_DONE: + span.set_status(Status(StatusCode.OK)) + return StreamingInterruptResponse( + request_id=request_id, + interrupted=False, + message="Streaming request already completed; nothing to interrupt", + ) + + span.set_status(Status(StatusCode.OK)) return StreamingInterruptResponse( request_id=request_id, - interrupted=False, - message="Streaming request already completed; nothing to interrupt", + interrupted=True, + message="Streaming request interrupted", ) - - return StreamingInterruptResponse( - request_id=request_id, - interrupted=True, - message="Streaming request interrupted", - ) diff --git a/src/app/endpoints/streaming_query.py b/src/app/endpoints/streaming_query.py index 1c305e257..483639e25 100644 --- a/src/app/endpoints/streaming_query.py +++ b/src/app/endpoints/streaming_query.py @@ -3,7 +3,7 @@ import asyncio import datetime from collections.abc import AsyncIterator -from typing import Annotated, Any, Optional +from typing import Annotated, Optional from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import StreamingResponse @@ -60,8 +60,8 @@ check_configuration_loaded, validate_and_retrieve_conversation, ) -from utils.mcp_headers import McpHeaders, mcp_headers_dependency -from utils.mcp_oauth_probe import check_mcp_auth +from utils.mcp.mcp_headers import McpHeaders, mcp_headers_dependency +from utils.mcp.mcp_oauth_probe import check_mcp_auth from utils.otel_tracing import ( SpanAttributes, SpanEvents, @@ -94,6 +94,7 @@ stream_start_event, ) from utils.suid import get_suid, normalize_conversation_id +from utils.types import Responses from utils.vector_search import build_rag_context logger = get_logger(__name__) @@ -103,7 +104,7 @@ # Tracks background topic summary tasks for graceful shutdown. _background_topic_summary_tasks: list[asyncio.Task[None]] = [] -streaming_query_responses: dict[int | str, dict[str, Any]] = { +streaming_query_responses: Responses = { 200: StreamingQueryResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response( examples=UNAUTHORIZED_OPENAPI_EXAMPLES_WITH_MCP_OAUTH diff --git a/src/app/endpoints/tools.py b/src/app/endpoints/tools.py index bd55e99c3..4f2b87598 100644 --- a/src/app/endpoints/tools.py +++ b/src/app/endpoints/tools.py @@ -1,6 +1,6 @@ """Handler for REST API call to list available tools from MCP servers.""" -from typing import Annotated, Any +from typing import Annotated from fastapi import APIRouter, Depends, Request from opentelemetry import trace @@ -23,23 +23,24 @@ from models.config import Action, ModelContextProtocolServer from utils.builtin_tools import get_file_search_tools from utils.endpoints import check_configuration_loaded -from utils.mcp_headers import ( +from utils.mcp.mcp_headers import ( McpHeaders, build_mcp_headers, find_unresolved_auth_headers, mcp_headers_dependency, ) -from utils.mcp_oauth_probe import check_mcp_auth -from utils.mcp_tools import list_mcp_tools +from utils.mcp.mcp_oauth_probe import check_mcp_auth +from utils.mcp.mcp_tools import list_mcp_tools from utils.pydantic_ai_helpers import get_agent_capability_tools from utils.tool_formatter import build_catalog_tool +from utils.types import Responses logger = get_logger(__name__) tracer = trace.get_tracer(__name__) router = APIRouter(tags=["tools"]) -tools_responses: dict[int | str, dict[str, Any]] = { +tools_responses: Responses = { 200: ToolsResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), diff --git a/src/app/endpoints/vector_stores.py b/src/app/endpoints/vector_stores.py index 93fbf09ef..8ce1eff70 100644 --- a/src/app/endpoints/vector_stores.py +++ b/src/app/endpoints/vector_stores.py @@ -1,13 +1,23 @@ -"""Handler for REST API calls to manage vector stores and files.""" +"""Handler for REST API calls to manage vector stores and files. + +These routes proxy OGX vector-store and file APIs. They are deprecated and will +be removed in the next LCS release when OGX is dropped from the stack. Use BYOK +RAG configuration instead (see docs/user_doc/byok_guide.md). +""" + +VECTOR_STORES_DEPRECATED_REASON: str = ( + "OGX proxy API; deprecated and scheduled for removal in the next LCS release." +) import asyncio import os from functools import lru_cache -from typing import Annotated, Any, Optional +from typing import Annotated, Optional from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile, status from ogx_client import ApiException, BadRequestError from openai._exceptions import APIStatusError as OpenAIAPIStatusError +from typing_extensions import deprecated from authentication import get_auth_dependency from authentication.interface import AuthTuple @@ -43,9 +53,13 @@ from models.config import Action from utils.endpoints import check_configuration_loaded from utils.query import handle_known_apistatus_errors +from utils.types import Responses logger = get_logger(__name__) -router = APIRouter(tags=["vector-stores"]) +router = APIRouter( + tags=["vector-stores"], + deprecated=True, +) # Each upload/attach holds up to DEFAULT_MAX_FILE_UPLOAD_SIZE bytes in memory, # so unbounded concurrency multiplies memory usage linearly - these semaphores @@ -70,7 +84,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: # Response schemas for OpenAPI documentation -vector_stores_list_responses: dict[int | str, dict[str, Any]] = { +vector_stores_list_responses: Responses = { 200: VectorStoresListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -80,7 +94,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -vector_store_responses: dict[int | str, dict[str, Any]] = { +vector_store_responses: Responses = { 200: VectorStoreResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -91,7 +105,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -file_responses: dict[int | str, dict[str, Any]] = { +file_responses: Responses = { 200: FileResponse.openapi_response(), 413: FileTooLargeResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), @@ -103,7 +117,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -vector_store_file_responses: dict[int | str, dict[str, Any]] = { +vector_store_file_responses: Responses = { 200: VectorStoreFileResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -117,7 +131,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -vector_store_files_list_responses: dict[int | str, dict[str, Any]] = { +vector_store_files_list_responses: Responses = { 200: VectorStoreFilesListResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -128,7 +142,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -vector_store_delete_responses: dict[int | str, dict[str, Any]] = { +vector_store_delete_responses: Responses = { 200: VectorStoreDeleteResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -138,7 +152,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: ), } -vector_store_file_delete_responses: dict[int | str, dict[str, Any]] = { +vector_store_file_delete_responses: Responses = { 200: VectorStoreFileDeleteResponse.openapi_response(), 401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES), 403: ForbiddenResponse.openapi_response(examples=["endpoint"]), @@ -151,6 +165,7 @@ def _get_vector_store_attach_semaphore() -> asyncio.Semaphore: @router.post("/vector-stores", responses=vector_store_responses) @authorize(Action.MANAGE_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def create_vector_store( request: Request, auth: Annotated[AuthTuple, Depends(get_auth_dependency())], @@ -229,6 +244,7 @@ async def create_vector_store( @router.get("/vector-stores", responses=vector_stores_list_responses) @authorize(Action.READ_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def list_vector_stores( request: Request, auth: Annotated[AuthTuple, Depends(get_auth_dependency())], @@ -290,6 +306,7 @@ async def list_vector_stores( @router.get("/vector-stores/{vector_store_id}", responses=vector_store_responses) @authorize(Action.READ_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def get_vector_store( request: Request, vector_store_id: str, @@ -355,6 +372,7 @@ async def get_vector_store( @router.put("/vector-stores/{vector_store_id}", responses=vector_store_responses) @authorize(Action.MANAGE_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def update_vector_store( request: Request, vector_store_id: str, @@ -427,6 +445,7 @@ async def update_vector_store( responses=vector_store_delete_responses, ) @authorize(Action.MANAGE_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def delete_vector_store( request: Request, vector_store_id: str, @@ -478,6 +497,7 @@ async def delete_vector_store( @router.post("/files", responses=file_responses) @authorize(Action.MANAGE_FILES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def create_file( # pylint: disable=too-many-branches,too-many-statements request: Request, auth: Annotated[AuthTuple, Depends(get_auth_dependency())], @@ -618,6 +638,7 @@ async def create_file( # pylint: disable=too-many-branches,too-many-statements "/vector-stores/{vector_store_id}/files", responses=vector_store_file_responses ) @authorize(Action.MANAGE_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def add_file_to_vector_store( # pylint: disable=too-many-locals,too-many-statements,too-many-branches request: Request, vector_store_id: str, @@ -774,6 +795,7 @@ async def add_file_to_vector_store( # pylint: disable=too-many-locals,too-many- responses=vector_store_files_list_responses, ) @authorize(Action.READ_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def list_vector_store_files( request: Request, vector_store_id: str, @@ -844,6 +866,7 @@ async def list_vector_store_files( responses=vector_store_file_responses, ) @authorize(Action.READ_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def get_vector_store_file( request: Request, vector_store_id: str, @@ -915,6 +938,7 @@ async def get_vector_store_file( responses=vector_store_file_delete_responses, ) @authorize(Action.MANAGE_VECTOR_STORES) +@deprecated(VECTOR_STORES_DEPRECATED_REASON) async def delete_vector_store_file( request: Request, vector_store_id: str, diff --git a/src/app/main.py b/src/app/main.py index 5f2476cd6..b5970e45a 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -67,7 +67,13 @@ {"name": "streaming_query", "description": "Streaming query (SSE)."}, {"name": "streaming_query_interrupt", "description": "Streaming interrupt."}, {"name": "tools", "description": "Tools."}, - {"name": "vector-stores", "description": "Vector stores and files."}, + { + "name": "vector-stores", + "description": ( + "Vector stores and files (OGX proxy). Deprecated: will be removed in " + "the next LCS release." + ), + }, ] @@ -222,6 +228,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # Ignore paths that are not part of the app routes. if path not in app_routes_paths: + logger.debug("Ignoring path: %s", path) await self.app(scope, receive, send) return @@ -293,12 +300,12 @@ async def send_wrapper(message: Message) -> None: logger.info("Including routers") routers.include_routers(app) -app_routes_paths = [ - rc.original_route.path # pyright: ignore[reportAttributeAccessIssue] - for rc in iter_route_contexts(app.routes) - if hasattr(rc.original_route, "path") - and rc.original_route.path # pyright: ignore[reportAttributeAccessIssue] -] +app_routes_paths = [rc.path for rc in iter_route_contexts(app.routes) if rc.path] + +logger.debug("Route paths:") +for app_routes_path in app_routes_paths: + logger.debug(app_routes_path) + # Register pure ASGI middlewares. Middleware execution order is the reverse of # registration order: GlobalExceptionMiddleware (registered first) is innermost, diff --git a/src/client/ogx.py b/src/client/ogx.py index 7d1edfd2a..f6e54f30d 100644 --- a/src/client/ogx.py +++ b/src/client/ogx.py @@ -132,7 +132,7 @@ def _synthesize_library_config(self) -> str: "is not set" ) - with open(config_file, "r", encoding="utf-8") as f: + with open(config_file, encoding="utf-8") as f: lcs_config = yaml.safe_load(f) output_path = os.environ.get( @@ -158,8 +158,8 @@ def _load_service_client(self, config: OgxConfiguration) -> None: def _enrich_library_config(self, input_config_path: str) -> str: """Enrich OGX config with BYOK RAG and OKP Solr settings.""" try: - with open(input_config_path, "r", encoding="utf-8") as f: - ls_config = yaml.safe_load(f) + with open(input_config_path, encoding="utf-8") as f: + ogx_config = yaml.safe_load(f) except (OSError, yaml.YAMLError) as e: logger.warning("Failed to read OGX config: %s", e) return input_config_path @@ -167,26 +167,26 @@ def _enrich_library_config(self, input_config_path: str) -> str: config = configuration.configuration # Enrichment: BYOK RAG - enrich_byok_rag(ls_config, [s.model_dump() for s in config.rag.byok.stores]) + enrich_byok_rag(ogx_config, [s.model_dump() for s in config.rag.byok.stores]) # Enrichment: Solr - enabled when "okp" appears in either inline or tool list rag_config_for_solr = { "inline": config.rag.retrieval.inline.sources, "tool": config.rag.retrieval.tool.sources, } - enrich_solr(ls_config, rag_config_for_solr, config.rag.okp.model_dump()) + enrich_solr(ogx_config, rag_config_for_solr, config.rag.okp.model_dump()) # Enrichment: Azure Entra ID deferred auth entra_id_config = ( config.azure_entra_id.model_dump() if config.azure_entra_id else None ) - enrich_azure_entra_id_inference(ls_config, entra_id_config) + enrich_azure_entra_id_inference(ogx_config, entra_id_config) enriched_path = os.path.join(tempfile.gettempdir(), "ogx_enriched_config.yaml") try: with open(enriched_path, "w", encoding="utf-8") as f: - yaml.dump(ls_config, f, Dumper=YamlDumper, default_flow_style=False) + yaml.dump(ogx_config, f, Dumper=YamlDumper, default_flow_style=False) logger.info("Wrote enriched OGX config to %s", enriched_path) return enriched_path except OSError as e: diff --git a/src/configuration.py b/src/configuration.py index 23498e7cb..4e2750d61 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -1,6 +1,6 @@ """Configuration loader.""" -from typing import Any, Optional +from typing import Any, Optional, Self import yaml @@ -73,14 +73,14 @@ def replace_env_vars_preserving_native_override( ogx_section = config_dict.get("ogx") if ogx_section is None: ogx_section = config_dict.get("llama_stack") - ls_config = ogx_section.get("config") if isinstance(ogx_section, dict) else None - if not (isinstance(ls_config, dict) and "native_override" in ls_config): + ogx_config = ogx_section.get("config") if isinstance(ogx_section, dict) else None + if not (isinstance(ogx_config, dict) and "native_override" in ogx_config): return replace_env_vars(config_dict) - raw_override = ls_config["native_override"] - ls_config["native_override"] = {} # keep secrets out of env resolution + raw_override = ogx_config["native_override"] + ogx_config["native_override"] = {} # keep secrets out of env resolution resolved = replace_env_vars(config_dict) - ls_config["native_override"] = raw_override # restore source dict if reused + ogx_config["native_override"] = raw_override # restore source dict if reused resolved_ogx = (resolved.get("ogx") or resolved.get("llama_stack") or {}).get( "config" ) @@ -98,7 +98,7 @@ class AppConfig: # pylint: disable=too-many-public-methods _instance = None - def __new__(cls, *args: Any, **kwargs: Any) -> "AppConfig": + def __new__(cls, *args: Any, **kwargs: Any) -> Self: """Create a new instance of the class.""" if not isinstance(cls._instance, cls): cls._instance = super().__new__(cls, *args, **kwargs) diff --git a/src/lightspeed_stack.py b/src/lightspeed_stack.py index 1cc871e49..01ae41e20 100644 --- a/src/lightspeed_stack.py +++ b/src/lightspeed_stack.py @@ -32,10 +32,10 @@ def create_argument_parser() -> ArgumentParser: error_responses,common,agents,common_responses} dump schemas for selected models group into OpenAPI-compatible file and quit - -c / --config: path to the configuration file (default "lightspeed-stack.yaml") - - -g / --generate-ogx-configuration: generate an OGX - configuration from the service configuration - - -i / --input-config-file: OGX input configuration filename (default "run.yaml") - - -o / --output-config-file: OGX output configuration filename (default "run_.yaml") + - --synthesized-config-output: path for synthesized OGX run.yaml in unified library mode + - --migrate-config: migrate legacy two-file config to unified single file and exit + - --run-yaml: legacy OGX run.yaml path (with --migrate-config) + - --migrate-output: unified config output path (with --migrate-config) Returns: Configured ArgumentParser for parsing the service CLI options. @@ -154,8 +154,7 @@ def main() -> None: the quota scheduler, and starts the Uvicorn web service. Raises: - SystemExit: when configuration dumping or OGX generation fails - (exits with status 1). + SystemExit: when configuration dumping or migration fails (exits with status 1). """ logger.info("Lightspeed Core Stack startup") parser = create_argument_parser() diff --git a/src/models/common/feedback.py b/src/models/common/feedback.py index 432087315..eb353181c 100644 --- a/src/models/common/feedback.py +++ b/src/models/common/feedback.py @@ -1,9 +1,9 @@ """Predefined feedback categories for AI response quality signals.""" -from enum import Enum +from enum import StrEnum -class FeedbackCategory(str, Enum): +class FeedbackCategory(StrEnum): """Enum representing predefined feedback categories for AI responses. These categories help provide structured feedback about AI inference quality diff --git a/src/models/common/health.py b/src/models/common/health.py index 33c574a51..ec52db2de 100644 --- a/src/models/common/health.py +++ b/src/models/common/health.py @@ -1,12 +1,12 @@ """Health-related shared models for readiness and diagnostics.""" -from enum import Enum +from enum import StrEnum from typing import Optional from pydantic import BaseModel, Field -class HealthStatus(str, Enum): +class HealthStatus(StrEnum): """Health status enum for provider and service health checks. This enum serves two purposes: diff --git a/src/models/config.py b/src/models/config.py index 02e907ba3..55c611574 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -4,7 +4,7 @@ import os import re -from enum import Enum +from enum import StrEnum from functools import cached_property from pathlib import Path from re import Pattern @@ -31,7 +31,7 @@ import constants from log import get_logger from utils import checks -from utils.mcp_auth_headers import resolve_authorization_headers +from utils.mcp.mcp_auth_headers import resolve_authorization_headers from utils.types import CompiledPatterns logger = get_logger(__name__) @@ -854,7 +854,7 @@ class OgxConfiguration(ConfigurationBase): - [OGX](https://ogx-ai.github.io/) - [Python OGX client](https://github.com/ogx-ai/ogx-client-python) - - [Build AI Applications with OGX](https://ogx-ai.github.io/) + - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications) """ url: Optional[AnyHttpUrl] = Field( @@ -1140,7 +1140,7 @@ def check_storage_location_is_set_when_needed(self) -> Self: return self -class JsonPathOperator(str, Enum): +class JsonPathOperator(StrEnum): """Supported operators for JSONPath evaluation. Note: this is not a real model, just an enumeration of all supported JSONPath operators. @@ -1274,7 +1274,7 @@ def compiled_regex(self) -> Optional[Pattern[str]]: return None -class Action(str, Enum): +class Action(StrEnum): """Available actions in the system. Note: this is not a real model, just an enumeration of all action names. @@ -1742,7 +1742,7 @@ def check_customization_model(self) -> Self: checks.file_check(self.agent_card_path, "agent card") try: - with open(self.agent_card_path, "r", encoding="utf-8") as f: + with open(self.agent_card_path, encoding="utf-8") as f: self.agent_card_config = yaml.safe_load(f) except yaml.YAMLError as e: raise ValueError( @@ -3145,8 +3145,146 @@ class RedactionShieldConfiguration(ConfigurationBase): ) +class RiskDefinition(ConfigurationBase): + """ + Definition for a custom risk category. + + Custom risks allow applications to add use-case-specific safety checks + beyond the standard harm, jailbreak, leetspeak, amnesia, and + history_politics checks. + Example: + liability_risk = RiskDefinition( + name="liability", + description="Content requesting legal, medical, or financial advice", + threshold=0.55, + points=["input"], + ) + pii_risk = RiskDefinition( + name="pii_request", + description="User is asking the AI to reveal personal information", + threshold=0.50, + points=["input", "tool"], + ) + Note: + To enable think mode (detailed reasoning) for a risk, add the risk name + to the `thinking_enabled` list in `ModerationConfig`. Do not set + `enable_thinking` directly - it is managed internally. + """ + + name: str = Field( + ..., + title="Risk name", + description="Unique identifier for this risk (e.g., 'liability', 'competitor_mention')", + ) + description: str = Field( + ..., + title="Rist description", + description="Risk definition text passed to Granite Guardian as custom_criteria", + ) + threshold: float = Field( + default=0.65, + ge=0.0, + le=1.0, + title="Risk threshold", + description="Score threshold for flagging (lower = more sensitive)", + ) + enabled: bool = Field( + default=True, title="Risk enabled", description="Whether to run this check" + ) + enable_thinking: bool = Field( + default=False, + title="Risk enable thinking", + description=( + "Internal field - set via ModerationConfig.thinking_enabled list, " + "not directly. When True, Granite Guardian provides detailed " + "reasoning before scoring." + ), + ) + points: list[Literal["input", "output", "tool"]] = Field( + ..., + min_length=1, + title="Guardrail points", + description=( + "Where this risk is evaluated: `input` (user message), " + "`output` (model response), or `tool` (tool/MCP content)." + ), + ) + violation_message: str = Field( + ..., + title="Violation message", + description="Message to be displayed when this risk is violated", + ) + + +class GraniteGuardianConfig(ConfigurationBase): + """Configuration for the Granite Guardian moderation guardrail.""" + + url: str = Field( + ..., title="Base URL", description="The model_id to use for the guard" + ) + + api_key: Optional[SecretStr] = Field( + None, title="Granite Guardian API key", description="API key for the inference" + ) + + max_retries: PositiveInt = Field( + 2, ge=0, le=5, title="Max retries", description="Maximun number of retries" + ) + + timeout: PositiveInt = Field( + 30, ge=5, le=300, title="Timeout", description="Request timeout in seconds" + ) + + verify_ssl: bool | str = Field( + True, + title="Verify SSL", + description=( + "SSL certificate verification. Can be:\n" + " - True: Verify using system CA bundle (default, recommended)\n" + " - False: Disable verification (insecure, for dev only)\n" + " - str: Path to custom CA bundle file (for internal PKI)" + ), + ) + + risks: list[RiskDefinition] = Field( + ..., + title="Defined risks", + description="Risks to be considered while applying this guardrail", + ) + + +class GraniteGuardianShieldConfiguration(ConfigurationBase): + """Configuration for a named Granite Guardian guardrail shield. + + Attributes: + name: Unique, user-facing name identifying this shield instance. + provider_id: Discriminator identifying this as a granite-guardian shield. + config: Granite-guardian-specific configuration. + """ + + name: str = Field( + ..., + title="Shield name", + description="Unique, user-facing name identifying this shield instance.", + ) + + provider_id: Literal["granite_guardian"] = Field( + ..., + title="Shield provider id", + description="Discriminator identifying this as a granite-guardian shield.", + ) + + config: GraniteGuardianConfig = Field( + ..., + title="Shield configuration", + description="Granite-guardian-specific configuration for this shield", + ) + + ShieldConfiguration = Annotated[ - QuestionValidityShieldConfiguration | RedactionShieldConfiguration, + QuestionValidityShieldConfiguration + | RedactionShieldConfiguration + | GraniteGuardianShieldConfiguration, Field(discriminator="provider_id"), ] """Configuration for a single named guardrail shield (question validity or redaction). diff --git a/src/observability/README.md b/src/observability/README.md index 8dc8a8952..bfe0aadba 100644 --- a/src/observability/README.md +++ b/src/observability/README.md @@ -4,6 +4,10 @@ Observability module for telemetry and event collection. +## [profiling.py](profiling.py) + +Pyroscope CPU profiling initialization for Lightspeed Core Stack. + ## [responses_telemetry.py](responses_telemetry.py) Splunk telemetry helpers for the Responses API endpoint. diff --git a/src/observability/__init__.py b/src/observability/__init__.py index dd12ca4be..a68587915 100644 --- a/src/observability/__init__.py +++ b/src/observability/__init__.py @@ -36,7 +36,7 @@ org_id="12345678", system_id="abc-def-123", request_id="req_xyz789", - cla_version="CLA/0.7.0rc2", + cla_version="CLA/0.7.0rc3", system_os="RHEL", system_version="9.3", system_arch="x86_64", diff --git a/src/ogx_configuration.py b/src/ogx_configuration.py index 23d58201b..bf6b06457 100644 --- a/src/ogx_configuration.py +++ b/src/ogx_configuration.py @@ -30,7 +30,7 @@ from pydantic import SecretStr import constants -from log import get_logger +from log import get_logger, setup_logging logger = get_logger(__name__) @@ -155,7 +155,7 @@ def increase_indent(self, flow: bool = False, indentless: bool = False) -> None: def enrich_azure_entra_id_inference( - ls_config: dict[str, Any], + ogx_config: dict[str, Any], azure_entra_id: Optional[dict[str, Any]], ) -> None: """Enrich remote::azure inference provider for Entra ID authentication. @@ -164,7 +164,7 @@ def enrich_azure_entra_id_inference( with model_validation=false to defer model validation to runtime. Parameters: - ls_config (dict[str, Any]): Mutable OGX configuration dictionary to update. + ogx_config (dict[str, Any]): Mutable OGX configuration dictionary to update. azure_entra_id (Optional[dict[str, Any]]): Lightspeed azure_entra_id block, or None. @@ -174,7 +174,7 @@ def enrich_azure_entra_id_inference( if azure_entra_id is None: return - inference_providers = ls_config.get("providers", {}).get("inference", []) + inference_providers = ogx_config.get("providers", {}).get("inference", []) for provider in inference_providers: if provider.get("provider_type") != "remote::azure": @@ -215,18 +215,18 @@ def _dedupe_vector_io_list(entries: list[Any]) -> list[dict[str, Any]]: return out -def dedupe_providers_vector_io(ls_config: dict[str, Any]) -> None: +def dedupe_providers_vector_io(ogx_config: dict[str, Any]) -> None: """Collapse ``providers.vector_io`` to one entry per ``provider_id``.""" - if "providers" not in ls_config or "vector_io" not in ls_config["providers"]: + if "providers" not in ogx_config or "vector_io" not in ogx_config["providers"]: return - raw = ls_config["providers"]["vector_io"] + raw = ogx_config["providers"]["vector_io"] if not isinstance(raw, list): return - ls_config["providers"]["vector_io"] = _dedupe_vector_io_list(raw) + ogx_config["providers"]["vector_io"] = _dedupe_vector_io_list(raw) def construct_storage_backends_section( - ls_config: dict[str, Any], byok_rag: list[dict[str, Any]] + ogx_config: dict[str, Any], byok_rag: list[dict[str, Any]] ) -> dict[str, Any]: """Construct storage.backends section in OGX configuration file. @@ -235,7 +235,7 @@ def construct_storage_backends_section( Parameters: ---------- - ls_config (dict[str, Any]): Existing OGX configuration mapping. + ogx_config (dict[str, Any]): Existing OGX configuration mapping. byok_rag (list[dict[str, Any]]): List of BYOK RAG definitions. Returns: @@ -245,8 +245,8 @@ def construct_storage_backends_section( output: dict[str, Any] = {} # preserve existing backends - if "storage" in ls_config and "backends" in ls_config["storage"]: - output = ls_config["storage"]["backends"].copy() + if "storage" in ogx_config and "backends" in ogx_config["storage"]: + output = ogx_config["storage"]["backends"].copy() # add new backends for each BYOK RAG (skip types that don't need one) added = 0 @@ -273,7 +273,7 @@ def construct_storage_backends_section( def construct_vector_stores_section( - ls_config: dict[str, Any], byok_rag: list[dict[str, Any]] + ogx_config: dict[str, Any], byok_rag: list[dict[str, Any]] ) -> list[dict[str, Any]]: """Construct registered_resources.vector_stores section in OGX config. @@ -281,7 +281,7 @@ def construct_vector_stores_section( Parameters: ---------- - ls_config (dict[str, Any]): Existing OGX configuration mapping + ogx_config (dict[str, Any]): Existing OGX configuration mapping used as the base; existing `registered_resources.vector_stores` entries are preserved if present. byok_rag (list[dict[str, Any]]): List of BYOK RAG definitions to be added to @@ -299,9 +299,9 @@ def construct_vector_stores_section( output = [] # fill-in existing vector_stores entries from registered_resources - if "registered_resources" in ls_config: - if "vector_stores" in ls_config["registered_resources"]: - output = ls_config["registered_resources"]["vector_stores"].copy() + if "registered_resources" in ogx_config: + if "vector_stores" in ogx_config["registered_resources"]: + output = ogx_config["registered_resources"]["vector_stores"].copy() # append new vector_stores entries, skipping duplicates # Resolve ${env.VAR} patterns so comparisons work when existing entries @@ -342,7 +342,7 @@ def construct_vector_stores_section( def construct_models_section( - ls_config: dict[str, Any], byok_rag: list[dict[str, Any]] + ogx_config: dict[str, Any], byok_rag: list[dict[str, Any]] ) -> list[dict[str, Any]]: """Construct registered_resources.models section with embedding models. @@ -350,7 +350,7 @@ def construct_models_section( Parameters: ---------- - ls_config (dict[str, Any]): Existing OGX configuration mapping. + ogx_config (dict[str, Any]): Existing OGX configuration mapping. byok_rag (list[dict[str, Any]]): List of BYOK RAG definitions. Returns: @@ -360,9 +360,9 @@ def construct_models_section( output: list[dict[str, Any]] = [] # preserve existing models - if "registered_resources" in ls_config: - if "models" in ls_config["registered_resources"]: - output = ls_config["registered_resources"]["models"].copy() + if "registered_resources" in ogx_config: + if "models" in ogx_config["registered_resources"]: + output = ogx_config["registered_resources"]["models"].copy() # add embedding models for each BYOK RAG for brag in byok_rag: @@ -448,7 +448,7 @@ def _build_vector_io_config( def construct_vector_io_providers_section( - ls_config: dict[str, Any], byok_rag: list[dict[str, Any]] + ogx_config: dict[str, Any], byok_rag: list[dict[str, Any]] ) -> list[dict[str, Any]]: """Construct providers/vector_io section in OGX configuration file. @@ -458,7 +458,7 @@ def construct_vector_io_providers_section( Parameters: ---------- - ls_config (dict[str, Any]): Existing OGX configuration + ogx_config (dict[str, Any]): Existing OGX configuration dictionary; if it contains providers.vector_io, those entries are used as the starting list. byok_rag (list[dict[str, Any]]): List of BYOK RAG specifications to convert @@ -474,8 +474,8 @@ def construct_vector_io_providers_section( """ output: list[dict[str, Any]] = [] - if "providers" in ls_config and "vector_io" in ls_config["providers"]: - raw = ls_config["providers"]["vector_io"] + if "providers" in ogx_config and "vector_io" in ogx_config["providers"]: + raw = ogx_config["providers"]["vector_io"] if isinstance(raw, list): output = _dedupe_vector_io_list(raw) else: @@ -515,44 +515,44 @@ def construct_vector_io_providers_section( return output -def enrich_byok_rag(ls_config: dict[str, Any], byok_rag: list[dict[str, Any]]) -> None: +def enrich_byok_rag(ogx_config: dict[str, Any], byok_rag: list[dict[str, Any]]) -> None: """Enrich OGX config with BYOK RAG settings. Args: - ls_config: OGX configuration dict (modified in place) + ogx_config: OGX configuration dict (modified in place) byok_rag: List of BYOK RAG configurations """ if len(byok_rag) == 0: logger.info("BYOK RAG is not configured: skipping") - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) return logger.info("Enriching OGX config with BYOK RAG") # Add storage backends - if "storage" not in ls_config: - ls_config["storage"] = {} - ls_config["storage"]["backends"] = construct_storage_backends_section( - ls_config, byok_rag + if "storage" not in ogx_config: + ogx_config["storage"] = {} + ogx_config["storage"]["backends"] = construct_storage_backends_section( + ogx_config, byok_rag ) # Add vector_io providers - if "providers" not in ls_config: - ls_config["providers"] = {} - ls_config["providers"]["vector_io"] = construct_vector_io_providers_section( - ls_config, byok_rag + if "providers" not in ogx_config: + ogx_config["providers"] = {} + ogx_config["providers"]["vector_io"] = construct_vector_io_providers_section( + ogx_config, byok_rag ) # Add registered vector stores - if "registered_resources" not in ls_config: - ls_config["registered_resources"] = {} - ls_config["registered_resources"]["vector_stores"] = ( - construct_vector_stores_section(ls_config, byok_rag) + if "registered_resources" not in ogx_config: + ogx_config["registered_resources"] = {} + ogx_config["registered_resources"]["vector_stores"] = ( + construct_vector_stores_section(ogx_config, byok_rag) ) # Add embedding models - ls_config["registered_resources"]["models"] = construct_models_section( - ls_config, byok_rag + ogx_config["registered_resources"]["models"] = construct_models_section( + ogx_config, byok_rag ) @@ -585,7 +585,7 @@ def _vector_store_provider_by_id( def _upsert_vsprov_embedding_model( - ls_config: dict[str, Any], + ogx_config: dict[str, Any], provider_id: str, embedding_model: str, embedding_dimension: int, @@ -598,13 +598,13 @@ def _upsert_vsprov_embedding_model( ``model_id`` already exists. Parameters: - ls_config: OGX configuration modified in place. + ogx_config: OGX configuration modified in place. provider_id: Dynamic provider id used to name the model row. embedding_model: Configured embedding model path or id. embedding_dimension: Embedding vector dimensionality (required on validated ``vector_store.providers`` entries). """ - models = ls_config.setdefault("registered_resources", {}).setdefault("models", []) + models = ogx_config.setdefault("registered_resources", {}).setdefault("models", []) model_id = f"vsprov_{provider_id}_embedding" provider_model_id = embedding_model.removeprefix("sentence-transformers/") entry = { @@ -693,18 +693,18 @@ def _replace_or_append_vector_io( def _apply_vector_stores_defaults( - ls_config: dict[str, Any], designated: dict[str, Any] + ogx_config: dict[str, Any], designated: dict[str, Any] ) -> None: """Write vector_stores.default_* from the designated provider entry. Parameters: - ls_config: OGX configuration modified in place. + ogx_config: OGX configuration modified in place. designated: Provider entry selected by ``vector_store.default_provider``. """ - vector_stores = ls_config.get("vector_stores") + vector_stores = ogx_config.get("vector_stores") if not isinstance(vector_stores, dict): vector_stores = {} - ls_config["vector_stores"] = vector_stores + ogx_config["vector_stores"] = vector_stores provider_id = str(designated["id"]).strip() vector_stores["default_provider_id"] = provider_id # Match _upsert_vsprov_embedding_model model_id; OGX validates @@ -721,7 +721,7 @@ def _enrich_one_vector_store_provider( backends: dict[str, Any], vector_io: list[Any], existing_ids: set[str], - ls_config: dict[str, Any], + ogx_config: dict[str, Any], ) -> None: """Enrich LS config for a single ``vector_store.providers`` entry. @@ -730,11 +730,11 @@ def _enrich_one_vector_store_provider( backends: ``storage.backends`` map (modified in place for faiss). vector_io: ``providers.vector_io`` list (modified in place). existing_ids: Known ``provider_id`` values already in ``vector_io``. - ls_config: Full OGX config (for embedding model registration). + ogx_config: Full OGX config (for embedding model registration). """ provider_id = str(entry["id"]).strip() product_type = entry["type"] - ls_type = BACKEND_TO_PROVIDER_TYPE[product_type] + ogx_provider_type = BACKEND_TO_PROVIDER_TYPE[product_type] extra_fields, backend_name, backend_entry = _vsprov_fields_and_backend( product_type, provider_id, entry.get("config") or {} ) @@ -746,8 +746,10 @@ def _enrich_one_vector_store_provider( existing_ids, { "provider_id": provider_id, - "provider_type": ls_type, - "config": _build_vector_io_config(ls_type, backend_name, extra_fields), + "provider_type": ogx_provider_type, + "config": _build_vector_io_config( + ogx_provider_type, backend_name, extra_fields + ), }, ) @@ -755,7 +757,7 @@ def _enrich_one_vector_store_provider( embedding_dimension = entry.get("embedding_dimension") if embedding_model and embedding_dimension is not None: _upsert_vsprov_embedding_model( - ls_config, + ogx_config, provider_id=provider_id, embedding_model=embedding_model, embedding_dimension=embedding_dimension, @@ -763,7 +765,7 @@ def _enrich_one_vector_store_provider( def enrich_vector_store( - ls_config: dict[str, Any], + ogx_config: dict[str, Any], vector_store: Optional[dict[str, Any]] = None, ) -> None: """Enrich LS config with dynamic vector-store provider capacity. @@ -775,7 +777,7 @@ def enrich_vector_store( ``registered_resources.vector_stores``. Parameters: - ls_config: OGX configuration dictionary (modified in place). + ogx_config: OGX configuration dictionary (modified in place). vector_store: High-level ``vector_store`` section (``default_provider`` + ``providers``) as a dict. """ @@ -783,16 +785,16 @@ def enrich_vector_store( providers = vector_store.get("providers") or [] if not providers: logger.debug("vector_store.providers not configured: skipping") - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) return - backends = ls_config.setdefault("storage", {}).setdefault("backends", {}) - providers_section = ls_config.setdefault("providers", {}) + backends = ogx_config.setdefault("storage", {}).setdefault("backends", {}) + providers_section = ogx_config.setdefault("providers", {}) vector_io = providers_section.get("vector_io") if not isinstance(vector_io, list): vector_io = [] providers_section["vector_io"] = vector_io - ls_config.setdefault("registered_resources", {}).setdefault("models", []) + ogx_config.setdefault("registered_resources", {}).setdefault("models", []) existing_ids = { str(entry.get("provider_id")).strip() @@ -802,16 +804,16 @@ def enrich_vector_store( for entry in providers: _enrich_one_vector_store_provider( - entry, backends, vector_io, existing_ids, ls_config + entry, backends, vector_io, existing_ids, ogx_config ) designated = _vector_store_provider_by_id( providers, vector_store.get("default_provider") ) if designated is not None: - _apply_vector_stores_defaults(ls_config, designated) + _apply_vector_stores_defaults(ogx_config, designated) - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) # ============================================================================= @@ -820,14 +822,14 @@ def enrich_vector_store( def enrich_solr( # pylint: disable=too-many-locals,too-many-statements - ls_config: dict[str, Any], + ogx_config: dict[str, Any], rag_config: dict[str, Any], okp_config: dict[str, Any], ) -> None: """Enrich OGX config with Solr settings. Parameters: - ls_config: OGX configuration dict (modified in place) + ogx_config: OGX configuration dict (modified in place) rag_config: RAG configuration dict. Used keys: - inline (list[str]): inline RAG IDs - tool (list[str]): tool RAG IDs @@ -861,14 +863,14 @@ def enrich_solr( # pylint: disable=too-many-locals,too-many-statements logger.info("Enriching OGX config with OKP") # Add vector_io provider for Solr - if "providers" not in ls_config: - ls_config["providers"] = {} - if "vector_io" not in ls_config["providers"]: - ls_config["providers"]["vector_io"] = [] + if "providers" not in ogx_config: + ogx_config["providers"] = {} + if "vector_io" not in ogx_config["providers"]: + ogx_config["providers"]["vector_io"] = [] # Add Solr provider if not already present existing_providers = [ - p.get("provider_id") for p in ls_config["providers"]["vector_io"] + p.get("provider_id") for p in ogx_config["providers"]["vector_io"] ] if constants.SOLR_PROVIDER_ID not in existing_providers: collection_env = ( @@ -886,7 +888,7 @@ def enrich_solr( # pylint: disable=too-many-locals,too-many-statements embedding_dim_env = ( f"${{env.SOLR_EMBEDDING_DIM:={constants.SOLR_DEFAULT_EMBEDDING_DIMENSION}}}" ) - ls_config["providers"]["vector_io"].append( + ogx_config["providers"]["vector_io"].append( { "provider_id": constants.SOLR_PROVIDER_ID, "provider_type": "remote::solr_vector_io", @@ -919,18 +921,18 @@ def enrich_solr( # pylint: disable=too-many-locals,too-many-statements logger.info("Added OKP provider to providers/vector_io") # Add vector store registration for Solr - if "registered_resources" not in ls_config: - ls_config["registered_resources"] = {} - if "vector_stores" not in ls_config["registered_resources"]: - ls_config["registered_resources"]["vector_stores"] = [] + if "registered_resources" not in ogx_config: + ogx_config["registered_resources"] = {} + if "vector_stores" not in ogx_config["registered_resources"]: + ogx_config["registered_resources"]["vector_stores"] = [] # Add Solr vector store if not already present existing_stores = [ vs.get("vector_store_id") - for vs in ls_config["registered_resources"]["vector_stores"] + for vs in ogx_config["registered_resources"]["vector_stores"] ] if constants.SOLR_DEFAULT_VECTOR_STORE_ID not in existing_stores: - ls_config["registered_resources"]["vector_stores"].append( + ogx_config["registered_resources"]["vector_stores"].append( { "vector_store_id": constants.SOLR_DEFAULT_VECTOR_STORE_ID, "provider_id": constants.SOLR_PROVIDER_ID, @@ -944,21 +946,21 @@ def enrich_solr( # pylint: disable=too-many-locals,too-many-statements ) # Add Solr embedding model to registered_resources.models if not already present - if "models" not in ls_config["registered_resources"]: - ls_config["registered_resources"]["models"] = [] + if "models" not in ogx_config["registered_resources"]: + ogx_config["registered_resources"]["models"] = [] # Strip sentence-transformers/ prefix from constant for provider_model_id provider_model_id = constants.SOLR_DEFAULT_EMBEDDING_MODEL provider_model_id = provider_model_id.removeprefix("sentence-transformers/") # Check if already registered - registered_models = ls_config["registered_resources"]["models"] + registered_models = ogx_config["registered_resources"]["models"] existing_model_ids = [m.get("provider_model_id") for m in registered_models] if provider_model_id not in existing_model_ids: # Build environment variable expression provider_model_env = f"${{env.SOLR_EMBEDDING_MODEL:={provider_model_id}}}" - ls_config["registered_resources"]["models"].append( + ogx_config["registered_resources"]["models"].append( { "model_id": constants.SOLR_EMBEDDING_MODEL_ID, "model_type": "embedding", @@ -981,9 +983,9 @@ def enrich_solr( # pylint: disable=too-many-locals,too-many-statements # LCORE uses "semantic"; OGX uses "vector" if ogx_mode == "semantic": ogx_mode = "vector" - if "vector_stores" not in ls_config: - ls_config["vector_stores"] = {} - chunk_params = ls_config["vector_stores"].setdefault( + if "vector_stores" not in ogx_config: + ogx_config["vector_stores"] = {} + chunk_params = ogx_config["vector_stores"].setdefault( "chunk_retrieval_params", {} ) chunk_params["default_search_mode"] = ogx_mode @@ -1010,7 +1012,7 @@ def load_default_baseline() -> dict[str, Any]: OSError: If the shipped baseline file cannot be read. yaml.YAMLError: If the baseline file is not valid YAML. """ - with open(DEFAULT_BASELINE_RESOURCE, "r", encoding="utf-8") as file: + with open(DEFAULT_BASELINE_RESOURCE, encoding="utf-8") as file: return yaml.safe_load(file) @@ -1059,17 +1061,17 @@ def _matchable_provider_id(provider_id: Any) -> Any: return provider_id -def _strip_default_openai_inference(ls_config: dict[str, Any]) -> None: +def _strip_default_openai_inference(ogx_config: dict[str, Any]) -> None: """Remove the OpenAI inference provider from the default baseline. Parameters: - ls_config: The Llama Stack configuration being synthesized (modified + ogx_config: The OGX configuration being synthesized (modified in place). Returns: - None: ``ls_config`` is modified in place. + None: ``ogx_config`` is modified in place. """ - providers = ls_config.get("providers") + providers = ogx_config.get("providers") if not isinstance(providers, dict): return inference = providers.get("inference") @@ -1086,7 +1088,7 @@ def _strip_default_openai_inference(ls_config: dict[str, Any]) -> None: def apply_high_level_inference( - ls_config: dict[str, Any], inference: dict[str, Any] + ogx_config: dict[str, Any], inference: dict[str, Any] ) -> None: """Expand high-level ``inference.providers`` into OGX provider entries. @@ -1104,35 +1106,35 @@ def apply_high_level_inference( values (R6). Parameters: - ls_config: The OGX configuration being synthesized (modified in + ogx_config: The OGX configuration being synthesized (modified in place). inference: The root ``inference`` section as a dict; only its ``providers`` list is consumed here. Returns: - None: ``ls_config`` is modified in place. + None: ``ogx_config`` is modified in place. """ providers = inference.get("providers") or [] if not providers: return - providers_section = ls_config.setdefault("providers", {}) + providers_section = ogx_config.setdefault("providers", {}) inference_list = providers_section.setdefault("inference", []) for provider in providers: provider_type = provider["type"] emitted_id = provider.get("id") or provider_type.replace("_", "-") - ls_provider_type = PROVIDER_TYPE_MAP[provider_type] + ogx_provider_type = PROVIDER_TYPE_MAP[provider_type] entry: dict[str, Any] = { "provider_id": emitted_id, - "provider_type": ls_provider_type, + "provider_type": ogx_provider_type, } provider_config: dict[str, Any] = {} if provider.get("extra"): provider_config.update(provider["extra"]) if provider.get("api_key_env"): - key_field = API_KEY_FIELD_MAP.get(ls_provider_type, "api_key") + key_field = API_KEY_FIELD_MAP.get(ogx_provider_type, "api_key") provider_config[key_field] = "${env." + provider["api_key_env"] + "}" if provider.get("allowed_models"): provider_config["allowed_models"] = provider["allowed_models"] @@ -1162,8 +1164,8 @@ def apply_high_level_inference( ) -def ensure_mcp_tool_runtime(ls_config: dict[str, Any]) -> None: - """Ensure the default MCP tool_runtime provider exists in ``ls_config``. +def ensure_mcp_tool_runtime(ogx_config: dict[str, Any]) -> None: + """Ensure the default MCP tool_runtime provider exists in ``ogx_config``. Adds ``tool_runtime`` to ``apis`` when missing, then appends the default ``model-context-protocol`` provider under ``providers.tool_runtime`` when @@ -1171,17 +1173,17 @@ def ensure_mcp_tool_runtime(ls_config: dict[str, Any]) -> None: (including ``rag-runtime``) are left untouched. Parameters: - ls_config: The OGX configuration being synthesized (modified + ogx_config: The OGX configuration being synthesized (modified in place). Returns: - None: ``ls_config`` is modified in place. + None: ``ogx_config`` is modified in place. """ - apis = ls_config.setdefault("apis", []) + apis = ogx_config.setdefault("apis", []) if "tool_runtime" not in apis: apis.append("tool_runtime") - providers_section = ls_config.setdefault("providers", {}) + providers_section = ogx_config.setdefault("providers", {}) tool_runtime = providers_section.setdefault("tool_runtime", []) for existing in tool_runtime: if ( @@ -1257,7 +1259,7 @@ def synthesize_configuration( # pylint: disable=too-many-locals if unified and unified.get("profile"): profile_path = _resolve_profile_path(unified["profile"], config_file_dir) logger.info("Loading synthesis baseline from profile %s", profile_path) - with open(profile_path, "r", encoding="utf-8") as file: + with open(profile_path, encoding="utf-8") as file: baseline = yaml.safe_load(file) or {} elif unified and unified.get("baseline") == "empty": logger.info("Synthesizing from an empty baseline") @@ -1272,7 +1274,7 @@ def synthesize_configuration( # pylint: disable=too-many-locals else load_default_baseline() ) - ls_config: dict[str, Any] = copy.deepcopy(baseline) + ogx_config: dict[str, Any] = copy.deepcopy(baseline) # Profile and empty are unchanged. The shipped file either keeps OpenAI # (default/omitted, with a deprecation WARN) or drops it (byo-llm). @@ -1283,7 +1285,7 @@ def synthesize_configuration( # pylint: disable=too-many-locals # Engineering Support Agreement's one-minor deprecation phase applies. if loaded_shipped_baseline: if unified and unified.get("baseline") == "byo-llm": - _strip_default_openai_inference(ls_config) + _strip_default_openai_inference(ogx_config) else: logger.warning( "DEPRECATED: the built-in OpenAI inference provider in " @@ -1296,23 +1298,23 @@ def synthesize_configuration( # pylint: disable=too-many-locals ) # 3. Normalize duplicated vector_io providers in the baseline. - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) # 4. High-level inference providers (Decision S5 — a root-level section). inference = lcs_config.get("inference") or {} if inference.get("providers"): - apply_high_level_inference(ls_config, inference) + apply_high_level_inference(ogx_config, inference) # 5. Ensure MCP tool_runtime for default/profile baselines (skipped for # baseline: empty so migrate round-trips stay lossless). if not baseline_was_empty: - ensure_mcp_tool_runtime(ls_config) + ensure_mcp_tool_runtime(ogx_config) # 6. Raw escape hatch, deep-merged with list replacement. It wins over the # baseline and the high-level expansion (R5) but deliberately NOT over # enrichment (step 7). if unified and unified.get("native_override"): - ls_config = deep_merge_list_replace(ls_config, unified["native_override"]) + ogx_config = deep_merge_list_replace(ogx_config, unified["native_override"]) # 7. Existing enrichment — same calls as legacy generate_configuration so # unified output matches legacy output for equivalent inputs (R7). @@ -1322,23 +1324,23 @@ def synthesize_configuration( # pylint: disable=too-many-locals # get the same treatment or list-shaped enrichment artifacts # (vector_io providers, registered models, azure model_validation) are # replaced wholesale by the lifted lists and silently lost. - enrich_azure_entra_id_inference(ls_config, lcs_config.get("azure_entra_id")) + enrich_azure_entra_id_inference(ogx_config, lcs_config.get("azure_entra_id")) rag_section = lcs_config.get("rag", {}) byok_stores = rag_section.get("byok", {}).get("stores", []) - enrich_byok_rag(ls_config, byok_stores) + enrich_byok_rag(ogx_config, byok_stores) retrieval = rag_section.get("retrieval", {}) rag_config_for_solr = { "inline": retrieval.get("inline", {}).get("sources", []), "tool": retrieval.get("tool", {}).get("sources", []), } okp_config = rag_section.get("okp", {}) - enrich_solr(ls_config, rag_config_for_solr, okp_config) - enrich_vector_store(ls_config, lcs_config.get("vector_store")) + enrich_solr(ogx_config, rag_config_for_solr, okp_config) + enrich_vector_store(ogx_config, lcs_config.get("vector_store")) # 8. Dedupe again in case native_override or enrichment reintroduced dupes. - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) - return ls_config + return ogx_config def synthesize_to_file( @@ -1364,7 +1366,7 @@ def synthesize_to_file( Returns: None. """ - ls_config = synthesize_configuration(lcs_config, config_file_dir, default_baseline) + ogx_config = synthesize_configuration(lcs_config, config_file_dir, default_baseline) path = Path(output_file) if path.parent != Path(""): @@ -1374,7 +1376,7 @@ def synthesize_to_file( # the write guarantees 0600 even when overwriting a pre-existing file. fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as file: - yaml.dump(ls_config, file, Dumper=YamlDumper, default_flow_style=False) + yaml.dump(ogx_config, file, Dumper=YamlDumper, default_flow_style=False) os.chmod(str(path), 0o600) logger.info("Wrote synthesized OGX configuration to %s (mode 0600)", path) @@ -1421,9 +1423,9 @@ def migrate_config_dumb( ValueError: If either input file does not parse to a mapping (e.g. an empty or comment-only file). """ - with open(run_yaml_path, "r", encoding="utf-8") as file: + with open(run_yaml_path, encoding="utf-8") as file: run_yaml = yaml.safe_load(file) - with open(lightspeed_yaml_path, "r", encoding="utf-8") as file: + with open(lightspeed_yaml_path, encoding="utf-8") as file: lcs_config = yaml.safe_load(file) # An empty or comment-only YAML file parses to None; fail with a clear @@ -1479,18 +1481,18 @@ def generate_configuration( """ logger.info("Reading OGX configuration from file %s", input_file) - with open(input_file, "r", encoding="utf-8") as file: - ls_config = yaml.safe_load(file) + with open(input_file, encoding="utf-8") as file: + ogx_config = yaml.safe_load(file) - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) # Enrichment: Azure Entra ID deferred auth - enrich_azure_entra_id_inference(ls_config, config.get("azure_entra_id")) + enrich_azure_entra_id_inference(ogx_config, config.get("azure_entra_id")) # Enrichment: BYOK RAG rag_section = config.get("rag", {}) byok_stores = rag_section.get("byok", {}).get("stores", []) - enrich_byok_rag(ls_config, byok_stores) + enrich_byok_rag(ogx_config, byok_stores) # Enrichment: Solr - enabled when "okp" appears in either inline or tool list retrieval = rag_section.get("retrieval", {}) @@ -1499,14 +1501,14 @@ def generate_configuration( "tool": retrieval.get("tool", {}).get("sources", []), } okp_config = rag_section.get("okp", {}) - enrich_solr(ls_config, rag_config_for_solr, okp_config) + enrich_solr(ogx_config, rag_config_for_solr, okp_config) - dedupe_providers_vector_io(ls_config) + dedupe_providers_vector_io(ogx_config) logger.info("Writing OGX configuration into file %s", output_file) with open(output_file, "w", encoding="utf-8") as file: - yaml.dump(ls_config, file, Dumper=YamlDumper, default_flow_style=False) + yaml.dump(ogx_config, file, Dumper=YamlDumper, default_flow_style=False) # ============================================================================= @@ -1548,6 +1550,10 @@ def main() -> None: run.yaml needs to exist; otherwise the legacy path enriches the ``--input`` run.yaml in place. Server-mode container entrypoints rely on this dispatch to serve both modes with a single invocation. + + Configures logging first so the INFO lines this module emits reach the + container log: run as a bare script there is no handler on the root + logger, and ``logging.lastResort`` would drop everything below WARNING. """ parser = ArgumentParser( description="Generate the OGX run configuration from a " @@ -1575,7 +1581,16 @@ def main() -> None: ) args = parser.parse_args() - with open(args.config, "r", encoding="utf-8") as f: + # Configure logging before doing any work. This module runs as a bare + # script from the container entrypoint (scripts/ogx-entrypoint.sh), so + # nothing has installed a handler on the root logger; Python's lastResort + # then emits WARNING and above only, and every INFO line this module + # writes -- including which config shape was detected and where the + # synthesized run.yaml was written -- is silently dropped. AsyncOgxClient + # already does this for the in-process path, for the same reason. + setup_logging() + + with open(args.config, encoding="utf-8") as f: config = yaml.safe_load(f) if has_synthesis_input(config): diff --git a/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py b/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py index 29c3ef910..15681d8b4 100644 --- a/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py +++ b/src/pydantic_ai_lightspeed/capabilities/redaction/_capability.py @@ -321,13 +321,11 @@ async def after_model_request( A new ModelResponse with redacted text parts, or the original if no redaction occurred. """ - new_response = _redact_response( + return _redact_response( response, self.config.compiled_patterns, ) - return new_response - async def run(self, input_text: str) -> ShieldModerationResult: """Run PII redaction on input text and return a moderation result.""" result = redact_text(input_text, self.config.compiled_patterns) diff --git a/src/pydantic_ai_lightspeed/llamastack/README.md b/src/pydantic_ai_lightspeed/llamastack/README.md new file mode 100644 index 000000000..473d0e008 --- /dev/null +++ b/src/pydantic_ai_lightspeed/llamastack/README.md @@ -0,0 +1,2 @@ +# List of source files stored in `src/pydantic_ai_lightspeed/llamastack` directory + diff --git a/src/telemetry/configuration_snapshot.py b/src/telemetry/configuration_snapshot.py index 1bab8fa69..66f0c73ce 100644 --- a/src/telemetry/configuration_snapshot.py +++ b/src/telemetry/configuration_snapshot.py @@ -647,7 +647,7 @@ def _extract_snapshot_fields( # ============================================================================= -def _extract_store_info(ls_config: dict[str, Any], store_name: str) -> dict[str, Any]: +def _extract_store_info(ogx_config: dict[str, Any], store_name: str) -> dict[str, Any]: """Extract store type and db_path from OGX storage configuration. Resolves the store → backend → type/db_path chain in the OGX @@ -655,14 +655,14 @@ def _extract_store_info(ls_config: dict[str, Any], store_name: str) -> dict[str, Parameters: ---------- - ls_config: The parsed OGX configuration dict. + ogx_config: The parsed OGX configuration dict. store_name: Name of the store to look up (e.g., "inference", "metadata"). Returns: ------- A dict with 'type' and 'db_path' keys, plus 'namespace' for metadata store. """ - store = get_nested_value(ls_config, f"storage.stores.{store_name}") + store = get_nested_value(ogx_config, f"storage.stores.{store_name}") if store is None or not isinstance(store, dict): return {"type": NOT_CONFIGURED, "db_path": NOT_CONFIGURED} @@ -670,7 +670,7 @@ def _extract_store_info(ls_config: dict[str, Any], store_name: str) -> dict[str, if backend_name is None: return {"type": NOT_CONFIGURED, "db_path": NOT_CONFIGURED} - backends = get_nested_value(ls_config, "storage.backends") or {} + backends = get_nested_value(ogx_config, "storage.backends") or {} backend = backends.get(backend_name, {}) result: dict[str, Any] = { @@ -721,7 +721,7 @@ def _read_yaml_file(config_path: str) -> Any: The parsed YAML content, or None on failure. """ try: - with open(config_path, "r", encoding="utf-8") as f: + with open(config_path, encoding="utf-8") as f: return yaml.safe_load(f) except (OSError, yaml.YAMLError) as e: logger.warning("Failed to read OGX config for snapshot: %s", e) @@ -750,15 +750,15 @@ async def build_ogx_snapshot( if config_path is None: return {"status": NOT_AVAILABLE} - ls_config = await asyncio.to_thread(_read_yaml_file, config_path) + ogx_config = await asyncio.to_thread(_read_yaml_file, config_path) - if not isinstance(ls_config, dict): + if not isinstance(ogx_config, dict): logger.warning("OGX config is not a dict, skipping snapshot") return {"status": NOT_AVAILABLE} - snapshot = _extract_snapshot_fields(ls_config, OGX_FIELDS) - snapshot["inference_store"] = _extract_store_info(ls_config, "inference") - snapshot["metadata_store"] = _extract_store_info(ls_config, "metadata") + snapshot = _extract_snapshot_fields(ogx_config, OGX_FIELDS) + snapshot["inference_store"] = _extract_store_info(ogx_config, "inference") + snapshot["metadata_store"] = _extract_store_info(ogx_config, "metadata") return snapshot diff --git a/src/transcripts/README.md b/src/transcripts/README.md new file mode 100644 index 000000000..49d64bcb2 --- /dev/null +++ b/src/transcripts/README.md @@ -0,0 +1,6 @@ +# List of source files stored in `src/transcripts` directory + +## [transcripts.py](transcripts.py) + +Transcript handling. + diff --git a/src/utils/transcripts.py b/src/transcripts/transcripts.py similarity index 100% rename from src/utils/transcripts.py rename to src/transcripts/transcripts.py diff --git a/src/utils/README.md b/src/utils/README.md index 0efedb069..efd2169da 100644 --- a/src/utils/README.md +++ b/src/utils/README.md @@ -48,37 +48,21 @@ Input sanitization to detect and block obfuscated prompt injection attempts. Function to transform a JSON Schema-like dictionary into an OpenAPI-compatible schema. -## [ogx_version.py](ogx_version.py) - -Check if the OGX version is supported by the LCS. - ## [markdown_repair.py](markdown_repair.py) Utilities for repairing truncated markdown content. -## [mcp_auth_headers.py](mcp_auth_headers.py) - -Utilities for resolving MCP server authorization headers. - -## [mcp_headers.py](mcp_headers.py) - -MCP headers handling. - -## [mcp_oauth_probe.py](mcp_oauth_probe.py) - -Probe MCP servers for OAuth and raise 401 with WWW-Authenticate when required. - -## [mcp_tools.py](mcp_tools.py) - -Utilities for discovering tools from remote MCP servers without OGX. - ## [model_list.py](model_list.py) Helpers for normalizing OGX ``models.list()`` union responses. -## [openapi_schema_dumper.py](openapi_schema_dumper.py) +## [ogx_serialization.py](ogx_serialization.py) -Utility function to dump schema with list of models into OpenAPI-compatible JSON format. +Serialization helpers for ``ogx_client`` models. + +## [ogx_version.py](ogx_version.py) + +Check if the OGX version is supported by the LCS. ## [otel_tracing.py](otel_tracing.py) @@ -144,10 +128,6 @@ Pre-LLM-call token estimation. Utility functions for formatting and parsing MCP tool descriptions. -## [transcripts.py](transcripts.py) - -Transcript handling. - ## [types.py](types.py) Common types for the project. diff --git a/src/utils/agents/query.py b/src/utils/agents/query.py index d39634be2..fb78c1dac 100644 --- a/src/utils/agents/query.py +++ b/src/utils/agents/query.py @@ -2,7 +2,7 @@ from __future__ import annotations -from enum import Enum +from enum import StrEnum from typing import Optional from fastapi import HTTPException @@ -60,7 +60,7 @@ tracer = trace.get_tracer(__name__) -class AgentFinishReason(str, Enum): +class AgentFinishReason(StrEnum): """Finish reason for a completed agent model response.""" CONTENT_FILTER = "content_filter" diff --git a/src/utils/agents/tool_processor.py b/src/utils/agents/tool_processor.py index 161cb05a6..2607ef1fe 100644 --- a/src/utils/agents/tool_processor.py +++ b/src/utils/agents/tool_processor.py @@ -29,7 +29,11 @@ ToolInfoSummary, ToolResultSummary, ) -from utils.responses import _build_okp_doc_url, resolve_source_for_result +from utils.responses import ( + _build_okp_doc_url, + parse_arguments_string, + resolve_source_for_result, +) logger = get_logger(__name__) @@ -95,10 +99,13 @@ def summarize_native_tool_call( ) # MCP call + tool_args = args.get("tool_args") or {} + if isinstance(tool_args, str): + tool_args = parse_arguments_string(tool_args) return ToolCallSummary( id=call_id, name=args.get("tool_name") or "", - args=args.get("tool_args", {}), + args=tool_args, type="mcp_call", ) case _: diff --git a/src/utils/conversations.py b/src/utils/conversations.py index b52d417bb..373463b36 100644 --- a/src/utils/conversations.py +++ b/src/utils/conversations.py @@ -564,7 +564,7 @@ async def append_turn_items_to_conversation( async def get_all_conversation_items( client: AsyncOgxClient, - conversation_id_llama_stack: str, + conversation_id_ogx: str, ) -> list[ConversationItem]: """Fetch all items for a conversation (Conversations API), paginating as needed. @@ -581,7 +581,7 @@ async def get_all_conversation_items( try: while has_more: page = await client.items.list( - conversation_id=conversation_id_llama_stack, + conversation_id=conversation_id_ogx, order="asc", after=after, ) diff --git a/src/utils/dumpers/README.md b/src/utils/dumpers/README.md index f3e7acda3..dc107130b 100644 --- a/src/utils/dumpers/README.md +++ b/src/utils/dumpers/README.md @@ -8,3 +8,7 @@ Function to dump the configuration schema into OpenAPI-compatible format. Function to dump the schema of all data models into OpenAPI-compatible format. +## [openapi_schema_dumper.py](openapi_schema_dumper.py) + +Utility function to dump schema with list of models into OpenAPI-compatible JSON format. + diff --git a/src/utils/mcp/README.md b/src/utils/mcp/README.md new file mode 100644 index 000000000..75b4ab996 --- /dev/null +++ b/src/utils/mcp/README.md @@ -0,0 +1,18 @@ +# List of source files stored in `src/utils/mcp` directory + +## [mcp_auth_headers.py](mcp_auth_headers.py) + +Utilities for resolving MCP server authorization headers. + +## [mcp_headers.py](mcp_headers.py) + +MCP headers handling. + +## [mcp_oauth_probe.py](mcp_oauth_probe.py) + +Probe MCP servers for OAuth and raise 401 with WWW-Authenticate when required. + +## [mcp_tools.py](mcp_tools.py) + +Utilities for discovering tools from remote MCP servers without OGX. + diff --git a/src/utils/mcp_auth_headers.py b/src/utils/mcp/mcp_auth_headers.py similarity index 100% rename from src/utils/mcp_auth_headers.py rename to src/utils/mcp/mcp_auth_headers.py diff --git a/src/utils/mcp_headers.py b/src/utils/mcp/mcp_headers.py similarity index 83% rename from src/utils/mcp_headers.py rename to src/utils/mcp/mcp_headers.py index 980d7a421..f3473884f 100644 --- a/src/utils/mcp_headers.py +++ b/src/utils/mcp/mcp_headers.py @@ -129,6 +129,50 @@ def extract_propagated_headers( return propagated +def strip_bearer_prefix(value: str) -> str: + """Strip a leading ``Bearer`` scheme from an Authorization header value. + + OGX's MCP tool ``authorization`` field expects a raw token/credential and + unconditionally prepends its own ``Bearer `` prefix before forwarding the + request to the downstream MCP server. Values built by this module (e.g. + the resolved Kubernetes token, or an ``Authorization`` header propagated + verbatim from the incoming request) already include the ``Bearer`` scheme + since that's the correct on-the-wire header representation. Passing such + a value straight through to OGX's ``authorization`` field would result in + a duplicated ``Bearer Bearer `` header at the downstream MCP + server, so callers that populate that field must strip the scheme first. + + Args: + value: The raw Authorization header value, with or without a + ``Bearer`` scheme. + + Returns: + The value with a leading ``Bearer`` scheme (exact casing) and its + separating whitespace removed. Values without the scheme, including + those with a differently-cased scheme (e.g. ``bearer``), are + returned unchanged. + """ + return value.removeprefix("Bearer ") + + +def ensure_bearer_prefix(value: str) -> str: + """Ensure an Authorization header value carries the ``Bearer`` scheme. + + Idempotent counterpart to :func:`strip_bearer_prefix`: a value that + already has the exact ``Bearer`` scheme is not double-prefixed, while a + raw token (or a value with a differently-cased scheme, e.g. ``bearer``, + which is treated as a raw token) gets the ``Bearer`` scheme added. + + Args: + value: A raw token, or a value that may already start with the + exact ``Bearer`` scheme. + + Returns: + The value guaranteed to start with a single ``Bearer `` prefix. + """ + return f"Bearer {strip_bearer_prefix(value)}" + + def find_unresolved_auth_headers( configured: Mapping[str, str], resolved: Mapping[str, str], diff --git a/src/utils/mcp_oauth_probe.py b/src/utils/mcp/mcp_oauth_probe.py similarity index 93% rename from src/utils/mcp_oauth_probe.py rename to src/utils/mcp/mcp_oauth_probe.py index 570e968eb..6fad35153 100644 --- a/src/utils/mcp_oauth_probe.py +++ b/src/utils/mcp/mcp_oauth_probe.py @@ -15,7 +15,7 @@ from configuration import AppConfig from log import get_logger from models.api.responses.error import UnauthorizedResponse -from utils.mcp_headers import McpHeaders, build_mcp_headers +from utils.mcp.mcp_headers import McpHeaders, build_mcp_headers, ensure_bearer_prefix logger = get_logger(__name__) @@ -57,14 +57,9 @@ async def check_mcp_auth( for mcp_server in configuration.mcp_servers: headers = complete_headers.get(mcp_server.name, {}) auth_header = headers.get("Authorization") - if auth_header is not None: - authorization = ( - auth_header - if auth_header.startswith("Bearer ") - else f"Bearer {auth_header}" - ) - else: - authorization = None + authorization = ( + ensure_bearer_prefix(auth_header) if auth_header is not None else None + ) if ( authorization diff --git a/src/utils/mcp_tools.py b/src/utils/mcp/mcp_tools.py similarity index 96% rename from src/utils/mcp_tools.py rename to src/utils/mcp/mcp_tools.py index 8f30699de..997eb9c32 100644 --- a/src/utils/mcp_tools.py +++ b/src/utils/mcp/mcp_tools.py @@ -11,6 +11,7 @@ from log import get_logger from models.common.tools import ListedMcpTool +from utils.mcp.mcp_headers import ensure_bearer_prefix logger = get_logger(__name__) @@ -106,12 +107,8 @@ def _prepare_mcp_request_headers(headers: dict[str, str]) -> dict[str, str]: """ prepared = dict(headers) for header_name, value in list(prepared.items()): - if ( - header_name.lower() == "authorization" - and value - and not value.startswith("Bearer ") - ): - prepared[header_name] = f"Bearer {value}" + if header_name.lower() == "authorization" and value: + prepared[header_name] = ensure_bearer_prefix(value) return prepared diff --git a/src/utils/otel_tracing.py b/src/utils/otel_tracing.py index f24b35311..9b4d887c4 100644 --- a/src/utils/otel_tracing.py +++ b/src/utils/otel_tracing.py @@ -52,6 +52,9 @@ class SpanAttributes(StrEnum): FEEDBACK_CATEGORIES = "feedback.categories" FEEDBACK_STATUS_CODE = "feedback.status.code" FEEDBACK_STORAGE_OUTCOME = "feedback.storage.outcome" + INTERRUPT_REQUEST_ID = "interrupt.request_id" + STREAM_CONVERSATION_ID = "stream.conversation.id" + INTERRUPT_RESULT = "interrupt.result" MCP_SERVER_NAME = "mcp.server.name" MCP_SERVER_PROVIDER_ID = "mcp.server.provider_id" MCP_SERVERS_COUNT = "mcp.servers.count" diff --git a/src/utils/pydantic_ai_helpers.py b/src/utils/pydantic_ai_helpers.py index d98108f8b..d7ff1fa83 100644 --- a/src/utils/pydantic_ai_helpers.py +++ b/src/utils/pydantic_ai_helpers.py @@ -17,6 +17,7 @@ from models.common.skills import SkillMetadata from models.common.tools import CatalogTool, CatalogToolParameter from models.config import ( + GraniteGuardianConfig, QuestionValidityConfig, RedactionConfig, ShieldConfiguration, @@ -175,6 +176,8 @@ def _shield_capability(shield: ShieldConfiguration) -> AgentCapability[object]: return QuestionValidity(config=shield.config) case RedactionConfig(): return PiiRedactionCapability(config=shield.config) + case GraniteGuardianConfig(): + raise NotImplementedError("Granite Guardian capability not implemented") case _: raise ValueError( f"Unsupported shield config type for shield '{shield.name}': " diff --git a/src/utils/query.py b/src/utils/query.py index 04a3aa308..0ba27d6c5 100644 --- a/src/utils/query.py +++ b/src/utils/query.py @@ -31,14 +31,14 @@ from models.common.turn_summary import TurnSummary from models.config import Action from models.database.conversations import UserConversation, UserTurn -from utils.quota_utils import consume_tokens -from utils.suid import is_moderation_id, normalize_conversation_id -from utils.token_counter import TokenCounter -from utils.transcripts import ( +from transcripts.transcripts import ( create_transcript, create_transcript_metadata, store_transcript, ) +from utils.quota_utils import consume_tokens +from utils.suid import is_moderation_id, normalize_conversation_id +from utils.token_counter import TokenCounter logger = get_logger(__name__) diff --git a/src/utils/responses.py b/src/utils/responses.py index 607002efa..c6e5811b1 100644 --- a/src/utils/responses.py +++ b/src/utils/responses.py @@ -9,7 +9,10 @@ from fastapi import HTTPException from ogx_api import OpenAIResponseObject -from ogx_api.openai_responses import ApprovalFilter +from ogx_api.openai_responses import ( + ApprovalFilter, + OpenAIResponseReasoning, +) from ogx_api.openai_responses import ( OpenAIResponseContentPartRefusal as ContentPartRefusal, ) @@ -81,6 +84,7 @@ ) from ogx_client import ApiException, AsyncOgxClient from opentelemetry import trace +from pydantic_ai.profiles.openai import openai_model_profile import constants from configuration import configuration @@ -110,10 +114,11 @@ ) from models.config import RagStore from models.database.conversations import UserConversation -from utils.mcp_headers import ( +from utils.mcp.mcp_headers import ( McpHeaders, build_mcp_headers, find_unresolved_auth_headers, + strip_bearer_prefix, ) from utils.model_list import parse_model_list_response from utils.otel_tracing import ( @@ -777,6 +782,13 @@ async def get_mcp_tools( continue authorization = headers.pop("Authorization", None) + if authorization is not None: + # OGX's "authorization" field expects a raw token/credential and + # prepends its own "Bearer " scheme downstream; strip any scheme + # already present (e.g. from Kubernetes auth or a propagated + # request header) to avoid a duplicated "Bearer Bearer " + # header at the MCP server. + authorization = strip_bearer_prefix(authorization) require_approval = ( mcp_server.require_approval @@ -856,6 +868,10 @@ def apply_mcp_headers_to_explicit_tools( continue authorization = headers.pop("Authorization", None) + if authorization is not None: + # See the equivalent comment in get_mcp_tools: OGX prepends its + # own "Bearer " scheme, so strip any scheme already present here. + authorization = strip_bearer_prefix(authorization) out.append( mcp_tool.model_copy( update={ @@ -1826,6 +1842,53 @@ async def _resolve_server_tools( ) +def model_reasoning_enabled_by_default(model_id: str) -> bool: + """Return whether the model applies non-``none`` reasoning when omitted. + + Uses pydantic-ai's OpenAI model profile table (prefix-matched, live-verified). + Only these models need an explicit ``reasoning.effort: none`` when tools are + present on the OGX chat-completions path — e.g. gpt-5.6-terra, not gpt-4o-mini + or opt-in gpt-5.4. + """ + _, model_name = extract_provider_and_model_from_model_id(model_id) + profile = openai_model_profile(model_name) + return bool(profile.get("openai_reasoning_enabled_by_default", False)) + + +def apply_reasoning_for_resolved_tools( + reasoning: Optional[OpenAIResponseReasoning], + tools: Optional[list[InputTool]], + model_id: str, +) -> Optional[OpenAIResponseReasoning]: + """Set reasoning effort to ``none`` when resolved tools are present. + + Only applied when the model reasons by default if ``reasoning`` is omitted + (OpenAI gpt-5.6, gpt-5, o-series, etc.). Opt-in and non-reasoning models are + left unchanged. + + Args: + reasoning: Optional reasoning configuration to modify. + tools: Optional list of tools that have been resolved. + model_id: Resolved model identifier in ``provider/model`` form. + + Returns: + Modified reasoning configuration with effort set to "none" if tools + are present on a default-on reasoning model, or the original reasoning + otherwise. + """ + if not tools or not model_reasoning_enabled_by_default(model_id): + return reasoning + + base = reasoning or OpenAIResponseReasoning() + if base.effort is not None and base.effort != "none": + logger.warning( + "reasoning effort '%s' is not supported with tools in LCORE; " + "using effort='none'", + base.effort, + ) + return base.model_copy(update={"effort": "none"}) + + async def resolve_tool_choice( tools: Optional[list[InputTool]], tool_choice: Optional[ToolChoice], diff --git a/src/utils/shields.py b/src/utils/shields.py index 05825f14c..662e96304 100644 --- a/src/utils/shields.py +++ b/src/utils/shields.py @@ -21,7 +21,12 @@ ShieldModerationPassed, ShieldModerationResult, ) -from models.config import QuestionValidityConfig, RedactionConfig, ShieldConfiguration +from models.config import ( + GraniteGuardianConfig, + QuestionValidityConfig, + RedactionConfig, + ShieldConfiguration, +) from pydantic_ai_lightspeed.capabilities.base import AbstractSafetyCapability from pydantic_ai_lightspeed.capabilities.question_validity._capability import ( QuestionValidity, @@ -158,6 +163,13 @@ def build_shield(shield_config: ShieldConfiguration) -> AbstractSafetyCapability return QuestionValidity(shield_config.config) case RedactionConfig(): return PiiRedactionCapability(shield_config.config) + case GraniteGuardianConfig(): + raise NotImplementedError("Granite Guardian capability not implemented") + case _: + raise ValueError( + f"Unsupported shield config type for shield '{shield_config.name}': " + f"{type(shield_config.config).__name__}" + ) async def run_shield_moderation( diff --git a/src/utils/stream_interrupts.py b/src/utils/stream_interrupts.py index 8ad40650c..df075025b 100644 --- a/src/utils/stream_interrupts.py +++ b/src/utils/stream_interrupts.py @@ -4,7 +4,7 @@ import datetime from collections.abc import Callable, Coroutine from dataclasses import dataclass, field -from enum import Enum +from enum import StrEnum from threading import Lock from typing import Any, Optional, cast @@ -41,6 +41,8 @@ class ActiveStream: on_interrupt: Optional async callback invoked when the stream is cancelled, scheduled as a separate task so it runs regardless of where the ``CancelledError`` lands. + conversation_id: Conversation the streaming request belongs to, + surfaced for observability of interrupt requests. """ user_id: str @@ -48,9 +50,10 @@ class ActiveStream: on_interrupt: Optional[Callable[[], Coroutine[Any, Any, None]]] = field( default=None, repr=False ) + conversation_id: Optional[str] = None -class CancelStreamResult(str, Enum): +class CancelStreamResult(StrEnum): """Outcomes when attempting to cancel a stream.""" CANCELLED = "cancelled" @@ -73,6 +76,7 @@ def register_stream( user_id: str, task: asyncio.Task[None], on_interrupt: Optional[Callable[[], Coroutine[Any, Any, None]]] = None, + conversation_id: Optional[str] = None, ) -> None: """Register an active stream task for interrupt support. @@ -83,10 +87,15 @@ def register_stream( task: Asyncio task associated with the stream. on_interrupt: Optional async callback to run when the stream is cancelled, executed in a separate task. + conversation_id: Conversation the stream belongs to, retained + for observability of subsequent interrupt requests. """ with self._lock: self._streams[request_id] = ActiveStream( - user_id=user_id, task=task, on_interrupt=on_interrupt + user_id=user_id, + task=task, + on_interrupt=on_interrupt, + conversation_id=conversation_id, ) def cancel_stream(self, request_id: str, user_id: str) -> CancelStreamResult: @@ -384,6 +393,7 @@ async def _on_interrupt() -> None: user_id=context.user_id, task=current_task, on_interrupt=_on_interrupt, + conversation_id=context.conversation_id, ) else: logger.warning( diff --git a/src/utils/types.py b/src/utils/types.py index 3847d1b71..6cf53582a 100644 --- a/src/utils/types.py +++ b/src/utils/types.py @@ -1,10 +1,12 @@ """Common types for the project.""" from re import Pattern -from typing import TypeVar, cast +from typing import Any, TypeVar, cast type SingletonInstances = dict[type, object] +type Responses = dict[int | str, dict[str, Any]] + CompiledPatterns = list[tuple[Pattern[str], str]] T = TypeVar("T") diff --git a/src/version.py b/src/version.py index 77bdde301..3f3c23f06 100644 --- a/src/version.py +++ b/src/version.py @@ -9,4 +9,4 @@ # [tool.pdm.version] # source = "file" # path = "src/version.py" -__version__ = "0.7.0rc2" +__version__ = "0.7.0rc3" diff --git a/tests/benchmarks/data/go_10000_lines.go b/tests/benchmarks/data/go_10000_lines.go new file mode 100644 index 000000000..e9e1b4a15 --- /dev/null +++ b/tests/benchmarks/data/go_10000_lines.go @@ -0,0 +1,10000 @@ +// +// Apache License +// Version 2.0, January 2004 +// http://www.apache.org/licenses/ +// +// TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +// +// 1. Definitions. +// +// "License" shall mean the terms and conditions for use, reproduction, +// and distribution as defined by Sections 1 through 9 of this document. +// +// "Licensor" shall mean the copyright owner or entity authorized by +// the copyright owner that is granting the License. +// +// "Legal Entity" shall mean the union of the acting entity and all +// other entities that control, are controlled by, or are under common +// control with that entity. For the purposes of this definition, +// "control" means (i) the power, direct or indirect, to cause the +// direction or management of such entity, whether by contract or +// otherwise, or (ii) ownership of fifty percent (50%) or more of the +// outstanding shares, or (iii) beneficial ownership of such entity. +// +// "You" (or "Your") shall mean an individual or Legal Entity +// exercising permissions granted by this License. +// +// "Source" form shall mean the preferred form for making modifications, +// including but not limited to software source code, documentation +// source, and configuration files. +// +// "Object" form shall mean any form resulting from mechanical +// transformation or translation of a Source form, including but +// not limited to compiled object code, generated documentation, +// and conversions to other media types. +// +// "Work" shall mean the work of authorship, whether in Source or +// Object form, made available under the License, as indicated by a +// copyright notice that is included in or attached to the work +// (an example is provided in the Appendix below). +// +// "Derivative Works" shall mean any work, whether in Source or Object +// form, that is based on (or derived from) the Work and for which the +// editorial revisions, annotations, elaborations, or other modifications +// represent, as a whole, an original work of authorship. For the purposes +// of this License, Derivative Works shall not include works that remain +// separable from, or merely link (or bind by name) to the interfaces of, +// the Work and Derivative Works thereof. +// +// "Contribution" shall mean any work of authorship, including +// the original version of the Work and any modifications or additions +// to that Work or Derivative Works thereof, that is intentionally +// submitted to Licensor for inclusion in the Work by the copyright owner +// or by an individual or Legal Entity authorized to submit on behalf of +// the copyright owner. For the purposes of this definition, "submitted" +// means any form of electronic, verbal, or written communication sent +// to the Licensor or its representatives, including but not limited to +// communication on electronic mailing lists, source code control systems, +// and issue tracking systems that are managed by, or on behalf of, the +// Licensor for the purpose of discussing and improving the Work, but +// excluding communication that is conspicuously marked or otherwise +// designated in writing by the copyright owner as "Not a Contribution." +// +// "Contributor" shall mean Licensor and any individual or Legal Entity +// on behalf of whom a Contribution has been received by Licensor and +// subsequently incorporated within the Work. +// +// 2. Grant of Copyright License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// copyright license to reproduce, prepare Derivative Works of, +// publicly display, publicly perform, sublicense, and distribute the +// Work and such Derivative Works in Source or Object form. +// +// 3. Grant of Patent License. Subject to the terms and conditions of +// this License, each Contributor hereby grants to You a perpetual, +// worldwide, non-exclusive, no-charge, royalty-free, irrevocable +// (except as stated in this section) patent license to make, have made, +// use, offer to sell, sell, import, and otherwise transfer the Work, +// where such license applies only to those patent claims licensable +// by such Contributor that are necessarily infringed by their +// Contribution(s) alone or by combination of their Contribution(s) +// with the Work to which such Contribution(s) was submitted. If You +// institute patent litigation against any entity (including a +// cross-claim or counterclaim in a lawsuit) alleging that the Work +// or a Contribution incorporated within the Work constitutes direct +// or contributory patent infringement, then any patent licenses +// granted to You under this License for that Work shall terminate +// as of the date such litigation is filed. +// +// 4. Redistribution. You may reproduce and distribute copies of the +// Work or Derivative Works thereof in any medium, with or without +// modifications, and in Source or Object form, provided that You +// meet the following conditions: +// +// (a) You must give any other recipients of the Work or +// Derivative Works a copy of this License; and +// +// (b) You must cause any modified files to carry prominent notices +// stating that You changed the files; and +// +// (c) You must retain, in the Source form of any Derivative Works +// that You distribute, all copyright, patent, trademark, and +// attribution notices from the Source form of the Work, +// excluding those notices that do not pertain to any part of +// the Derivative Works; and +// +// (d) If the Work includes a "NOTICE" text file as part of its +// distribution, then any Derivative Works that You distribute must +// include a readable copy of the attribution notices contained +// within such NOTICE file, excluding those notices that do not +// pertain to any part of the Derivative Works, in at least one +// of the following places: within a NOTICE text file distributed +// as part of the Derivative Works; within the Source form or +// documentation, if provided along with the Derivative Works; or, +// within a display generated by the Derivative Works, if and +// wherever such third-party notices normally appear. The contents +// of the NOTICE file are for informational purposes only and +// do not modify the License. You may add Your own attribution +// notices within Derivative Works that You distribute, alongside +// or as an addendum to the NOTICE text from the Work, provided +// that such additional attribution notices cannot be construed +// as modifying the License. +// +// You may add Your own copyright statement to Your modifications and +// may provide additional or different license terms and conditions +// for use, reproduction, or distribution of Your modifications, or +// for any such Derivative Works as a whole, provided Your use, +// reproduction, and distribution of the Work otherwise complies with +// the conditions stated in this License. +// +// 5. Submission of Contributions. Unless You explicitly state otherwise, +// any Contribution intentionally submitted for inclusion in the Work +// by You to the Licensor shall be under the terms and conditions of +// this License, without any additional terms or conditions. +// Notwithstanding the above, nothing herein shall supersede or modify +// the terms of any separate license agreement you may have executed +// with Licensor regarding such Contributions. +// +// 6. Trademarks. This License does not grant permission to use the trade +// names, trademarks, service marks, or product names of the Licensor, +// except as required for reasonable and customary use in describing the +// origin of the Work and reproducing the content of the NOTICE file. +// +// 7. Disclaimer of Warranty. Unless required by applicable law or +// agreed to in writing, Licensor provides the Work (and each +// Contributor provides its Contributions) on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied, including, without limitation, any warranties or conditions +// of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +// PARTICULAR PURPOSE. You are solely responsible for determining the +// appropriateness of using or redistributing the Work and assume any +// risks associated with Your exercise of permissions under this License. +// +// 8. Limitation of Liability. In no event and under no legal theory, +// whether in tort (including negligence), contract, or otherwise, +// unless required by applicable law (such as deliberate and grossly +// negligent acts) or agreed to in writing, shall any Contributor be +// liable to You for damages, including any direct, indirect, special, +// incidental, or consequential damages of any character arising as a +// result of this License or out of the use or inability to use the +// Work (including but not limited to damages for loss of goodwill, +// work stoppage, computer failure or malfunction, or any and all +// other commercial damages or losses), even if such Contributor +// has been advised of the possibility of such damages. +// +// 9. Accepting Warranty or Additional Liability. While redistributing +// the Work or Derivative Works thereof, You may choose to offer, +// and charge a fee for, acceptance of support, warranty, indemnity, +// or other liability obligations and/or rights consistent with this +// License. However, in accepting such obligations, You may act only +// on Your own behalf and on Your sole responsibility, not on behalf +// of any other Contributor, and only if You agree to indemnify, +// defend, and hold each Contributor harmless for any liability +// incurred by, or claims asserted against, such Contributor by reason +// of your accepting any such warranty or additional liability. +// +// END OF TERMS AND CONDITIONS +// +// APPENDIX: How to apply the Apache License to your work. +// +// To apply the Apache License to your work, attach the following +// boilerplate notice, with the fields enclosed by brackets "[]" +// replaced with your own identifying information. (Don't include +// the brackets!) The text should be enclosed in the appropriate +// comment syntax for the file format. We also recommend that a +// file or class name and description of purpose be included on the +// same "printed page" as the copyright notice for easier +// identification within third-party archives. +// +// Copyright [yyyy] [name of copyright owner] +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// + +package main + +import ( + "errors" + "fmt" + "image" + "log" + "math" + "math/cmplx" + "os" +) + +// IImage is representation of raster image consisting of IPixels +type IImage [][]IPixel + +// NewIImage constructs new instance of ZImage +func NewIImage(resolution Resolution) IImage { + iimage := make([][]IPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + iimage[y] = make([]IPixel, resolution.Width) + } + return iimage +} + +// IPixel is a representation of pixel as one unsigned integer value +type IPixel uint64 + +// RImage is representation of raster image consisting of RPixels +type RImage [][]RPixel + +// NewRImage constructs new instance of RImage +func NewRImage(resolution Resolution) RImage { + rimage := make([][]RPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + rimage[y] = make([]RPixel, resolution.Width) + } + return rimage +} + +func (image *RImage) minMax(width, height uint) (float64, float64) { + min := float64(math.Inf(1)) + max := float64(math.Inf(-1)) + + for j := range height { + for i := range width { + z := float64((*image)[j][i]) + if max < z { + max = z + } + if min > z { + min = z + } + } + } + return min, max +} + +// RPixel is a representation of pixel as one real value +type RPixel float64 + +// ZImage is representation of raster image consisting of ZPixels +type ZImage [][]ZPixel + +// NewZImage constructs new instance of ZImage +func NewZImage(resolution Resolution) ZImage { + zimage := make([][]ZPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + zimage[y] = make([]ZPixel, resolution.Width) + } + return zimage +} + +// ZPixel is a representation of pixel in complex plane +type ZPixel complex128 + +// Palette structure +type Palette struct { + Name string `toml:"name"` + Shift int `toml:"shift"` + Slope int `toml:"slope"` +} + +// FractalParameter structure contains information about all fractal parameters. +type FractalParameter struct { + Name string `toml:"name"` + Type string `toml:"type"` + Class string `toml:"class"` + Cx0 float64 `toml:"cx0"` + Cy0 float64 `toml:"cy0"` + Palette Palette `toml:"palette"` + Maxiter uint `toml:"maxiter"` + Bailout uint `toml:"bailout"` + Function1 string `toml:"function1"` + Function2 string `toml:"function2"` + Xmin float64 `toml:"xmin"` + Ymin float64 `toml:"ymin"` + Xmax float64 `toml:"xmax"` + Ymax float64 `toml:"ymax"` + A float64 `toml:"A"` + B float64 `toml:"B"` + C float64 `toml:"C"` + D float64 `toml:"D"` + Scale float64 `toml:"scale"` + XOffset float64 `toml:"x_offset"` + YOffset float64 `toml:"y_offset"` +} + +// FractalParameter2 structure contains information about all fractal parameters. +type FractalParameter2 struct { + Name string `toml:"name"` + Type string `toml:"type"` + Class string `toml:"class"` + Cx0 float64 `toml:"cx0"` + Cy0 float64 `toml:"cy0"` + Palette Palette `toml:"palette"` + Maxiter uint `toml:"maxiter"` + Bailout uint `toml:"bailout"` + Function1 string `toml:"function1"` + Function2 string `toml:"function2"` + Xmin float64 `toml:"xmin"` + Ymin float64 `toml:"ymin"` + Xmax float64 `toml:"xmax"` + Ymax float64 `toml:"ymax"` + A float64 `toml:"A"` + B float64 `toml:"B"` + C float64 `toml:"C"` + D float64 `toml:"D"` + Scale float64 `toml:"scale"` + XOffset float64 `toml:"x_offset"` + YOffset float64 `toml:"y_offset"` +} + +// Sequence of fractal parameters +type FractalParameters struct { + Parameters []FractalParameter `toml:"fractal"` +} + +// LoadFractalParameters function reads fractal parameters from external text file +func LoadFractalParameters(filename string) (map[string]FractalParameter, error) { + var parameters FractalParameters + asMap := map[string]FractalParameter{} + + _, err := os.Stat(filename) + + if os.IsNotExist(err) { + return asMap, errors.New("Parameter file does not exist.") + } + if err != nil { + log.Fatal(err) + return asMap, err + } + + for _, parameter := range parameters.Parameters { + if _, exists := asMap[parameter.Name]; exists { + return asMap, fmt.Errorf( + "duplicate parameter name %q in %s", + parameter.Name, filename) + } + if parameter.Palette.Name == "" { + parameter.Palette.Slope = 1 + } + asMap[parameter.Name] = parameter + } + return asMap, nil +} + +// Resolution describes the image dimensions in pixels. +type Resolution struct { + Width uint + Height uint +} + +// NewResolution constructs a Resolution with the given width and height. +// Width and height are expected to be positive numbers. +func NewResolution(width, height uint) (Resolution, error) { + // Check for zero dimensions + if width == 0 { + return Resolution{}, errors.New("width cannot be zero") + } + if height == 0 { + return Resolution{}, errors.New("height cannot be zero") + } + + // Check for reasonable maximum dimensions to prevent memory issues + const maxDimension = 65535 // 2^16 - 1, reasonable for image processing + if width > maxDimension { + return Resolution{}, fmt.Errorf("width %d exceeds maximum allowed dimension %d", width, maxDimension) + } + if height > maxDimension { + return Resolution{}, fmt.Errorf("height %d exceeds maximum allowed dimension %d", height, maxDimension) + } + + return Resolution{ + Width: width, + Height: height, + }, nil +} + +func getSteps( + params FractalParameter, + image Image) (float64, float64) { + stepX := float64(params.Xmax-params.Xmin) / float64(image.Resolution.Width) + stepY := float64(params.Ymax-params.Ymin) / float64(image.Resolution.Height) + return stepX, stepY +} + +func calcIndex(params FractalParameter, i uint) uint { + index := params.Palette.Shift + int(i)*params.Palette.Slope + if index < 0 { + return 0 + } + return uint(index) +} + +// Image structure +type Image struct { + Resolution Resolution + Z ZImage + R RImage + I IImage + RGBA *image.NRGBA +} + +// Image constructor +func New(width uint, height uint) (Image, error) { + resolution, err := NewResolution(width, height) + + if err != nil { + return Image{}, err + } + + return Image{ + Resolution: resolution, + Z: NewZImage(resolution), + R: NewRImage(resolution), + I: NewIImage(resolution), + }, nil +} + +// Palette represents color palette used to map fractal calculation result +// (number of iterations, for example) into RGB or RGBA color. Palettes have +// usually 256 records, but it can be more or less. +type RGBPalette [][]byte + +func (i *Image) ApplyPalette(palette RGBPalette) { + r := i.Resolution + i.RGBA = image.NewNRGBA(image.Rect(0, 0, int(r.Width), int(r.Height))) + + for y := 0; y < int(r.Height); y++ { + offset := i.RGBA.PixOffset(0, y) + for x := uint(0); x < r.Width; x++ { + index := byte(i.I[y][x]) + i.RGBA.Pix[offset] = palette[index][0] + offset++ + i.RGBA.Pix[offset] = palette[index][1] + offset++ + i.RGBA.Pix[offset] = palette[index][2] + offset++ + i.RGBA.Pix[offset] = 0xff + offset++ + } + } +} + +func (image *Image) RImage2IImage() { + r := image.Resolution + width := r.Width + height := r.Height + + min, max := image.R.minMax(width, height) + k := 255.0 / (max - min) + + for y := uint(0); y < height; y++ { + for x := uint(0); x < width; x++ { + f := float64(image.R[y][x]) + f -= min + f *= k + if f > 255.0 { + f = 255 + } + i := int(f) & 255 + image.Z[y][x] = ZPixel(complex(float32(x), float32(y))) + image.I[y][x] = IPixel(i) + } + } +} + +func (image *Image) RImage2IImageWithFactor(maxFactor float64) { + r := image.Resolution + width := r.Width + height := r.Height + + min, max := image.R.minMax(width, height) + max *= maxFactor + k := 255.0 / (max - min) + + for y := uint(0); y < height; y++ { + for x := uint(0); x < width; x++ { + f := float64(image.R[y][x]) + f -= min + f *= k + if f > 255.0 { + f = 255 + } + i := int(f) & 255 + image.Z[y][x] = ZPixel(complex(float32(x), float32(y))) + image.I[y][x] = IPixel(i) + } + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +/* taken from Fractint */ + +var map1 = [...][3]byte{ + {255, 255, 255}, {224, 224, 224}, {216, 216, 216}, {208, 208, 208}, + {200, 200, 200}, {192, 192, 192}, {184, 184, 184}, {176, 176, 176}, + {168, 168, 168}, {160, 160, 160}, {152, 152, 152}, {144, 144, 144}, + {136, 136, 136}, {128, 128, 128}, {120, 120, 120}, {112, 112, 112}, + {104, 104, 104}, {96, 96, 96}, {88, 88, 88}, {80, 80, 80}, + {72, 72, 72}, {64, 64, 64}, {56, 56, 56}, {48, 48, 56}, + {40, 40, 56}, {32, 32, 56}, {24, 24, 56}, {16, 16, 56}, + {8, 8, 56}, {000, 000, 60}, {000, 000, 64}, {000, 000, 72}, + {000, 000, 80}, {000, 000, 88}, {000, 000, 96}, {000, 000, 104}, + {000, 000, 108}, {000, 000, 116}, {000, 000, 124}, {000, 000, 132}, + {000, 000, 140}, {000, 000, 148}, {000, 000, 156}, {000, 000, 160}, + {000, 000, 168}, {000, 000, 176}, {000, 000, 184}, {000, 000, 192}, + {000, 000, 200}, {000, 000, 204}, {000, 000, 212}, {000, 000, 220}, + {000, 000, 228}, {000, 000, 236}, {000, 000, 244}, {000, 000, 252}, + {000, 4, 252}, {4, 12, 252}, {8, 20, 252}, {12, 28, 252}, + {16, 36, 252}, {20, 44, 252}, {20, 52, 252}, {24, 60, 252}, + {28, 68, 252}, {32, 76, 252}, {36, 84, 252}, {40, 92, 252}, + {40, 100, 252}, {44, 108, 252}, {48, 116, 252}, {52, 120, 252}, + {56, 128, 252}, {60, 136, 252}, {60, 144, 252}, {64, 152, 252}, + {68, 160, 252}, {72, 168, 252}, {76, 176, 252}, {80, 184, 252}, + {80, 192, 252}, {84, 200, 252}, {88, 208, 252}, {92, 216, 252}, + {96, 224, 252}, {100, 232, 252}, {100, 228, 248}, {96, 224, 244}, + {92, 216, 240}, {88, 212, 236}, {88, 204, 232}, {84, 200, 228}, + {80, 192, 220}, {76, 188, 216}, {76, 180, 212}, {72, 176, 208}, + {68, 168, 204}, {64, 164, 200}, {64, 156, 196}, {60, 152, 188}, + {56, 144, 184}, {52, 140, 180}, {52, 132, 176}, {48, 128, 172}, + {44, 120, 168}, {40, 116, 160}, {40, 108, 156}, {36, 104, 152}, + {32, 96, 148}, {28, 92, 144}, {28, 84, 140}, {24, 80, 136}, + {20, 72, 128}, {16, 68, 124}, {16, 60, 120}, {12, 56, 116}, + {8, 48, 112}, {4, 44, 108}, {000, 36, 100}, {4, 36, 104}, + {12, 40, 108}, {16, 44, 116}, {24, 48, 120}, {28, 52, 128}, + {36, 56, 132}, {40, 60, 140}, {48, 64, 144}, {52, 64, 148}, + {60, 68, 156}, {64, 72, 160}, {72, 76, 168}, {76, 80, 172}, + {84, 84, 180}, {88, 88, 184}, {96, 92, 192}, {104, 100, 192}, + {112, 112, 196}, {124, 120, 200}, {132, 132, 204}, {144, 140, 208}, + {152, 152, 212}, {164, 160, 216}, {172, 172, 220}, {180, 180, 224}, + {192, 192, 228}, {200, 200, 232}, {212, 212, 236}, {220, 220, 240}, + {232, 232, 244}, {240, 240, 248}, {252, 252, 252}, {252, 240, 244}, + {252, 224, 232}, {252, 208, 224}, {252, 192, 212}, {252, 176, 204}, + {252, 160, 192}, {252, 144, 184}, {252, 128, 172}, {252, 112, 164}, + {252, 96, 152}, {252, 80, 144}, {252, 64, 132}, {252, 48, 124}, + {252, 32, 112}, {252, 16, 104}, {252, 000, 92}, {236, 000, 88}, + {228, 000, 88}, {216, 4, 84}, {204, 4, 80}, {192, 8, 76}, + {180, 8, 76}, {168, 12, 72}, {156, 16, 68}, {144, 16, 64}, + {132, 20, 60}, {124, 20, 60}, {112, 24, 56}, {100, 24, 52}, + {88, 28, 48}, {76, 32, 44}, {64, 32, 44}, {52, 36, 40}, + {40, 36, 36}, {28, 40, 32}, {16, 44, 28}, {20, 52, 32}, + {24, 60, 36}, {28, 68, 44}, {32, 76, 48}, {36, 88, 56}, + {40, 96, 60}, {44, 104, 64}, {48, 112, 72}, {52, 120, 76}, + {56, 132, 84}, {48, 136, 84}, {40, 144, 80}, {52, 148, 88}, + {68, 156, 100}, {80, 164, 112}, {96, 168, 124}, {108, 176, 136}, + {124, 184, 144}, {136, 192, 156}, {152, 196, 168}, {164, 204, 180}, + {180, 212, 192}, {192, 220, 200}, {208, 224, 212}, {220, 232, 224}, + {236, 240, 236}, {252, 248, 248}, {252, 252, 252}, {252, 252, 240}, + {252, 252, 228}, {252, 252, 216}, {248, 248, 204}, {248, 248, 192}, + {248, 248, 180}, {248, 248, 164}, {244, 244, 152}, {244, 244, 140}, + {244, 244, 128}, {244, 244, 116}, {240, 240, 104}, {240, 240, 92}, + {240, 240, 76}, {240, 240, 64}, {236, 236, 52}, {236, 236, 40}, + {236, 236, 28}, {236, 236, 16}, {232, 232, 0}, {232, 232, 12}, + {232, 232, 28}, {232, 232, 40}, {236, 236, 56}, {236, 236, 68}, + {236, 236, 84}, {236, 236, 96}, {240, 240, 112}, {240, 240, 124}, + {240, 240, 140}, {244, 244, 152}, {244, 244, 168}, {244, 244, 180}, + {244, 244, 196}, {248, 248, 208}, {248, 248, 224}, {248, 248, 236}, + {252, 252, 252}, {248, 248, 248}, {240, 240, 240}, {232, 232, 232}} + +var map2 = [...][3]byte{ + {255, 255, 255}, {224, 224, 224}, {216, 216, 216}, {208, 208, 208}, + {200, 200, 200}, {192, 192, 192}, {184, 184, 184}, {176, 176, 176}, + {168, 168, 168}, {160, 160, 160}, {152, 152, 152}, {144, 144, 144}, + {136, 136, 136}, {128, 128, 128}, {120, 120, 120}, {112, 112, 112}, + {104, 104, 104}, {96, 96, 96}, {88, 88, 88}, {80, 80, 80}, + {72, 72, 72}, {64, 64, 64}, {56, 56, 56}, {48, 48, 56}, + {40, 40, 56}, {32, 32, 56}, {24, 24, 56}, {16, 16, 56}, + {8, 8, 56}, {000, 000, 60}, {000, 000, 64}, {000, 000, 72}, + {000, 000, 80}, {000, 000, 88}, {000, 000, 96}, {000, 000, 104}, + {000, 000, 108}, {000, 000, 116}, {000, 000, 124}, {000, 000, 132}, + {000, 000, 140}, {000, 000, 148}, {000, 000, 156}, {000, 000, 160}, + {000, 000, 168}, {000, 000, 176}, {000, 000, 184}, {000, 000, 192}, + {000, 000, 200}, {000, 000, 204}, {000, 000, 212}, {000, 000, 220}, + {000, 000, 228}, {000, 000, 236}, {000, 000, 244}, {000, 000, 252}, + {000, 4, 252}, {4, 12, 252}, {8, 20, 252}, {12, 28, 252}, + {16, 36, 252}, {20, 44, 252}, {20, 52, 252}, {24, 60, 252}, + {28, 68, 252}, {32, 76, 252}, {36, 84, 252}, {40, 92, 252}, + {40, 100, 252}, {44, 108, 252}, {48, 116, 252}, {52, 120, 252}, + {56, 128, 252}, {60, 136, 252}, {60, 144, 252}, {64, 152, 252}, + {68, 160, 252}, {72, 168, 252}, {76, 176, 252}, {80, 184, 252}, + {80, 192, 252}, {84, 200, 252}, {88, 208, 252}, {92, 216, 252}, + {96, 224, 252}, {100, 232, 252}, {100, 228, 248}, {96, 224, 244}, + {92, 216, 240}, {88, 212, 236}, {88, 204, 232}, {84, 200, 228}, + {80, 192, 220}, {76, 188, 216}, {76, 180, 212}, {72, 176, 208}, + {68, 168, 204}, {64, 164, 200}, {64, 156, 196}, {60, 152, 188}, + {56, 144, 184}, {52, 140, 180}, {52, 132, 176}, {48, 128, 172}, + {44, 120, 168}, {40, 116, 160}, {40, 108, 156}, {36, 104, 152}, + {32, 96, 148}, {28, 92, 144}, {28, 84, 140}, {24, 80, 136}, + {20, 72, 128}, {16, 68, 124}, {16, 60, 120}, {12, 56, 116}, + {8, 48, 112}, {4, 44, 108}, {000, 36, 100}, {4, 36, 104}, + {12, 40, 108}, {16, 44, 116}, {24, 48, 120}, {28, 52, 128}, + {36, 56, 132}, {40, 60, 140}, {48, 64, 144}, {52, 64, 148}, + {60, 68, 156}, {64, 72, 160}, {72, 76, 168}, {76, 80, 172}, + {84, 84, 180}, {88, 88, 184}, {96, 92, 192}, {104, 100, 192}, + {112, 112, 196}, {124, 120, 200}, {132, 132, 204}, {144, 140, 208}, + {152, 152, 212}, {164, 160, 216}, {172, 172, 220}, {180, 180, 224}, + {192, 192, 228}, {200, 200, 232}, {212, 212, 236}, {220, 220, 240}, + {232, 232, 244}, {240, 240, 248}, {252, 252, 252}, {252, 240, 244}, + {252, 224, 232}, {252, 208, 224}, {252, 192, 212}, {252, 176, 204}, + {252, 160, 192}, {252, 144, 184}, {252, 128, 172}, {252, 112, 164}, + {252, 96, 152}, {252, 80, 144}, {252, 64, 132}, {252, 48, 124}, + {252, 32, 112}, {252, 16, 104}, {252, 000, 92}, {236, 000, 88}, + {228, 000, 88}, {216, 4, 84}, {204, 4, 80}, {192, 8, 76}, + {180, 8, 76}, {168, 12, 72}, {156, 16, 68}, {144, 16, 64}, + {132, 20, 60}, {124, 20, 60}, {112, 24, 56}, {100, 24, 52}, + {88, 28, 48}, {76, 32, 44}, {64, 32, 44}, {52, 36, 40}, + {40, 36, 36}, {28, 40, 32}, {16, 44, 28}, {20, 52, 32}, + {24, 60, 36}, {28, 68, 44}, {32, 76, 48}, {36, 88, 56}, + {40, 96, 60}, {44, 104, 64}, {48, 112, 72}, {52, 120, 76}, + {56, 132, 84}, {48, 136, 84}, {40, 144, 80}, {52, 148, 88}, + {68, 156, 100}, {80, 164, 112}, {96, 168, 124}, {108, 176, 136}, + {124, 184, 144}, {136, 192, 156}, {152, 196, 168}, {164, 204, 180}, + {180, 212, 192}, {192, 220, 200}, {208, 224, 212}, {220, 232, 224}, + {236, 240, 236}, {252, 248, 248}, {252, 252, 252}, {252, 252, 240}, + {252, 252, 228}, {252, 252, 216}, {248, 248, 204}, {248, 248, 192}, + {248, 248, 180}, {248, 248, 164}, {244, 244, 152}, {244, 244, 140}, + {244, 244, 128}, {244, 244, 116}, {240, 240, 104}, {240, 240, 92}, + {240, 240, 76}, {240, 240, 64}, {236, 236, 52}, {236, 236, 40}, + {236, 236, 28}, {236, 236, 16}, {232, 232, 0}, {232, 232, 12}, + {232, 232, 28}, {232, 232, 40}, {236, 236, 56}, {236, 236, 68}, + {236, 236, 84}, {236, 236, 96}, {240, 240, 112}, {240, 240, 124}, + {240, 240, 140}, {244, 244, 152}, {244, 244, 168}, {244, 244, 180}, + {244, 244, 196}, {248, 248, 208}, {248, 248, 224}, {248, 248, 236}, + {252, 252, 252}, {248, 248, 248}, {240, 240, 240}, {232, 232, 232}} + +// BMPImageWriter implements image.Writer interface, it writes BMP format +type BMPImageWriter struct{} + +// WriteImage writes an image represented by byte slice into file with BMP format. +func (writer BMPImageWriter) WriteImage(filename string, img image.Image) error { + file, err := os.Create(filename) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + bounds := img.Bounds() + width := bounds.Max.X - bounds.Min.X + height := bounds.Max.Y - bounds.Min.Y + + bmpHeader := []byte{ + /* BMP header structure: */ + 0x42, 0x4d, /* magic number */ + 0x46, 0x00, 0x00, 0x00, /* size of header=70 bytes */ + 0x00, 0x00, /* unused */ + 0x00, 0x00, /* unused */ + 0x36, 0x00, 0x00, 0x00, /* 54 bytes - offset to data */ + 0x28, 0x00, 0x00, 0x00, /* 40 bytes - bytes in DIB header */ + 0x00, 0x00, 0x00, 0x00, /* width of bitmap */ + 0x00, 0x00, 0x00, 0x00, /* height of bitmap */ + 0x01, 0x0, /* 1 pixel plane */ + 0x18, 0x00, /* 24 bpp */ + 0x00, 0x00, 0x00, 0x00, /* no compression */ + 0x00, 0x00, 0x00, 0x00, /* size of pixel array */ + 0x13, 0x0b, 0x00, 0x00, /* 2835 pixels/meter */ + 0x13, 0x0b, 0x00, 0x00, /* 2835 pixels/meter */ + 0x00, 0x00, 0x00, 0x00, /* color palette */ + 0x00, 0x00, 0x00, 0x00, /* important colors */ + } + + bmpHeader[18] = byte(width & 0xff) + bmpHeader[19] = byte(width >> 8) + bmpHeader[20] = byte(width >> 16) + bmpHeader[21] = byte(width >> 24) + bmpHeader[22] = byte(height & 0xff) + bmpHeader[23] = byte(height >> 8) + bmpHeader[24] = byte(height >> 16) + bmpHeader[25] = byte(height >> 24) + + file.Write(bmpHeader) + + for y := range height { + for x := range width { + r, g, b, _ := img.At(x, y).RGBA() + // swap RGB + color := []byte{byte(b >> 8), byte(g >> 8), byte(r >> 8)} + file.Write(color) + } + } + // no error + return nil +} + +// NewBMPImageWriter is a constructor for BMP image writer +func NewBMPImageWriter() BMPImageWriter { + return BMPImageWriter{} +} + +// PPMImageWriter implements image.Writer interface, it writes into selected PPM format +type PPMImageWriter struct{} + +// WritePPMImage writes an image represented by standard image.Image structure into file with PPM format. +func (writer PPMImageWriter) WriteImage(filename string, img image.Image) error { + file, err := os.Create(filename) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + bounds := img.Bounds() + width := bounds.Max.X - bounds.Min.X + height := bounds.Max.Y - bounds.Min.Y + + fmt.Fprintln(file, "P3") + fmt.Fprintf(file, "%d %d\n", width, height) + fmt.Fprintln(file, "255") + + for y := range height { + for x := range width { + r, g, b, _ := img.At(x, y).RGBA() + fmt.Fprintf(file, "%d %d %d\n", r>>8, g>>8, b>>8) + } + } + return nil +} + +// NewPPMImageWriter is a constructor for PPM image writer +func NewPPMImageWriter() PPMImageWriter { + return PPMImageWriter{} +} + +// TGAImageWriter implements image.Writer interface, it writes TGA format +type TGAImageWriter struct{} + +// WriteTGAImage writes an image represented by byte slice into file with TGA format. +func (writer TGAImageWriter) WriteImage(filename string, img image.Image) error { + file, err := os.Create(filename) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + bounds := img.Bounds() + width := bounds.Max.X - bounds.Min.X + height := bounds.Max.Y - bounds.Min.Y + + tgaHeader := []byte{ + /* TGA header structure: */ + 0x00, /* without image ID */ + 0x00, /* color map type: without palette */ + 0x02, /* uncompressed true color image */ + 0x00, 0x00, /* start of color palette (it is not used) */ + 0x00, 0x00, /* length of color palette (it is not used) */ + 0x00, /* bits per palette entry */ + 0x00, 0x00, 0x00, 0x00, /* image coordinates */ + 0x00, 0x00, /* image width */ + 0x00, 0x00, /* image height */ + 0x18, /* bits per pixel = 24 */ + 0x20, /* picture orientation: top-left origin */ + } + + /* image size is specified in TGA header */ + tgaHeader[12] = byte(width & 0xff) + tgaHeader[13] = byte(width >> 8) + tgaHeader[14] = byte(height & 0xff) + tgaHeader[15] = byte(height >> 8) + + file.Write(tgaHeader) + + for y := range height { + for x := range width { + r, g, b, _ := img.At(x, y).RGBA() + // swap RGB + color := []byte{byte(b >> 8), byte(g >> 8), byte(r >> 8)} + file.Write(color) + } + } + // no error + return nil +} + +// NewTGAImageWriter is a constructor for TGA image writer +func NewTGAImageWriter() TGAImageWriter { + return TGAImageWriter{} +} + +type Float32Node struct { + Value float32 + Left *Float32Node + Right *Float32Node +} + +type Float32BinaryTree struct { + Root *Float32Node +} + +func (bt *Float32BinaryTree) Insert(value float32) { + node := &Float32Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertFloat32Node(bt.Root, node) + } +} + +func insertFloat32Node(node, newNode *Float32Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertFloat32Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertFloat32Node(node.Right, newNode) + } + } +} + +func printFloat32Tree(node *Float32Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printFloat32Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printFloat32Tree(node.Right, level) + } +} + +/* +func main() { +var bt Float32BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Float64Node struct { + Value float64 + Left *Float64Node + Right *Float64Node +} + +type Float64BinaryTree struct { + Root *Float64Node +} + +func (bt *Float64BinaryTree) Insert(value float64) { + node := &Float64Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertFloat64Node(bt.Root, node) + } +} + +func insertFloat64Node(node, newNode *Float64Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertFloat64Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertFloat64Node(node.Right, newNode) + } + } +} + +func printFloat64Tree(node *Float64Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printFloat64Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printFloat64Tree(node.Right, level) + } +} + +/* +func main() { +var bt Float64BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type IntNode struct { + Value int + Left *IntNode + Right *IntNode +} + +type IntBinaryTree struct { + Root *IntNode +} + +func (bt *IntBinaryTree) Insert(value int) { + node := &IntNode{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertIntNode(bt.Root, node) + } +} + +func insertIntNode(node, newNode *IntNode) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertIntNode(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertIntNode(node.Right, newNode) + } + } +} + +func printIntTree(node *IntNode, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printIntTree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printIntTree(node.Right, level) + } +} + +/* +func main() { +var bt IntBinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Int16Node struct { + Value int16 + Left *Int16Node + Right *Int16Node +} + +type Int16BinaryTree struct { + Root *Int16Node +} + +func (bt *Int16BinaryTree) Insert(value int16) { + node := &Int16Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertInt16Node(bt.Root, node) + } +} + +func insertInt16Node(node, newNode *Int16Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertInt16Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertInt16Node(node.Right, newNode) + } + } +} + +func printInt16Tree(node *Int16Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printInt16Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printInt16Tree(node.Right, level) + } +} + +/* +func main() { +var bt Int16BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Int32Node struct { + Value int32 + Left *Int32Node + Right *Int32Node +} + +type Int32BinaryTree struct { + Root *Int32Node +} + +func (bt *Int32BinaryTree) Insert(value int32) { + node := &Int32Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertInt32Node(bt.Root, node) + } +} + +func insertInt32Node(node, newNode *Int32Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertInt32Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertInt32Node(node.Right, newNode) + } + } +} + +func printInt32Tree(node *Int32Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printInt32Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printInt32Tree(node.Right, level) + } +} + +/* +func main() { +var bt Int32BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Int64Node struct { + Value int64 + Left *Int64Node + Right *Int64Node +} + +type Int64BinaryTree struct { + Root *Int64Node +} + +func (bt *Int64BinaryTree) Insert(value int64) { + node := &Int64Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertInt64Node(bt.Root, node) + } +} + +func insertInt64Node(node, newNode *Int64Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertInt64Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertInt64Node(node.Right, newNode) + } + } +} + +func printInt64Tree(node *Int64Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printInt64Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printInt64Tree(node.Right, level) + } +} + +/* +func main() { +var bt Int64BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Int8Node struct { + Value int8 + Left *Int8Node + Right *Int8Node +} + +type Int8BinaryTree struct { + Root *Int8Node +} + +func (bt *Int8BinaryTree) Insert(value int8) { + node := &Int8Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertInt8Node(bt.Root, node) + } +} + +func insertInt8Node(node, newNode *Int8Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertInt8Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertInt8Node(node.Right, newNode) + } + } +} + +func printInt8Tree(node *Int8Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printInt8Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printInt8Tree(node.Right, level) + } +} + +/* +func main() { +var bt Int8BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type UintNode struct { + Value uint + Left *UintNode + Right *UintNode +} + +type UintBinaryTree struct { + Root *UintNode +} + +func (bt *UintBinaryTree) Insert(value uint) { + node := &UintNode{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertUintNode(bt.Root, node) + } +} + +func insertUintNode(node, newNode *UintNode) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertUintNode(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertUintNode(node.Right, newNode) + } + } +} + +func printUintTree(node *UintNode, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printUintTree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printUintTree(node.Right, level) + } +} + +/* +func main() { +var bt UintBinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Uint16Node struct { + Value uint16 + Left *Uint16Node + Right *Uint16Node +} + +type Uint16BinaryTree struct { + Root *Uint16Node +} + +func (bt *Uint16BinaryTree) Insert(value uint16) { + node := &Uint16Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertUint16Node(bt.Root, node) + } +} + +func insertUint16Node(node, newNode *Uint16Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertUint16Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertUint16Node(node.Right, newNode) + } + } +} + +func printUint16Tree(node *Uint16Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printUint16Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printUint16Tree(node.Right, level) + } +} + +/* +func main() { +var bt Uint16BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Uint32Node struct { + Value uint32 + Left *Uint32Node + Right *Uint32Node +} + +type Uint32BinaryTree struct { + Root *Uint32Node +} + +func (bt *Uint32BinaryTree) Insert(value uint32) { + node := &Uint32Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertUint32Node(bt.Root, node) + } +} + +func insertUint32Node(node, newNode *Uint32Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertUint32Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertUint32Node(node.Right, newNode) + } + } +} + +func printUint32Tree(node *Uint32Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printUint32Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printUint32Tree(node.Right, level) + } +} + +/* +func main() { +var bt Uint32BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Uint64Node struct { + Value uint64 + Left *Uint64Node + Right *Uint64Node +} + +type Uint64BinaryTree struct { + Root *Uint64Node +} + +func (bt *Uint64BinaryTree) Insert(value uint64) { + node := &Uint64Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertUint64Node(bt.Root, node) + } +} + +func insertUint64Node(node, newNode *Uint64Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertUint64Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertUint64Node(node.Right, newNode) + } + } +} + +func printUint64Tree(node *Uint64Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printUint64Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printUint64Tree(node.Right, level) + } +} + +/* +func main() { +var bt Uint64BinaryTree ; + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + printTree(bt.Root, 0) +} +*/ + +type Uint8Node struct { + Value uint8 + Left *Uint8Node + Right *Uint8Node +} + +type Uint8BinaryTree struct { + Root *Uint8Node +} + +func (bt *Uint8BinaryTree) Insert(value uint8) { + node := &Uint8Node{value, nil, nil} + if bt.Root == nil { + bt.Root = node + } else { + insertUint8Node(bt.Root, node) + } +} + +func insertUint8Node(node, newNode *Uint8Node) { + if newNode.Value < node.Value { + if node.Left == nil { + node.Left = newNode + } else { + insertUint8Node(node.Left, newNode) + } + } else { + if node.Right == nil { + node.Right = newNode + } else { + insertUint8Node(node.Right, newNode) + } + } +} + +func printUint8Tree(node *Uint8Node, level int) { + if node != nil { + format := "" + for i := 0; i < level; i++ { + format += " " + } + format += "---[ " + level++ + printUint8Tree(node.Left, level) + fmt.Printf(format+"%v\n", node.Value) + printUint8Tree(node.Right, level) + } +} + +// main +func main() { + var bt Uint8BinaryTree + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + + bt.Insert(8) + + bt.Insert(3) + bt.Insert(11) + + bt.Insert(1) + bt.Insert(0) + bt.Insert(2) + + bt.Insert(5) + bt.Insert(4) + bt.Insert(6) + + bt.Insert(9) + bt.Insert(8) + bt.Insert(10) + + bt.Insert(13) + bt.Insert(12) + bt.Insert(14) + printUint8Tree(bt.Root, 0) +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A1CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A1CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A1CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A1CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A1CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A1CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A1CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A1CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A1CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A1CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A1CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A1CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A1CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A1CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A1CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A1CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A1CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A1CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A1CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A1CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A1CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A1CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A1CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A1CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A1CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A2CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A2CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A2CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A2CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A2CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A2CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A2CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A2CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A2CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A2CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A2CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A2CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A2CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A2CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A2CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A2CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A2CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A2CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A2CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A2CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A2CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A2CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A2CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A2CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A2CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A3CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A3CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A3CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A3CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A3CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A3CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A3CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A3CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A3CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A3CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A3CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A3CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A3CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A3CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A3CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A3CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A3CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A3CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A3CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A3CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A3CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A3CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A3CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A3CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A3CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A4CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A4CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A4CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A4CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A4CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A4CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A4CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A4CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A4CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A4CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A4CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A4CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A4CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A4CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A4CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A4CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A4CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A4CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A4CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A4CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A4CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A4CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A4CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A4CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A4CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A5CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A5CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A5CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A5CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A5CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A5CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A5CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A5CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A5CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A5CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A5CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A5CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A5CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A5CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A5CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A5CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A5CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A5CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A5CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A5CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A5CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A5CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A5CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A5CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A5CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A6CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A6CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A6CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A6CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A6CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A6CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A6CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A6CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A6CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A6CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A6CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A6CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A6CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A6CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A6CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A6CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A6CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A6CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A6CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A6CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A6CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A6CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A6CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A6CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A6CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A7CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A7CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A7CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A7CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A7CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A7CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A7CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A7CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A7CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A7CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A7CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A7CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A7CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A7CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A7CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A7CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A7CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A7CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A7CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A7CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A7CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A7CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A7CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A7CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A7CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func A8CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func A8CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func A8CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func A8CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func A8CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func A8CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func A8CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func A8CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func A8CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func A8CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func A8CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func A8CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func A8CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A8CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func A8CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func A8CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func A8CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func A8CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func A8CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarM calculates Manowar Mandelbrot-like set +func A8CalcManowarM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z = c + var z1 = c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcManowarJ calculates Manowar Julia-like set +func A8CalcManowarJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var z1 = z + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z2 := z*z + z1 + c + z1 = z + z = z2 + i++ + } + i *= 3 + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcNewton calculates Newton fractal +func A8CalcNewton( + params FractalParameter, + image Image) { + + const Epsilon = 0.001 + + var RootX1 = 1.0 + var RootY1 = 0.0 + + var RootX2 = -0.5 + var RootY2 = math.Sqrt(3) / 2 + + var RootX3 = -0.5 + var RootY3 = -math.Sqrt(3) / 2 + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + zx := zx0 + zy := zy0 + i := uint(0) + + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := 2.0/3.0*zx + (zx2-zy2)/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zyn := 2.0/3.0*zy - 2.0*zx*zy/(3.0*(zx2*zx2+zy2*zy2+2.0*zx2*zy2)) + zx = zxn + zy = zyn + if math.Hypot(zx-RootX1, zy-RootY1) < Epsilon { + break + } + if math.Hypot(zx-RootX2, zy-RootY2) < Epsilon { + i += 128 + break + } + if math.Hypot(zx-RootX3, zy-RootY3) < Epsilon { + i += 192 + break + } + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixJ calculates Phoenix Julia-like set +func A8CalcPhoenixJ( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcPhoenixM calculates Phoenix Mandelbrot-like set +func A8CalcPhoenixM( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var ynx = 0.0 + var yny = 0.0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + zxn := zx2 - zy2 + cx + cy*ynx + zyn := 2.0*zx*zy + cy*yny + if zx2+zy2 > float64(params.Bailout) { + break + } + ynx = zx + yny = zy + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcZPowerMandelbrot calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func A8CalcZPowerMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + + for y := uint(0); y < image.Resolution.Height; y++ { + + var cx float64 = params.Xmin + + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(cx, cy) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = cmplx.Pow(z, z) + z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// +// finito +// diff --git a/tests/benchmarks/data/go_1000_lines.go b/tests/benchmarks/data/go_1000_lines.go new file mode 100644 index 000000000..fe8e54516 --- /dev/null +++ b/tests/benchmarks/data/go_1000_lines.go @@ -0,0 +1,1000 @@ +// 1000 lines of Go + +package main + +import ( + "errors" + "fmt" + "image" + "log" + "math" + "math/cmplx" + "os" +) + +// IImage is representation of raster image consisting of IPixels +type IImage [][]IPixel + +// NewIImage constructs new instance of ZImage +func NewIImage(resolution Resolution) IImage { + iimage := make([][]IPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + iimage[y] = make([]IPixel, resolution.Width) + } + return iimage +} + +// IPixel is a representation of pixel as one unsigned integer value +type IPixel uint64 + +// RImage is representation of raster image consisting of RPixels +type RImage [][]RPixel + +// NewRImage constructs new instance of RImage +func NewRImage(resolution Resolution) RImage { + rimage := make([][]RPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + rimage[y] = make([]RPixel, resolution.Width) + } + return rimage +} + +// calcuate minimum and maximum pixel value +func (image *RImage) minMax(width, height uint) (float64, float64) { + min := float64(math.Inf(1)) + max := float64(math.Inf(-1)) + + for j := range height { + for i := range width { + z := float64((*image)[j][i]) + if max < z { + max = z + } + if min > z { + min = z + } + } + } + return min, max +} + +// RPixel is a representation of pixel as one real value +type RPixel float64 + +// ZImage is representation of raster image consisting of ZPixels +type ZImage [][]ZPixel + +// NewZImage constructs new instance of ZImage +func NewZImage(resolution Resolution) ZImage { + zimage := make([][]ZPixel, resolution.Height) + for y := uint(0); y < resolution.Height; y++ { + zimage[y] = make([]ZPixel, resolution.Width) + } + return zimage +} + +// ZPixel is a representation of pixel in complex plane +type ZPixel complex128 + +// Palette structure +type Palette struct { + Name string `toml:"name"` + Shift int `toml:"shift"` + Slope int `toml:"slope"` +} + +// FractalParameter structure contains information about all fractal parameters. +type FractalParameter struct { + Name string `toml:"name"` + Type string `toml:"type"` + Class string `toml:"class"` + Cx0 float64 `toml:"cx0"` + Cy0 float64 `toml:"cy0"` + Palette Palette `toml:"palette"` + Maxiter uint `toml:"maxiter"` + Bailout uint `toml:"bailout"` + Function1 string `toml:"function1"` + Function2 string `toml:"function2"` + Xmin float64 `toml:"xmin"` + Ymin float64 `toml:"ymin"` + Xmax float64 `toml:"xmax"` + Ymax float64 `toml:"ymax"` + A float64 `toml:"A"` + B float64 `toml:"B"` + C float64 `toml:"C"` + D float64 `toml:"D"` + Scale float64 `toml:"scale"` + XOffset float64 `toml:"x_offset"` + YOffset float64 `toml:"y_offset"` +} + +// FractalParameter2 structure contains information about all fractal parameters. +type FractalParameter2 struct { + Name string `toml:"name"` + Type string `toml:"type"` + Class string `toml:"class"` + Cx0 float64 `toml:"cx0"` + Cy0 float64 `toml:"cy0"` + Palette Palette `toml:"palette"` + Maxiter uint `toml:"maxiter"` + Bailout uint `toml:"bailout"` + Function1 string `toml:"function1"` + Function2 string `toml:"function2"` + Xmin float64 `toml:"xmin"` + Ymin float64 `toml:"ymin"` + Xmax float64 `toml:"xmax"` + Ymax float64 `toml:"ymax"` + A float64 `toml:"A"` + B float64 `toml:"B"` + C float64 `toml:"C"` + D float64 `toml:"D"` + Scale float64 `toml:"scale"` + XOffset float64 `toml:"x_offset"` + YOffset float64 `toml:"y_offset"` +} + +// Sequence of fractal parameters +type FractalParameters struct { + Parameters []FractalParameter `toml:"fractal"` +} + +// LoadFractalParameters function reads fractal parameters from external text file +func LoadFractalParameters(filename string) (map[string]FractalParameter, error) { + var parameters FractalParameters + asMap := map[string]FractalParameter{} + + _, err := os.Stat(filename) + + if os.IsNotExist(err) { + return asMap, errors.New("Parameter file does not exist.") + } + if err != nil { + log.Fatal(err) + return asMap, err + } + + for _, parameter := range parameters.Parameters { + if _, exists := asMap[parameter.Name]; exists { + return asMap, fmt.Errorf( + "duplicate parameter name %q in %s", + parameter.Name, filename) + } + if parameter.Palette.Name == "" { + parameter.Palette.Slope = 1 + } + asMap[parameter.Name] = parameter + } + return asMap, nil +} + +// Resolution describes the image dimensions in pixels. +type Resolution struct { + Width uint + Height uint +} + +// NewResolution constructs a Resolution with the given width and height. +// Width and height are expected to be positive numbers. +func NewResolution(width, height uint) (Resolution, error) { + // check for zero dimensions + if width == 0 { + return Resolution{}, errors.New("width cannot be zero") + } + + // check for zero dimensions + if height == 0 { + return Resolution{}, errors.New("height cannot be zero") + } + + // Check for reasonable maximum dimensions to prevent memory issues + const maxDimension = 65535 // 2^16 - 1, reasonable for image processing + + if width > maxDimension { + return Resolution{}, fmt.Errorf("width %d exceeds maximum allowed dimension %d", width, maxDimension) + } + if height > maxDimension { + return Resolution{}, fmt.Errorf("height %d exceeds maximum allowed dimension %d", height, maxDimension) + } + + return Resolution{ + Width: width, + Height: height, + }, nil +} + +func getSteps( + params FractalParameter, + image Image) (float64, float64) { + stepX := float64(params.Xmax-params.Xmin) / float64(image.Resolution.Width) + stepY := float64(params.Ymax-params.Ymin) / float64(image.Resolution.Height) + return stepX, stepY +} + +func calcIndex(params FractalParameter, i uint) uint { + index := params.Palette.Shift + int(i)*params.Palette.Slope + if index < 0 { + return 0 + } + return uint(index) +} + +// Image structure +type Image struct { + Resolution Resolution + Z ZImage + R RImage + I IImage + RGBA *image.NRGBA +} + +// Image constructor +func New(width uint, height uint) (Image, error) { + resolution, err := NewResolution(width, height) + + if err != nil { + return Image{}, err + } + + return Image{ + Resolution: resolution, + Z: NewZImage(resolution), + R: NewRImage(resolution), + I: NewIImage(resolution), + }, nil +} + +// Palette represents color palette used to map fractal calculation result +// (number of iterations, for example) into RGB or RGBA color. Palettes have +// usually 256 records, but it can be more or less. +type RGBPalette [][]byte + +func (i *Image) ApplyPalette(palette RGBPalette) { + r := i.Resolution + i.RGBA = image.NewNRGBA(image.Rect(0, 0, int(r.Width), int(r.Height))) + + for y := 0; y < int(r.Height); y++ { + offset := i.RGBA.PixOffset(0, y) + for x := uint(0); x < r.Width; x++ { + index := byte(i.I[y][x]) + i.RGBA.Pix[offset] = palette[index][0] + offset++ + i.RGBA.Pix[offset] = palette[index][1] + offset++ + i.RGBA.Pix[offset] = palette[index][2] + offset++ + i.RGBA.Pix[offset] = 0xff + offset++ + } + } +} + +func (image *Image) RImage2IImage() { + r := image.Resolution + width := r.Width + height := r.Height + + min, max := image.R.minMax(width, height) + k := 255.0 / (max - min) + + for y := uint(0); y < height; y++ { + for x := uint(0); x < width; x++ { + f := float64(image.R[y][x]) + f -= min + f *= k + if f > 255.0 { + f = 255 + } + i := int(f) & 255 + image.Z[y][x] = ZPixel(complex(float32(x), float32(y))) + image.I[y][x] = IPixel(i) + } + } +} + +func (image *Image) RImage2IImageWithFactor(maxFactor float64) { + r := image.Resolution + width := r.Width + height := r.Height + + min, max := image.R.minMax(width, height) + max *= maxFactor + k := 255.0 / (max - min) + + for y := uint(0); y < height; y++ { + for x := uint(0); x < width; x++ { + f := float64(image.R[y][x]) + f -= min + f *= k + if f > 255.0 { + f = 255 + } + i := int(f) & 255 + image.Z[y][x] = ZPixel(complex(float32(x), float32(y))) + image.I[y][x] = IPixel(i) + } + } +} + +// CalcBarnsleyJuliaJ1 calculates Barnsley J1 Mandelbrot-like set +func CalcBarnsleyJuliaJ1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyJuliaJ2 calculates Barnsley J2 Mandelbrot-like set +func CalcBarnsleyJuliaJ2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcBarnsleyMandelbrotM1 calculates Barnsley M1 Mandelbrot-like set +func CalcBarnsleyMandelbrotM1( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM2 calculates Barnsley M2 Mandelbrot-like set +func CalcBarnsleyMandelbrotM2( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx*cy+zy*cx >= 0 { + zxn = zx*cx - zy*cy - cx + zyn = zx*cy + zy*cx - cy + } else { + zxn = zx*cx - zy*cy + cx + zyn = zx*cy + zy*cx + cy + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcBarnsleyMandelbrotM3 calculates Barnsley M3 Mandelbrot-like set +func CalcBarnsleyMandelbrotM3( + params FractalParameter, + image Image) { + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = cx + var zy float64 = cy + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + if zx > 0 { + zxn = zx2 - zy2 - 1 + zyn = 2.0 * zx * zy + } else { + zxn = zx2 - zy2 - 1 + cx*zx + zyn = 2.0*zx*zy + cy*zx + } + zx = zxn + zy = zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcJuliaFn calculates Julia set into the provided ZPixels +func CalcJuliaFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + cx := params.Cx0 + cy := params.Cy0 + var c complex128 = complex(cx, cy) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates classic Julia fractal +func CalcJulia( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + params.Cy0 + zx = zx2 - zy2 + params.Cx0 + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^3+c +func CalcJuliaZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcJulia calculates Julia fractal for Z=Z^4+c +func CalcJuliaZ4( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var zy0 float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(params.Cx0, params.Cy0) + var z complex128 = complex(zx0, zy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += stepX + } + zy0 += stepY + } +} + +// CalcMandelLambda calculates Mandelbrot variant of Lambda fractal +func CalcMandelLambda( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = c * z * (1 - z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMagnet calculates Magnet Mandelbrot-like set +func CalcMagnet( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + var cy float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 4.0 / float64(image.Resolution.Width) + } + cy += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMagnet calculates Magnet Julia-like set +func CalcMagnetJulia( + params FractalParameter, + image Image) { + const MIN_VALUE = 1.0 - 100 + + cx := params.Cx0 + cy := params.Cy0 + var zy0 float64 = -2.0 + for y := uint(0); y < image.Resolution.Height; y++ { + var zx0 float64 = -2.0 + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = zx0 + var zy float64 = zy0 + var i uint + for i < params.Maxiter { + var zxn float64 + var zyn float64 + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > 100.0 { + break + } + if ((zx-1.0)*(zx-1.0) + zy*zy) < 0.001 { + break + } + tzx := zx2 - zy2 + cx - 1 + tzy := 2.0*zx*zy + cy + bzx := 2.0*zx + cx - 2 + bzy := 2.0*zy + cy + div := bzx*bzx + bzy*bzy + if div < MIN_VALUE { + break + } + zxn = (tzx*bzx + tzy*bzy) / div + zyn = (tzy*bzx - tzx*bzy) / div + zx = (zxn + zyn) * (zxn - zyn) + zy = 2.0 * zxn * zyn + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + zx0 += 4.0 / float64(image.Resolution.Width) + } + zy0 += 4.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrot calculates Mandelbrot set into the provided ZPixels +func CalcMandelbrot( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var zx float64 = params.Cx0 + var zy float64 = params.Cy0 + var i uint + for i < params.Maxiter { + zx2 := zx * zx + zy2 := zy * zy + if zx2+zy2 > float64(params.Bailout) { + break + } + zy = 2.0*zx*zy + cy + zx = zx2 - zy2 + cx + i++ + } + image.Z[y][x] = ZPixel(complex(zx, zy)) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotComplex calculates Mandelbrot set into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotComplex( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + if cmplx.Abs(z) > float64(params.Bailout) { + break + } + z = z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ2pZ calculates Mandelbrot set z=z^2+z+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ2pZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z + z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ2mZ calculates Mandelbrot set z=z^2-z+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ2mZ( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z - z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotZ3 calculates Mandelbrot set z=z^3+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ3( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += stepX + } + cy += stepY + } +} + +// CalcMandelbrotZ4 calculates Mandelbrot set z=z^4+c into the provided ZPixels +// Calculations use complex numbers +func CalcMandelbrotZ4( + params FractalParameter, + image Image) { + + var cy float64 = -1.5 + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = -1.5 + for x := uint(0); x < image.Resolution.Width; x++ { + var c complex128 = complex(cx, cy) + var z complex128 = complex(params.Cx0, params.Cy0) + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > 4.0 { + break + } + z = z*z*z*z + c + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(calcIndex(params, i)) + cx += 3.0 / float64(image.Resolution.Width) + } + cy += 3.0 / float64(image.Resolution.Height) + } +} + +// CalcMandelbrotFn calculates Mandelbrot set into the provided ZPixels +func CalcMandelbrotFn( + params FractalParameter, + image Image) { + + stepX, stepY := getSteps(params, image) + + var cy float64 = params.Ymin + for y := uint(0); y < image.Resolution.Height; y++ { + var cx float64 = params.Xmin + for x := uint(0); x < image.Resolution.Width; x++ { + c := complex(cx, cy) + z := c + var i uint + for i < params.Maxiter { + zx := real(z) + zy := imag(z) + if zx*zx+zy*zy > float64(params.Bailout) { + break + } + z = c * cmplx.Sin(z) + i++ + } + image.Z[y][x] = ZPixel(z) + image.I[y][x] = IPixel(i) + cx += stepX + } + cy += stepY + } +} + +// main function +func main() { + fmt.Println("1000 lines of Go") +} + +// +// finito +// diff --git a/tests/benchmarks/data/go_100_lines.go b/tests/benchmarks/data/go_100_lines.go new file mode 100644 index 000000000..2a94ce6f2 --- /dev/null +++ b/tests/benchmarks/data/go_100_lines.go @@ -0,0 +1,100 @@ +// 100 lines of go +package main + +import ( + "errors" + "fmt" + "log" + "os" +) + +// FractalParameter structure contains information about all fractal parameters. +type FractalParameter struct { + Name string `toml:"name"` + Type string `toml:"type"` + Class string `toml:"class"` + Cx0 float64 `toml:"cx0"` + Cy0 float64 `toml:"cy0"` + Palette Palette `toml:"palette"` + Maxiter uint `toml:"maxiter"` + Bailout uint `toml:"bailout"` + Function1 string `toml:"function1"` + Function2 string `toml:"function2"` + Xmin float64 `toml:"xmin"` + Ymin float64 `toml:"ymin"` + Xmax float64 `toml:"xmax"` + Ymax float64 `toml:"ymax"` + A float64 `toml:"A"` + Scale float64 `toml:"scale"` + XOffset float64 `toml:"x_offset"` + YOffset float64 `toml:"y_offset"` +} + +// Sequence of fractal parameters +type FractalParameters struct { + Parameters []FractalParameter `toml:"fractal"` +} + +// LoadFractalParameters function reads fractal parameters from external text file +func LoadFractalParameters(filename string) (map[string]FractalParameter, error) { + var parameters FractalParameters + asMap := map[string]FractalParameter{} + + _, err := os.Stat(filename) + + if os.IsNotExist(err) { + return asMap, errors.New("Parameter file does not exist.") + } + if err != nil { + log.Fatal(err) + return asMap, err + } + + for _, parameter := range parameters.Parameters { + if _, exists := asMap[parameter.Name]; exists { + return asMap, fmt.Errorf( + "duplicate parameter name %q in %s", + parameter.Name, filename) + } + if parameter.Palette.Name == "" { + parameter.Palette.Slope = 1 + } + asMap[parameter.Name] = parameter + } + return asMap, nil +} + +// Resolution describes the image dimensions in pixels. +type Resolution struct { + Width uint + Height uint +} + +// NewResolution constructs a Resolution with the given width and height. +// Width and height are expected to be positive numbers. +func NewResolution(width, height uint) (Resolution, error) { + // Check for zero dimensions + if width == 0 { + return Resolution{}, errors.New("width cannot be zero") + } + if height == 0 { + return Resolution{}, errors.New("height cannot be zero") + } + + const maxDimension = 65535 // 2^16 - 1, reasonable for image processing + if width > maxDimension { + return Resolution{}, fmt.Errorf("width %d exceeds maximum allowed dimension %d", width, maxDimension) + } + if height > maxDimension { + return Resolution{}, fmt.Errorf("height %d exceeds maximum allowed dimension %d", height, maxDimension) + } + + return Resolution{ + Width: width, + Height: height, + }, nil +} + +func main() { + fmt.Println("100 lines") +} diff --git a/tests/benchmarks/data/go_10_lines.go b/tests/benchmarks/data/go_10_lines.go new file mode 100644 index 000000000..a776b41cd --- /dev/null +++ b/tests/benchmarks/data/go_10_lines.go @@ -0,0 +1,10 @@ +// 10 lines of go +package main + +import "fmt" + +func main() { + fmt.Println("10 lines") +} + +// finito diff --git a/tests/benchmarks/test_token_estimator.py b/tests/benchmarks/test_token_estimator.py index 9a79edcae..4a81a5212 100644 --- a/tests/benchmarks/test_token_estimator.py +++ b/tests/benchmarks/test_token_estimator.py @@ -435,3 +435,59 @@ def test_javascript_source_10000_lines(benchmark: BenchmarkFixture) -> None: None """ benchmark_file_tokenization(benchmark, "js_10000_lines.js") + + +def test_go_source_10_lines(benchmark: BenchmarkFixture) -> None: + """Test tokenizing Go source code containing just 10 lines. + + Parameters: + ---------- + benchmark (BenchmarkFixture): pytest-benchmark fixture. + + Returns: + ------- + None + """ + benchmark_file_tokenization(benchmark, "go_10_lines.go") + + +def test_go_source_100_lines(benchmark: BenchmarkFixture) -> None: + """Test tokenizing Go source code containing just 100 lines. + + Parameters: + ---------- + benchmark (BenchmarkFixture): pytest-benchmark fixture. + + Returns: + ------- + None + """ + benchmark_file_tokenization(benchmark, "go_100_lines.go") + + +def test_go_source_1000_lines(benchmark: BenchmarkFixture) -> None: + """Test tokenizing Go source code containing just 1000 lines. + + Parameters: + ---------- + benchmark (BenchmarkFixture): pytest-benchmark fixture. + + Returns: + ------- + None + """ + benchmark_file_tokenization(benchmark, "go_1000_lines.go") + + +def test_go_source_10000_lines(benchmark: BenchmarkFixture) -> None: + """Test tokenizing Go source code containing just 10000 lines. + + Parameters: + ---------- + benchmark (BenchmarkFixture): pytest-benchmark fixture. + + Returns: + ------- + None + """ + benchmark_file_tokenization(benchmark, "go_10000_lines.go") diff --git a/tests/configuration/benchmarks-postgres.yaml b/tests/configuration/benchmarks-postgres.yaml index ab94a7d59..718fc785c 100644 --- a/tests/configuration/benchmarks-postgres.yaml +++ b/tests/configuration/benchmarks-postgres.yaml @@ -9,12 +9,12 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: - url: http://llama-stack:8321 + # library_client_config_path: + url: http://ogx:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/configuration/benchmarks-sqlite.yaml b/tests/configuration/benchmarks-sqlite.yaml index 63ea9d925..1284c25c1 100644 --- a/tests/configuration/benchmarks-sqlite.yaml +++ b/tests/configuration/benchmarks-sqlite.yaml @@ -9,12 +9,12 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: - url: http://llama-stack:8321 + # library_client_config_path: + url: http://ogx:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/configuration/lightspeed-stack-proper-name.yaml b/tests/configuration/lightspeed-stack-proper-name.yaml index 60add8957..5ede2acf5 100644 --- a/tests/configuration/lightspeed-stack-proper-name.yaml +++ b/tests/configuration/lightspeed-stack-proper-name.yaml @@ -22,11 +22,11 @@ service: - baz_header ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://localhost:8321 api_key: xyzzy user_data_collection: diff --git a/tests/configuration/lightspeed-stack.yaml b/tests/configuration/lightspeed-stack.yaml index b548c96fe..416647300 100644 --- a/tests/configuration/lightspeed-stack.yaml +++ b/tests/configuration/lightspeed-stack.yaml @@ -22,11 +22,11 @@ service: - baz_header ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: + # library_client_config_path: url: http://localhost:8321 api_key: xyzzy user_data_collection: diff --git a/tests/configuration/minimal-stack.yaml b/tests/configuration/minimal-stack.yaml index c0e5f5305..7ff9e4432 100644 --- a/tests/configuration/minimal-stack.yaml +++ b/tests/configuration/minimal-stack.yaml @@ -1,5 +1,5 @@ version: '2' -distro_name: llamastack-minimal-stack +distro_name: ogx-minimal-stack container_image: null external_providers_dir: /tmp @@ -9,10 +9,10 @@ storage: backends: kv_default: type: kv_sqlite - db_path: '/tmp/test_llama_stack_kv.db' + db_path: '/tmp/test_ogx_kv.db' sql_default: type: sql_sqlite - db_path: '/tmp/test_llama_stack_sql.db' + db_path: '/tmp/test_ogx_sql.db' stores: metadata: namespace: registry diff --git a/tests/configuration/run.yaml b/tests/configuration/run.yaml index 374dab491..1ccf733cc 100644 --- a/tests/configuration/run.yaml +++ b/tests/configuration/run.yaml @@ -1,5 +1,5 @@ version: '2' -distro_name: minimal-viable-llama-stack-configuration +distro_name: minimal-viable-ogx-configuration apis: - responses diff --git a/tests/configuration/unified-mode/lightspeed-stack-invalid-config-and-legacy.yaml b/tests/configuration/unified-mode/lightspeed-stack-invalid-config-and-legacy.yaml new file mode 100644 index 000000000..a33693698 --- /dev/null +++ b/tests/configuration/unified-mode/lightspeed-stack-invalid-config-and-legacy.yaml @@ -0,0 +1,25 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + profile: tests/configuration/run.yaml + # INVALID: config block plus the legacy path (mutual exclusion, R3) + library_client_config_path: tests/configuration/run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/configuration/unified-mode/lightspeed-stack-invalid-providers-and-legacy.yaml b/tests/configuration/unified-mode/lightspeed-stack-invalid-providers-and-legacy.yaml new file mode 100644 index 000000000..19c299ab9 --- /dev/null +++ b/tests/configuration/unified-mode/lightspeed-stack-invalid-providers-and-legacy.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + # INVALID: synthesis input plus the legacy path (mutual exclusion, R3) + library_client_config_path: tests/configuration/run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/configuration/unified-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml b/tests/configuration/unified-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml new file mode 100644 index 000000000..9ae7b389d --- /dev/null +++ b/tests/configuration/unified-mode/lightspeed-stack-invalid-version-legacy-unified-body.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} +# INVALID: explicit legacy marker on a unified-shaped body (R11, LCORE-2872) +config_format_version: legacy diff --git a/tests/configuration/unified-mode/lightspeed-stack-legacy-for-migration.yaml b/tests/configuration/unified-mode/lightspeed-stack-legacy-for-migration.yaml new file mode 100644 index 000000000..396b24fcb --- /dev/null +++ b/tests/configuration/unified-mode/lightspeed-stack-legacy-for-migration.yaml @@ -0,0 +1,23 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + # Legacy two-file shape: external run.yaml, no synthesis input + library_client_config_path: tests/e2e/configs/run-ci.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e-prow/rhoai/manifests/lightspeed/lightspeed-stack.yaml b/tests/e2e-prow/rhoai/manifests/lightspeed/lightspeed-stack.yaml index 9d7d3a58a..7e3c244d3 100644 --- a/tests/e2e-prow/rhoai/manifests/lightspeed/lightspeed-stack.yaml +++ b/tests/e2e-prow/rhoai/manifests/lightspeed/lightspeed-stack.yaml @@ -43,7 +43,7 @@ spec: seccompProfile: type: RuntimeDefault env: - - name: E2E_LLAMA_HOSTNAME + - name: E2E_OGX_HOSTNAME valueFrom: secretKeyRef: name: llama-stack-ip-secret diff --git a/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-openai.yaml b/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-openai.yaml index 20ec60a8b..28e79a1dc 100644 --- a/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-openai.yaml +++ b/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-openai.yaml @@ -183,7 +183,7 @@ spec: secretKeyRef: name: faiss-vector-store-secret key: id - - name: E2E_LLAMA_HOSTNAME + - name: E2E_OGX_HOSTNAME valueFrom: secretKeyRef: name: llama-stack-ip-secret diff --git a/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-prow.yaml b/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-prow.yaml index 2efea832c..22fa0caf2 100644 --- a/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-prow.yaml +++ b/tests/e2e-prow/rhoai/manifests/lightspeed/ogx-prow.yaml @@ -126,7 +126,7 @@ spec: secretKeyRef: name: faiss-vector-store-secret key: id - - name: E2E_LLAMA_HOSTNAME + - name: E2E_OGX_HOSTNAME valueFrom: secretKeyRef: name: llama-stack-ip-secret @@ -155,7 +155,7 @@ spec: ENRICHED_CONFIG="/opt/app-root/run.yaml" LIGHTSPEED_CONFIG="${LIGHTSPEED_CONFIG:-/opt/app-root/lightspeed-stack.yaml}" if [[ -f "$LIGHTSPEED_CONFIG" ]]; then - echo "Enriching llama-stack config..." + echo "Enriching OGX config..." ENRICHMENT_FAILED=0 /opt/app-root/.venv/bin/python3 /opt/app-root/src/ogx_configuration.py \ -c "$LIGHTSPEED_CONFIG" \ diff --git a/tests/e2e-prow/rhoai/pipeline-konflux.sh b/tests/e2e-prow/rhoai/pipeline-konflux.sh index 14020dbcf..5697dbea2 100755 --- a/tests/e2e-prow/rhoai/pipeline-konflux.sh +++ b/tests/e2e-prow/rhoai/pipeline-konflux.sh @@ -296,9 +296,9 @@ fi # So behave/e2e-ops can kill this listener before rebinding 8080 (restart-lightspeed hooks). # Debug hook/port churn: export E2E_OPS_VERBOSE=1 before running pipeline.sh export E2E_LSC_PORT_FORWARD_PID_FILE="${E2E_LSC_PORT_FORWARD_PID_FILE:-/tmp/e2e-lightspeed-port-forward.pid}" -export E2E_LLAMA_PORT_FORWARD_PID_FILE="${E2E_LLAMA_PORT_FORWARD_PID_FILE:-/tmp/e2e-llama-port-forward.pid}" +export E2E_OGX_PORT_FORWARD_PID_FILE="${E2E_OGX_PORT_FORWARD_PID_FILE:-/tmp/e2e-ogx-port-forward.pid}" rm -f "$E2E_LSC_PORT_FORWARD_PID_FILE" -rm -f "$E2E_LLAMA_PORT_FORWARD_PID_FILE" +rm -f "$E2E_OGX_PORT_FORWARD_PID_FILE" oc label pod lightspeed-stack-service pod=lightspeed-stack-service -n $NAMESPACE @@ -336,8 +336,8 @@ PF_JWKS_PID=$! # OGX directly — mirror LCS and forward llama-stack-service-svc to localhost:8321. log "Starting port-forward for llama-stack (MCP / ogx_client hooks)..." oc port-forward svc/llama-stack-service-svc 8321:8321 -n $NAMESPACE & -PF_LLAMA_PID=$! -echo "$PF_LLAMA_PID" >"$E2E_LLAMA_PORT_FORWARD_PID_FILE" +PF_OGX_PID=$! +echo "$PF_OGX_PID" >"$E2E_OGX_PORT_FORWARD_PID_FILE" # Wait for port-forward to be usable (app may not be listening immediately; port-forward can drop) log "Waiting for port-forward to lightspeed-stack to be ready..." @@ -358,7 +358,7 @@ for i in $(seq 1 36); do done < <(oc get events -n "$NAMESPACE" --sort-by='.lastTimestamp' 2>&1 | tail -40) || true kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true - kill $PF_LLAMA_PID 2>/dev/null || true + kill $PF_OGX_PID 2>/dev/null || true exit 1 fi # If port-forward process died, restart it (e.g. "connection refused" / "lost connection to pod") @@ -384,22 +384,22 @@ for i in $(seq 1 36); do e2e_echo_pod_logs 250 kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true - kill $PF_LLAMA_PID 2>/dev/null || true + kill $PF_OGX_PID 2>/dev/null || true exit 1 fi - if ! kill -0 $PF_LLAMA_PID 2>/dev/null; then + if ! kill -0 $PF_OGX_PID 2>/dev/null; then log "Llama port-forward died, restarting (attempt $i)..." oc port-forward svc/llama-stack-service-svc 8321:8321 -n $NAMESPACE & - PF_LLAMA_PID=$! - echo "$PF_LLAMA_PID" >"$E2E_LLAMA_PORT_FORWARD_PID_FILE" + PF_OGX_PID=$! + echo "$PF_OGX_PID" >"$E2E_OGX_PORT_FORWARD_PID_FILE" fi sleep 5 done export E2E_LSC_HOSTNAME="localhost" export E2E_JWKS_HOSTNAME="localhost" -export E2E_LLAMA_HOSTNAME="localhost" -export E2E_LLAMA_PORT="8321" +export E2E_OGX_HOSTNAME="localhost" +export E2E_OGX_PORT="8321" # Same pattern as tests/e2e-prow/rhoai/pipeline.sh and .github/workflows/e2e_tests_*.yaml: # Behave {MODEL}/{PROVIDER} use these when set; avoids wrong fallbacks if /v1/models # discovery in before_all is empty (matches run-ci.yaml openai + E2E_OPENAI_MODEL). @@ -413,7 +413,7 @@ fi export E2E_DEFAULT_PROVIDER_OVERRIDE E2E_DEFAULT_MODEL_OVERRIDE log "LCS accessible at: http://$E2E_LSC_HOSTNAME:8080" log "Mock JWKS accessible at: http://$E2E_JWKS_HOSTNAME:8000" -log "OGX (e2e client hooks) at: http://$E2E_LLAMA_HOSTNAME:$E2E_LLAMA_PORT" +log "OGX (e2e client hooks) at: http://$E2E_OGX_HOSTNAME:$E2E_OGX_PORT" #======================================== # 7. RUN TESTS @@ -441,20 +441,20 @@ if [[ -n "${E2E_LSC_PORT_FORWARD_PID_FILE:-}" && -f "$E2E_LSC_PORT_FORWARD_PID_F fi rm -f "$E2E_LSC_PORT_FORWARD_PID_FILE" fi -if [[ -n "${E2E_LLAMA_PORT_FORWARD_PID_FILE:-}" && -f "$E2E_LLAMA_PORT_FORWARD_PID_FILE" ]]; then - read -r _ll_pf <"$E2E_LLAMA_PORT_FORWARD_PID_FILE" 2>/dev/null || true +if [[ -n "${E2E_OGX_PORT_FORWARD_PID_FILE:-}" && -f "$E2E_OGX_PORT_FORWARD_PID_FILE" ]]; then + read -r _ll_pf <"$E2E_OGX_PORT_FORWARD_PID_FILE" 2>/dev/null || true if [[ "${_ll_pf:-}" =~ ^[0-9]+$ ]]; then kill -9 "$_ll_pf" 2>/dev/null || true fi - rm -f "$E2E_LLAMA_PORT_FORWARD_PID_FILE" + rm -f "$E2E_OGX_PORT_FORWARD_PID_FILE" fi kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true -kill $PF_LLAMA_PID 2>/dev/null || true +kill $PF_OGX_PID 2>/dev/null || true wait $PF_LCS_PID 2>/dev/null || true wait $PF_JWKS_PID 2>/dev/null || true -wait $PF_LLAMA_PID 2>/dev/null || true +wait $PF_OGX_PID 2>/dev/null || true set -e trap 'echo "❌ Pipeline failed at line $LINENO"; exit 1' ERR diff --git a/tests/e2e-prow/rhoai/pipeline-services-konflux.sh b/tests/e2e-prow/rhoai/pipeline-services-konflux.sh index b34a77959..d907c1fce 100755 --- a/tests/e2e-prow/rhoai/pipeline-services-konflux.sh +++ b/tests/e2e-prow/rhoai/pipeline-services-konflux.sh @@ -19,7 +19,7 @@ if [ -f "$REPO_ROOT/tests/e2e/secrets/invalid-mcp-token" ]; then fi # 1. OGX (run from source). Cluster DNS name matches oc expose --name=llama-stack-service-svc. -# Secret must exist before the pod: both LCS and OGX-container use E2E_LLAMA_HOSTNAME from it. +# Secret must exist before the pod: both LCS and OGX-container use E2E_OGX_HOSTNAME from it. _LLAMA_SVC_FQDN="llama-stack-service-svc.${NAMESPACE}.svc.cluster.local" oc create secret generic llama-stack-ip-secret \ --from-literal=key="$_LLAMA_SVC_FQDN" \ diff --git a/tests/e2e-prow/rhoai/pipeline-services.sh b/tests/e2e-prow/rhoai/pipeline-services.sh index 152e54392..29badff28 100755 --- a/tests/e2e-prow/rhoai/pipeline-services.sh +++ b/tests/e2e-prow/rhoai/pipeline-services.sh @@ -4,9 +4,9 @@ BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" NAMESPACE="${NAMESPACE:-e2e-rhoai-dsc}" # Create OGX-ip-secret before deploying the pod (it references the secret as an env var) -export E2E_LLAMA_HOSTNAME="llama-stack-service-svc.${NAMESPACE}.svc.cluster.local" +export E2E_OGX_HOSTNAME="llama-stack-service-svc.${NAMESPACE}.svc.cluster.local" oc create secret generic llama-stack-ip-secret \ - --from-literal=key="$E2E_LLAMA_HOSTNAME" \ + --from-literal=key="$E2E_OGX_HOSTNAME" \ -n "$NAMESPACE" 2>/dev/null || echo "Secret llama-stack-ip-secret exists" # Deploy OGX (substitute only LLAMA_STACK_IMAGE, leave other ${} intact) diff --git a/tests/e2e-prow/rhoai/pipeline.sh b/tests/e2e-prow/rhoai/pipeline.sh index fab42c8ae..af0ebf592 100755 --- a/tests/e2e-prow/rhoai/pipeline.sh +++ b/tests/e2e-prow/rhoai/pipeline.sh @@ -356,10 +356,10 @@ oc describe pod llama-stack-service -n "$NAMESPACE" || true # Export PID file paths so e2e-ops.sh can find and kill stale port-forwards # during test-triggered pod restarts (matches pipeline-konflux.sh). export E2E_LSC_PORT_FORWARD_PID_FILE="${E2E_LSC_PORT_FORWARD_PID_FILE:-/tmp/e2e-lightspeed-port-forward.pid}" -export E2E_LLAMA_PORT_FORWARD_PID_FILE="${E2E_LLAMA_PORT_FORWARD_PID_FILE:-/tmp/e2e-llama-port-forward.pid}" +export E2E_OGX_PORT_FORWARD_PID_FILE="${E2E_OGX_PORT_FORWARD_PID_FILE:-/tmp/e2e-ogx-port-forward.pid}" export E2E_JWKS_PORT_FORWARD_PID_FILE="${E2E_JWKS_PORT_FORWARD_PID_FILE:-/tmp/e2e-jwks-port-forward.pid}" rm -f "$E2E_LSC_PORT_FORWARD_PID_FILE" -rm -f "$E2E_LLAMA_PORT_FORWARD_PID_FILE" +rm -f "$E2E_OGX_PORT_FORWARD_PID_FILE" rm -f "$E2E_JWKS_PORT_FORWARD_PID_FILE" oc label pod lightspeed-stack-service pod=lightspeed-stack-service -n $NAMESPACE @@ -398,8 +398,8 @@ echo "$PF_JWKS_PID" >"$E2E_JWKS_PORT_FORWARD_PID_FILE" # need localhost:8321. Without this forward those tests hit "Connection refused". echo "Starting port-forward for llama-stack..." oc port-forward svc/llama-stack-service-svc 8321:8321 -n $NAMESPACE & -PF_LLAMA_PID=$! -echo "$PF_LLAMA_PID" >"$E2E_LLAMA_PORT_FORWARD_PID_FILE" +PF_OGX_PID=$! +echo "$PF_OGX_PID" >"$E2E_OGX_PORT_FORWARD_PID_FILE" # Wait for port-forward to be usable (app may not be listening immediately; port-forward can drop) echo "Waiting for port-forward to lightspeed-stack to be ready..." @@ -421,7 +421,7 @@ for i in $(seq 1 36); do oc get pods -n "$NAMESPACE" -o wide || true kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true - kill $PF_LLAMA_PID 2>/dev/null || true + kill $PF_OGX_PID 2>/dev/null || true exit 1 fi # If port-forward process died, restart it (e.g. "connection refused" / "lost connection to pod") @@ -446,14 +446,14 @@ for i in $(seq 1 36); do oc logs llama-stack-service -n "$NAMESPACE" --tail=100 || true kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true - kill $PF_LLAMA_PID 2>/dev/null || true + kill $PF_OGX_PID 2>/dev/null || true exit 1 fi - if ! kill -0 $PF_LLAMA_PID 2>/dev/null; then + if ! kill -0 $PF_OGX_PID 2>/dev/null; then echo "Llama port-forward died, restarting (attempt $i)..." oc port-forward svc/llama-stack-service-svc 8321:8321 -n $NAMESPACE & - PF_LLAMA_PID=$! - echo "$PF_LLAMA_PID" >"$E2E_LLAMA_PORT_FORWARD_PID_FILE" + PF_OGX_PID=$! + echo "$PF_OGX_PID" >"$E2E_OGX_PORT_FORWARD_PID_FILE" fi sleep 5 done @@ -487,11 +487,11 @@ TEST_EXIT_CODE=$(cat "$E2E_EXIT_CODE_FILE" 2>/dev/null || echo 1) # Kill first so wait doesn't block (if a port-forward is still running, wait would hang) kill $PF_LCS_PID 2>/dev/null || true kill $PF_JWKS_PID 2>/dev/null || true -kill $PF_LLAMA_PID 2>/dev/null || true +kill $PF_OGX_PID 2>/dev/null || true wait $PF_LCS_PID 2>/dev/null || true wait $PF_JWKS_PID 2>/dev/null || true -wait $PF_LLAMA_PID 2>/dev/null || true -rm -f "$E2E_LSC_PORT_FORWARD_PID_FILE" "$E2E_LLAMA_PORT_FORWARD_PID_FILE" "$E2E_JWKS_PORT_FORWARD_PID_FILE" +wait $PF_OGX_PID 2>/dev/null || true +rm -f "$E2E_LSC_PORT_FORWARD_PID_FILE" "$E2E_OGX_PORT_FORWARD_PID_FILE" "$E2E_JWKS_PORT_FORWARD_PID_FILE" set -e trap 'echo "❌ Pipeline failed at line $LINENO"; exit 1' ERR diff --git a/tests/e2e-prow/rhoai/scripts/e2e-ops.sh b/tests/e2e-prow/rhoai/scripts/e2e-ops.sh index 955d8463d..d3dd02b57 100755 --- a/tests/e2e-prow/rhoai/scripts/e2e-ops.sh +++ b/tests/e2e-prow/rhoai/scripts/e2e-ops.sh @@ -12,21 +12,21 @@ # Behave steps that call OGX directly (MCP toolgroups, shields). When the llama # pod is recreated, that forward must be restarted or you get "PodSandbox ... not found" / # APIConnectionError on subsequent scenarios. -# - E2E_LLAMA_PORT_FORWARD_PID_FILE coordinates killing/restarting the 8321 forward. +# - E2E_OGX_PORT_FORWARD_PID_FILE coordinates killing/restarting the 8321 forward. # - restart-lightspeed ensures Llama is running before LCS recreate when needed. -# - restart-both-services is available explicitly; restart-lightspeed / restart-llama-stack +# - restart-both-services is available explicitly; restart-lightspeed / restart-ogx # do not auto-trigger a full stack restart on failure. # # Commands: # restart-lightspeed - Restart lightspeed-stack pod and port-forward -# restart-llama-stack - Restart/restore OGX pod and localhost:8321 forward +# restart-ogx - Restart/restore OGX pod and localhost:8321 forward # restart-both-services - Full OGX then lightspeed-stack restart (explicit only) # restart-port-forward - Re-establish port-forward for lightspeed -# restart-llama-port-forward - Re-establish port-forward for OGX (8321) +# restart-ogx-port-forward - Re-establish port-forward for OGX (8321) # wait-for-pod [attempts] - Wait for a pod to be ready # update-configmap - Update ConfigMap from file # get-configmap-content - Get ConfigMap content (outputs to stdout) -# disrupt-llama-stack - Delete OGX pod to disrupt connection +# disrupt-ogx - Delete OGX pod to disrupt connection # deploy-e2e-tunnel-proxy - Deploy in-cluster tunnel proxy (proxy.feature step) # deploy-e2e-interception-proxy - Deploy in-cluster interception proxy (proxy.feature step) # deploy-e2e-mock-tls-inference - Deploy mock HTTPS inference server (tls-*.feature) @@ -41,7 +41,7 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" MANIFEST_DIR="$SCRIPT_DIR/../manifests/lightspeed" # Written by pipeline.sh when it starts LCS port-forward; e2e-ops kills this PID before rebinding 8080. E2E_LSC_PORT_FORWARD_PID_FILE="${E2E_LSC_PORT_FORWARD_PID_FILE:-/tmp/e2e-lightspeed-port-forward.pid}" -E2E_LLAMA_PORT_FORWARD_PID_FILE="${E2E_LLAMA_PORT_FORWARD_PID_FILE:-/tmp/e2e-llama-port-forward.pid}" +E2E_OGX_PORT_FORWARD_PID_FILE="${E2E_OGX_PORT_FORWARD_PID_FILE:-/tmp/e2e-ogx-port-forward.pid}" E2E_JWKS_PORT_FORWARD_PID_FILE="${E2E_JWKS_PORT_FORWARD_PID_FILE:-/tmp/e2e-jwks-port-forward.pid}" # ============================================================================ @@ -177,8 +177,8 @@ kill_stale_lightspeed_forward() { kill_stale_llama_forward() { local port="${1:-8321}" local saved_pf - if [[ -f "$E2E_LLAMA_PORT_FORWARD_PID_FILE" ]]; then - read -r saved_pf <"$E2E_LLAMA_PORT_FORWARD_PID_FILE" 2>/dev/null || true + if [[ -f "$E2E_OGX_PORT_FORWARD_PID_FILE" ]]; then + read -r saved_pf <"$E2E_OGX_PORT_FORWARD_PID_FILE" 2>/dev/null || true if [[ "$saved_pf" =~ ^[0-9]+$ ]]; then kill -9 "$saved_pf" 2>/dev/null || true fi @@ -225,7 +225,7 @@ e2e_ops_diagnose_forward_failure() { tail -30 /tmp/port-forward.log 2>/dev/null | sed 's/^/[e2e-ops] /' || true fi e2e_ops_dump_pod_logs "lightspeed-stack-service" 10000 - if [[ "${E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA:-0}" == "1" ]]; then + if [[ "${E2E_COPY_MOCK_TLS_CERTS_TO_OGX:-0}" == "1" ]]; then e2e_ops_dump_pod_logs "llama-stack-service" 10000 fi } @@ -333,14 +333,14 @@ _restart_llama_stack_core() { echo "Applying pod manifest..." if [[ "${E2E_KONFLUX_E2E:-0}" == "1" ]]; then - if [[ "${E2E_COPY_INTERCEPTION_CA_TO_LLAMA:-0}" == "1" ]]; then + if [[ "${E2E_COPY_INTERCEPTION_CA_TO_OGX:-0}" == "1" ]]; then echo "[e2e-ops] Syncing e2e-interception-proxy-ca secret before llama-stack apply..." if ! cmd_sync_interception_proxy_ca_secret; then echo "===== Llama-stack restore FAILED (interception CA secret sync) =====" return 1 fi fi - if [[ "${E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA:-0}" == "1" ]] \ + if [[ "${E2E_COPY_MOCK_TLS_CERTS_TO_OGX:-0}" == "1" ]] \ && [[ "${E2E_SYNC_MOCK_TLS_CERTS:-0}" == "1" ]]; then echo "[e2e-ops] Syncing e2e-mock-tls-certs secret before llama-stack apply..." if ! cmd_sync_mock_tls_certs_secret; then @@ -365,7 +365,7 @@ _restart_llama_stack_core() { fi echo "Labeling pod for service..." oc label pod llama-stack-service pod=llama-stack-service -n "$NAMESPACE" --overwrite - if [[ "${E2E_COPY_INTERCEPTION_CA_TO_LLAMA:-0}" == "1" ]]; then + if [[ "${E2E_COPY_INTERCEPTION_CA_TO_OGX:-0}" == "1" ]]; then if ! _verify_interception_ca_mounted_in_llama; then echo "===== Llama-stack restore FAILED (interception CA not mounted) =====" return 1 @@ -387,7 +387,7 @@ _restart_llama_stack_core() { fi if ! cmd_restart_llama_port_forward; then - echo "ERROR: Llama pod is up but localhost:${LOCAL_LLAMA_PORT:-8321} port-forward failed" + echo "ERROR: Llama pod is up but localhost:${LOCAL_OGX_PORT:-8321} port-forward failed" e2e_ops_dump_pod_logs "llama-stack-service" 200 return 1 fi @@ -401,10 +401,10 @@ _restart_lightspeed_core() { echo "Restarting lightspeed-stack service..." # Degraded-mode e2e must start LCS while llama is down. Default path restores - # llama first so pods can come up; set E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART=1 + # llama first so pods can come up; set E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART=1 # to keep llama disrupted for allow_degraded_mode startup checks. - if [[ "${E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART:-0}" == "1" ]]; then - echo "⚠️ Skipping llama restore before LCS restart (E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART=1)" + if [[ "${E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART:-0}" == "1" ]]; then + echo "⚠️ Skipping llama restore before LCS restart (E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART=1)" elif ! _llama_stack_http_health_once 2>/dev/null; then echo "⚠️ OGX not healthy — restoring before LCS restart..." if ! _restart_llama_stack_core; then @@ -555,8 +555,8 @@ verify_llama_local_forward() { } cmd_restart_llama_port_forward() { - local local_port="${LOCAL_LLAMA_PORT:-8321}" - local remote_port="${REMOTE_LLAMA_PORT:-8321}" + local local_port="${LOCAL_OGX_PORT:-8321}" + local remote_port="${REMOTE_OGX_PORT:-8321}" local max_attempts=6 local pf_pid local pf_resource @@ -594,7 +594,7 @@ cmd_restart_llama_port_forward() { sleep 4 if verify_llama_local_forward 12; then - echo "$pf_pid" >"$E2E_LLAMA_PORT_FORWARD_PID_FILE" + echo "$pf_pid" >"$E2E_OGX_PORT_FORWARD_PID_FILE" echo "[e2e-ops] Llama through port-forward: GET http://127.0.0.1:$local_port/v1/health -> OK" echo "✓ OGX port-forward established (PID: $pf_pid, $pf_resource)" return 0 @@ -972,7 +972,7 @@ cmd_dump_pod_logs() { e2e_ops_dump_pod_logs "${1:?pod name required}" "${2:-150}" } -cmd_disrupt_llama_stack() { +cmd_disrupt_ogx() { local pod_name="llama-stack-service" local phase @@ -1000,13 +1000,13 @@ case "$COMMAND" in restart-lightspeed) cmd_restart_lightspeed ;; - restart-llama-stack) + restart-ogx) cmd_restart_llama_stack ;; restart-both-services) cmd_restart_both_services ;; - restart-llama-port-forward) + restart-ogx-port-forward) cmd_restart_llama_port_forward ;; restart-jwks-port-forward) @@ -1024,8 +1024,8 @@ case "$COMMAND" in get-configmap-content) cmd_get_configmap_content "$@" ;; - disrupt-llama-stack) - cmd_disrupt_llama_stack + disrupt-ogx) + cmd_disrupt_ogx ;; tunnel-proxy-stats) cmd_tunnel_proxy_stats @@ -1065,14 +1065,14 @@ case "$COMMAND" in echo "" echo "Commands:" echo " restart-lightspeed - Restart lightspeed-stack pod and port-forward" - echo " restart-llama-stack - Restart/restore llama-stack pod" + echo " restart-ogx - Restart/restore llama-stack pod" echo " restart-both-services - Full llama-stack + lightspeed-stack restart (explicit)" - echo " restart-llama-port-forward - Re-establish port-forward for OGX (8321)" + echo " restart-ogx-port-forward - Re-establish port-forward for OGX (8321)" echo " restart-port-forward - Re-establish port-forward for lightspeed" echo " wait-for-pod [attempts] - Wait for a pod to be ready" echo " update-configmap - Update ConfigMap from file" echo " get-configmap-content - Get ConfigMap content (outputs to stdout)" - echo " disrupt-llama-stack - Delete llama-stack pod to disrupt connection" + echo " disrupt-ogx - Delete llama-stack pod to disrupt connection" echo " tunnel-proxy-stats - JSON stats from in-cluster e2e-tunnel-proxy" echo " interception-proxy-stats - JSON stats from in-cluster e2e-interception-proxy" echo " copy-interception-proxy-ca-to-llama - Alias for sync-interception-proxy-ca-secret" diff --git a/tests/e2e/README.md b/tests/e2e/README.md index cdecbfd1f..e7f88ea80 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -2,8 +2,10 @@ End-to-end tests for the Lightspeed Core Stack REST API (Behave, Gherkin). -**Full guide:** [docs/e2e_testing.md](../../docs/e2e_testing.md) — how to run, environment variables, deployment modes, tags and hooks, Gherkin keywords, configuration, and troubleshooting. +**Full guide:** [docs/testing/e2e_testing.md](../../docs/testing/e2e_testing.md) — how to run, environment variables, deployment modes, tags and hooks, Gherkin keywords, configuration, and troubleshooting. * Tests: `tests/e2e/features/*.feature` * Step definitions: `tests/e2e/features/steps/` * Feature list (run order): `test_list.txt` + +**Not sure a scenario is e2e?** Steps must never touch `src/`; validation, migration and synthesis live in `tests/integration/`. See [Choosing the Test Layer](../../docs/testing/e2e_testing.md#choosing-the-test-layer-e2e-or-integration). diff --git a/tests/e2e/configuration/README.md b/tests/e2e/configuration/README.md index c9dad6890..397d3088d 100644 --- a/tests/e2e/configuration/README.md +++ b/tests/e2e/configuration/README.md @@ -10,10 +10,11 @@ This directory contains configuration files used for end-to-end testing of Light ## Library mode uses unified configs (LCORE-2342) The library-mode configurations use the unified single-file format: instead of -the legacy `llama_stack.library_client_config_path`, they carry +the legacy `ogx.library_client_config_path` (the deprecated `llama_stack` YAML +section alias is still accepted), they carry ```yaml -llama_stack: +ogx: use_as_library_client: true config: profile: run.yaml diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml new file mode 100644 index 000000000..6b4ea3c09 --- /dev/null +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-legacy.yaml @@ -0,0 +1,47 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Legacy two-file shape (R2 deprecation window): external run.yaml consumed + # via library_client_config_path; no unified synthesis input. Kept as a + # dedicated fixture because the standard library-mode baseline migrated to + # unified mode in LCORE-2342, which silently removed legacy boot coverage. + use_as_library_client: true + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini +rag: + byok: + stores: + - rag_id: e2e-test-docs + backend: faiss + embedding_model: sentence-transformers/all-mpnet-base-v2 + embedding_dimension: 768 + vector_db_id: ${env.FAISS_VECTOR_STORE_ID} + db_path: ${env.KV_RAG_PATH:=~/.llama/storage/rag/kv_store.db} + score_multiplier: 1.0 + retrieval: + tool: + sources: + - e2e-test-docs + +shields: + - name: pii-redaction + provider_id: redaction + config: + rules: + - pattern: '\d+' + replacement: '[NUM]' diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml index 0ccb9b2d5..5a5021bf0 100644 --- a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-empty.yaml @@ -9,8 +9,8 @@ service: workers: 1 color_log: true access_log: true -llama_stack: - # Library mode - embeds llama-stack as library +ogx: + # Library mode - embeds OGX as library use_as_library_client: true # Unified mode: run.yaml (materialized per provider by CI/the harness) # is consumed as the synthesis profile instead of the legacy two-file path. diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml index d93431032..f93d32297 100644 --- a/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-shields-override-disabled.yaml @@ -11,8 +11,8 @@ service: workers: 1 color_log: true access_log: true -llama_stack: - # Library mode - embeds llama-stack as library +ogx: + # Library mode - embeds OGX as library use_as_library_client: true # Unified mode: run.yaml (materialized per provider by CI/the harness) # is consumed as the synthesis profile instead of the legacy two-file path. diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml index 715f254c6..b6c308d58 100644 --- a/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-shields.yaml @@ -1,5 +1,5 @@ # @cfg_shields -# LCORE-owned safety shields (not Llama Stack / OGX Safety API resources). +# LCORE-owned safety shields (not OGX Safety API resources). # Configures one shield of each supported type so GET /v1/shields can be # asserted against both `question_validity` and `redaction` shapes. See # tests/e2e/features/shields.feature. @@ -11,8 +11,8 @@ service: workers: 1 color_log: true access_log: true -llama_stack: - # Library mode - embeds llama-stack as library +ogx: + # Library mode - embeds OGX as library use_as_library_client: true # Unified mode: run.yaml (materialized per provider by CI/the harness) # is consumed as the synthesis profile instead of the legacy two-file path. diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml index b39110c29..941f5657b 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-authorized.yaml @@ -12,12 +12,12 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + # library_client_config_path: + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-default.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-default.yaml index 12ff8fcca..294603b16 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-default.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-default.yaml @@ -13,7 +13,7 @@ service: access_log: true ogx: use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-degraded.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-degraded.yaml index 11d276e5d..798d407c1 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-degraded.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-degraded.yaml @@ -11,7 +11,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy # Enable degraded mode to allow startup without OGX allow_degraded_mode: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-api-auth.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-api-auth.yaml index 373717732..a246a7aa7 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-api-auth.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-api-auth.yaml @@ -11,7 +11,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-client-auth.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-client-auth.yaml index 25cbdf836..bedfbbe46 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-client-auth.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-client-auth.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-file-auth.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-file-auth.yaml index ca60006c9..3191da2ce 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-file-auth.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-file-auth.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-invalid.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-invalid.yaml index 976a51853..865336d2b 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-invalid.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-invalid.yaml @@ -12,7 +12,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-kubernetes-auth.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-kubernetes-auth.yaml index 39b61ef3a..8dadb72d8 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-kubernetes-auth.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-kubernetes-auth.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-oauth-auth.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-oauth-auth.yaml index 9237b6019..4c18a8ace 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-oauth-auth.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp-oauth-auth.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml index 837d44aab..46e07d4b5 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-mcp.yaml @@ -15,7 +15,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml index d7a22b5ee..7d7832f11 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-negative.yaml @@ -13,12 +13,12 @@ service: access_log: true ogx: # Uses a remote OGX service - # The instance would have already been started with a llama-stack-run.yaml file + # The instance would have already been started with a run.yaml file use_as_library_client: false # Alternative for "as library use" # use_as_library_client: true - # library_client_config_path: - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + # library_client_config_path: + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-rbac.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-rbac.yaml index 237d80849..c17233a65 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-rbac.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-rbac.yaml @@ -11,7 +11,7 @@ service: ogx: use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-rh-identity.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-rh-identity.yaml index f6959cd97..a73efbc62 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-rh-identity.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-rh-identity.yaml @@ -10,7 +10,7 @@ service: access_log: true ogx: use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-rhelai.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-rhelai.yaml index c1294a5b0..ff0615804 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-rhelai.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-rhelai.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-rhoai.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-rhoai.yaml index c1294a5b0..ff0615804 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-rhoai.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-rhoai.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml index 5df05d93a..1a5233af3 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-empty.yaml @@ -9,10 +9,10 @@ service: workers: 1 color_log: true access_log: true -llama_stack: - # Server mode - connects to separate llama-stack service +ogx: + # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml index b51d86570..40220313b 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-shields-override-disabled.yaml @@ -11,10 +11,10 @@ service: workers: 1 color_log: true access_log: true -llama_stack: - # Server mode - connects to separate llama-stack service +ogx: + # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml index 17e011c0a..f0a9d33e4 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-shields.yaml @@ -1,5 +1,5 @@ # @cfg_shields -# LCORE-owned safety shields (not Llama Stack / OGX Safety API resources). +# LCORE-owned safety shields (not OGX Safety API resources). # Configures one shield of each supported type so GET /v1/shields can be # asserted against both `question_validity` and `redaction` shapes. See # tests/e2e/features/shields.feature. @@ -11,10 +11,10 @@ service: workers: 1 color_log: true access_log: true -llama_stack: - # Server mode - connects to separate llama-stack service +ogx: + # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-skills-directory.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-skills-directory.yaml index 02da39f40..6418577ce 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-skills-directory.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-skills-directory.yaml @@ -10,7 +10,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-skills.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-skills.yaml index 93f100ebe..3c7a1b7c4 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-skills.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-skills.yaml @@ -10,7 +10,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-tls.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-tls.yaml index 24a0817b8..8f78c833e 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack-tls.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-tls.yaml @@ -10,7 +10,7 @@ service: access_log: true ogx: use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack.yaml index 680552574..ece1c9c66 100644 --- a/tests/e2e/configuration/server-mode/lightspeed-stack.yaml +++ b/tests/e2e/configuration/server-mode/lightspeed-stack.yaml @@ -9,7 +9,7 @@ service: ogx: # Server mode - connects to separate OGX service use_as_library_client: false - url: http://${env.E2E_LLAMA_HOSTNAME}:8321 + url: http://${env.E2E_OGX_HOSTNAME}:8321 api_key: xyzzy user_data_collection: feedback_enabled: true diff --git a/tests/e2e/configuration/unified-mode/README.md b/tests/e2e/configuration/unified-mode/README.md new file mode 100644 index 000000000..98131ba4f --- /dev/null +++ b/tests/e2e/configuration/unified-mode/README.md @@ -0,0 +1,25 @@ +# Unified-mode e2e configuration fixtures + +Fixtures for the `unified-mode-*.feature` files (LCORE-2341/LCORE-2343). +Same layout as the parent directory: `library-mode/` and `server-mode/` +variants differing only in the `llama_stack` block; the harness resolves +`//` via the standard `configure_service` logic. + +All profile-based fixtures reference `run.yaml` — the repo-root copy the CI +harness materializes from `tests/e2e/configs/run-.yaml` — so they stay +provider-agnostic across the providers matrix. + +Only bootable fixtures live here. The validation-only and synthesis-only +inputs (invalid configs, `native_override` shapes) belong to the integration +layer — `tests/configuration/unified-mode/` and +`tests/integration/test_unified_synthesis.py` — because e2e steps never run +`src/` CLIs (see `docs/testing/e2e_testing.md`, "Choosing the Test Layer"). + +| Fixture | Purpose | +|---|---| +| `lightspeed-stack-unified-providers.yaml` | Minimal unified config driven only by top-level `inference.providers` (default baseline, R1/S5). openai-specific — used by `@openai-only` scenarios. | +| `lightspeed-stack-unified-config-only.yaml` | Unified config driven only by `llama_stack.config` (`profile: run.yaml`, R1). | +| `lightspeed-stack-unified-relative-profile.yaml` | Same shape as config-only; exists to pin R8 (relative `profile:` resolves against the config file's directory) as a distinct intent. | +| `lightspeed-stack-unified-absolute-profile.yaml` | `profile:` as a container-absolute path (differs per mode subdir). | +| `lightspeed-stack-legacy-for-migration.yaml` | Legacy half of the migration fixture pair; paired with `tests/e2e/configs/run-ci.yaml`. Deliberately free of enrichment sections so migrate→synthesize round-trips losslessly (see LCORE-3370). Input to the drift guard below; never booted. | +| `lightspeed-stack-unified-migrated.yaml` | **Committed** output of `--migrate-config` for the pair above. Booted by `unified-mode-migration.feature` (`@openai-only`: it inlines the openai run-ci.yaml). `tests/integration/test_unified_mode_cli.py::test_committed_migrated_fixture_matches_cli_output` fails when the CLI output drifts; its docstring has the regeneration command. | diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-legacy-for-migration.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-legacy-for-migration.yaml new file mode 100644 index 000000000..6393142b5 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-legacy-for-migration.yaml @@ -0,0 +1,23 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + # Legacy two-file shape: external run.yaml, no synthesis input + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-absolute-profile.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-absolute-profile.yaml new file mode 100644 index 000000000..089fb0afb --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-absolute-profile.yaml @@ -0,0 +1,24 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + # Absolute path as mounted in the library-mode container + profile: /app-root/run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-config-only.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-config-only.yaml new file mode 100644 index 000000000..b3df26828 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-config-only.yaml @@ -0,0 +1,24 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + # Synthesis baseline: the CI-materialized run.yaml (provider-agnostic) + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-migrated.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-migrated.yaml new file mode 100644 index 000000000..576b1950a --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-migrated.yaml @@ -0,0 +1,126 @@ +authentication: + module: noop +inference: + default_model: gpt-4o-mini + default_provider: openai +name: Lightspeed Core Service (LCS) +ogx: + config: + baseline: empty + native_override: + apis: + - responses + - batches + - files + - inference + - tool_runtime + - conversations + - vector_io + distro_name: starter + providers: + batches: + - config: + sqlstore: + backend: sql_default + table_name: batches + provider_id: reference + provider_type: inline::reference + files: + - config: + metadata_store: + backend: sql_default + table_name: files_metadata + storage_dir: ~/.llama/storage/files + provider_id: meta-reference-files + provider_type: inline::localfs + inference: + - config: + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} + api_key: ${env.OPENAI_API_KEY} + provider_id: openai + provider_type: remote::openai + - config: {} + provider_id: sentence-transformers + provider_type: inline::sentence-transformers + responses: + - config: + persistence: + responses: + backend: sql_default + table_name: agents_responses + provider_id: builtin + provider_type: inline::builtin + tool_runtime: + - config: {} + provider_id: file-search + provider_type: inline::file-search + - config: {} + provider_id: model-context-protocol + provider_type: remote::model-context-protocol + vector_io: + - config: + persistence: + backend: kv_rag + namespace: vector_io::faiss + provider_id: faiss + provider_type: inline::faiss + registered_resources: + models: + - metadata: + embedding_dimension: 768 + model_id: all-mpnet-base-v2 + model_type: embedding + provider_id: sentence-transformers + provider_model_id: all-mpnet-base-v2 + vector_stores: [] + server: + port: 8321 + storage: + backends: + kv_default: + db_path: ${env.KV_STORE_PATH:=~/.llama/storage/kv_store.db} + type: kv_sqlite + kv_rag: + db_path: ${env.KV_RAG_PATH:=~/.llama/storage/rag/kv_store.db} + type: kv_sqlite + sql_default: + db_path: ${env.SQL_STORE_PATH:=~/.llama/storage/sql_store.db} + type: sql_sqlite + stores: + connectors: + backend: sql_default + table_name: connectors + conversations: + backend: sql_default + table_name: openai_conversations + inference: + backend: sql_default + max_write_queue_size: 10000 + num_writers: 4 + table_name: inference_store + metadata: + backend: kv_default + namespace: registry + prompts: + backend: sql_default + table_name: prompts + vector_stores: + default_embedding_model: + model_id: all-mpnet-base-v2 + provider_id: sentence-transformers + default_provider_id: faiss + version: 2 + use_as_library_client: true +service: + access_log: true + auth_enabled: false + color_log: true + host: 0.0.0.0 + port: 8080 + workers: 1 +user_data_collection: + feedback_enabled: true + feedback_storage: /tmp/data/feedback + transcripts_enabled: true + transcripts_storage: /tmp/data/transcripts diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-providers.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-providers.yaml new file mode 100644 index 000000000..731c39b5d --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-providers.yaml @@ -0,0 +1,29 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-relative-profile.yaml b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-relative-profile.yaml new file mode 100644 index 000000000..228b2d40b --- /dev/null +++ b/tests/e2e/configuration/unified-mode/library-mode/lightspeed-stack-unified-relative-profile.yaml @@ -0,0 +1,24 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Library mode - embeds the stack in-process + use_as_library_client: true + config: + # R8: relative profile resolves against this file's loaded location + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml new file mode 100644 index 000000000..18890c9fc --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-legacy-for-migration.yaml @@ -0,0 +1,23 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate ogx service + use_as_library_client: false + url: http://${env.E2E_OGX_HOSTNAME}:8321 + api_key: xyzzy +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml new file mode 100644 index 000000000..7f9b31dd5 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-absolute-profile.yaml @@ -0,0 +1,26 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate ogx service + use_as_library_client: false + url: http://${env.E2E_OGX_HOSTNAME}:8321 + api_key: xyzzy + config: + # Absolute path as mounted in the ogx container + profile: /opt/app-root/run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml new file mode 100644 index 000000000..34ace93fa --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-config-only.yaml @@ -0,0 +1,26 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate ogx service + use_as_library_client: false + url: http://${env.E2E_OGX_HOSTNAME}:8321 + api_key: xyzzy + config: + # Synthesis baseline: the CI-materialized run.yaml (provider-agnostic) + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-migrated.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-migrated.yaml new file mode 100644 index 000000000..7a31fc7af --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-migrated.yaml @@ -0,0 +1,128 @@ +authentication: + module: noop +inference: + default_model: gpt-4o-mini + default_provider: openai +name: Lightspeed Core Service (LCS) +ogx: + api_key: xyzzy + config: + baseline: empty + native_override: + apis: + - responses + - batches + - files + - inference + - tool_runtime + - conversations + - vector_io + distro_name: starter + providers: + batches: + - config: + sqlstore: + backend: sql_default + table_name: batches + provider_id: reference + provider_type: inline::reference + files: + - config: + metadata_store: + backend: sql_default + table_name: files_metadata + storage_dir: ~/.llama/storage/files + provider_id: meta-reference-files + provider_type: inline::localfs + inference: + - config: + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} + api_key: ${env.OPENAI_API_KEY} + provider_id: openai + provider_type: remote::openai + - config: {} + provider_id: sentence-transformers + provider_type: inline::sentence-transformers + responses: + - config: + persistence: + responses: + backend: sql_default + table_name: agents_responses + provider_id: builtin + provider_type: inline::builtin + tool_runtime: + - config: {} + provider_id: file-search + provider_type: inline::file-search + - config: {} + provider_id: model-context-protocol + provider_type: remote::model-context-protocol + vector_io: + - config: + persistence: + backend: kv_rag + namespace: vector_io::faiss + provider_id: faiss + provider_type: inline::faiss + registered_resources: + models: + - metadata: + embedding_dimension: 768 + model_id: all-mpnet-base-v2 + model_type: embedding + provider_id: sentence-transformers + provider_model_id: all-mpnet-base-v2 + vector_stores: [] + server: + port: 8321 + storage: + backends: + kv_default: + db_path: ${env.KV_STORE_PATH:=~/.llama/storage/kv_store.db} + type: kv_sqlite + kv_rag: + db_path: ${env.KV_RAG_PATH:=~/.llama/storage/rag/kv_store.db} + type: kv_sqlite + sql_default: + db_path: ${env.SQL_STORE_PATH:=~/.llama/storage/sql_store.db} + type: sql_sqlite + stores: + connectors: + backend: sql_default + table_name: connectors + conversations: + backend: sql_default + table_name: openai_conversations + inference: + backend: sql_default + max_write_queue_size: 10000 + num_writers: 4 + table_name: inference_store + metadata: + backend: kv_default + namespace: registry + prompts: + backend: sql_default + table_name: prompts + vector_stores: + default_embedding_model: + model_id: all-mpnet-base-v2 + provider_id: sentence-transformers + default_provider_id: faiss + version: 2 + url: http://${env.E2E_OGX_HOSTNAME}:8321 + use_as_library_client: false +service: + access_log: true + auth_enabled: false + color_log: true + host: 0.0.0.0 + port: 8080 + workers: 1 +user_data_collection: + feedback_enabled: true + feedback_storage: /tmp/data/feedback + transcripts_enabled: true + transcripts_storage: /tmp/data/transcripts diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml new file mode 100644 index 000000000..a40465cca --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-providers.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate ogx service + use_as_library_client: false + url: http://${env.E2E_OGX_HOSTNAME}:8321 + api_key: xyzzy +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini + # Unified synthesis input (Decision S5): the high-level provider entry + # replaces the default baseline's openai provider by id at synthesis time. + providers: + - type: openai + id: openai + api_key_env: OPENAI_API_KEY + allowed_models: + - ${env.E2E_OPENAI_MODEL:=gpt-4o-mini} diff --git a/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml new file mode 100644 index 000000000..0647f4c87 --- /dev/null +++ b/tests/e2e/configuration/unified-mode/server-mode/lightspeed-stack-unified-relative-profile.yaml @@ -0,0 +1,26 @@ +name: Lightspeed Core Service (LCS) +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + # Server mode - connects to the separate ogx service + use_as_library_client: false + url: http://${env.E2E_OGX_HOSTNAME}:8321 + api_key: xyzzy + config: + # R8: relative profile resolves against this file's loaded location + profile: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +inference: + default_provider: openai + default_model: gpt-4o-mini diff --git a/tests/e2e/features/degraded_mode_startup.feature b/tests/e2e/features/degraded_mode_startup.feature index 27b5cc6cb..c8dd2038d 100644 --- a/tests/e2e/features/degraded_mode_startup.feature +++ b/tests/e2e/features/degraded_mode_startup.feature @@ -1,7 +1,7 @@ @cfg_degraded @skip-in-library-mode @Authorized Feature: Degraded mode startup - End-to-end scenarios that test LCORE startup behavior when llama-stack + End-to-end scenarios that test LCORE startup behavior when ogx is NOT available at startup time and allow_degraded_mode is enabled. These tests verify that LCORE metrics correctly reflect startup state @@ -13,26 +13,26 @@ Feature: Degraded mode startup And REST API service prefix is /v1 And the Lightspeed stack configuration directory is "tests/e2e/configuration" - Scenario: Degraded mode metric is set to 0.0 when started with llama-stack + Scenario: Degraded mode metric is set to 0.0 when started with ogx Given The service uses the lightspeed-stack-degraded.yaml configuration And The service is restarted When I access endpoint "metrics" using HTTP GET method Then The status code of the response is 200 And The response body contains "ls_started_in_degraded_mode 0.0" - Scenario: Degraded mode metric is set to 1.0 when started without llama-stack - Given The llama-stack connection is disrupted + Scenario: Degraded mode metric is set to 1.0 when started without ogx + Given The ogx connection is disrupted And The service uses the lightspeed-stack-degraded.yaml configuration - # Konflux restart-lightspeed otherwise restores llama before LCS boots. - And The service is restarted without restoring llama-stack + # Konflux restart-lightspeed otherwise restores OGX before LCS boots. + And The service is restarted without restoring ogx When I access endpoint "metrics" using HTTP GET method Then The status code of the response is 200 And The response body contains "ls_started_in_degraded_mode 1.0" - Scenario: Readiness endpoint reports degraded state when started without llama-stack - Given The llama-stack connection is disrupted + Scenario: Readiness endpoint reports degraded state when started without ogx + Given The ogx connection is disrupted And The service uses the lightspeed-stack-degraded.yaml configuration - And The service is restarted without restoring llama-stack + And The service is restarted without restoring ogx When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 And The body of the response, ignoring the "providers" field, is the following diff --git a/tests/e2e/features/environment.py b/tests/e2e/features/environment.py index 4e449f86b..6be0216a9 100644 --- a/tests/e2e/features/environment.py +++ b/tests/e2e/features/environment.py @@ -18,13 +18,13 @@ from behave.runner import Context from tests.e2e.features.steps.common import ( - get_llama_stack_hostname, - get_llama_stack_port, + get_ogx_hostname, + get_ogx_port, reset_active_lightspeed_stack_config_basename, ) from tests.e2e.features.steps.health import ( get_ogx_was_running, - reset_llama_stack_disrupt_once_tracking, + reset_ogx_disrupt_once_tracking, reset_ogx_was_running, ) from tests.e2e.features.steps.tls import ( @@ -32,7 +32,6 @@ prepare_tls_feature_entry_on_prow, reset_tls_prow_state, ) -from tests.e2e.utils.ogx_utils import register_shield from tests.e2e.utils.prow_utils import ( restart_pod, restore_ogx_pod, @@ -159,10 +158,10 @@ def _ensure_prow_port_forward(context: Context) -> None: restart-port-forward to re-establish the tunnel before the scenario runs. Treat HTTP 503 like 200/401 here: it means the tunnel reached Lightspeed and the - app responded. ``ogx_disrupted`` leaves Llama stopped on purpose; readiness + app responded. ``ogx_disrupted`` leaves OGX stopped on purpose; readiness then returns 503. Previously we treated 503 as a dead tunnel and ran - ``restart-lightspeed``, which restores Llama via e2e-ops and breaks later scenarios - that skip disruption (once-per-feature) while expecting Llama to stay down. + ``restart-lightspeed``, which restores OGX via e2e-ops and breaks later scenarios + that skip disruption (once-per-feature) while expecting OGX to stay down. """ host = os.getenv("E2E_LSC_HOSTNAME", "localhost") port = os.getenv("E2E_LSC_PORT", "8080") @@ -187,7 +186,7 @@ def _ensure_prow_port_forward(context: Context) -> None: # Port-forward alone failed — the pod itself may be dead (e.g. OGX # was never restored after a disruption feature). Attempt a full restart, - # which also checks Llama health before recreating LCS. + # which also checks OGX health before recreating LCS. print("[before_scenario] Port-forward failed; attempting full pod restart...") try: result = run_e2e_ops("restart-lightspeed", timeout=200) @@ -222,7 +221,7 @@ def before_scenario(context: Context, scenario: Scenario) -> None: # Skip scenarios that require separate OGX container in library mode if context.is_library_mode and "skip-in-library-mode" in scenario.effective_tags: - scenario.skip("Skipped in library mode (no separate llama-stack container)") + scenario.skip("Skipped in library mode (no separate OGX container)") return # Skip scenarios that rely on a non-default BYOK store. Only library mode @@ -232,7 +231,7 @@ def before_scenario(context: Context, scenario: Scenario) -> None: if not context.is_library_mode and "skip-in-server-mode" in scenario.effective_tags: scenario.skip( "Skipped in server mode (feature-specific BYOK store is not loaded " - "into the external llama-stack)" + "into the external OGX service)" ) return @@ -242,6 +241,21 @@ def before_scenario(context: Context, scenario: Scenario) -> None: scenario.skip("Skipped in Prow (requires Docker Compose services)") return + # Skip openai-specific scenarios on non-openai provider matrices: the + # providers workflow runs the full test list with E2E_DEFAULT_PROVIDER_OVERRIDE + # set (azure/watsonx/...), and fixtures that hardcode an openai provider + # (e.g. the unified-mode inference.providers fixture) cannot serve queries + # for those models. + provider_override = os.getenv("E2E_DEFAULT_PROVIDER_OVERRIDE", "") + if "openai-only" in scenario.effective_tags and provider_override not in ( + "", + "openai", + ): + scenario.skip( + f"Skipped on provider matrix '{provider_override}' (openai-only fixture)" + ) + return + # In Prow, verify the lightspeed port-forward is alive before each scenario. # Port-forwards can silently die between scenarios (e.g. pod restart, TCP reset). if is_prow_environment(): @@ -252,15 +266,6 @@ def before_scenario(context: Context, scenario: Scenario) -> None: # Reset force-restart from a prior disrupt/MCP reset scenario. context.force_lightspeed_restart_after_mcp_config_reset = False - # Clear shield unregister state from previous scenarios (see ``shields_are_disabled_for_scenario``). - for _attr in ( - "shields_disabled_for_scenario", - "llama_guard_provider_id", - "llama_guard_provider_shield_id", - ): - if hasattr(context, _attr): - delattr(context, _attr) - def _dump_pod_logs_on_failure( context: Context, scenario: Scenario, namespace: str @@ -268,6 +273,7 @@ def _dump_pod_logs_on_failure( """Dump container logs when a scenario fails in Prow.""" if scenario.status != "failed": return + # Pod names match tests/e2e-prow manifests (legacy llama-stack-service id). pods: tuple[str, ...] = ("llama-stack-service", "lightspeed-stack-service") feature = getattr(context, "feature", None) feat_file = getattr(feature, "filename", "") or "" if feature else "" @@ -287,7 +293,7 @@ def _dump_pod_logs_on_failure( def after_scenario(context: Context, scenario: Scenario) -> None: """Run after each scenario is run. - Perform per-scenario teardown: failure logs (Prow) and shield re-register. + Perform per-scenario teardown: failure logs (Prow). If ``configure_service`` applied a non-baseline YAML during the scenario (``context.scenario_lightspeed_override_active``), clears that flag only; @@ -302,9 +308,9 @@ def after_scenario(context: Context, scenario: Scenario) -> None: - is_library_mode (bool): whether tests run in library mode. - ogx_was_running (bool, optional): whether OGX was running before the scenario. - - hostname_llama, port_llama (str/int, optional): host and port + - hostname_ogx, port_ogx (str/int, optional): host and port used for the OGX health check. - scenario (Scenario): Behave scenario (unused; shield restore uses context flags). + scenario (Scenario): Behave scenario used for failure log dumps in Prow. """ if is_prow_environment(): _dump_pod_logs_on_failure( @@ -314,28 +320,13 @@ def after_scenario(context: Context, scenario: Scenario) -> None: if getattr(context, "scenario_lightspeed_override_active", False): context.scenario_lightspeed_override_active = False - # Re-register shield if ``Given shields are disabled for this scenario`` unregistered it. - if getattr(context, "shields_disabled_for_scenario", False): - provider_id = getattr(context, "llama_guard_provider_id", None) - provider_shield_id = getattr(context, "llama_guard_provider_shield_id", None) - if provider_id is not None and provider_shield_id is not None: - try: - register_shield( - "llama-guard", - provider_id=provider_id, - provider_shield_id=provider_shield_id, - ) - print("Re-registered shield llama-guard") - except (TypeError, ValueError, RuntimeError, KeyboardInterrupt) as e: - print(f"Warning: Could not re-register shield: {e}") - def _print_ogx_diagnostics() -> None: """Print container state, health, and recent logs to diagnose why OGX did not recover.""" - print("--- llama-stack diagnostics ---") + print("--- ogx diagnostics ---") for label, cmd in [ - ("State", ["docker", "inspect", "--format={{.State}}", "llama-stack"]), - ("Health", ["docker", "inspect", "--format={{.State.Health}}", "llama-stack"]), + ("State", ["docker", "inspect", "--format={{.State}}", "ogx"]), + ("Health", ["docker", "inspect", "--format={{.State.Health}}", "ogx"]), ]: try: r = subprocess.run( @@ -346,7 +337,7 @@ def _print_ogx_diagnostics() -> None: print(f" {label}: (inspect timed out)") try: r = subprocess.run( - ["docker", "logs", "--tail", "40", "llama-stack"], + ["docker", "logs", "--tail", "40", "ogx"], capture_output=True, text=True, timeout=10, @@ -361,10 +352,10 @@ def _print_ogx_diagnostics() -> None: print("--- end diagnostics ---") -def _restore_llama_stack() -> None: +def _restore_ogx_service() -> None: """Restore OGX connection after disruption.""" if is_prow_environment(): - # Recreate llama pod, then restart LCS so in-process clients reconnect (Llama IP/pod changed). + # Recreate OGX pod, then restart LCS so in-process clients reconnect (OGX IP/pod changed). try: restore_ogx_pod() except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: @@ -380,7 +371,7 @@ def _restore_llama_stack() -> None: "✓ Prow: OGX restored and lightspeed-stack restarted " "for clean reconnect" ) - reset_llama_stack_disrupt_once_tracking() + reset_ogx_disrupt_once_tracking() return except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: last_lcs_err = e @@ -398,9 +389,7 @@ def _restore_llama_stack() -> None: try: # Start the OGX container again - subprocess.run( - ["docker", "start", "llama-stack"], check=True, capture_output=True - ) + subprocess.run(["docker", "start", "ogx"], check=True, capture_output=True) # Wait for the service to be healthy print("Restoring OGX connection...") @@ -411,10 +400,10 @@ def _restore_llama_stack() -> None: [ "docker", "exec", - "llama-stack", + "ogx", "curl", "-sf", - f"http://{get_llama_stack_hostname()}:{get_llama_stack_port()}/v1/health", + f"http://{get_ogx_hostname()}:{get_ogx_port()}/v1/health", ], capture_output=True, timeout=5, @@ -422,7 +411,7 @@ def _restore_llama_stack() -> None: ) if result.returncode == 0: print("✓ OGX connection restored successfully") - reset_llama_stack_disrupt_once_tracking() + reset_ogx_disrupt_once_tracking() break except subprocess.TimeoutExpired: print( @@ -468,8 +457,8 @@ def before_feature(context: Context, feature: Feature) -> None: context.feature_config = None context.scenario_lightspeed_override_active = False context.active_lightspeed_stack_config_basename = None - # One real Llama disruption per feature (module-level flag; survives context resets) - reset_llama_stack_disrupt_once_tracking() + # One real OGX disruption per feature (module-level flag; survives context resets) + reset_ogx_disrupt_once_tracking() if feature.filename and is_tls_feature_file(feature.filename): reset_tls_prow_state() prepare_tls_feature_entry_on_prow(feature.filename) @@ -510,7 +499,7 @@ def after_feature(context: Context, feature: Feature) -> None: # Read from module-level state — Behave clears custom context attributes # between scenarios, so context.ogx_was_running is unreliable here. if get_ogx_was_running(): - _restore_llama_stack() + _restore_ogx_service() reset_ogx_was_running() if getattr(context, "feedback_e2e_conversation_cleanup", False): @@ -531,8 +520,16 @@ def after_feature(context: Context, feature: Feature) -> None: switch_config(backup_path) remove_config_backup(backup_path) if not context.is_library_mode: - restart_container("llama-stack") - restart_container("lightspeed-stack") + restart_container("ogx") + # restart_container hard-fails for lightspeed-stack when the + # service does not accept HTTP in time. That is right inside a + # scenario, but this runs in after_feature: an exception here is a + # hook error that takes down the whole run rather than failing one + # scenario. Warn and let the next feature's own restart surface it. + try: + restart_container("lightspeed-stack") + except AssertionError as exc: + print(f"⚠ after_feature restore: lightspeed-stack not ready ({exc})") reset_active_lightspeed_stack_config_basename() else: remove_config_backup(backup_path) diff --git a/tests/e2e/features/info.feature b/tests/e2e/features/info.feature index 0cd4be200..a0c5b2c85 100644 --- a/tests/e2e/features/info.feature +++ b/tests/e2e/features/info.feature @@ -18,7 +18,7 @@ Feature: Info tests Scenario: Check if info endpoint is working When I access REST API endpoint "info" using HTTP GET method Then The status code of the response is 200 - And The body of the response has proper name Lightspeed Core Service (LCS) and version 0.7.0rc2 + And The body of the response has proper name Lightspeed Core Service (LCS) and version 0.7.0rc3 And The body of the response has ogx version 1.2.5 Scenario: Check if tools endpoint is working diff --git a/tests/e2e/features/ogx_disrupted.feature b/tests/e2e/features/ogx_disrupted.feature index 32fbee99e..73fa4da85 100644 --- a/tests/e2e/features/ogx_disrupted.feature +++ b/tests/e2e/features/ogx_disrupted.feature @@ -1,7 +1,7 @@ @skip-in-library-mode @Authorized -Feature: Llama Stack connection disrupted +Feature: OGX connection disrupted - End-to-end scenarios that stop the Llama Stack container (or simulate disconnect) and + End-to-end scenarios that stop the OGX container (or simulate disconnect) and assert degraded responses (503, readiness, etc.). Config order matches test_list.txt: default stack, then noop-token (query/conversations/…), then rbac (rlsapi errors). Skipped in library mode. @@ -18,11 +18,11 @@ Feature: Llama Stack connection disrupted # --- @cfg_default --- @cfg_default - Scenario: Check if models endpoint reports error when llama-stack is unreachable + Scenario: Check if models endpoint reports error when ogx is unreachable Given The service uses the lightspeed-stack-default.yaml configuration And The service is restarted Given The system is in default state - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "models" using HTTP GET method Then The status code of the response is 503 And The body of the response is the following @@ -32,11 +32,11 @@ Feature: Llama Stack connection disrupted @cfg_default - Scenario: Check if service report proper readiness state when llama stack is not available + Scenario: Check if service report proper readiness state when OGX is not available Given The service uses the lightspeed-stack-default.yaml configuration And The service is restarted Given The system is in default state - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 503 And The body of the response, ignoring the "providers" field, is the following @@ -46,11 +46,11 @@ Feature: Llama Stack connection disrupted @cfg_default - Scenario: Check if service report proper liveness state even when llama stack is not available + Scenario: Check if service report proper liveness state even when OGX is not available Given The service uses the lightspeed-stack-default.yaml configuration And The service is restarted Given The system is in default state - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access endpoint "liveness" using HTTP GET method Then The status code of the response is 200 And The body of the response is the following @@ -60,10 +60,10 @@ Feature: Llama Stack connection disrupted @cfg_default - Scenario: Check if info endpoint reports error when llama-stack connection is not working + Scenario: Check if info endpoint reports error when ogx connection is not working Given The service uses the lightspeed-stack-default.yaml configuration And The service is restarted - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "info" using HTTP GET method Then The status code of the response is 503 And The body of the response is the following @@ -75,10 +75,10 @@ Feature: Llama Stack connection disrupted # --- @cfg_default (noop auth; tools list needs no bearer) --- @cfg_default - Scenario: Check if tools endpoint reports error when llama-stack is unreachable + Scenario: Check if tools endpoint reports error when ogx is unreachable Given The service uses the lightspeed-stack-default.yaml configuration And The service is restarted - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "tools" using HTTP GET method Then The status code of the response is 503 And The body of the response is the following @@ -90,12 +90,12 @@ Feature: Llama Stack connection disrupted # --- lightspeed-stack-authorized.yaml (aligned with query, responses, conversations, …) --- @cfg_authorized - Scenario: Check if LLM responds for query request with error for inability to connect to llama-stack - Given Llama Stack is restarted + Scenario: Check if LLM responds for query request with error for inability to connect to ogx + Given OGX is restarted And The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I use "query" to ask question with authorization header """ {"query": "Say hello"} @@ -105,11 +105,11 @@ Feature: Llama Stack connection disrupted @cfg_authorized - Scenario: Responses returns error when unable to connect to llama-stack + Scenario: Responses returns error when unable to connect to ogx Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted Given The system is in default state - And The llama-stack connection is disrupted + And The ogx connection is disrupted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva When I use "responses" to ask question with authorization header """ @@ -120,11 +120,11 @@ Feature: Llama Stack connection disrupted @cfg_authorized - Scenario: Streaming responses returns error when unable to connect to llama-stack + Scenario: Streaming responses returns error when unable to connect to ogx Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I use "responses" to ask question with authorization header """ {"input": "Say hello", "model": "{PROVIDER}/{MODEL}", "stream": true} @@ -134,33 +134,33 @@ Feature: Llama Stack connection disrupted @cfg_authorized - Scenario: Check if rags endpoint fails when llama-stack is unavailable + Scenario: Check if rags endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint rags using HTTP GET method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check if prompts list endpoint fails when llama-stack is unavailable + Scenario: Check if prompts list endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "prompts" using HTTP GET method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check if prompts create endpoint fails when llama-stack is unavailable + Scenario: Check if prompts create endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "prompts" using HTTP POST method """ {"prompt": "Summarize: {{text}}", "variables": ["text"]} @@ -170,22 +170,22 @@ Feature: Llama Stack connection disrupted @cfg_authorized - Scenario: Check if prompts get by id endpoint fails when llama-stack is unavailable + Scenario: Check if prompts get by id endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "prompts/pmpt_5c76d7f7c633ef97477adeb2f642150d8d08e8a6526e9909" using HTTP GET method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check if prompts update endpoint fails when llama-stack is unavailable + Scenario: Check if prompts update endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "prompts/pmpt_5c76d7f7c633ef97477adeb2f642150d8d08e8a6526e9909" using HTTP PUT method """ {"prompt": "Summarize in bullets: {{text}}", "version": 1, "set_as_default": true, "variables": ["text"]} @@ -195,19 +195,19 @@ Feature: Llama Stack connection disrupted @cfg_authorized - Scenario: Check if prompts delete endpoint fails when llama-stack is unavailable + Scenario: Check if prompts delete endpoint fails when ogx is unavailable Given The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I access REST API endpoint "prompts/pmpt_5c76d7f7c633ef97477adeb2f642150d8d08e8a6526e9909" using HTTP DELETE method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check if conversations/{conversation_id} GET endpoint fails when llama-stack is unavailable - Given Llama Stack is restarted + Scenario: Check if conversations/{conversation_id} GET endpoint fails when ogx is unavailable + Given OGX is restarted And The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva @@ -217,15 +217,15 @@ Feature: Llama Stack connection disrupted """ And The status code of the response is 200 And I store conversation details - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I use REST API conversation endpoint with conversation_id from above using HTTP GET method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check if conversations/{conversation_id} DELETE endpoint fails when llama-stack is unavailable - Given Llama Stack is restarted + Scenario: Check if conversations/{conversation_id} DELETE endpoint fails when ogx is unavailable + Given OGX is restarted And The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva @@ -235,15 +235,15 @@ Feature: Llama Stack connection disrupted """ And The status code of the response is 200 And I store conversation details - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I use REST API conversation endpoint with conversation_id from above using HTTP DELETE method Then The status code of the response is 503 And The body of the response contains Unable to connect to OGX @cfg_authorized - Scenario: Check conversations/{conversation_id} works when llama-stack is down - Given Llama Stack is restarted + Scenario: Check conversations/{conversation_id} works when ogx is down + Given OGX is restarted And The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva @@ -254,7 +254,7 @@ Feature: Llama Stack connection disrupted """ And The status code of the response is 200 And I store conversation details - And The llama-stack connection is disrupted + And The ogx connection is disrupted And REST API service prefix is /v2 When I access REST API endpoint "conversations" using HTTP GET method Then The status code of the response is 200 @@ -268,8 +268,8 @@ Feature: Llama Stack connection disrupted # --- still @cfg_authorized (noop-with-token; not RBAC) --- @cfg_authorized - Scenario: V2 conversations DELETE endpoint works even when llama-stack is down - Given Llama Stack is restarted + Scenario: V2 conversations DELETE endpoint works even when ogx is down + Given OGX is restarted And The service uses the lightspeed-stack-authorized.yaml configuration And The service is restarted And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva @@ -280,7 +280,7 @@ Feature: Llama Stack connection disrupted """ And The status code of the response is 200 And I store conversation details - And The llama-stack connection is disrupted + And The ogx connection is disrupted And REST API service prefix is /v2 When I use REST API conversation endpoint with conversation_id from above using HTTP DELETE method Then The status code of the response is 200 @@ -297,12 +297,12 @@ Feature: Llama Stack connection disrupted # --- lightspeed-stack-rbac.yaml (aligned with rbac.feature / rlsapi_v1_errors.feature) --- @RBAC @cfg_rbac - Scenario: Returns 503 when llama-stack connection is broken - Given Llama Stack is restarted + Scenario: Returns 503 when ogx connection is broken + Given OGX is restarted And The service uses the lightspeed-stack-rbac.yaml configuration And The service is restarted And I authenticate as "user" user - And The llama-stack connection is disrupted + And The ogx connection is disrupted When I use "infer" to ask question with authorization header """ {"question": "How do I list files?"} diff --git a/tests/e2e/features/okp_rag.feature b/tests/e2e/features/okp_rag.feature index fb5c6654f..64211c258 100644 --- a/tests/e2e/features/okp_rag.feature +++ b/tests/e2e/features/okp_rag.feature @@ -38,7 +38,7 @@ Feature: OKP(Solr) RAG retrieval tests Scenario: Online mode streaming query with inline RAG returns referenced_documents Given The service uses the lightspeed-stack-okp-online.yaml configuration - And Llama Stack is restarted + And OGX is restarted And The service is restarted When I use "streaming_query" to ask question with authorization header """ diff --git a/tests/e2e/features/proxy.feature b/tests/e2e/features/proxy.feature index 17c8be4f5..0eb5edc7b 100644 --- a/tests/e2e/features/proxy.feature +++ b/tests/e2e/features/proxy.feature @@ -1,13 +1,13 @@ @cfg_default @skip-in-library-mode @skip-in-prow -Feature: Proxy and TLS networking tests for Llama Stack providers +Feature: Proxy and TLS networking tests for OGX providers - Verify that the Lightspeed Stack works correctly when Llama Stack's + Verify that the Lightspeed Stack works correctly when OGX's remote inference providers are configured with proxy and TLS settings via the run.yaml NetworkConfig. - Query bodies use shield_ids: [] because Llama Guard moderation can issue - separate provider calls inside Llama Stack that may not inherit the same - proxy/TLS CA trust as the scenario's remote inference provider. + Query bodies use shield_ids: [] so LCORE-owned shields (e.g. pii-redaction in + the default config) do not run; scenarios then exercise only the remote + inference provider's proxy/TLS path. Background: Given The service is started locally @@ -16,7 +16,7 @@ Feature: Proxy and TLS networking tests for Llama Stack providers And the Lightspeed stack configuration directory is "tests/e2e/configuration" And The service uses the lightspeed-stack-default.yaml configuration And The service is restarted - And The original Llama Stack config is restored if modified + And The original OGX config is restored if modified # --- AC1: Tunnel proxy routing --- @@ -24,8 +24,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @TunnelProxy Scenario: LLM traffic is routed through a configured tunnel proxy Given A tunnel proxy is running on port 8888 - And Llama Stack is configured to route inference through the tunnel proxy - And Llama Stack is restarted + And OGX is configured to route inference through the tunnel proxy + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -40,8 +40,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @TunnelProxy Scenario: LLM query fails gracefully when proxy is unreachable - Given Llama Stack is configured to route inference through proxy "http://127.0.0.1:19999" - And Llama Stack is restarted + Given OGX is configured to route inference through proxy "http://127.0.0.1:19999" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -56,8 +56,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @InterceptionProxy @flaky Scenario: LLM traffic works through interception proxy with correct CA Given An interception proxy with trustme CA is running on port 8889 - And Llama Stack is configured to route inference through the interception proxy with CA cert - And Llama Stack is restarted + And OGX is configured to route inference through the interception proxy with CA cert + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -69,8 +69,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @InterceptionProxy Scenario: LLM query fails when interception proxy CA is not provided Given An interception proxy with trustme CA is running on port 8890 - And Llama Stack is configured to route inference through the interception proxy without CA cert - And Llama Stack is restarted + And OGX is configured to route inference through the interception proxy without CA cert + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -84,8 +84,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @TLSVersion @flaky Scenario: TLS minimum version TLSv1.2 is respected - Given Llama Stack is configured with minimum TLS version "TLSv1.2" - And Llama Stack is restarted + Given OGX is configured with minimum TLS version "TLSv1.2" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -95,8 +95,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @TLSVersion @flaky Scenario: TLS minimum version TLSv1.3 is respected - Given Llama Stack is configured with minimum TLS version "TLSv1.3" - And Llama Stack is restarted + Given OGX is configured with minimum TLS version "TLSv1.3" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -106,8 +106,8 @@ Feature: Proxy and TLS networking tests for Llama Stack providers @TLSCipher @flaky Scenario: Custom cipher suite configuration is respected - Given Llama Stack is configured with ciphers "ECDHE+AESGCM:DHE+AESGCM" - And Llama Stack is restarted + Given OGX is configured with ciphers "ECDHE+AESGCM:DHE+AESGCM" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ diff --git a/tests/e2e/features/query.feature b/tests/e2e/features/query.feature index 67fd86322..8bdb7b701 100644 --- a/tests/e2e/features/query.feature +++ b/tests/e2e/features/query.feature @@ -317,7 +317,6 @@ Scenario: Check if LLM responds for query request with error for missing query #https://issues.redhat.com/browse/LCORE-1387 @skip Scenario: Check if query without shields returns 413 when question is too long for model context - Given shields are disabled for this scenario When I use "query" to ask question with too-long query and authorization header Then The status code of the response is 413 And The body of the response contains Prompt is too long diff --git a/tests/e2e/features/shields.feature b/tests/e2e/features/shields.feature index 5914b9012..1d8981627 100644 --- a/tests/e2e/features/shields.feature +++ b/tests/e2e/features/shields.feature @@ -2,7 +2,7 @@ Feature: Shields endpoint tests Tests for the LCORE-owned GET /v1/shields catalog endpoint. These shields (question_validity, redaction) are configured directly in - lightspeed-stack.yaml; they are not Llama Stack / OGX Safety API resources. + lightspeed-stack.yaml; they are not OGX Safety API resources. See docs/user_doc/shields_guide.md for the full shield configuration and runtime-behavior reference. diff --git a/tests/e2e/features/steps/README.md b/tests/e2e/features/steps/README.md index db2233cfe..1c4125b5f 100644 --- a/tests/e2e/features/steps/README.md +++ b/tests/e2e/features/steps/README.md @@ -64,10 +64,6 @@ Behave steps for POST /v1/responses (LCORE Responses API) multi-turn tests. rlsapi v1 endpoint test steps. -## [shields.py](shields.py) - -Behave steps for temporarily disabling OGX shields in e2e (server mode). - ## [tls.py](tls.py) Step definitions for TLS configuration e2e tests. diff --git a/tests/e2e/features/steps/common.py b/tests/e2e/features/steps/common.py index b11e292f3..5ce825486 100644 --- a/tests/e2e/features/steps/common.py +++ b/tests/e2e/features/steps/common.py @@ -23,7 +23,7 @@ # Behave clears user attributes on ``context`` between scenarios; store # OGX endpoint info at module level so ``after_feature`` can see it. -_llama_stack_endpoint: dict[str, str] = {"hostname": "localhost", "port": "8321"} +_ogx_endpoint: dict[str, str] = {"hostname": "localhost", "port": "8321"} def reset_active_lightspeed_stack_config_basename() -> None: @@ -40,14 +40,14 @@ def get_active_lightspeed_stack_config_basename() -> Optional[str]: return _active_lightspeed_stack_config_basename["basename"] -def get_llama_stack_hostname() -> str: +def get_ogx_hostname() -> str: """Return the OGX hostname surviving per-scenario context clearing.""" - return _llama_stack_endpoint["hostname"] + return _ogx_endpoint["hostname"] -def get_llama_stack_port() -> str: +def get_ogx_port() -> str: """Return the OGX port surviving per-scenario context clearing.""" - return _llama_stack_endpoint["port"] + return _ogx_endpoint["port"] @given("The service is started locally") @@ -65,12 +65,12 @@ def service_is_started_locally(context: Context) -> None: context.hostname = os.getenv("E2E_LSC_HOSTNAME", "localhost") context.port = os.getenv("E2E_LSC_PORT", "8080") if is_prow_environment(): - context.hostname_llama = os.getenv("E2E_LLAMA_HOSTNAME", "localhost") + context.hostname_ogx = os.getenv("E2E_OGX_HOSTNAME", "localhost") else: - context.hostname_llama = "localhost" - context.port_llama = os.getenv("E2E_LLAMA_PORT", "8321") - _llama_stack_endpoint["hostname"] = context.hostname_llama - _llama_stack_endpoint["port"] = context.port_llama + context.hostname_ogx = "localhost" + context.port_ogx = os.getenv("E2E_OGX_PORT", "8321") + _ogx_endpoint["hostname"] = context.hostname_ogx + _ogx_endpoint["port"] = context.port_ogx @given('the Lightspeed stack configuration directory is "{directory}"') @@ -120,7 +120,7 @@ def configure_service(context: Context, config_name: str) -> None: """ config_name = config_name.strip() if _active_lightspeed_stack_config_basename["basename"] == config_name: - # MCP reset or llama disrupt: do not skip the next restart. + # MCP reset or OGX disrupt: do not skip the next restart. if getattr(context, "force_lightspeed_restart_after_mcp_config_reset", False): context.lightspeed_stack_skip_restart = False context.force_lightspeed_restart_after_mcp_config_reset = False @@ -201,19 +201,19 @@ def restart_service(context: Context) -> None: restart_container("lightspeed-stack") -@given("The service is restarted without restoring llama-stack") -def restart_service_without_restoring_llama(context: Context) -> None: - """Restart LCS while leaving llama disrupted (degraded-mode startup e2e). +@given("The service is restarted without restoring ogx") +def restart_service_without_restoring_ogx(context: Context) -> None: + """Restart LCS while leaving OGX disrupted (degraded-mode startup e2e). - On Prow/Konflux, the default ``restart-lightspeed`` path restores llama when + On Prow/Konflux, the default ``restart-lightspeed`` path restores OGX when it is unhealthy so LCS can come up. Degraded-mode scenarios need the - opposite: LCS must boot with llama still down. Docker Compose already + opposite: LCS must boot with OGX still down. Docker Compose already restarts only the LCS container, so this matches local server-mode behavior. """ if getattr(context, "lightspeed_stack_skip_restart", False): context.lightspeed_stack_skip_restart = False return - restart_lightspeed_stack_service(skip_llama_restore=True, wait_http=False) + restart_lightspeed_stack_service(skip_ogx_restore=True) @given("The system is in default state") diff --git a/tests/e2e/features/steps/common_http.py b/tests/e2e/features/steps/common_http.py index 5ce2bd52c..7efffef5d 100644 --- a/tests/e2e/features/steps/common_http.py +++ b/tests/e2e/features/steps/common_http.py @@ -35,7 +35,7 @@ def check_status_code(context: Context, status: int) -> None: error_body = context.response.json() except JSONDecodeError: error_body = context.response.text - assert False, ( + raise AssertionError( f"Status code is {context.response.status_code}, expected {status}. " f"Response: {error_body}" ) @@ -52,7 +52,7 @@ def check_status_code_one_of(context: Context, first: int, second: int) -> None: error_body = context.response.json() except JSONDecodeError: error_body = context.response.text - assert False, ( + raise AssertionError( f"Status code is {actual}, expected one of {sorted(allowed)}. " f"Response: {error_body}" ) diff --git a/tests/e2e/features/steps/health.py b/tests/e2e/features/steps/health.py index 4afbc0dd4..7409da4bd 100644 --- a/tests/e2e/features/steps/health.py +++ b/tests/e2e/features/steps/health.py @@ -11,7 +11,7 @@ # Behave may clear user attributes on ``context`` between scenarios; keep this # in module scope so "disrupt once per feature" survives per-scenario resets. # Mutate one dict entry so we need not reassign a module-level bool (no global). -_llama_stack_disrupt_once: dict[str, bool] = {"applied": False} +_ogx_disrupt_once: dict[str, bool] = {"applied": False} # Behave clears user attributes on ``context`` between scenarios; store # ``was_running`` at module level so ``after_feature`` can still see it. @@ -28,40 +28,34 @@ def reset_ogx_was_running() -> None: _ogx_was_running["value"] = False -def reset_llama_stack_disrupt_once_tracking() -> None: +def reset_ogx_disrupt_once_tracking() -> None: """Reset before each feature; see ``environment.before_feature``.""" - _llama_stack_disrupt_once["applied"] = False + _ogx_disrupt_once["applied"] = False _ogx_was_running["value"] = False -def _force_lightspeed_restart_after_llama_disrupt(context: Context) -> None: +def _force_lightspeed_restart_after_ogx_disrupt(context: Context) -> None: """Do not skip the next Lightspeed restart after OGX is disrupted.""" context.force_lightspeed_restart_after_mcp_config_reset = True context.lightspeed_stack_skip_restart = False -def _force_lightspeed_restart_after_llama_disrupt(context: Context) -> None: - """Do not skip the next Lightspeed restart after Llama is disrupted.""" - context.force_lightspeed_restart_after_mcp_config_reset = True - context.lightspeed_stack_skip_restart = False - - -@given("The llama-stack connection is disrupted") -def llama_stack_connection_broken(context: Context) -> None: - """Break llama_stack connection by stopping the container. +@given("The ogx connection is disrupted") +def ogx_connection_broken(context: Context) -> None: + """Break the OGX connection by stopping the container. Disrupts the OGX service by stopping its Docker container and records whether it was running. The real disruption runs only once per feature until OGX is running again: the first invocation performs Docker/Prow disruption; later invocations no-op. - ``reset_llama_stack_disrupt_once_tracking`` clears the skip flag from + ``reset_ogx_disrupt_once_tracking`` clears the skip flag from ``before_feature`` and after OGX is restored (``restart_container``, - ``_restore_llama_stack``) so the next disrupt step stops the container again. + ``_restore_ogx_service``) so the next disrupt step stops the container again. Tracking uses module state (not ``context`` alone) because Behave can clear custom attributes on ``context`` between scenarios. - Checks whether the Docker container named "llama-stack" is running; if it + Checks whether the Docker container named "ogx" is running; if it is, stops the container, waits briefly for the disruption to take effect, and sets `context.ogx_was_running` to True so callers can restore state later. If the container is not running, the flag remains False. On @@ -73,9 +67,9 @@ def llama_stack_connection_broken(context: Context) -> None: context (behave.runner.Context): Behave context used to store `ogx_was_running` and share state between steps. """ - if _llama_stack_disrupt_once["applied"]: + if _ogx_disrupt_once["applied"]: print("OGX disruption skipped (already applied once this feature)") - _force_lightspeed_restart_after_llama_disrupt(context) + _force_lightspeed_restart_after_ogx_disrupt(context) return # Store original state for restoration (only on the real disruption path). @@ -85,19 +79,19 @@ def llama_stack_connection_broken(context: Context) -> None: _ogx_was_running["value"] = False if is_prow_environment(): - from tests.e2e.utils.prow_utils import disrupt_llama_stack_pod + from tests.e2e.utils.prow_utils import disrupt_ogx_pod - was_running = disrupt_llama_stack_pod() + was_running = disrupt_ogx_pod() context.ogx_was_running = was_running _ogx_was_running["value"] = was_running - _llama_stack_disrupt_once["applied"] = True - _force_lightspeed_restart_after_llama_disrupt(context) + _ogx_disrupt_once["applied"] = True + _force_lightspeed_restart_after_ogx_disrupt(context) return # Docker-based disruption try: result = subprocess.run( - ["docker", "inspect", "-f", "{{.State.Running}}", "llama-stack"], + ["docker", "inspect", "-f", "{{.State.Running}}", "ogx"], capture_output=True, text=True, check=True, @@ -106,9 +100,7 @@ def llama_stack_connection_broken(context: Context) -> None: if result.stdout.strip(): context.ogx_was_running = True _ogx_was_running["value"] = True - subprocess.run( - ["docker", "stop", "llama-stack"], check=True, capture_output=True - ) + subprocess.run(["docker", "stop", "ogx"], check=True, capture_output=True) # Wait a moment for the connection to be fully disrupted time.sleep(2) @@ -121,5 +113,5 @@ def llama_stack_connection_broken(context: Context) -> None: print(f"Warning: Could not disrupt OGX connection: {e}") return - _llama_stack_disrupt_once["applied"] = True - _force_lightspeed_restart_after_llama_disrupt(context) + _ogx_disrupt_once["applied"] = True + _force_lightspeed_restart_after_ogx_disrupt(context) diff --git a/tests/e2e/features/steps/proxy.py b/tests/e2e/features/steps/proxy.py index eb2e3ced3..f5651b6cb 100644 --- a/tests/e2e/features/steps/proxy.py +++ b/tests/e2e/features/steps/proxy.py @@ -34,15 +34,14 @@ from tests.e2e.proxy.tunnel_proxy import DEFAULT_PROXY_PORT from tests.e2e.utils.ogx_config_utils import ( backup_ogx_config, - load_llama_config, - restore_llama_config_if_modified, - write_llama_config, + load_ogx_config, + restore_ogx_config_if_modified, + write_ogx_config, ) from tests.e2e.utils.prow_utils import get_namespace, run_e2e_ops from tests.e2e.utils.utils import ( is_prow_environment, restart_container, - wait_for_lightspeed_stack_http_ready, ) _CLUSTER_INTERCEPTION_PROXY_PORTS = frozenset( @@ -55,12 +54,12 @@ def _is_docker_mode() -> bool: if is_prow_environment(): return False result = subprocess.run( - ["docker", "ps", "--filter", "name=llama-stack", "--format", "{{.Names}}"], + ["docker", "ps", "--filter", "name=ogx", "--format", "{{.Names}}"], capture_output=True, text=True, check=False, ) - return "llama-stack" in result.stdout + return "ogx" in result.stdout def _host_special_dns_from_container(hostname: str) -> Optional[str]: @@ -89,7 +88,7 @@ def _host_special_dns_from_container(hostname: str) -> Optional[str]: [ "docker", "exec", - "llama-stack", + "ogx", "python3", "-c", probe, @@ -188,11 +187,11 @@ def _fetch_cluster_interception_proxy_stats() -> dict[str, Any]: return stats -_INTERCEPTION_CA_LLAMA_PATH = "/tmp/interception-proxy-ca.pem" +_INTERCEPTION_CA_OGX_PATH = "/tmp/interception-proxy-ca.pem" def _sync_interception_proxy_ca_secret() -> None: - """Publish trustme CA to Secret ``e2e-interception-proxy-ca`` (mounted by llama pod).""" + """Publish trustme CA to Secret ``e2e-interception-proxy-ca`` (mounted by OGX pod).""" result = run_e2e_ops("sync-interception-proxy-ca-secret", timeout=90) print(result.stdout, end="") if result.returncode != 0: @@ -298,7 +297,7 @@ def _stop_proxy(context: Context, attr: str, loop_attr: str) -> None: delattr(context, loop_attr) -@given("The original Llama Stack config is restored if modified") +@given("The original OGX config is restored if modified") def restore_if_modified(context: Context) -> None: """Restore original run.yaml if a previous scenario modified it. @@ -309,37 +308,36 @@ def restore_if_modified(context: Context) -> None: # Stop any leftover proxy servers from previous scenario _stop_proxy(context, "tunnel_proxy", "proxy_loop") _stop_proxy(context, "interception_proxy", "interception_proxy_loop") - os.environ.pop("E2E_COPY_INTERCEPTION_CA_TO_LLAMA", None) - os.environ.pop("E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA", None) - if hasattr(context, "needs_interception_ca_on_llama"): - delattr(context, "needs_interception_ca_on_llama") + os.environ.pop("E2E_COPY_INTERCEPTION_CA_TO_OGX", None) + os.environ.pop("E2E_COPY_MOCK_TLS_CERTS_TO_OGX", None) + if hasattr(context, "needs_interception_ca_on_ogx"): + delattr(context, "needs_interception_ca_on_ogx") - if restore_llama_config_if_modified(): + if restore_ogx_config_if_modified(): print("Restoring original OGX config from backup...") # --- Service Restart Steps --- -@given("Llama Stack is restarted") +@given("OGX is restarted") def restart_ogx(context: Context) -> None: """Restart the OGX container.""" from tests.e2e.features.steps.tls import ( is_tls_configuration_feature, - restart_llama_for_tls_feature, + restart_ogx_for_tls_feature, ) if is_tls_configuration_feature(context): - restart_llama_for_tls_feature(context) + restart_ogx_for_tls_feature(context) return - restart_container("llama-stack") + restart_container("ogx") @given("Lightspeed Stack is restarted") def restart_lightspeed_stack(context: Context) -> None: """Restart the Lightspeed Stack container.""" restart_container("lightspeed-stack") - wait_for_lightspeed_stack_http_ready() # --- Tunnel Proxy Steps --- @@ -392,8 +390,8 @@ def run_proxy() -> None: time.sleep(1) -@given("Llama Stack is configured to route inference through the tunnel proxy") -def configure_llama_tunnel_proxy(context: Context) -> None: +@given("OGX is configured to route inference through the tunnel proxy") +def configure_ogx_tunnel_proxy(context: Context) -> None: """Modify run.yaml with proxy config pointing to the tunnel proxy.""" backup_ogx_config() if is_prow_environment(): @@ -402,7 +400,7 @@ def configure_llama_tunnel_proxy(context: Context) -> None: proxy = context.tunnel_proxy proxy_port = proxy.port proxy_host = _get_proxy_host(context.is_docker_mode) - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -413,14 +411,14 @@ def configure_llama_tunnel_proxy(context: Context) -> None: } } - write_llama_config(config) + write_ogx_config(config) -@given('Llama Stack is configured to route inference through proxy "{proxy_url}"') -def configure_llama_unreachable_proxy(context: Context, proxy_url: str) -> None: +@given('OGX is configured to route inference through proxy "{proxy_url}"') +def configure_ogx_unreachable_proxy(context: Context, proxy_url: str) -> None: """Modify run.yaml with a proxy URL (may be unreachable).""" backup_ogx_config() - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -431,7 +429,7 @@ def configure_llama_unreachable_proxy(context: Context, proxy_url: str) -> None: } } - write_llama_config(config) + write_ogx_config(config) # --- Interception Proxy Steps --- @@ -444,7 +442,7 @@ def start_interception_proxy(context: Context, port: int) -> None: cluster_port = _cluster_interception_proxy_port(port) context.interception_proxy = None context.cluster_interception_proxy_port = cluster_port - context.ca_cert_path_for_config = _INTERCEPTION_CA_LLAMA_PATH + context.ca_cert_path_for_config = _INTERCEPTION_CA_OGX_PATH _deploy_cluster_interception_proxy() print( f"Using in-cluster interception proxy at " @@ -466,7 +464,7 @@ def start_interception_proxy(context: Context, port: int) -> None: if context.is_docker_mode: container_cert_path = "/tmp/interception-proxy-ca.pem" subprocess.run( - ["docker", "cp", str(ca_cert_path), f"llama-stack:{container_cert_path}"], + ["docker", "cp", str(ca_cert_path), f"ogx:{container_cert_path}"], check=True, ) context.ca_cert_path_for_config = container_cert_path @@ -499,15 +497,15 @@ def run_proxy() -> None: @given( - "Llama Stack is configured to route inference through " + "OGX is configured to route inference through " "the interception proxy with CA cert" ) -def configure_llama_interception_with_ca(context: Context) -> None: +def configure_ogx_interception_with_ca(context: Context) -> None: """Modify run.yaml with interception proxy and CA cert config.""" backup_ogx_config() - context.needs_interception_ca_on_llama = True + context.needs_interception_ca_on_ogx = True if is_prow_environment(): - os.environ["E2E_COPY_INTERCEPTION_CA_TO_LLAMA"] = "1" + os.environ["E2E_COPY_INTERCEPTION_CA_TO_OGX"] = "1" if is_prow_environment(): proxy_port = getattr( context, "cluster_interception_proxy_port", DEFAULT_INTERCEPTION_PROXY_PORT @@ -517,7 +515,7 @@ def configure_llama_interception_with_ca(context: Context) -> None: proxy = context.interception_proxy proxy_port = proxy.port proxy_host = _get_proxy_host(context.is_docker_mode) - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -532,20 +530,20 @@ def configure_llama_interception_with_ca(context: Context) -> None: }, } - write_llama_config(config) + write_ogx_config(config) if is_prow_environment(): _sync_interception_proxy_ca_secret() @given( - "Llama Stack is configured to route inference through " + "OGX is configured to route inference through " "the interception proxy without CA cert" ) -def configure_llama_interception_no_ca(context: Context) -> None: +def configure_ogx_interception_no_ca(context: Context) -> None: """Modify run.yaml with interception proxy but NO CA cert.""" backup_ogx_config() - context.needs_interception_ca_on_llama = False - os.environ.pop("E2E_COPY_INTERCEPTION_CA_TO_LLAMA", None) + context.needs_interception_ca_on_ogx = False + os.environ.pop("E2E_COPY_INTERCEPTION_CA_TO_OGX", None) if is_prow_environment(): proxy_port = getattr( context, "cluster_interception_proxy_port", DEFAULT_INTERCEPTION_PROXY_PORT @@ -555,7 +553,7 @@ def configure_llama_interception_no_ca(context: Context) -> None: proxy = context.interception_proxy proxy_port = proxy.port proxy_host = _get_proxy_host(context.is_docker_mode) - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -566,17 +564,17 @@ def configure_llama_interception_no_ca(context: Context) -> None: }, } - write_llama_config(config) + write_ogx_config(config) # --- TLS Steps --- -@given('Llama Stack is configured with minimum TLS version "{version}"') -def configure_llama_tls_version(context: Context, version: str) -> None: +@given('OGX is configured with minimum TLS version "{version}"') +def configure_ogx_tls_version(context: Context, version: str) -> None: """Modify run.yaml with TLS version config.""" backup_ogx_config() - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -587,14 +585,14 @@ def configure_llama_tls_version(context: Context, version: str) -> None: } } - write_llama_config(config) + write_ogx_config(config) -@given('Llama Stack is configured with ciphers "{ciphers}"') -def configure_llama_ciphers(context: Context, ciphers: str) -> None: +@given('OGX is configured with ciphers "{ciphers}"') +def configure_ogx_ciphers(context: Context, ciphers: str) -> None: """Modify run.yaml with cipher suite config.""" backup_ogx_config() - config = load_llama_config() + config = load_ogx_config() provider = _find_inference_provider(context, config) if "config" not in provider: @@ -605,7 +603,7 @@ def configure_llama_ciphers(context: Context, ciphers: str) -> None: } } - write_llama_config(config) + write_ogx_config(config) # --- Proxy Verification Steps --- diff --git a/tests/e2e/features/steps/shields.py b/tests/e2e/features/steps/shields.py deleted file mode 100644 index 0e1861746..000000000 --- a/tests/e2e/features/steps/shields.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Behave steps for temporarily disabling OGX shields in e2e (server mode).""" - -from behave import given # pyright: ignore[reportAttributeAccessIssue] -from behave.runner import Context - -from tests.e2e.utils.ogx_utils import unregister_shield - - -@given("shields are disabled for this scenario") -def shields_are_disabled_for_scenario(context: Context) -> None: - """Unregister ``llama-guard`` for this scenario; ``after_scenario`` restores it when possible. - - Sets ``context.shields_disabled_for_scenario`` so ``environment.after_scenario`` - re-registers the shield. **Server mode only**; in library mode the scenario is skipped - (no separate OGX to call). - - Parameters: - ---------- - context: Behave context; must expose ``is_library_mode`` and ``scenario``. - """ - if context.is_library_mode: - context.scenario.skip( - "Shield unregister/register only applies in server mode (OGX as a " - "separate service). In library mode the app's shields cannot be disabled from e2e." - ) - return - - try: - saved = unregister_shield("llama-guard") - context.llama_guard_provider_id = saved[0] if saved else None - context.llama_guard_provider_shield_id = saved[1] if saved else None - context.shields_disabled_for_scenario = True - print("Unregistered shield llama-guard for this scenario") - except Exception as e: # pylint: disable=broad-exception-caught - context.scenario.skip(f"Could not unregister shield (is OGX reachable?): {e}") diff --git a/tests/e2e/features/steps/tls.py b/tests/e2e/features/steps/tls.py index 7c31d9b71..443704184 100644 --- a/tests/e2e/features/steps/tls.py +++ b/tests/e2e/features/steps/tls.py @@ -17,10 +17,10 @@ from tests.e2e.utils.ogx_config_utils import ( backup_ogx_config, - clear_llama_config_backup, - load_llama_config, - reset_llama_run_config_to_pipeline_default, - write_llama_config, + clear_ogx_config_backup, + load_ogx_config, + reset_ogx_run_config_to_pipeline_default, + write_ogx_config, ) from tests.e2e.utils.prow_utils import get_namespace, restart_pod, run_e2e_ops from tests.e2e.utils.utils import is_prow_environment @@ -38,10 +38,10 @@ def reset_tls_prow_state() -> None: """Reset per-feature TLS test state (call from ``before_feature``).""" - os.environ.pop("E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA", None) + os.environ.pop("E2E_COPY_MOCK_TLS_CERTS_TO_OGX", None) os.environ.pop("E2E_SYNC_MOCK_TLS_CERTS", None) os.environ.pop("E2E_MOCK_TLS_INFERENCE_HOST", None) - clear_llama_config_backup() + clear_ogx_config_backup() def is_tls_feature_file(feature_filename: Optional[str]) -> bool: @@ -64,11 +64,11 @@ def prepare_tls_feature_entry_on_prow(feature_filename: Optional[str] = None) -> return label = os.path.basename(feature_filename or "tls.feature") print(f"[{label}] Prow/Konflux entry: ensure mock TLS, reset run.yaml, warm OGX...") - reset_llama_run_config_to_pipeline_default() + reset_ogx_run_config_to_pipeline_default() _ensure_cluster_mock_tls_inference() - _prepare_tls_prow_llama_restart_env() + _prepare_tls_prow_ogx_restart_env() os.environ.pop("E2E_SYNC_MOCK_TLS_CERTS", None) - restart_pod("llama-stack") + restart_pod("ogx") print(f"[{label}] Prow/Konflux entry baseline complete", flush=True) @@ -84,12 +84,12 @@ def is_tls_configuration_feature(context: Context) -> bool: return "TLS configuration" in name -def _prepare_tls_prow_llama_restart_env() -> None: - """Set env for full llama pod recreate with mock TLS certs mounted.""" - os.environ["E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA"] = "1" +def _prepare_tls_prow_ogx_restart_env() -> None: + """Set env for full OGX pod recreate with mock TLS certs mounted.""" + os.environ["E2E_COPY_MOCK_TLS_CERTS_TO_OGX"] = "1" -def _restart_lightspeed_after_llama_tls(context: Context) -> None: +def _restart_lightspeed_after_ogx_tls(context: Context) -> None: """Restart LCS after OGX recreate so the in-process OGX client reconnects. TLS scenarios only change OGX run.yaml; LCS yaml is unchanged. Without this, @@ -98,7 +98,6 @@ def _restart_lightspeed_after_llama_tls(context: Context) -> None: """ from tests.e2e.utils.utils import ( restart_container, - wait_for_lightspeed_stack_http_ready, ) scenario = getattr(getattr(context, "scenario", None), "name", "") or "?" @@ -111,15 +110,14 @@ def _restart_lightspeed_after_llama_tls(context: Context) -> None: flush=True, ) restart_container("lightspeed-stack") - wait_for_lightspeed_stack_http_ready() -def restart_llama_for_tls_feature(context: Context) -> None: +def restart_ogx_for_tls_feature(context: Context) -> None: """Restart OGX for TLS tests (full pod recreate on Prow/Konflux).""" from tests.e2e.utils.utils import restart_container if is_prow_environment(): - _prepare_tls_prow_llama_restart_env() + _prepare_tls_prow_ogx_restart_env() os.environ.pop("E2E_SYNC_MOCK_TLS_CERTS", None) scenario = getattr(getattr(context, "scenario", None), "name", "") or "?" feature_file = os.path.basename( @@ -129,7 +127,7 @@ def restart_llama_for_tls_feature(context: Context) -> None: f"[{feature_file}] OGX restart: full recreate scenario={scenario!r}", flush=True, ) - restart_container("llama-stack") + restart_container("ogx") def _cluster_mock_tls_inference_host() -> str: @@ -173,7 +171,7 @@ def _ensure_cluster_mock_tls_inference() -> None: """Deploy mock TLS on Prow if missing; keep one pod for the whole tls suite. ``deploy-e2e-mock-tls-inference`` copies all PEMs into Secret ``e2e-mock-tls-certs`` - once. Scenarios only change which cert path Llama uses in run.yaml. + once. Scenarios only change which cert path OGX uses in run.yaml. """ if _mock_tls_inference_pod_ready(): print("Using existing e2e-mock-tls-inference deployment") @@ -239,7 +237,7 @@ def _configure_tls(tls_config: dict[str, Any], base_url: Optional[str] = None) - base_url: Optional base URL override for the provider. """ backup_ogx_config() - config = load_llama_config() + config = load_ogx_config() provider = _ensure_tls_provider(config) provider.setdefault("config", {}).setdefault("network", {}) if base_url is not None: @@ -248,9 +246,9 @@ def _configure_tls(tls_config: dict[str, Any], base_url: Optional[str] = None) - provider["config"]["base_url"] = _mock_tls_base_url(_MOCK_TLS_PORT_TLS) provider.setdefault("config", {})["refresh_models"] = False provider["config"]["network"]["tls"] = tls_config - write_llama_config(config) + write_ogx_config(config) if is_prow_environment(): - _prepare_tls_prow_llama_restart_env() + _prepare_tls_prow_ogx_restart_env() # --- Background Steps --- @@ -270,25 +268,25 @@ def deploy_mock_tls_inference_server(context: Context) -> None: # --- TLS Configuration Steps --- -@given("Llama Stack is configured with TLS verification disabled") +@given("OGX is configured with TLS verification disabled") def configure_tls_verify_false(context: Context) -> None: """Configure run.yaml with TLS verify: false.""" _configure_tls({"verify": False}) -@given("Llama Stack is configured with CA certificate verification") +@given("OGX is configured with CA certificate verification") def configure_tls_verify_ca(context: Context) -> None: """Configure run.yaml with TLS verify: /certs/ca.crt.""" _configure_tls({"verify": "/certs/ca.crt", "min_version": "TLSv1.2"}) -@given("Llama Stack is configured with TLS verification enabled") +@given("OGX is configured with TLS verification enabled") def configure_tls_verify_true(context: Context) -> None: """Configure run.yaml with TLS verify: true (fails with self-signed certs).""" _configure_tls({"verify": True}) -@given("Llama Stack is configured with mutual TLS authentication") +@given("OGX is configured with mutual TLS authentication") def configure_tls_mtls(context: Context) -> None: """Configure run.yaml with mutual TLS (client cert and key).""" _configure_tls( @@ -302,7 +300,7 @@ def configure_tls_mtls(context: Context) -> None: ) -@given("Llama Stack is configured for mTLS without client certificate") +@given("OGX is configured for mTLS without client certificate") def configure_tls_mtls_no_client_cert(context: Context) -> None: """Configure run.yaml for mTLS port without client cert (should fail).""" _configure_tls( @@ -311,7 +309,7 @@ def configure_tls_mtls_no_client_cert(context: Context) -> None: ) -@given("Llama Stack is configured for mTLS with wrong client certificate") +@given("OGX is configured for mTLS with wrong client certificate") def configure_tls_mtls_wrong_client_cert(context: Context) -> None: """Configure run.yaml for mTLS with invalid client cert (CA cert as client cert).""" _configure_tls( @@ -324,7 +322,7 @@ def configure_tls_mtls_wrong_client_cert(context: Context) -> None: ) -@given("Llama Stack is configured for mTLS with untrusted client certificate") +@given("OGX is configured for mTLS with untrusted client certificate") def configure_tls_mtls_untrusted_client_cert(context: Context) -> None: """Configure run.yaml with untrusted client certificate.""" _configure_tls( @@ -338,7 +336,7 @@ def configure_tls_mtls_untrusted_client_cert(context: Context) -> None: ) -@given("Llama Stack is configured for mTLS with expired client certificate") +@given("OGX is configured for mTLS with expired client certificate") def configure_tls_mtls_expired_client_cert(context: Context) -> None: """Configure run.yaml with expired client certificate.""" _configure_tls( @@ -352,7 +350,7 @@ def configure_tls_mtls_expired_client_cert(context: Context) -> None: ) -@given("Llama Stack is configured with CA certificate and hostname mismatch server") +@given("OGX is configured with CA certificate and hostname mismatch server") def configure_tls_ca_hostname_mismatch(context: Context) -> None: """Configure run.yaml to connect to hostname-mismatch server (should fail).""" _configure_tls( @@ -361,7 +359,7 @@ def configure_tls_ca_hostname_mismatch(context: Context) -> None: ) -@given("Llama Stack is configured with mutual TLS and hostname mismatch server") +@given("OGX is configured with mutual TLS and hostname mismatch server") def configure_tls_mtls_hostname_mismatch(context: Context) -> None: """Configure run.yaml with mTLS against hostname-mismatch server.""" _configure_tls( @@ -375,14 +373,14 @@ def configure_tls_mtls_hostname_mismatch(context: Context) -> None: ) -@given('Llama Stack is configured with CA certificate path "{path}"') +@given('OGX is configured with CA certificate path "{path}"') def configure_tls_ca_path(context: Context, path: str) -> None: """Configure run.yaml with TLS verify pointing to a specific CA cert path.""" _configure_tls({"verify": path}) @given( - 'Llama Stack is configured with TLS minimum version "{version}" and CA certificate path "{path}"' + 'OGX is configured with TLS minimum version "{version}" and CA certificate path "{path}"' ) def configure_tls_min_version_and_ca(context: Context, version: str, path: str) -> None: """Configure run.yaml with TLS minimum version and a specific CA cert path.""" @@ -390,7 +388,7 @@ def configure_tls_min_version_and_ca(context: Context, version: str, path: str) @given( - 'Llama Stack is configured with TLS minimum version "{version}" and hostname mismatch server' + 'OGX is configured with TLS minimum version "{version}" and hostname mismatch server' ) def configure_tls_min_version_hostname_mismatch(context: Context, version: str) -> None: """Configure run.yaml with TLS min version against hostname-mismatch server.""" diff --git a/tests/e2e/features/steps/unified_mode.py b/tests/e2e/features/steps/unified_mode.py new file mode 100644 index 000000000..3e110ed2c --- /dev/null +++ b/tests/e2e/features/steps/unified_mode.py @@ -0,0 +1,107 @@ +"""Step definitions for the unified-mode e2e features (LCORE-2343). + +Only the startup-log evidence step lives here. Everything else the five +``unified-mode-*.feature`` files need — applying a configuration, restarting +containers, hitting ``readiness`` and ``query`` — resolves through the generic +steps, and the configuration validation, migration and synthesis assertions +moved to ``tests/integration/`` (``test_unified_mode_cli.py``, +``test_unified_synthesis.py``): e2e steps observe the deployed stack from +outside and never import from or execute anything under ``src/``. See +``docs/testing/e2e_testing.md``, "Choosing the Test Layer". + +The log step is mode-aware: in server mode the synthesis evidence is emitted by +the ogx container (entrypoint + config CLI), not the lightspeed-stack +container the Gherkin names; the scenario's intent (R10: the synthesized path +is logged at startup) is asserted against the container that actually +synthesizes. +""" + +import re +import subprocess + +from behave import then # pyright: ignore +from behave.runner import Context + + +def _container_started_at(container: str) -> str: + """Return the container's last start timestamp (RFC 3339) from ``docker inspect``.""" + result = subprocess.run( + ["docker", "inspect", "-f", "{{.State.StartedAt}}", container], + capture_output=True, + text=True, + timeout=60, + check=False, + ) + assert ( + result.returncode == 0 + ), f"docker inspect {container} failed: {result.stderr[-500:]}" + started_at = result.stdout.strip() + assert started_at, f"docker inspect {container} returned no StartedAt" + return started_at + + +@then("the lightspeed-stack container logs contain synthesized run.yaml") +def container_logs_show_synthesis(context: Context) -> None: + """Assert the container that synthesizes logged the synthesized-config path. + + Library mode: the lightspeed-stack container itself synthesizes in-process + and logs "Using synthesized OGX config at ". Server mode: synthesis + happens in the ogx container (entrypoint + config CLI), which + echoes the generated-config path — the Gherkin names lightspeed-stack, but + the scenario's intent (R10: the path is logged at startup) can only be + observed on the synthesizing container. Deviation agreed in planning (Q2). + + ``docker logs`` accumulates across ``docker restart``, and the CI baseline + configurations already synthesize (library) or generate (server) on the + very first compose boot — so an unscoped read would pass on evidence from + an earlier boot. The read is therefore limited to lines since the + container's current ``StartedAt``, i.e. the restart the scenario just + performed under the unified fixture. + + The pattern must carry a path and must be unique to synthesis. The + entrypoint's own lines cannot provide that: it echoes "(mode + auto-detected)" before generation runs and unconditionally, and it echoes + "Using generated config: " identically for the synthesis and the + legacy-enrichment branch (scripts/ogx-entrypoint.sh). Matching either + would let the scenario pass when the unified fixture never reached the + container and the entrypoint enriched a run.yaml instead — which is the + one thing this scenario exists to rule out. The fallback line "Using + original config:" does not discriminate either; it is printed only when + generation *failed*, so it is absent from a successful enrichment too. + + So both modes match a line that only the synthesis path writes: + src/client/ogx.py in library mode, src/ogx_configuration.py in server + mode. The latter reaches the container log only because main() configures + logging — as a bare script nothing installs a root handler and + logging.lastResort drops everything below WARNING. + """ + if context.is_library_mode: + container = "lightspeed-stack" + pattern = r"Using synthesized OGX config at \S+" + else: + container = "ogx" + pattern = r"Wrote synthesized OGX configuration to \S+" + + started_at = _container_started_at(container) + result = subprocess.run( + ["docker", "logs", "--since", started_at, container], + capture_output=True, + text=True, + timeout=60, + check=False, + ) + assert ( + result.returncode == 0 + ), f"docker logs {container} failed: {result.stderr[-500:]}" + logs = result.stdout + result.stderr + # Checked before the pattern assert: when the entrypoint genuinely fell + # back, the pattern is absent too, and this is the message that says why. + if not context.is_library_mode: + assert "Using original config:" not in logs, ( + f"{container} fell back to the original run.yaml on this boot; " + "the unified fixture was not synthesized" + ) + assert re.search(pattern, logs), ( + f"{container} logs since {started_at} carry no synthesis-path evidence " + f"(pattern {pattern!r} not found)" + ) diff --git a/tests/e2e/features/streaming_query.feature b/tests/e2e/features/streaming_query.feature index 24f78d398..5455fbe79 100644 --- a/tests/e2e/features/streaming_query.feature +++ b/tests/e2e/features/streaming_query.feature @@ -282,8 +282,9 @@ Feature: streaming_query endpoint API tests Then The status code of the response is 413 And The body of the response contains Prompt is too long + # https://issues.redhat.com/browse/LCORE-1387 + @skip Scenario: Check if streaming_query without shields returns 200 and error in stream when question is too long for model context - Given shields are disabled for this scenario When I use "streaming_query" to ask question with too-long query and authorization header Then The status code of the response is 200 And The streamed response contains error message Prompt is too long diff --git a/tests/e2e/features/tls-ca.feature b/tests/e2e/features/tls-ca.feature index 7ebb9e51a..45b1f9e37 100644 --- a/tests/e2e/features/tls-ca.feature +++ b/tests/e2e/features/tls-ca.feature @@ -1,6 +1,6 @@ @cfg_tls @skip-in-library-mode @skip-in-prow Feature: TLS configuration — CA certificate verification - Validate Llama Stack NetworkConfig.tls CA trust settings against the mock HTTPS + Validate OGX NetworkConfig.tls CA trust settings against the mock HTTPS inference provider (standard TLS port). Background: @@ -8,14 +8,14 @@ Feature: TLS configuration — CA certificate verification And The system is in default state And REST API service prefix is /v1 And the Lightspeed stack configuration directory is "tests/e2e/configuration" - And The original Llama Stack config is restored if modified + And The original OGX config is restored if modified And The mock TLS inference server is deployed And The service uses the lightspeed-stack-tls.yaml configuration And The service is restarted Scenario: Inference succeeds with TLS verification disabled - Given Llama Stack is configured with TLS verification disabled - And Llama Stack is restarted + Given OGX is configured with TLS verification disabled + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -25,8 +25,8 @@ Feature: TLS configuration — CA certificate verification And The body of the response contains Hello from the TLS mock inference server Scenario: Inference succeeds with CA certificate verification - Given Llama Stack is configured with CA certificate verification - And Llama Stack is restarted + Given OGX is configured with CA certificate verification + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -36,8 +36,8 @@ Feature: TLS configuration — CA certificate verification And The body of the response contains Hello from the TLS mock inference server Scenario: Inference fails with an untrusted CA certificate - Given Llama Stack is configured with CA certificate path "/certs/untrusted-ca.crt" - And Llama Stack is restarted + Given OGX is configured with CA certificate path "/certs/untrusted-ca.crt" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -47,8 +47,8 @@ Feature: TLS configuration — CA certificate verification And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails with an expired CA certificate - Given Llama Stack is configured with CA certificate path "/certs/expired-ca.crt" - And Llama Stack is restarted + Given OGX is configured with CA certificate path "/certs/expired-ca.crt" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -58,8 +58,8 @@ Feature: TLS configuration — CA certificate verification And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails when TLS verify is true against self-signed cert - Given Llama Stack is configured with TLS verification enabled - And Llama Stack is restarted + Given OGX is configured with TLS verification enabled + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -69,8 +69,8 @@ Feature: TLS configuration — CA certificate verification And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails with CA certificate verification and hostname mismatch - Given Llama Stack is configured with CA certificate and hostname mismatch server - And Llama Stack is restarted + Given OGX is configured with CA certificate and hostname mismatch server + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ diff --git a/tests/e2e/features/tls-mtls.feature b/tests/e2e/features/tls-mtls.feature index 053144bb6..f6eec2e70 100644 --- a/tests/e2e/features/tls-mtls.feature +++ b/tests/e2e/features/tls-mtls.feature @@ -1,6 +1,6 @@ @cfg_tls @skip-in-library-mode @skip-in-prow Feature: TLS configuration — mutual TLS authentication - Validate Llama Stack NetworkConfig.tls client certificate settings against the + Validate OGX NetworkConfig.tls client certificate settings against the mock HTTPS inference provider (mTLS port). Background: @@ -8,14 +8,14 @@ Feature: TLS configuration — mutual TLS authentication And The system is in default state And REST API service prefix is /v1 And the Lightspeed stack configuration directory is "tests/e2e/configuration" - And The original Llama Stack config is restored if modified + And The original OGX config is restored if modified And The mock TLS inference server is deployed And The service uses the lightspeed-stack-tls.yaml configuration And The service is restarted Scenario: Inference succeeds with mutual TLS authentication - Given Llama Stack is configured with mutual TLS authentication - And Llama Stack is restarted + Given OGX is configured with mutual TLS authentication + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -25,8 +25,8 @@ Feature: TLS configuration — mutual TLS authentication And The body of the response contains Hello from the TLS mock inference server Scenario: Inference fails when mTLS is required but no client certificate is provided - Given Llama Stack is configured for mTLS without client certificate - And Llama Stack is restarted + Given OGX is configured for mTLS without client certificate + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -36,8 +36,8 @@ Feature: TLS configuration — mutual TLS authentication And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails when mTLS is required but wrong client certificate is provided - Given Llama Stack is configured for mTLS with wrong client certificate - And Llama Stack is restarted + Given OGX is configured for mTLS with wrong client certificate + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -47,8 +47,8 @@ Feature: TLS configuration — mutual TLS authentication And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails when mTLS is required but untrusted client certificate is provided - Given Llama Stack is configured for mTLS with untrusted client certificate - And Llama Stack is restarted + Given OGX is configured for mTLS with untrusted client certificate + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -58,8 +58,8 @@ Feature: TLS configuration — mutual TLS authentication And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails when mTLS is required but expired client certificate is provided - Given Llama Stack is configured for mTLS with expired client certificate - And Llama Stack is restarted + Given OGX is configured for mTLS with expired client certificate + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -69,8 +69,8 @@ Feature: TLS configuration — mutual TLS authentication And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails with mutual TLS and hostname mismatch - Given Llama Stack is configured with mutual TLS and hostname mismatch server - And Llama Stack is restarted + Given OGX is configured with mutual TLS and hostname mismatch server + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ diff --git a/tests/e2e/features/tls-tlsv13.feature b/tests/e2e/features/tls-tlsv13.feature index 692b8fc1d..60a157700 100644 --- a/tests/e2e/features/tls-tlsv13.feature +++ b/tests/e2e/features/tls-tlsv13.feature @@ -1,6 +1,6 @@ @cfg_tls @skip-in-library-mode @skip-in-prow Feature: TLS configuration — TLS minimum version 1.3 - Validate Llama Stack NetworkConfig.tls min_version TLSv1.3 against the mock + Validate OGX NetworkConfig.tls min_version TLSv1.3 against the mock HTTPS inference provider. Background: @@ -8,14 +8,14 @@ Feature: TLS configuration — TLS minimum version 1.3 And The system is in default state And REST API service prefix is /v1 And the Lightspeed stack configuration directory is "tests/e2e/configuration" - And The original Llama Stack config is restored if modified + And The original OGX config is restored if modified And The mock TLS inference server is deployed And The service uses the lightspeed-stack-tls.yaml configuration And The service is restarted Scenario: Inference succeeds with TLS minimum version TLSv1.3 - Given Llama Stack is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/ca.crt" - And Llama Stack is restarted + Given OGX is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/ca.crt" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -25,8 +25,8 @@ Feature: TLS configuration — TLS minimum version 1.3 And The body of the response contains Hello from the TLS mock inference server Scenario: Inference fails with TLS minimum version TLSv1.3 and untrusted CA certificate - Given Llama Stack is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/untrusted-ca.crt" - And Llama Stack is restarted + Given OGX is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/untrusted-ca.crt" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -36,8 +36,8 @@ Feature: TLS configuration — TLS minimum version 1.3 And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails with TLS minimum version TLSv1.3 and hostname mismatch - Given Llama Stack is configured with TLS minimum version "TLSv1.3" and hostname mismatch server - And Llama Stack is restarted + Given OGX is configured with TLS minimum version "TLSv1.3" and hostname mismatch server + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ @@ -47,8 +47,8 @@ Feature: TLS configuration — TLS minimum version 1.3 And The body of the response does not contain Hello from the TLS mock inference server Scenario: Inference fails with TLS minimum version TLSv1.3 and expired CA certificate - Given Llama Stack is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/expired-ca.crt" - And Llama Stack is restarted + Given OGX is configured with TLS minimum version "TLSv1.3" and CA certificate path "/certs/expired-ca.crt" + And OGX is restarted And Lightspeed Stack is restarted When I use "query" to ask question """ diff --git a/tests/e2e/features/unified-mode-boot.feature b/tests/e2e/features/unified-mode-boot.feature index 7c1413818..feab84a8d 100644 --- a/tests/e2e/features/unified-mode-boot.feature +++ b/tests/e2e/features/unified-mode-boot.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip-in-prow Feature: Unified mode configuration boot Background: @@ -10,7 +10,7 @@ Feature: Unified mode configuration boot # --- library mode (@skip-in-server-mode) --- - @skip-in-server-mode + @skip-in-server-mode @openai-only Scenario: Unified config with inference.providers boots and serves requests in library mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration And The service is restarted @@ -32,7 +32,7 @@ Feature: Unified mode configuration boot @skip-in-server-mode - Scenario: Unified config with llama_stack.config only boots and serves requests in library mode + Scenario: Unified config with ogx.config only boots and serves requests in library mode Given The service uses the lightspeed-stack-unified-config-only.yaml configuration And The service is restarted When I access endpoint "readiness" using HTTP GET method @@ -62,10 +62,10 @@ Feature: Unified mode configuration boot # --- server mode (@skip-in-library-mode) --- - @skip-in-library-mode + @skip-in-library-mode @openai-only Scenario: Unified config with inference.providers boots and serves requests in server mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 @@ -85,9 +85,9 @@ Feature: Unified mode configuration boot @skip-in-library-mode - Scenario: Unified config with llama_stack.config only boots and serves requests in server mode + Scenario: Unified config with ogx.config only boots and serves requests in server mode Given The service uses the lightspeed-stack-unified-config-only.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 @@ -101,7 +101,7 @@ Feature: Unified mode configuration boot @skip-in-library-mode Scenario: Unified config with relative profile path boots in server mode Given The service uses the lightspeed-stack-unified-relative-profile.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 @@ -110,7 +110,7 @@ Feature: Unified mode configuration boot @skip-in-library-mode Scenario: Unified config with absolute profile path boots in server mode Given The service uses the lightspeed-stack-unified-absolute-profile.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 diff --git a/tests/e2e/features/unified-mode-legacy.feature b/tests/e2e/features/unified-mode-legacy.feature index 407c43e6a..c27fe523e 100644 --- a/tests/e2e/features/unified-mode-legacy.feature +++ b/tests/e2e/features/unified-mode-legacy.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip-in-prow Feature: Legacy two-file configuration during deprecation window Background: @@ -12,7 +12,10 @@ Feature: Legacy two-file configuration during deprecation window @skip-in-server-mode Scenario: Legacy two-file configuration still boots and serves requests in library mode - Given The service uses the lightspeed-stack.yaml configuration + # lightspeed-stack-legacy.yaml (not the standard baseline): LCORE-2342 + # migrated the library-mode baseline to unified mode, so only a dedicated + # legacy-shaped fixture still exercises the deprecated two-file path (R2). + Given The service uses the lightspeed-stack-legacy.yaml configuration And The service is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 @@ -28,7 +31,7 @@ Feature: Legacy two-file configuration during deprecation window @skip-in-library-mode Scenario: Legacy two-file configuration still boots and serves requests in server mode Given The service uses the lightspeed-stack.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 diff --git a/tests/e2e/features/unified-mode-migration.feature b/tests/e2e/features/unified-mode-migration.feature index b5a2d06eb..8462ccc2c 100644 --- a/tests/e2e/features/unified-mode-migration.feature +++ b/tests/e2e/features/unified-mode-migration.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip-in-prow Feature: Legacy to unified configuration migration Background: @@ -8,24 +8,20 @@ Feature: Legacy to unified configuration migration And the Lightspeed stack configuration directory is "tests/e2e/configuration/unified-mode" - Scenario: migrate-config produces a unified configuration from a legacy pair - When lightspeed-stack --migrate-config is run for the legacy migration fixture pair - Then the file lightspeed-stack-unified-migrated.yaml contains native_override - And the file lightspeed-stack-unified-migrated.yaml does not contain library_client_config_path - - - Scenario: migrate then synthesize round-trips to the original run.yaml - When lightspeed-stack --migrate-config is run for the legacy migration fixture pair - And the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml parses to the same data as the legacy migration fixture run.yaml - + # The --migrate-config CLI itself (output shape, owner-only mode, migrate-then- + # synthesize round trip) is covered by tests/integration/test_unified_mode_cli.py: + # e2e steps never run src/ CLIs (docs/testing/e2e_testing.md, "Choosing the + # Test Layer"). The scenarios below boot the committed + # lightspeed-stack-unified-migrated.yaml fixture — generated once from the + # legacy migration fixture pair (lightspeed-stack-legacy-for-migration.yaml + + # tests/e2e/configs/run-ci.yaml) and guarded against CLI drift by that same + # integration module. It inlines the openai run-ci.yaml, hence @openai-only. # --- library mode (@skip-in-server-mode) --- - @skip-in-server-mode - Scenario: Migrated unified configuration drives byte-identical Llama Stack behavior in library mode - Given lightspeed-stack --migrate-config is run for the legacy migration fixture pair - And The service uses the lightspeed-stack-unified-migrated.yaml configuration + @skip-in-server-mode @openai-only + Scenario: Migrated unified configuration boots and serves queries in library mode + Given The service uses the lightspeed-stack-unified-migrated.yaml configuration And The service is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 @@ -38,11 +34,10 @@ Feature: Legacy to unified configuration migration # --- server mode (@skip-in-library-mode) --- - @skip-in-library-mode - Scenario: Migrated unified configuration drives byte-identical Llama Stack behavior in server mode - Given lightspeed-stack --migrate-config is run for the legacy migration fixture pair - And The service uses the lightspeed-stack-unified-migrated.yaml configuration - And Llama Stack is restarted + @skip-in-library-mode @openai-only + Scenario: Migrated unified configuration boots and serves queries in server mode + Given The service uses the lightspeed-stack-unified-migrated.yaml configuration + And OGX is restarted And Lightspeed Stack is restarted When I access endpoint "readiness" using HTTP GET method Then The status code of the response is 200 diff --git a/tests/e2e/features/unified-mode-synthesis.feature b/tests/e2e/features/unified-mode-synthesis.feature index 52b4254fa..528a4683a 100644 --- a/tests/e2e/features/unified-mode-synthesis.feature +++ b/tests/e2e/features/unified-mode-synthesis.feature @@ -1,4 +1,4 @@ -@cfg_unified @skip +@cfg_unified @skip-in-prow Feature: Unified mode configuration synthesis Background: @@ -7,42 +7,17 @@ Feature: Unified mode configuration synthesis And the Lightspeed stack configuration directory is "tests/e2e/configuration/unified-mode" - Scenario: native_override replaces an overlapping scalar key - Given The service uses the lightspeed-stack-unified-native-override-scalar.yaml configuration - When the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml contains the native_override scalar value for safety.excluded_categories - - - Scenario: native_override replaces an overlapping list key wholesale - Given The service uses the lightspeed-stack-unified-native-override-list.yaml configuration - When the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml contains exactly the native_override list for apis - - - Scenario: LCORE-emitted secrets remain as environment references on disk - Given The service uses the lightspeed-stack-unified-providers.yaml configuration - When the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml contains ${env.OPENAI_API_KEY} - And the synthesized run.yaml does not contain the resolved OPENAI_API_KEY value - - - Scenario: Synthesized run.yaml is written with owner-only permissions - Given The service uses the lightspeed-stack-unified-providers.yaml configuration - When the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml file permissions are 0600 - - - Scenario: synthesized-config-output overrides the default synthesis location - Given The service uses the lightspeed-stack-unified-providers.yaml configuration - And lightspeed-stack is started with --synthesized-config-output set to a custom path - When the active unified configuration is synthesized to run.yaml - Then the synthesized run.yaml is written to the custom output path - And the default synthesized run.yaml path does not exist - + # Synthesis semantics — native_override replacement (R5), secrets kept as + # environment references (R6), owner-only output mode (R10) and the + # --synthesized-config-output override — are covered in-process by + # tests/integration/test_unified_synthesis.py: e2e steps never run src/ CLIs + # (docs/testing/e2e_testing.md, "Choosing the Test Layer"). What remains here + # is the one thing only a deployed stack can show: the synthesized path is + # logged at startup (R10). # --- library mode (@skip-in-server-mode) --- - @skip-in-server-mode + @skip-in-server-mode @openai-only Scenario: Synthesized run.yaml path is logged at startup in library mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration And The service is restarted @@ -51,9 +26,9 @@ Feature: Unified mode configuration synthesis # --- server mode (@skip-in-library-mode) --- - @skip-in-library-mode + @skip-in-library-mode @openai-only Scenario: Synthesized run.yaml path is logged at startup in server mode Given The service uses the lightspeed-stack-unified-providers.yaml configuration - And Llama Stack is restarted + And OGX is restarted And Lightspeed Stack is restarted Then the lightspeed-stack container logs contain synthesized run.yaml diff --git a/tests/e2e/features/unified-mode-validation.feature b/tests/e2e/features/unified-mode-validation.feature deleted file mode 100644 index 5a1688335..000000000 --- a/tests/e2e/features/unified-mode-validation.feature +++ /dev/null @@ -1,26 +0,0 @@ -@cfg_unified @skip -Feature: Unified mode configuration validation - - Background: - Given The service is started locally - And The system is in default state - And the Lightspeed stack configuration directory is "tests/e2e/configuration/unified-mode" - - - Scenario: inference.providers together with library_client_config_path fails at load - Given The service uses the lightspeed-stack-invalid-providers-and-legacy.yaml configuration - When configuration validation is attempted for the active configuration - Then the validation error contains --migrate-config - - - Scenario: llama_stack.config together with library_client_config_path fails at load - Given The service uses the lightspeed-stack-invalid-config-and-legacy.yaml configuration - When configuration validation is attempted for the active configuration - Then the validation error contains --migrate-config - - - - Scenario: config_format_version legacy with unified-shaped body fails at load - Given The service uses the lightspeed-stack-invalid-version-legacy-unified-body.yaml configuration - When configuration validation is attempted for the active configuration - Then the validation error contains config_format_version diff --git a/tests/e2e/gen_scenario_list.py b/tests/e2e/gen_scenario_list.py index ac9587e06..19f70f284 100644 --- a/tests/e2e/gen_scenario_list.py +++ b/tests/e2e/gen_scenario_list.py @@ -52,9 +52,7 @@ if filename.endswith(".feature"): # feature file header print(f"## [`{filename}`]({FEATURES_URL_PREFIX}/{filename})\n") - with open( - os.path.join(FEATURE_DIRECTORY, filename), "r", encoding="utf-8" - ) as fin: + with open(os.path.join(FEATURE_DIRECTORY, filename), encoding="utf-8") as fin: for line in fin: line = line.strip() # process all scenarios and scenario outlines diff --git a/tests/e2e/mock_mcp_server/server.py b/tests/e2e/mock_mcp_server/server.py index 3fa5a2df8..dbbdec8e7 100644 --- a/tests/e2e/mock_mcp_server/server.py +++ b/tests/e2e/mock_mcp_server/server.py @@ -73,44 +73,45 @@ def do_POST(self) -> None: # pylint: disable=invalid-name req_id = 1 method = "" - if method == "initialize": - self._json_response( - { - "jsonrpc": "2.0", - "id": req_id, - "result": { - "protocolVersion": "2024-11-05", - "capabilities": {"tools": {}}, - "serverInfo": {"name": "mock-mcp-e2e", "version": "1.0.0"}, - }, - } - ) - elif method == "tools/list": - self._json_response( - { - "jsonrpc": "2.0", - "id": req_id, - "result": { - "tools": [ - { - "name": "mock_tool_e2e", - "description": "Mock tool for E2E", - "inputSchema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "Test message", - } + match method: + case "initialize": + self._json_response( + { + "jsonrpc": "2.0", + "id": req_id, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {}}, + "serverInfo": {"name": "mock-mcp-e2e", "version": "1.0.0"}, + }, + } + ) + case "tools/list": + self._json_response( + { + "jsonrpc": "2.0", + "id": req_id, + "result": { + "tools": [ + { + "name": "mock_tool_e2e", + "description": "Mock tool for E2E", + "inputSchema": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Test message", + } + }, }, - }, - } - ], - }, - } - ) - else: - self._json_response({"jsonrpc": "2.0", "id": req_id, "result": {}}) + } + ], + }, + } + ) + case _: + self._json_response({"jsonrpc": "2.0", "id": req_id, "result": {}}) def log_message(self, format: str, *args: Any) -> None: """Suppress request logging for minimal output.""" diff --git a/tests/e2e/test_list.txt b/tests/e2e/test_list.txt index 79f34fcb5..e80a173cd 100644 --- a/tests/e2e/test_list.txt +++ b/tests/e2e/test_list.txt @@ -40,7 +40,6 @@ features/tls-tlsv13.feature features/degraded_mode_startup.feature features/unified-mode-boot.feature features/unified-mode-legacy.feature -features/unified-mode-validation.feature features/unified-mode-migration.feature features/unified-mode-synthesis.feature features/okp_rag.feature diff --git a/tests/e2e/utils/README.md b/tests/e2e/utils/README.md index 1f7e3a4ac..6d8c8c55d 100644 --- a/tests/e2e/utils/README.md +++ b/tests/e2e/utils/README.md @@ -8,10 +8,6 @@ Helpers for reading and updating OGX run.yaml across environments. Thin Prow/OpenShift wrappers for OGX run.yaml ConfigMap operations. -## [ogx_utils.py](ogx_utils.py) - -E2E test utilities for OGX shields. - ## [prow_utils.py](prow_utils.py) Prow/OpenShift-specific utility functions for E2E tests. diff --git a/tests/e2e/utils/ogx_config_utils.py b/tests/e2e/utils/ogx_config_utils.py index 42af3b1d5..6002f20b8 100644 --- a/tests/e2e/utils/ogx_config_utils.py +++ b/tests/e2e/utils/ogx_config_utils.py @@ -9,75 +9,75 @@ import yaml from tests.e2e.utils.ogx_prow_utils import ( - backup_llama_run_config_to_memory, - get_llama_run_config_content, - remove_llama_run_config_backup, - update_llama_run_configmap, + backup_ogx_run_config_to_memory, + get_ogx_run_config_content, + remove_ogx_run_config_backup, + update_ogx_run_configmap, ) from tests.e2e.utils.utils import is_prow_environment -_DEFAULT_LOCAL_LLAMA_CONFIG_PATH = "run.yaml" -_DEFAULT_LOCAL_LLAMA_CONFIG_BACKUP_PATH = "run.yaml.proxy-backup" -_llama_config_backup_key: dict[str, Optional[str]] = {"value": None} +_DEFAULT_LOCAL_OGX_CONFIG_PATH = "run.yaml" +_DEFAULT_LOCAL_OGX_CONFIG_BACKUP_PATH = "run.yaml.proxy-backup" +_ogx_config_backup_key: dict[str, Optional[str]] = {"value": None} -def clear_llama_config_backup() -> None: +def clear_ogx_config_backup() -> None: """Drop in-memory run.yaml backup (e.g. at start of tls.feature).""" - _llama_config_backup_key["value"] = None + _ogx_config_backup_key["value"] = None -def reset_llama_run_config_to_pipeline_default() -> None: - """Reset llama-stack-config run.yaml to Konflux/Prow pipeline seed (run-ci.yaml).""" +def reset_ogx_run_config_to_pipeline_default() -> None: + """Reset OGX run.yaml ConfigMap to Konflux/Prow pipeline seed (run-ci.yaml).""" if not is_prow_environment(): return run_ci = Path(__file__).resolve().parents[1] / "configs" / "run-ci.yaml" if not run_ci.is_file(): print(f"WARN: pipeline run.yaml seed not found at {run_ci}", flush=True) return - print(f"Resetting llama-stack-config from {run_ci.name}...", flush=True) - update_llama_run_configmap(str(run_ci)) + print(f"Resetting OGX run config from {run_ci.name}...", flush=True) + update_ogx_run_configmap(str(run_ci)) -def _local_llama_config_path() -> str: +def _local_ogx_config_path() -> str: """Return local run.yaml path for Docker/local e2e execution.""" - return os.getenv("E2E_LLAMA_CONFIG_PATH", _DEFAULT_LOCAL_LLAMA_CONFIG_PATH) + return os.getenv("E2E_OGX_CONFIG_PATH", _DEFAULT_LOCAL_OGX_CONFIG_PATH) -def _local_llama_config_backup_path() -> str: +def _local_ogx_config_backup_path() -> str: """Return backup path used for local run.yaml mutations.""" return os.getenv( - "E2E_LLAMA_CONFIG_BACKUP_PATH", - _DEFAULT_LOCAL_LLAMA_CONFIG_BACKUP_PATH, + "E2E_OGX_CONFIG_BACKUP_PATH", + _DEFAULT_LOCAL_OGX_CONFIG_BACKUP_PATH, ) def backup_ogx_config() -> None: """Create a backup of the current OGX run config once per scenario.""" if is_prow_environment(): - if _llama_config_backup_key["value"] is None: - _llama_config_backup_key["value"] = backup_llama_run_config_to_memory() + if _ogx_config_backup_key["value"] is None: + _ogx_config_backup_key["value"] = backup_ogx_run_config_to_memory() return - backup_path = _local_llama_config_backup_path() + backup_path = _local_ogx_config_backup_path() if not os.path.exists(backup_path): - shutil.copy(_local_llama_config_path(), backup_path) + shutil.copy(_local_ogx_config_path(), backup_path) -def load_llama_config() -> dict[str, Any]: +def load_ogx_config() -> dict[str, Any]: """Load run.yaml configuration as a dictionary.""" if is_prow_environment(): - content = get_llama_run_config_content() + content = get_ogx_run_config_content() loaded = yaml.safe_load(content) or {} assert isinstance(loaded, dict), "Expected run.yaml to deserialize to a mapping" return loaded - with open(_local_llama_config_path(), encoding="utf-8") as file: + with open(_local_ogx_config_path(), encoding="utf-8") as file: loaded = yaml.safe_load(file) or {} assert isinstance(loaded, dict), "Expected run.yaml to deserialize to a mapping" return loaded -def write_llama_config(config: dict[str, Any]) -> None: +def write_ogx_config(config: dict[str, Any]) -> None: """Write run.yaml configuration in local or Prow environment.""" if is_prow_environment(): with tempfile.NamedTemporaryFile( @@ -89,33 +89,33 @@ def write_llama_config(config: dict[str, Any]) -> None: yaml.dump(config, file, default_flow_style=False) temp_path = file.name try: - update_llama_run_configmap(temp_path) + update_ogx_run_configmap(temp_path) finally: if os.path.exists(temp_path): os.remove(temp_path) return - with open(_local_llama_config_path(), "w", encoding="utf-8") as file: + with open(_local_ogx_config_path(), "w", encoding="utf-8") as file: yaml.dump(config, file, default_flow_style=False) -def restore_llama_config_if_modified() -> bool: +def restore_ogx_config_if_modified() -> bool: """Restore run config when a backup exists. Returns: True when a restore happened, otherwise False. """ if is_prow_environment(): - backup_key = _llama_config_backup_key["value"] + backup_key = _ogx_config_backup_key["value"] if backup_key is None: return False - update_llama_run_configmap(backup_key) - remove_llama_run_config_backup(backup_key) - _llama_config_backup_key["value"] = None + update_ogx_run_configmap(backup_key) + remove_ogx_run_config_backup(backup_key) + _ogx_config_backup_key["value"] = None return True - backup_path = _local_llama_config_backup_path() + backup_path = _local_ogx_config_backup_path() if not os.path.exists(backup_path): return False - shutil.move(backup_path, _local_llama_config_path()) + shutil.move(backup_path, _local_ogx_config_path()) return True diff --git a/tests/e2e/utils/ogx_prow_utils.py b/tests/e2e/utils/ogx_prow_utils.py index 57b15fe0c..f3247a845 100644 --- a/tests/e2e/utils/ogx_prow_utils.py +++ b/tests/e2e/utils/ogx_prow_utils.py @@ -7,35 +7,36 @@ update_config_configmap, ) -_LLAMA_CONFIGMAP_NAME = "llama-stack-config" -_LLAMA_CONFIGMAP_KEY = "run.yaml" +# OpenShift ConfigMap name (legacy K8s resource id in Prow manifests). +_OGX_CONFIGMAP_NAME = "llama-stack-config" +_OGX_CONFIGMAP_KEY = "run.yaml" -def get_llama_run_config_content() -> str: - """Return llama-stack-config run.yaml content in Prow/OpenShift.""" +def get_ogx_run_config_content() -> str: + """Return OGX run.yaml ConfigMap content in Prow/OpenShift.""" return get_configmap_content( - configmap_name=_LLAMA_CONFIGMAP_NAME, - configmap_key=_LLAMA_CONFIGMAP_KEY, + configmap_name=_OGX_CONFIGMAP_NAME, + configmap_key=_OGX_CONFIGMAP_KEY, ) -def backup_llama_run_config_to_memory() -> str: - """Backup llama-stack-config run.yaml into in-memory backup storage.""" +def backup_ogx_run_config_to_memory() -> str: + """Backup OGX run.yaml ConfigMap into in-memory backup storage.""" return backup_configmap_to_memory( - configmap_name=_LLAMA_CONFIGMAP_NAME, - configmap_key=_LLAMA_CONFIGMAP_KEY, + configmap_name=_OGX_CONFIGMAP_NAME, + configmap_key=_OGX_CONFIGMAP_KEY, ) -def update_llama_run_configmap(source: str) -> None: - """Update or restore llama-stack-config run.yaml from file or backup key.""" +def update_ogx_run_configmap(source: str) -> None: + """Update or restore OGX run.yaml ConfigMap from file or backup key.""" update_config_configmap( source, - configmap_name=_LLAMA_CONFIGMAP_NAME, - configmap_key=_LLAMA_CONFIGMAP_KEY, + configmap_name=_OGX_CONFIGMAP_NAME, + configmap_key=_OGX_CONFIGMAP_KEY, ) -def remove_llama_run_config_backup(backup_key: str) -> None: - """Remove a llama-stack-config run.yaml backup from in-memory storage.""" +def remove_ogx_run_config_backup(backup_key: str) -> None: + """Remove an OGX run.yaml ConfigMap backup from in-memory storage.""" remove_configmap_backup(backup_key) diff --git a/tests/e2e/utils/ogx_utils.py b/tests/e2e/utils/ogx_utils.py deleted file mode 100644 index 2a661ff19..000000000 --- a/tests/e2e/utils/ogx_utils.py +++ /dev/null @@ -1,115 +0,0 @@ -"""E2E test utilities for OGX shields. - -This module provides functions to manage shields on a running OGX -instance during end-to-end tests: unregister/re-register shields (e.g. from the -``Given shields are disabled for this scenario`` step). - -Only applies when running OGX as a separate service (server mode). -Requires E2E_LLAMA_STACK_URL or E2E_LLAMA_HOSTNAME and E2E_LLAMA_PORT. -""" - -import asyncio -import os -from typing import Optional - -from ogx_client import ( - ApiException, - AsyncOgxClient, -) - -from tests.e2e.utils.utils import is_prow_environment - - -def _get_ogx_client() -> AsyncOgxClient: - """Build an AsyncOgxClient from env (for e2e test use).""" - base_url = os.getenv("E2E_LLAMA_STACK_URL") - if not base_url: - if is_prow_environment(): - host = os.getenv("E2E_LLAMA_HOSTNAME", "localhost") - else: - host = "localhost" - port = os.getenv("E2E_LLAMA_PORT", "8321") - base_url = f"http://{host}:{port}" - api_key = os.getenv("E2E_LLAMA_STACK_API_KEY", "xyzzy") - timeout = int(os.getenv("E2E_LLAMA_STACK_TIMEOUT", "60")) - return AsyncOgxClient(base_url=base_url, api_key=api_key, timeout=timeout) - - -# ----------------------------------------------------------------------------- -# Shields -# ----------------------------------------------------------------------------- - - -async def _unregister_shield_async(identifier: str) -> Optional[tuple[str, str]]: - """Unregister a shield by identifier; return (provider_id, provider_shield_id) for restore.""" - client = _get_ogx_client() - try: - shields = await client.shields.list() - provider_id = None - provider_shield_id = None - found = False - for shield in shields: - if getattr(shield, "identifier", None) == identifier: - provider_id = getattr(shield, "provider_id", None) - provider_shield_id = getattr( - shield, "provider_resource_id", None - ) or getattr(shield, "provider_shield_id", None) - found = True - break - if not found: - # Shield not registered; nothing to delete, scenario can proceed - return None - try: - await client.shields.delete(identifier) - except ApiException as e: - if not e.status: - raise - # 400 "not found": shield already absent, scenario can proceed - if e.status == 400 and "not found" in str(e).lower(): - return None - raise - if provider_id is not None and provider_shield_id is not None: - return (provider_id, provider_shield_id) - return None - finally: - await client.close() - - -async def _register_shield_async( - shield_id: str, - provider_id: str, - provider_shield_id: str, -) -> None: - """Register a shield (restore after unregister).""" - client = _get_ogx_client() - try: - await client.shields.register( - shield_id=shield_id, - provider_id=provider_id, - provider_shield_id=provider_shield_id, - ) - finally: - await client.close() - - -def unregister_shield( - identifier: str = "llama-guard", -) -> Optional[tuple[str, str]]: - """Unregister the shield via client.shields.delete(); return (provider_id, provider_shield_id).""" - return asyncio.run(_unregister_shield_async(identifier)) - - -def register_shield( - shield_id: str = "llama-guard", - provider_id: Optional[str] = None, - provider_shield_id: Optional[str] = None, -) -> None: - """Re-register the shield via client.shields.register().""" - if not provider_id: - provider_id = os.getenv("E2E_LLAMA_GUARD_PROVIDER_ID", "llama-guard") - if not provider_shield_id: - provider_shield_id = os.getenv( - "E2E_LLAMA_GUARD_PROVIDER_SHIELD_ID", - "openai/gpt-4o-mini", - ) - asyncio.run(_register_shield_async(shield_id, provider_id, provider_shield_id)) diff --git a/tests/e2e/utils/prow_utils.py b/tests/e2e/utils/prow_utils.py index 9e19f0874..0dc855a3a 100644 --- a/tests/e2e/utils/prow_utils.py +++ b/tests/e2e/utils/prow_utils.py @@ -19,7 +19,8 @@ def get_namespace() -> str: # Mapping from container names (used in tests) to pod names (used in OpenShift) _POD_NAME_MAP = { "lightspeed-stack": "lightspeed-stack-service", - "llama-stack": "llama-stack-service", + "ogx": "llama-stack-service", + "llama-stack": "llama-stack-service", # legacy alias } @@ -86,14 +87,14 @@ def wait_for_pod_health(pod_name: str, max_attempts: int = 60) -> None: raise -_LLAMA_RESTART_NAMES = frozenset({"llama-stack", "llama-stack-service"}) +_OGX_RESTART_NAMES = frozenset({"ogx", "llama-stack", "llama-stack-service"}) _LIGHTSPEED_RESTART_NAMES = frozenset({"lightspeed-stack", "lightspeed-stack-service"}) def restart_pod(container_name: str) -> None: """Restart OGX or Lightspeed pod in OpenShift/Prow (not Docker). - Maps ``container_name`` to the correct e2e-ops command: ``restart-llama-stack`` + Maps ``container_name`` to the correct e2e-ops command: ``restart-ogx`` vs ``restart-lightspeed``. Unknown names default to Lightspeed with a warning. For Lightspeed restarts, e2e-ops ensures OGX is running first. OGX pod logs @@ -102,11 +103,11 @@ def restart_pod(container_name: str) -> None: CI failures with healthy pod logs are often **localhost port-forward** contention (pipeline forward vs hook restart), not application crashes—see e2e-ops.sh header. """ - if container_name in _LLAMA_RESTART_NAMES: - op = "restart-llama-stack" + if container_name in _OGX_RESTART_NAMES: + op = "restart-ogx" # Subprocess cap must exceed e2e-ops internal waits (pod + in-pod health + port-forward). - # Konflux TLS full recreate: ~6–12 min typical, 15+ min under load (user-reported 400s+). - if os.environ.get("E2E_COPY_MOCK_TLS_CERTS_TO_LLAMA") == "1": + # Konflux TLS full recreate: ~6-12 min typical, 15+ min under load (user-reported 400s+). + if os.environ.get("E2E_COPY_MOCK_TLS_CERTS_TO_OGX") == "1": timeout = 1200 elif os.environ.get("E2E_KONFLUX_E2E") == "1": timeout = 720 @@ -114,7 +115,7 @@ def restart_pod(container_name: str) -> None: timeout = 420 elif container_name in _LIGHTSPEED_RESTART_NAMES: op = "restart-lightspeed" - # Konflux LCS: TCP readiness + Llama handshake; full recreate can exceed 10 min under load. + # Konflux LCS: TCP readiness + OGX handshake; full recreate can exceed 10 min under load. timeout = 1200 if os.environ.get("E2E_KONFLUX_E2E") == "1" else 320 else: print( @@ -158,24 +159,24 @@ def restore_ogx_pod() -> None: timeout = 600 else: timeout = 420 - result = run_e2e_ops("restart-llama-stack", timeout=timeout) + result = run_e2e_ops("restart-ogx", timeout=timeout) print(result.stdout, end="") if result.returncode != 0: print(result.stderr, end="") raise subprocess.CalledProcessError( - result.returncode, "restart-llama-stack", result.stderr + result.returncode, "restart-ogx", result.stderr ) print("✓ OGX pod restored successfully") -def disrupt_llama_stack_pod() -> bool: +def disrupt_ogx_pod() -> bool: """Disrupt OGX connection in Prow/OpenShift environment. Returns: True if the pod was running and has been disrupted, False otherwise. """ try: - result = run_e2e_ops("disrupt-llama-stack", timeout=90) + result = run_e2e_ops("disrupt-ogx", timeout=90) print(result.stdout, end="") # Exit code 0 = disrupted (was running), exit code 2 = was not running diff --git a/tests/e2e/utils/utils.py b/tests/e2e/utils/utils.py index 1025114fe..c10e87d49 100644 --- a/tests/e2e/utils/utils.py +++ b/tests/e2e/utils/utils.py @@ -180,10 +180,10 @@ def validate_json(message: Any, schema: Any) -> None: ) except jsonschema.ValidationError as e: - assert False, "The message doesn't fit the expected schema:" + str(e) + raise AssertionError("The message doesn't fit the expected schema:" + str(e)) except jsonschema.SchemaError as e: - assert False, "The provided schema is faulty:" + str(e) + raise AssertionError("The provided schema is faulty:" + str(e)) def wait_for_container_health( @@ -268,7 +268,61 @@ def wait_for_ogx_ready( ------- True if healthy; False if the wait soft-failed. """ - return wait_for_container_health("llama-stack", max_attempts=max_attempts) + return wait_for_container_health("ogx", max_attempts=max_attempts) + + +def _parsed_json_container(value: Any) -> Optional[Any]: + """Return ``value`` parsed as a JSON object or array, or None. + + Only objects and arrays qualify. A bare string such as ``"1e3"`` is plain + text here, not a document whose formatting may be normalised. + + Parameters: + ---------- + value: Candidate value, only strings are considered. + + Returns: + ------- + The parsed ``dict`` or ``list``, or None when ``value`` is not a string + holding a JSON object or array. + """ + if not isinstance(value, str): + return None + try: + parsed = json.loads(value) + except ValueError: + return None + return parsed if isinstance(parsed, (dict, list)) else None + + +def _json_values_equal(left: Any, right: Any) -> bool: + """Return True when two parsed JSON values are equal, including their types. + + Plain ``==`` is not enough: Python treats ``True == 1``, ``False == 0`` + and ``1 == 1.0`` as equal, so ``{"enabled": true}`` would match + ``{"enabled": 1}``. Object key order is ignored; array order is not. + + Parameters: + ---------- + left: First parsed JSON value. + right: Second parsed JSON value. + + Returns: + ------- + True when both values have the same JSON types and contents. + """ + if type(left) is not type(right): + return False + if isinstance(left, dict): + return left.keys() == right.keys() and all( + _json_values_equal(left[key], right[key]) for key in left + ) + if isinstance(left, list): + return len(left) == len(right) and all( + _json_values_equal(item_left, item_right) + for item_left, item_right in zip(left, right) + ) + return left == right def validate_json_partially(actual: Any, expected: Any) -> None: @@ -276,6 +330,14 @@ def validate_json_partially(actual: Any, expected: Any) -> None: Extra elements/keys are ignored. Raises AssertionError if validation fails. + Values that are strings holding a serialized JSON object or array are + compared by parsed content rather than byte for byte, so a producer that + emits its keys in a different order still matches. The comparison stays + exact — same keys, same values, same JSON types, and array element order + still significant — because relaxing it to the partial semantics used + elsewhere would silently weaken every existing assertion over an embedded + JSON document. + Returns: None @@ -303,6 +365,16 @@ def validate_json_partially(actual: Any, expected: Any) -> None: ), f"No matching element found in list for schema item {schema_item}, got {actual}" else: + if actual != expected: + parsed_actual = _parsed_json_container(actual) + parsed_expected = _parsed_json_container(expected) + if parsed_actual is not None and parsed_expected is not None: + assert _json_values_equal(parsed_actual, parsed_expected), ( + f"JSON-in-string mismatch: expected {parsed_expected}, " + f"got {parsed_actual}" + ) + return + assert actual == expected, f"Value mismatch: expected {expected}, got {actual}" @@ -447,15 +519,20 @@ def restart_container(container_name: str) -> None: Raises: subprocess.CalledProcessError: if the `docker restart` command fails. subprocess.TimeoutExpired: if the `docker restart` command times out. + AssertionError: for ``lightspeed-stack``, if the service does not + accept HTTP within ``wait_for_lightspeed_stack_http_ready``'s + budget. Docker health itself stays a soft failure; the HTTP wait + does not, so callers that must not fail (teardown hooks) have to + guard the call. """ if is_prow_environment(): restart_pod(container_name) - if container_name == "llama-stack": + if container_name == "ogx": from tests.e2e.features.steps.health import ( - reset_llama_stack_disrupt_once_tracking, + reset_ogx_disrupt_once_tracking, ) - reset_llama_stack_disrupt_once_tracking() + reset_ogx_disrupt_once_tracking() return try: @@ -476,48 +553,51 @@ def restart_container(container_name: str) -> None: # that restart the container don't time out. wait_for_container_health(container_name) - if container_name == "llama-stack": + # Docker health can report healthy before uvicorn binds the published + # port (the documented race wait_for_lightspeed_stack_http_ready exists + # for). Unified-mode first boots are the slowest restarts in the suite + # and hit that window reliably, so close it here for every restart + # rather than only in the proxy steps. + if container_name == "lightspeed-stack": + wait_for_lightspeed_stack_http_ready() + + if container_name == "ogx": from tests.e2e.features.steps.health import ( - reset_llama_stack_disrupt_once_tracking, + reset_ogx_disrupt_once_tracking, ) - reset_llama_stack_disrupt_once_tracking() + reset_ogx_disrupt_once_tracking() -def restart_lightspeed_stack_service( - *, wait_http: bool = False, skip_llama_restore: bool = False -) -> None: +def restart_lightspeed_stack_service(*, skip_ogx_restore: bool = False) -> None: """Restart the lightspeed-stack container used by Behave steps. - Wraps ``restart_container("lightspeed-stack")`` and optionally polls the - host-mapped port so step modules share one LCS restart path. + Wraps ``restart_container("lightspeed-stack")`` so step modules share one + LCS restart path. That path already waits for Docker health and then for + HTTP on the host-mapped port, so callers need no wait of their own. Parameters: ---------- - wait_http: When True, also call ``wait_for_lightspeed_stack_http_ready`` - after Docker health. Default False — generic ``The service is - restarted`` relies on Docker health only; proxy/tls steps opt in. - skip_llama_restore: When True on Prow/Konflux, tell e2e-ops not to + skip_ogx_restore: When True on Prow/Konflux, tell e2e-ops not to bring llama back before recreating LCS (degraded-mode startup). """ - previous = os.environ.get("E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART") - if skip_llama_restore: - os.environ["E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART"] = "1" + previous = os.environ.get("E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART") + if skip_ogx_restore: + os.environ["E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART"] = "1" try: restart_container("lightspeed-stack") - if wait_http: - wait_for_lightspeed_stack_http_ready() finally: - if skip_llama_restore: + if skip_ogx_restore: if previous is None: - os.environ.pop("E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART", None) + os.environ.pop("E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART", None) else: - os.environ["E2E_SKIP_LLAMA_RESTORE_ON_LCS_RESTART"] = previous + os.environ["E2E_SKIP_OGX_RESTORE_ON_LCS_RESTART"] = previous def wait_for_lightspeed_stack_http_ready( - max_attempts: int = 80, + timeout_s: float = 120.0, delay_s: float = 1.5, + request_timeout_s: float = 5.0, ) -> None: """Block until Lightspeed Stack accepts HTTP on the host-mapped port. @@ -528,10 +608,21 @@ def wait_for_lightspeed_stack_http_ready( Treats HTTP 200 and 401 as success: the process is listening. Auth-enabled configs (e.g. RBAC jwk-token) return 401 on probes without a Bearer token. + Bounded by a single monotonic deadline covering both the requests and the + sleeps, and each request is additionally capped at the time remaining, so + the wait stays within ``timeout_s`` plus at most one request timeout — + ``requests`` applies its scalar ``timeout`` to the connect and the read + phase separately, so an attempt started just under the deadline can + overrun by that much. An attempt-counted loop cannot give even that + guarantee: with a per-request timeout the worst case is + ``attempts * request_timeout + (attempts - 1) * delay``, which for the + previous defaults was 518.5s while the failure message reported 120s. + Parameters: ---------- - max_attempts: Maximum GET attempts. + timeout_s: Total wall-clock budget for becoming reachable. delay_s: Sleep between attempts. + request_timeout_s: Per-request timeout, clamped to the time remaining. Raises: ------ AssertionError: If ``/liveness`` does not return an accepted status in time. @@ -541,26 +632,35 @@ def wait_for_lightspeed_stack_http_ready( host = os.getenv("E2E_LSC_HOSTNAME", "localhost") port = os.getenv("E2E_LSC_PORT", "8080") url = f"http://{host}:{port}/liveness" - for attempt in range(max_attempts): + started = time.monotonic() + deadline = started + timeout_s + attempt = 0 + while True: + remaining = deadline - time.monotonic() + if remaining <= 0: + break + attempt += 1 try: - response = requests.get(url, timeout=5) + response = requests.get(url, timeout=min(request_timeout_s, remaining)) if response.status_code in (200, 401): return detail = response.text[:200].replace("\n", " ") print( - f"⏱ HTTP wait LSC {attempt + 1}/{max_attempts} " + f"⏱ HTTP wait LSC attempt {attempt} " f"({url} -> {response.status_code}: {detail})..." ) except requests.RequestException as exc: print( - f"⏱ HTTP wait LSC {attempt + 1}/{max_attempts} " + f"⏱ HTTP wait LSC attempt {attempt} " f"({url} -> {exc.__class__.__name__}: {exc})..." ) - if attempt < max_attempts - 1: - time.sleep(delay_s) + if time.monotonic() + delay_s >= deadline: + break + time.sleep(delay_s) + elapsed = time.monotonic() - started raise AssertionError( f"Lightspeed Stack did not become reachable at {url!r} " - f"after {max_attempts} attempts (~{max_attempts * delay_s:.0f}s)" + f"after {attempt} attempts / {elapsed:.0f}s (budget {timeout_s:.0f}s)" ) diff --git a/tests/integration/README.md b/tests/integration/README.md index 6863e4869..e0949b787 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -219,6 +219,9 @@ Integration tests should verify: 3. **External mocks only** - Mock only external services (OGX, external APIs) 4. **Error handling** - HTTP status codes, error messages 5. **Data flow** - Database persistence, cache updates, etc. +6. **CLI contracts** - Repo entrypoints (`src/lightspeed_stack.py --migrate-config`, `--dump-configuration`, `src/ogx_configuration.py`) run as subprocesses: exit codes, messages, written files and their modes + +Anything that needs a *deployed* stack — HTTP against a running service, container restarts, container logs — is an e2e concern instead. Conversely, e2e steps must never touch `src/`; see [Choosing the Test Layer](../../docs/testing/e2e_testing.md#choosing-the-test-layer-e2e-or-integration). ### What NOT to Test diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 55e3ead36..7ea307159 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -544,8 +544,11 @@ def set_streaming_query_agent_run( OTEL_INSTRUMENTED_MODULES = ( + "app.endpoints.authorized", + "app.endpoints.feedback", "app.endpoints.query", "app.endpoints.responses", + "utils.agents.query", "utils.quota_utils", "utils.responses", "utils.shields", diff --git a/tests/integration/endpoints/test_conversations_v1_integration.py b/tests/integration/endpoints/test_conversations_v1_integration.py index bd8cca00e..131a29df1 100644 --- a/tests/integration/endpoints/test_conversations_v1_integration.py +++ b/tests/integration/endpoints/test_conversations_v1_integration.py @@ -236,26 +236,29 @@ async def test_conversation_validation_errors( # Call the appropriate endpoint with pytest.raises(HTTPException) as exc_info: - if endpoint == "get": - await get_conversation_endpoint_handler( - request=non_admin_test_request, - conversation_id=conversation_id, - auth=test_auth, - ) - elif endpoint == "delete": - await delete_conversation_endpoint_handler( - request=non_admin_test_request, - conversation_id=conversation_id, - auth=test_auth, - ) - elif endpoint == "update": - update_request = ConversationUpdateRequest(topic_summary="Updated summary") - await update_conversation_endpoint_handler( - request=non_admin_test_request, - conversation_id=conversation_id, - update_request=update_request, - auth=test_auth, - ) + match endpoint: + case "get": + await get_conversation_endpoint_handler( + request=non_admin_test_request, + conversation_id=conversation_id, + auth=test_auth, + ) + case "delete": + await delete_conversation_endpoint_handler( + request=non_admin_test_request, + conversation_id=conversation_id, + auth=test_auth, + ) + case "update": + update_request = ConversationUpdateRequest( + topic_summary="Updated summary" + ) + await update_conversation_endpoint_handler( + request=non_admin_test_request, + conversation_id=conversation_id, + update_request=update_request, + auth=test_auth, + ) # Verify error status code assert exc_info.value.status_code == expected_status diff --git a/tests/integration/endpoints/test_conversations_v2_integration.py b/tests/integration/endpoints/test_conversations_v2_integration.py index f81b0d139..f6fa0ef44 100644 --- a/tests/integration/endpoints/test_conversations_v2_integration.py +++ b/tests/integration/endpoints/test_conversations_v2_integration.py @@ -175,30 +175,31 @@ async def test_conversation_cache_unavailable_error_handling( test_config.conversation_cache_configuration.type = None with pytest.raises(HTTPException) as exc_info: - if endpoint == "list": - await get_conversations_list_endpoint_handler( - request=non_admin_test_request, - auth=test_auth, - ) - elif endpoint == "get": - await get_conversation_endpoint_handler( - request=non_admin_test_request, - conversation_id=conversation_id, - auth=test_auth, - ) - elif endpoint == "delete": - await delete_conversation_endpoint_handler( - request=non_admin_test_request, - conversation_id=conversation_id, - auth=test_auth, - ) - elif endpoint == "update": - update_request = ConversationUpdateRequest(topic_summary="New topic") - await update_conversation_endpoint_handler( - conversation_id=conversation_id, - update_request=update_request, - auth=test_auth, - ) + match endpoint: + case "list": + await get_conversations_list_endpoint_handler( + request=non_admin_test_request, + auth=test_auth, + ) + case "get": + await get_conversation_endpoint_handler( + request=non_admin_test_request, + conversation_id=conversation_id, + auth=test_auth, + ) + case "delete": + await delete_conversation_endpoint_handler( + request=non_admin_test_request, + conversation_id=conversation_id, + auth=test_auth, + ) + case "update": + update_request = ConversationUpdateRequest(topic_summary="New topic") + await update_conversation_endpoint_handler( + conversation_id=conversation_id, + update_request=update_request, + auth=test_auth, + ) # Verify error details (all should return 500) assert exc_info.value.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR diff --git a/tests/integration/test_configuration.py b/tests/integration/test_configuration.py index 0dc5a6365..f766812f4 100644 --- a/tests/integration/test_configuration.py +++ b/tests/integration/test_configuration.py @@ -68,11 +68,11 @@ def test_loading_proper_configuration(configuration_filename: str) -> None: assert cors_config.allow_headers == ["foo_header", "bar_header", "baz_header"] # check 'ogx' section - ls_config = cfg.ogx_configuration - assert ls_config.use_as_library_client is False - assert str(ls_config.url) == "http://localhost:8321/" - assert ls_config.api_key is not None - assert ls_config.api_key.get_secret_value() == "xyzzy" + ogx_configuration = cfg.ogx_configuration + assert ogx_configuration.use_as_library_client is False + assert str(ogx_configuration.url) == "http://localhost:8321/" + assert ogx_configuration.api_key is not None + assert ogx_configuration.api_key.get_secret_value() == "xyzzy" # check 'user_data_collection' section udc_config = cfg.user_data_collection_configuration diff --git a/tests/integration/test_feedback_otel_trace.py b/tests/integration/test_feedback_otel_trace.py new file mode 100644 index 000000000..bac9b569e --- /dev/null +++ b/tests/integration/test_feedback_otel_trace.py @@ -0,0 +1,95 @@ +"""Integration tests for OpenTelemetry span tree on POST /feedback. + +This module covers the component interaction the feedback handler produces: +the ``feedback.storage`` span is opened inside the ``feedback.submit`` span, +so the two must share a trace and ``feedback.storage`` must be parented to +``feedback.submit``. +""" + +from pathlib import Path + +import pytest +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from pytest_mock import MockerFixture + +from app.endpoints.feedback import feedback_endpoint_handler +from authentication.interface import AuthTuple +from configuration import configuration +from models.api.requests import FeedbackRequest +from models.common.feedback import FeedbackCategory + +ROOT_SPAN_NAME = "feedback.submit" +STORAGE_SPAN_NAME = "feedback.storage" +FEEDBACK_CONVERSATION_ID = "12345678-abcd-0000-0123-456789abcdef" + + +@pytest.fixture(autouse=True) +def _clear_spans(otel_collector: InMemorySpanExporter) -> None: + """Clear collected spans before each test.""" + otel_collector.clear() + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("test_config") +async def test_feedback_storage_span_nested_under_submit( + tmp_path: Path, + test_auth: AuthTuple, + otel_collector: InMemorySpanExporter, + mocker: MockerFixture, +) -> None: + """POST /feedback nests feedback.storage under feedback.submit. + + Feedback storage is pointed at a writable temp directory so the real + filesystem write drives a successful storage span. The test asserts only the + parent-child hierarchy: both spans exist, share a single trace, and + ``feedback.storage`` is parented to ``feedback.submit``. Individual span + attributes and events are covered at the unit level. + + Args: + tmp_path: Writable temp directory used as the feedback storage location. + test_auth: Authentication tuple from the real noop auth dependency. + otel_collector: In-memory OTEL exporter collecting finished spans. + mocker: pytest-mock fixture used to patch conversation retrieval. + """ + user_id, _, _, _ = test_auth + configuration.user_data_collection_configuration.feedback_storage = str(tmp_path) + + # The conversation must exist and belong to the authenticated user. + mock_conversation = mocker.Mock() + mock_conversation.user_id = user_id + mocker.patch( + "app.endpoints.feedback.retrieve_conversation", + return_value=mock_conversation, + ) + + result = await feedback_endpoint_handler( + feedback_request=FeedbackRequest( + conversation_id=FEEDBACK_CONVERSATION_ID, + user_question="What is Kubernetes?", + llm_response="Kubernetes is an open-source container orchestrator.", + user_feedback="The answer was too vague.", + sentiment=-1, + categories=[FeedbackCategory.INCORRECT, FeedbackCategory.INCOMPLETE], + ), + auth=test_auth, + _ensure_feedback_enabled=None, + ) + assert result.response == "feedback received" + + spans = otel_collector.get_finished_spans() + span_names = {span.name for span in spans} + missing = {ROOT_SPAN_NAME, STORAGE_SPAN_NAME} - span_names + assert not missing, f"Missing expected spans: {missing}" + + root = next(span for span in spans if span.name == ROOT_SPAN_NAME) + storage = next(span for span in spans if span.name == STORAGE_SPAN_NAME) + assert root.context is not None + assert storage.context is not None + + assert root.context.trace_id == storage.context.trace_id + assert storage.parent is not None, "feedback.storage should have a parent" + assert ( + storage.parent.span_id == root.context.span_id + ), "feedback.storage should be parented to feedback.submit" diff --git a/tests/integration/test_unified_mode_cli.py b/tests/integration/test_unified_mode_cli.py new file mode 100644 index 000000000..3987af8dc --- /dev/null +++ b/tests/integration/test_unified_mode_cli.py @@ -0,0 +1,177 @@ +"""Integration tests for the unified-mode CLI contracts (LCORE-2343). + +These cover the surfaces the unified-mode e2e features used to exercise by +shelling out to ``src/`` — which e2e steps must not do (see +``docs/testing/e2e_testing.md``, "Choosing the Test Layer"): configuration +validation through ``lightspeed_stack.py --dump-configuration``, +legacy-to-unified migration through ``--migrate-config``, and the committed +migrated e2e fixture that replaced the migration step in +``unified-mode-migration.feature``. + +Everything here runs the real entrypoint as a subprocess from the repository +root, the way operators and the container entrypoint invoke it; the in-process +half of the same pipeline lives in ``test_unified_synthesis.py``. +""" + +import os +import stat +import subprocess +import sys +from pathlib import Path +from typing import Any + +import pytest +import yaml + +from ogx_configuration import synthesize_configuration + +_REPO_ROOT = Path(__file__).resolve().parents[2] +_ENTRYPOINT = _REPO_ROOT / "src" / "lightspeed_stack.py" +_FIXTURES = _REPO_ROOT / "tests" / "configuration" / "unified-mode" +_E2E_FIXTURES = _REPO_ROOT / "tests" / "e2e" / "configuration" / "unified-mode" +# The run.yaml the e2e harness materializes at the repo root in CI; the +# committed migrated e2e fixtures were generated against it. +_E2E_RUN_YAML = _REPO_ROOT / "tests" / "e2e" / "configs" / "run-ci.yaml" +_MIGRATED_FIXTURE = "lightspeed-stack-unified-migrated.yaml" +_LEGACY_PAIR_FIXTURE = "lightspeed-stack-legacy-for-migration.yaml" +_CLI_TIMEOUT_SECONDS = 120 + + +def _run_cli(*args: str) -> subprocess.CompletedProcess[str]: + """Run the service entrypoint as a subprocess from the repo root, never raising.""" + return subprocess.run( + [sys.executable, str(_ENTRYPOINT), *args], + cwd=_REPO_ROOT, + capture_output=True, + text=True, + timeout=_CLI_TIMEOUT_SECONDS, + check=False, + ) + + +def _load_yaml(path: Path) -> Any: + """Parse a YAML file.""" + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def _migrate(lcs_config: Path, run_yaml: Path, output: Path) -> None: + """Run ``--migrate-config`` for a legacy pair and assert it succeeded.""" + result = _run_cli( + "--migrate-config", + "--run-yaml", + str(run_yaml), + "-c", + str(lcs_config), + "--migrate-output", + str(output), + ) + assert result.returncode == 0 and output.is_file(), ( + f"--migrate-config failed (rc={result.returncode}).\n" + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + + +# --------------------------------------------------------------------------- +# Validation through the CLI (R3 mutual exclusion, R11 format-version marker) +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("fixture", "expected_message"), + [ + pytest.param( + "lightspeed-stack-invalid-providers-and-legacy.yaml", + "--migrate-config", + id="inference-providers-plus-legacy-path", + ), + pytest.param( + "lightspeed-stack-invalid-config-and-legacy.yaml", + "--migrate-config", + id="config-block-plus-legacy-path", + ), + pytest.param( + "lightspeed-stack-invalid-version-legacy-unified-body.yaml", + "config_format_version", + id="legacy-version-marker-on-unified-body", + ), + ], +) +def test_cli_rejects_invalid_configuration(fixture: str, expected_message: str) -> None: + """``--dump-configuration`` exits non-zero and names the problem. + + ``main()`` loads (and thereby validates) the configuration before any dump + handling, so a failed cross-field validation surfaces as a non-zero exit + with the Pydantic message on stderr. The fixtures point their legacy path + at an existing run.yaml so the captured failure is the intended + cross-field error, not a file-not-found. + """ + result = _run_cli("--dump-configuration", "-c", str(_FIXTURES / fixture)) + output = result.stdout + result.stderr + + assert result.returncode != 0, ( + f"expected {fixture} to fail validation, but the load succeeded. " + f"Output:\n{output}" + ) + assert expected_message in output, ( + f"validation failure for {fixture} does not mention " + f"{expected_message!r}. Full output:\n{output}" + ) + + +# --------------------------------------------------------------------------- +# --migrate-config contract +# --------------------------------------------------------------------------- + + +def test_cli_migrate_config_writes_unified_owner_only(tmp_path: Path) -> None: + """``--migrate-config`` emits a unified file, owner-only, that round-trips. + + The output carries the run.yaml as ``native_override`` and drops the legacy + ``library_client_config_path``; it is written 0600 because migrated files + may carry lifted secrets (R10); and synthesizing it reproduces the pair's + run.yaml data (migrate-then-synthesize round trip). + """ + output = tmp_path / "unified.yaml" + _migrate(_FIXTURES / _LEGACY_PAIR_FIXTURE, _E2E_RUN_YAML, output) + + mode = stat.S_IMODE(os.stat(output).st_mode) + assert mode == 0o600, f"migrated file mode is {oct(mode)}, expected 0o600" + + text = output.read_text(encoding="utf-8") + migrated = yaml.safe_load(text) + assert migrated["ogx"]["config"][ + "native_override" + ], "migrated config carries no native_override" + assert "library_client_config_path" not in text + + synthesized = synthesize_configuration(migrated, config_file_dir=str(tmp_path)) + assert synthesized == _load_yaml(_E2E_RUN_YAML) + + +@pytest.mark.parametrize("mode", ["library-mode", "server-mode"]) +def test_committed_migrated_fixture_matches_cli_output( + tmp_path: Path, mode: str +) -> None: + """The committed migrated e2e fixture is exactly what the CLI produces today. + + ``unified-mode-migration.feature`` boots + ``tests/e2e/configuration/unified-mode//lightspeed-stack-unified-migrated.yaml`` + instead of generating it in a step (e2e steps never run ``src/`` CLIs). + This guard fails the moment ``--migrate-config`` output drifts from the + committed file. To refresh the fixture, run from the repo root with + ``DIR=tests/e2e/configuration/unified-mode/``:: + + uv run python src/lightspeed_stack.py --migrate-config \\ + --run-yaml tests/e2e/configs/run-ci.yaml \\ + -c $DIR/lightspeed-stack-legacy-for-migration.yaml \\ + --migrate-output $DIR/lightspeed-stack-unified-migrated.yaml + chmod 644 $DIR/lightspeed-stack-unified-migrated.yaml + """ + output = tmp_path / "migrated.yaml" + _migrate(_E2E_FIXTURES / mode / _LEGACY_PAIR_FIXTURE, _E2E_RUN_YAML, output) + + committed = _E2E_FIXTURES / mode / _MIGRATED_FIXTURE + assert _load_yaml(output) == _load_yaml(committed), ( + f"{committed} no longer matches --migrate-config output; regenerate it " + "(see this test's docstring)" + ) diff --git a/tests/integration/test_unified_synthesis.py b/tests/integration/test_unified_synthesis.py index 28a7d5d5a..0d5f9adba 100644 --- a/tests/integration/test_unified_synthesis.py +++ b/tests/integration/test_unified_synthesis.py @@ -23,6 +23,8 @@ import yaml from pydantic import ValidationError +import constants +from client.ogx import AsyncOgxClientHolder from configuration import configuration from ogx_configuration import ( CONDITIONAL_OPENAI_PROVIDER_ID, @@ -35,7 +37,7 @@ # A complete, valid lightspeed-stack.yaml used as the base for configs that # are loaded through the real AppConfig.load_configuration pipeline; -# individual tests override its llama_stack / inference sections. +# individual tests override its ogx / inference sections. _BASE_CONFIG_PATH = "tests/configuration/lightspeed-stack.yaml" # A representative operator-authored legacy run.yaml. It deliberately carries @@ -168,7 +170,7 @@ def _legacy_enriched(tmp_path: Path, enrichment: dict[str, Any]) -> dict[str, An def _base_config_dict() -> dict[str, Any]: """Load the base lightspeed-stack.yaml fixture as a fresh dict.""" - with open(_BASE_CONFIG_PATH, "r", encoding="utf-8") as file: + with open(_BASE_CONFIG_PATH, encoding="utf-8") as file: return copy.deepcopy(yaml.safe_load(file)) @@ -471,7 +473,7 @@ def test_migrate_then_synthesize_preserves_enrichment_parity( def test_load_rejects_config_block_and_legacy_path_together( tmp_path: Path, ) -> None: - """A llama_stack.config block plus a legacy path fails the real load (R3).""" + """An ogx.config block plus a legacy path fails the real load (R3).""" lcs_dict = _base_config_dict() lcs_dict["ogx"] = { "use_as_library_client": True, @@ -522,3 +524,81 @@ def test_load_accepts_minimal_unified_config(tmp_path: Path) -> None: loaded = configuration.configuration assert loaded.ogx.config is None assert loaded.inference.providers[0].type == "openai" + + +# --------------------------------------------------------------------------- +# R6: emitted secrets stay environment references on disk +# --------------------------------------------------------------------------- + + +def test_synthesized_secrets_stay_env_references( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A provider's ``api_key_env`` lands on disk as ``${env.NAME}``, never resolved. + + The synthesizer must emit the environment reference for OGX to resolve at + its own startup; the literal secret value must never be written (R6). + """ + secret = "sk-resolved-secret-that-must-not-land-on-disk" + monkeypatch.setenv("OPENAI_API_KEY", secret) + lcs_dict = _base_config_dict() + lcs_dict["ogx"] = {"use_as_library_client": True} + lcs_dict["inference"] = { + "providers": [{"type": "openai", "api_key_env": "OPENAI_API_KEY"}] + } + _, out_path = _load_and_synthesize(tmp_path, lcs_dict) + + text = out_path.read_text(encoding="utf-8") + assert "${env.OPENAI_API_KEY}" in text + assert secret not in text + + +# --------------------------------------------------------------------------- +# R11: explicit config_format_version must agree with the detected shape +# --------------------------------------------------------------------------- + + +def test_load_rejects_legacy_version_marker_on_unified_body(tmp_path: Path) -> None: + """``config_format_version: legacy`` on a unified-shaped body fails the real load.""" + lcs_dict = _base_config_dict() + lcs_dict["ogx"] = {"use_as_library_client": True} + lcs_dict["inference"] = { + "providers": [{"type": "openai", "api_key_env": "OPENAI_API_KEY"}] + } + lcs_dict["config_format_version"] = "legacy" + cfg_path = _write_yaml(tmp_path / "lightspeed-stack.yaml", lcs_dict) + with pytest.raises(ValidationError, match="config_format_version"): + configuration.load_configuration(str(cfg_path)) + + +# --------------------------------------------------------------------------- +# --synthesized-config-output: the override the workers honour +# --------------------------------------------------------------------------- + + +def test_synthesized_config_output_override( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """The synthesized-config-output override redirects library-mode synthesis. + + ``--synthesized-config-output`` reaches the uvicorn workers as + ``LIGHTSPEED_STACK_SYNTHESIZED_CONFIG_PATH``; the client holder must write + the synthesized run.yaml there and leave the default location untouched. + """ + lcs_dict = _base_config_dict() + lcs_dict["ogx"] = { + "use_as_library_client": True, + "config": {"baseline": "empty", "native_override": {"version": 2}}, + } + cfg_path = _write_yaml(tmp_path / "lightspeed-stack.yaml", lcs_dict) + custom_output = tmp_path / "custom-run.yaml" + monkeypatch.chdir(tmp_path) + monkeypatch.setenv(constants.CONFIG_PATH_ENV_VAR, str(cfg_path)) + monkeypatch.setenv(constants.SYNTHESIZED_CONFIG_PATH_ENV_VAR, str(custom_output)) + + # pylint: disable-next=protected-access + written = AsyncOgxClientHolder()._synthesize_library_config() + + assert Path(written) == custom_output + assert isinstance(yaml.safe_load(custom_output.read_text(encoding="utf-8")), dict) + assert not (tmp_path / constants.DEFAULT_SYNTHESIZED_CONFIG_PATH).exists() diff --git a/tests/unit/README.md b/tests/unit/README.md index 691a2585e..c98915e6b 100644 --- a/tests/unit/README.md +++ b/tests/unit/README.md @@ -24,6 +24,10 @@ Unit tests for the degraded mode tracker. Unit tests for functions defined in src/lightspeed_stack.py. +## [test_log.py](test_log.py) + +Unit tests for functions defined in src/log.py. + ## [test_ogx_configuration.py](test_ogx_configuration.py) Unit tests for src/ogx_configuration.py. @@ -32,7 +36,3 @@ Unit tests for src/ogx_configuration.py. Unit tests for unified-mode OGX configuration synthesis (LCORE-2336). -## [test_log.py](test_log.py) - -Unit tests for functions defined in src/log.py. - diff --git a/tests/unit/app/endpoints/README.md b/tests/unit/app/endpoints/README.md index 5493e35c4..e9dd97e91 100644 --- a/tests/unit/app/endpoints/README.md +++ b/tests/unit/app/endpoints/README.md @@ -72,6 +72,10 @@ Unit tests for the /providers REST API endpoints. Unit tests for the /query (v2) REST API endpoint using Responses API. +## [test_query_otel.py](test_query_otel.py) + +OpenTelemetry unit tests for the /query REST API endpoint. + ## [test_rags.py](test_rags.py) Unit tests for the /rags REST API endpoints. diff --git a/tests/unit/app/endpoints/test_query_otel.py b/tests/unit/app/endpoints/test_query_otel.py new file mode 100644 index 000000000..9b76e6f14 --- /dev/null +++ b/tests/unit/app/endpoints/test_query_otel.py @@ -0,0 +1,206 @@ +# pylint: disable=redefined-outer-name +"""OpenTelemetry unit tests for the /query REST API endpoint.""" + +from typing import Any + +import pytest +from fastapi import HTTPException, Request, status +from ogx_client import AsyncOgxClient +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from pytest_mock import MockerFixture + +from app.endpoints.query import query_endpoint_handler +from configuration import AppConfig +from models.api.requests import QueryRequest +from models.api.responses.error import QuotaExceededResponse +from models.common.moderation import ShieldModerationPassed +from models.common.responses.responses_api_params import ResponsesApiParams +from models.common.turn_summary import TurnSummary +from quota.quota_exceed_error import QuotaExceedError +from utils.otel_tracing import SpanAttributes, SpanEvents + +MODULE = "app.endpoints.query" +QUERY_SPAN_NAME = "query.handle_request" +QUERY_TEXT = "What is Kubernetes?" + +# User ID must be a proper UUID. +MOCK_AUTH = ( + "00000001-0001-0001-0001-000000000001", + "mock_username", + False, + "mock_token", +) + + +@pytest.fixture(name="dummy_request") +def dummy_request_fixture() -> Request: + """Minimal FastAPI Request for query endpoint unit tests.""" + return Request(scope={"type": "http", "headers": []}) + + +@pytest.fixture(name="minimal_config") +def minimal_config_fixture() -> AppConfig: + """Minimal AppConfig for query endpoint OTEL tests.""" + cfg = AppConfig() + cfg.init_from_dict( + { + "name": "test", + "service": {"host": "localhost", "port": 8080}, + "ogx": { + "api_key": "test-key", + "url": "http://test.com:1234", + "use_as_library_client": False, + }, + "user_data_collection": {"transcripts_enabled": False}, + "mcp_servers": [], + "conversation_cache": {"type": "noop"}, + } + ) + return cfg + + +def _patch_query_success(mocker: MockerFixture) -> None: + """Patch the query handler dependencies for a successful turn.""" + mocker.patch(f"{MODULE}.check_configuration_loaded") + mocker.patch(f"{MODULE}.check_tokens_available") + mocker.patch(f"{MODULE}.validate_model_provider_override") + mocker.patch(f"{MODULE}.check_mcp_auth", new=mocker.AsyncMock()) + + mock_response_obj = mocker.Mock() + mock_response_obj.output = [] + mock_client = mocker.AsyncMock(spec=AsyncOgxClient) + mock_client.responses = mocker.Mock() + mock_client.responses.create = mocker.AsyncMock(return_value=mock_response_obj) + mock_holder = mocker.Mock() + mock_holder.get_client.return_value = mock_client + mocker.patch(f"{MODULE}.AsyncOgxClientHolder", return_value=mock_holder) + + mocker.patch( + f"{MODULE}.maybe_get_topic_summary", + new=mocker.AsyncMock(return_value=None), + ) + mocker.patch( + f"{MODULE}.run_shield_moderation", + new=mocker.AsyncMock(return_value=ShieldModerationPassed()), + ) + + mock_params = mocker.Mock(spec=ResponsesApiParams) + mock_params.model = "provider1/model1" + mock_params.conversation = "conv_123" + mock_params.tools = None + mock_params.model_dump.return_value = {"input": "test", "model": "provider1/model1"} + mocker.patch( + f"{MODULE}.prepare_responses_params", + new=mocker.AsyncMock(return_value=mock_params), + ) + + turn_summary = TurnSummary() + turn_summary.llm_response = "Kubernetes is a container orchestration platform" + mocker.patch( + f"{MODULE}.retrieve_agent_response", + new=mocker.AsyncMock(return_value=turn_summary), + ) + + mocker.patch(f"{MODULE}.normalize_conversation_id", return_value="123") + mocker.patch(f"{MODULE}.store_query_results") + mocker.patch(f"{MODULE}.consume_query_tokens") + mocker.patch(f"{MODULE}.get_available_quotas", return_value={}) + + +@pytest.mark.asyncio +async def test_query_root_span_attributes_and_events( + dummy_request: Request, + minimal_config: AppConfig, + mocker: MockerFixture, + otel: tuple[Any, InMemorySpanExporter], +) -> None: + """The /query root span carries setup attributes and all lifecycle events. + + The mocked success path validates the request, persists the turn, and + completes the LLM response, so the validation/turn-persisted/LLM-response + events are all recorded, and the anonymized user/input attributes are set. + """ + tracer, exporter = otel + mocker.patch(f"{MODULE}.configuration", minimal_config) + mocker.patch(f"{MODULE}.tracer", tracer) + mocker.patch( + f"{MODULE}.anonymize_value", side_effect=lambda value: f"[anon:{value}]" + ) + _patch_query_success(mocker) + + await query_endpoint_handler( + request=dummy_request, + query_request=QueryRequest( + query=QUERY_TEXT + ), # pyright: ignore[reportCallIssue] + auth=MOCK_AUTH, + mcp_headers={}, + ) + + root = next(s for s in exporter.get_finished_spans() if s.name == QUERY_SPAN_NAME) + attrs = dict(root.attributes or {}) + assert attrs[SpanAttributes.USER_ID] == f"[anon:{MOCK_AUTH[0]}]" + assert attrs[SpanAttributes.INPUT] == f"[anon:{QUERY_TEXT}]" + assert attrs[SpanAttributes.REQUEST_ATTACHMENTS_COUNT] == 0 + assert SpanAttributes.OUTPUT in attrs + assert SpanAttributes.SESSION_ID in attrs + + event_names = {event.name for event in root.events} + assert SpanEvents.VALIDATION_COMPLETED in event_names + assert SpanEvents.TURN_PERSISTED in event_names + assert SpanEvents.LLM_RESPONSE_COMPLETED in event_names + + +@pytest.mark.asyncio +async def test_query_quota_exceeded_records_attributes_without_events( + dummy_request: Request, + minimal_config: AppConfig, + mocker: MockerFixture, + otel: tuple[Any, InMemorySpanExporter], +) -> None: + """A 429 from the quota check leaves the root span attributes but no events. + + The quota check runs after the root attributes are recorded but before any + lifecycle event fires. When it raises HTTP 429 the request is aborted, so the + ``query.handle_request`` span is still exported with its user/input + attributes, but none of the validation/turn-persisted/LLM-response events are + recorded, and tracing does not crash on the error path. + """ + tracer, exporter = otel + mocker.patch(f"{MODULE}.configuration", minimal_config) + mocker.patch(f"{MODULE}.tracer", tracer) + mocker.patch( + f"{MODULE}.anonymize_value", side_effect=lambda value: f"[anon:{value}]" + ) + mocker.patch(f"{MODULE}.check_configuration_loaded") + mocker.patch(f"{MODULE}.check_mcp_auth", new=mocker.AsyncMock()) + + def _raise_quota_exceeded(*_args: object, **_kwargs: object) -> None: + error = QuotaExceedError(subject_id=MOCK_AUTH[0], subject_type="u", available=0) + raise HTTPException(**QuotaExceededResponse.from_exception(error).model_dump()) + + mocker.patch(f"{MODULE}.check_tokens_available", side_effect=_raise_quota_exceeded) + + with pytest.raises(HTTPException) as exc_info: + await query_endpoint_handler( + request=dummy_request, + query_request=QueryRequest( # pyright: ignore[reportCallIssue] + query=QUERY_TEXT + ), + auth=MOCK_AUTH, + mcp_headers={}, + ) + assert exc_info.value.status_code == status.HTTP_429_TOO_MANY_REQUESTS + + root = next(s for s in exporter.get_finished_spans() if s.name == QUERY_SPAN_NAME) + attrs = dict(root.attributes or {}) + assert attrs[SpanAttributes.USER_ID] == f"[anon:{MOCK_AUTH[0]}]" + assert attrs[SpanAttributes.INPUT] == f"[anon:{QUERY_TEXT}]" + assert attrs[SpanAttributes.REQUEST_ATTACHMENTS_COUNT] == 0 + + event_names = {event.name for event in root.events} + assert SpanEvents.VALIDATION_COMPLETED not in event_names + assert SpanEvents.TURN_PERSISTED not in event_names + assert SpanEvents.LLM_RESPONSE_COMPLETED not in event_names diff --git a/tests/unit/app/endpoints/test_responses_otel.py b/tests/unit/app/endpoints/test_responses_otel.py index b1de65e81..e6139a71e 100644 --- a/tests/unit/app/endpoints/test_responses_otel.py +++ b/tests/unit/app/endpoints/test_responses_otel.py @@ -25,11 +25,15 @@ from models.config import Action from tests.unit.app.endpoints.responses_otel_helpers import ( MOCK_AUTH, + MODEL, MODULE, + OTEL_CONV_ID, + ROOT_SPAN_NAME, assert_root_setup_attributes, find_span, make_turn_summary_with_tools, make_turn_summary_without_tools, + patch_handler_success_mocks, patch_responses_endpoint_setup, patch_responses_otel_tracers, run_responses_setup_smoke, @@ -252,3 +256,47 @@ async def test_streaming_root_span_closed_on_setup_error( ) find_span(exporter.get_finished_spans(), "responses.handle_request") + + @pytest.mark.asyncio + async def test_llm_failure_keeps_validation_without_response_event( + self, + dummy_request: Request, + minimal_config: AppConfig, + mocker: MockerFixture, + otel: tuple[Any, InMemorySpanExporter], + ) -> None: + """A failing LLM call keeps validation.completed but drops llm.response.completed. + + ``validation.completed`` fires before the model is called; the + LLM-response event only fires once the turn is finalized. Forcing + ``responses.create`` to raise aborts the request after validation, so the + root span is exported with the validation event but without the + LLM-response event, and tracing does not crash on the error path. + """ + tracer, exporter = otel + patch_responses_otel_tracers(mocker, tracer, minimal_config) + mock_client = patch_responses_endpoint_setup(mocker, minimal_config) + patch_handler_success_mocks(mocker) + mock_client.responses.create = mocker.AsyncMock( + side_effect=ApiException(status=None, reason="connection failed") + ) + + with pytest.raises(HTTPException): + await responses_endpoint_handler( + request=dummy_request, + responses_request=ResponsesRequest( + input=INPUT_TEXT, + model=MODEL, + stream=False, + store=False, + conversation=OTEL_CONV_ID, + generate_topic_summary=False, + ), + auth=MOCK_AUTH, + mcp_headers={}, + ) + + root = find_span(exporter.get_finished_spans(), ROOT_SPAN_NAME) + event_names = [event.name for event in root.events] + assert SpanEvents.VALIDATION_COMPLETED in event_names + assert SpanEvents.LLM_RESPONSE_COMPLETED not in event_names diff --git a/tests/unit/app/endpoints/test_stream_interrupt.py b/tests/unit/app/endpoints/test_stream_interrupt.py index d257f56f4..182edc60c 100644 --- a/tests/unit/app/endpoints/test_stream_interrupt.py +++ b/tests/unit/app/endpoints/test_stream_interrupt.py @@ -3,9 +3,15 @@ import asyncio import threading from collections.abc import Generator +from typing import Any import pytest from fastapi import HTTPException +from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, +) +from opentelemetry.trace import StatusCode +from pytest_mock import MockerFixture from app.endpoints.stream_interrupt import stream_interrupt_endpoint_handler from models.api.requests import StreamingInterruptRequest @@ -31,6 +37,8 @@ REQUEST_ID_ALREADY_COMPLETED, ) +CONVERSATION_ID = "323e4567-e89b-12d3-a456-426614174777" + @pytest.fixture(name="registry") def registry_fixture() -> Generator[StreamInterruptRegistry, None, None]: @@ -225,3 +233,162 @@ async def pending_stream() -> None: registry=registry, ) assert second_response.interrupted is False + + +class TestStreamInterruptOtelSpans: + """OTEL instrumentation tests for the stream interrupt endpoint.""" + + @pytest.mark.asyncio + async def test_interrupt_success_emits_span_with_attributes( + self, + mocker: MockerFixture, + registry: StreamInterruptRegistry, + otel: tuple[Any, InMemorySpanExporter], + ) -> None: + """A successful interrupt emits one span with the expected attributes.""" + tracer, exporter = otel + mocker.patch("app.endpoints.stream_interrupt.tracer", tracer) + + async def pending_stream() -> None: + await asyncio.sleep(10) + + task = asyncio.create_task(pending_stream()) + registry.register_stream( + REQUEST_ID_SUCCESS, + OWNER_USER_ID, + task, + conversation_id=CONVERSATION_ID, + ) + + await stream_interrupt_endpoint_handler( + interrupt_request=StreamingInterruptRequest(request_id=REQUEST_ID_SUCCESS), + auth=(OWNER_USER_ID, "mock_username", False, "mock_token"), + registry=registry, + ) + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.name == "stream.interrupt" + + attrs = dict(span.attributes or {}) + assert attrs["interrupt.request_id"] == REQUEST_ID_SUCCESS + assert attrs["stream.conversation.id"] == CONVERSATION_ID + assert attrs["interrupt.result"] == CancelStreamResult.CANCELLED.value + assert span.status.status_code == StatusCode.OK + + with pytest.raises(asyncio.CancelledError): + await task + + @pytest.mark.asyncio + async def test_interrupt_not_found_sets_error_status( + self, + mocker: MockerFixture, + registry: StreamInterruptRegistry, + otel: tuple[Any, InMemorySpanExporter], + ) -> None: + """An unknown request id records the not_found result and error status.""" + tracer, exporter = otel + mocker.patch("app.endpoints.stream_interrupt.tracer", tracer) + + with pytest.raises(HTTPException): + await stream_interrupt_endpoint_handler( + interrupt_request=StreamingInterruptRequest( + request_id=REQUEST_ID_NOT_FOUND + ), + auth=(OWNER_USER_ID, "mock_username", False, "mock_token"), + registry=registry, + ) + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + attrs = dict(span.attributes or {}) + assert attrs["interrupt.request_id"] == REQUEST_ID_NOT_FOUND + assert attrs["interrupt.result"] == CancelStreamResult.NOT_FOUND.value + # No conversation id is recorded when the stream is unknown. + assert "stream.conversation.id" not in attrs + assert span.status.status_code == StatusCode.ERROR + + @pytest.mark.asyncio + async def test_interrupt_wrong_user_sets_error_and_hides_conversation( + self, + mocker: MockerFixture, + registry: StreamInterruptRegistry, + otel: tuple[Any, InMemorySpanExporter], + ) -> None: + """A non-owner interrupt records forbidden and does not leak conversation id.""" + tracer, exporter = otel + mocker.patch("app.endpoints.stream_interrupt.tracer", tracer) + + async def pending_stream() -> None: + await asyncio.sleep(10) + + task = asyncio.create_task(pending_stream()) + registry.register_stream( + request_id=REQUEST_ID_WRONG_USER, + user_id=OWNER_USER_ID, + task=task, + conversation_id=CONVERSATION_ID, + ) + + with pytest.raises(HTTPException): + await stream_interrupt_endpoint_handler( + interrupt_request=StreamingInterruptRequest( + request_id=REQUEST_ID_WRONG_USER + ), + auth=(NON_OWNER_USER_ID, "mock_username", False, "mock_token"), + registry=registry, + ) + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + attrs = dict(span.attributes or {}) + assert attrs["interrupt.result"] == CancelStreamResult.FORBIDDEN.value + # A non-owner must not see the stream's conversation id. + assert "stream.conversation.id" not in attrs + assert span.status.status_code == StatusCode.ERROR + + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + + @pytest.mark.asyncio + async def test_interrupt_already_done_records_ok_status( + self, + mocker: MockerFixture, + registry: StreamInterruptRegistry, + otel: tuple[Any, InMemorySpanExporter], + ) -> None: + """An already-completed stream records already_done with an OK status.""" + tracer, exporter = otel + mocker.patch("app.endpoints.stream_interrupt.tracer", tracer) + + async def completed_stream() -> None: + return None + + task = asyncio.create_task(completed_stream()) + await task + registry.register_stream( + REQUEST_ID_ALREADY_COMPLETED, + OWNER_USER_ID, + task, + conversation_id=CONVERSATION_ID, + ) + + await stream_interrupt_endpoint_handler( + interrupt_request=StreamingInterruptRequest( + request_id=REQUEST_ID_ALREADY_COMPLETED + ), + auth=(OWNER_USER_ID, "mock_username", False, "mock_token"), + registry=registry, + ) + + spans = exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + attrs = dict(span.attributes or {}) + assert attrs["interrupt.result"] == CancelStreamResult.ALREADY_DONE.value + assert attrs["stream.conversation.id"] == CONVERSATION_ID + assert span.status.status_code == StatusCode.OK diff --git a/tests/unit/app/test_main_middleware.py b/tests/unit/app/test_main_middleware.py index bd364a7a1..017edf689 100644 --- a/tests/unit/app/test_main_middleware.py +++ b/tests/unit/app/test_main_middleware.py @@ -303,3 +303,50 @@ def test_app_routes_paths_contains_application_routes() -> None: assert "/liveness" in app_routes_paths assert "/readiness" in app_routes_paths assert len(app_routes_paths) > 4 + + +def test_app_routes_paths_contains_versioned_routes() -> None: + """app_routes_paths must include full mount-prefixed paths like /v1/infer. + + iter_route_contexts() exposes two path attributes per route: + - rc.original_route.path — path relative to the sub-router (e.g. /infer) + - rc.path — effective path with the mount prefix (e.g. /v1/infer) + + Using rc.original_route.path silently omits the /v1 mount prefix, so the + middleware path check never matches requests that arrive as /v1/infer and + ls_rest_api_calls_total is never recorded for versioned endpoints. + """ + assert "/v1/infer" in app_routes_paths + + +@pytest.mark.asyncio +async def test_rest_api_metrics_records_when_proxy_strips_prefix( + mocker: MockerFixture, +) -> None: + """Metrics must be recorded when the proxy already stripped root_path. + + When nginx (or any proxy) strips the root_path prefix before forwarding, + lightspeed-stack receives /v1/infer directly — not /api/lightspeed/v1/infer. + The middleware must match /v1/infer against app_routes_paths without any + prefix stripping, and still record the metric. + """ + mocker.patch("app.main.app_routes_paths", ["/v1/infer"]) + mocker.patch.object(fastapi_app, "root_path", "/api/lightspeed") + mock_measure_duration = mocker.patch( + "app.main.recording.measure_response_duration", return_value=nullcontext() + ) + mock_record_call = mocker.patch("app.main.recording.record_rest_api_call") + + async def ok_app(_scope: Scope, _receive: Receive, send: Send) -> None: + await send({"type": "http.response.start", "status": 200, "headers": []}) + await send({"type": "http.response.body", "body": b"ok"}) + + middleware = RestApiMetricsMiddleware(ok_app) + collector = _ResponseCollector() + + # Proxy stripped /api/lightspeed; lightspeed-stack receives /v1/infer directly. + await middleware(_make_scope("/v1/infer"), _noop_receive, collector) + + assert collector.status_code == 200 + mock_measure_duration.assert_called_once_with("/v1/infer") + mock_record_call.assert_called_once_with("/v1/infer", 200) diff --git a/tests/unit/authorization/test_middleware.py b/tests/unit/authorization/test_middleware.py index adffe56d1..e990a96c1 100644 --- a/tests/unit/authorization/test_middleware.py +++ b/tests/unit/authorization/test_middleware.py @@ -315,12 +315,13 @@ async def test_request_state_handling( kwargs = {"auth": dummy_auth_tuple} args = [] - if request_location == "kwargs": - kwargs["request"] = mock_request - elif request_location == "args": - args = [ - mock_request, - ] + match request_location: + case "kwargs": + kwargs["request"] = mock_request + case "args": + args = [ + mock_request, + ] await _perform_authorization_check(Action.QUERY, args, kwargs) diff --git a/tests/unit/models/config/README.md b/tests/unit/models/config/README.md index c60c41ee9..c251ab0ec 100644 --- a/tests/unit/models/config/README.md +++ b/tests/unit/models/config/README.md @@ -56,10 +56,6 @@ Unit tests for InferenceConfiguration model. Unit tests for JwtRoleRule model. -## [test_ogx_configuration.py](test_ogx_configuration.py) - -Unit tests for OgxConfiguration model. - ## [test_model_context_protocol_server.py](test_model_context_protocol_server.py) Unit tests for ModelContextProtocolServer model. @@ -68,6 +64,10 @@ Unit tests for ModelContextProtocolServer model. Unit tests for ObservabilityConfiguration model. +## [test_ogx_configuration.py](test_ogx_configuration.py) + +Unit tests for OgxConfiguration model. + ## [test_postgresql_database_configuration.py](test_postgresql_database_configuration.py) Unit tests for PostgreSQLDatabaseConfiguration model. diff --git a/tests/unit/models/config/test_dump_configuration.py b/tests/unit/models/config/test_dump_configuration.py index 15e3baee9..c02c42ca6 100644 --- a/tests/unit/models/config/test_dump_configuration.py +++ b/tests/unit/models/config/test_dump_configuration.py @@ -110,7 +110,7 @@ def test_dump_configuration_minimal_cfg(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -336,7 +336,7 @@ def test_dump_configuration_valid_values(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -552,7 +552,7 @@ def test_dump_configuration_with_one_mcp_server(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) assert content is not None assert "mcp_servers" in content @@ -604,7 +604,7 @@ def test_dump_configuration_with_more_mcp_servers(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) assert content is not None assert "mcp_servers" in content @@ -715,7 +715,7 @@ def test_dump_configuration_with_quota_limiters(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -995,7 +995,7 @@ def test_dump_configuration_with_quota_limiters_different_values( dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -1230,7 +1230,7 @@ def test_dump_configuration_with_vector_store(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) assert content["vector_store"] == { @@ -1310,7 +1310,7 @@ def test_dump_configuration_byok(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -1566,7 +1566,7 @@ def test_dump_configuration_pg_namespace(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -1807,7 +1807,7 @@ def test_dump_configuration_with_one_skill(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -1884,7 +1884,7 @@ def test_dump_configuration_with_skills(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -1962,7 +1962,7 @@ def test_dump_configuration_allow_degraded_mode(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -2206,7 +2206,7 @@ def test_dump_configuration_max_retries_settings(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -2450,7 +2450,7 @@ def test_dump_configuration_retry_count_settings(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None @@ -2700,7 +2700,7 @@ def test_dump_configuration_specific_compaction_values(tmp_path: Path) -> None: dump_file = tmp_path / "test.json" cfg.dump(dump_file) - with open(dump_file, "r", encoding="utf-8") as fin: + with open(dump_file, encoding="utf-8") as fin: content = json.load(fin) # content should be loaded assert content is not None diff --git a/tests/unit/models/config/test_ogx_configuration.py b/tests/unit/models/config/test_ogx_configuration.py index 93183f77b..578961bbe 100644 --- a/tests/unit/models/config/test_ogx_configuration.py +++ b/tests/unit/models/config/test_ogx_configuration.py @@ -17,79 +17,79 @@ from utils.checks import InvalidConfigurationError # A complete, valid lightspeed-stack.yaml used as the base for root-model -# (Configuration) validation tests; individual tests override its llama_stack +# (Configuration) validation tests; individual tests override its ogx # and inference sections to exercise unified-vs-legacy mode detection. _BASE_CONFIG_PATH = "tests/configuration/lightspeed-stack.yaml" def _base_config_dict() -> dict[str, Any]: """Load the base lightspeed-stack.yaml fixture as a fresh dict.""" - with open(_BASE_CONFIG_PATH, "r", encoding="utf-8") as file: + with open(_BASE_CONFIG_PATH, encoding="utf-8") as file: return copy.deepcopy(yaml.safe_load(file)) -def test_llama_stack_configuration_constructor(subtests: SubTests) -> None: +def test_ogx_cfg_constructor(subtests: SubTests) -> None: """ Verify that the OgxConfiguration constructor accepts valid combinations of parameters and creates instances successfully. """ with subtests.test(msg="Configuration for library mode"): - llama_stack_configuration = OgxConfiguration( + ogx_cfg = OgxConfiguration( use_as_library_client=True, library_client_config_path="tests/configuration/run.yaml", url=None, api_key=None, timeout=60, ) - assert llama_stack_configuration is not None - assert llama_stack_configuration.allow_degraded_mode is False - assert llama_stack_configuration.max_retries == constants.DEFAULT_MAX_RETRIES - assert llama_stack_configuration.retry_delay == constants.DEFAULT_RETRY_DELAY + assert ogx_cfg is not None + assert ogx_cfg.allow_degraded_mode is False + assert ogx_cfg.max_retries == constants.DEFAULT_MAX_RETRIES + assert ogx_cfg.retry_delay == constants.DEFAULT_RETRY_DELAY with subtests.test(msg="Configuration for server mode"): - llama_stack_configuration = OgxConfiguration( + ogx_cfg = OgxConfiguration( use_as_library_client=False, url=AnyHttpUrl("http://localhost"), library_client_config_path=None, api_key=None, timeout=60, ) - assert llama_stack_configuration is not None - assert llama_stack_configuration.allow_degraded_mode is False - assert llama_stack_configuration.max_retries == constants.DEFAULT_MAX_RETRIES - assert llama_stack_configuration.retry_delay == constants.DEFAULT_RETRY_DELAY + assert ogx_cfg is not None + assert ogx_cfg.allow_degraded_mode is False + assert ogx_cfg.max_retries == constants.DEFAULT_MAX_RETRIES + assert ogx_cfg.retry_delay == constants.DEFAULT_RETRY_DELAY with subtests.test(msg="Minimal configuration for server mode"): - llama_stack_configuration = OgxConfiguration( + ogx_cfg = OgxConfiguration( url="http://localhost" ) # pyright: ignore[reportCallIssue] - assert llama_stack_configuration is not None - assert llama_stack_configuration.allow_degraded_mode is False - assert llama_stack_configuration.max_retries == constants.DEFAULT_MAX_RETRIES - assert llama_stack_configuration.retry_delay == constants.DEFAULT_RETRY_DELAY + assert ogx_cfg is not None + assert ogx_cfg.allow_degraded_mode is False + assert ogx_cfg.max_retries == constants.DEFAULT_MAX_RETRIES + assert ogx_cfg.retry_delay == constants.DEFAULT_RETRY_DELAY with subtests.test(msg="Full configuration for server mode"): - llama_stack_configuration = OgxConfiguration( + ogx_cfg = OgxConfiguration( use_as_library_client=False, url="http://localhost", api_key="foo" ) # pyright: ignore[reportCallIssue] - assert llama_stack_configuration is not None - assert llama_stack_configuration.allow_degraded_mode is False - assert llama_stack_configuration.max_retries == constants.DEFAULT_MAX_RETRIES - assert llama_stack_configuration.retry_delay == constants.DEFAULT_RETRY_DELAY + assert ogx_cfg is not None + assert ogx_cfg.allow_degraded_mode is False + assert ogx_cfg.max_retries == constants.DEFAULT_MAX_RETRIES + assert ogx_cfg.retry_delay == constants.DEFAULT_RETRY_DELAY with subtests.test(msg="Degraded mode enabled"): - llama_stack_configuration = OgxConfiguration( + ogx_cfg = OgxConfiguration( url="http://localhost", allow_degraded_mode=True, ) # pyright: ignore[reportCallIssue] - assert llama_stack_configuration is not None - assert llama_stack_configuration.allow_degraded_mode is True - assert llama_stack_configuration.max_retries == constants.DEFAULT_MAX_RETRIES - assert llama_stack_configuration.retry_delay == constants.DEFAULT_RETRY_DELAY + assert ogx_cfg is not None + assert ogx_cfg.allow_degraded_mode is True + assert ogx_cfg.max_retries == constants.DEFAULT_MAX_RETRIES + assert ogx_cfg.retry_delay == constants.DEFAULT_RETRY_DELAY -def test_llama_stack_configuration_no_run_yaml() -> None: +def test_ogx_cfg_no_run_yaml() -> None: """ Verify that constructing a OgxConfiguration with a non-existent or invalid library_client_config_path raises @@ -105,7 +105,7 @@ def test_llama_stack_configuration_no_run_yaml() -> None: ) # pyright: ignore[reportCallIssue] -def test_llama_stack_wrong_configuration_constructor_no_url() -> None: +def test_ogx_wrong_configuration_constructor_no_url() -> None: """ Verify that constructing a OgxConfiguration without specifying either a URL or enabling library client mode raises @@ -118,7 +118,7 @@ def test_llama_stack_wrong_configuration_constructor_no_url() -> None: OgxConfiguration() # pyright: ignore[reportCallIssue] -def test_llama_stack_wrong_configuration_constructor_library_mode_off() -> None: +def test_ogx_wrong_configuration_constructor_library_mode_off() -> None: """Test the OgxConfiguration constructor.""" with pytest.raises( ValueError, @@ -129,7 +129,7 @@ def test_llama_stack_wrong_configuration_constructor_library_mode_off() -> None: ) # pyright: ignore[reportCallIssue] -def test_llama_stack_library_mode_without_source_is_allowed_on_nested_model() -> None: +def test_ogx_library_mode_without_source_is_allowed_on_nested_model() -> None: """The nested model no longer requires a run source in library mode. A library-mode config may be driven by the root-level inference.providers, @@ -146,7 +146,7 @@ def test_llama_stack_library_mode_without_source_is_allowed_on_nested_model() -> assert cfg.config is None -def test_llama_stack_configuration_valid_http_url() -> None: +def test_ogx_cfg_valid_http_url() -> None: """Test that valid HTTP URLs are accepted.""" config = OgxConfiguration( url="http://localhost:8321" @@ -155,55 +155,55 @@ def test_llama_stack_configuration_valid_http_url() -> None: assert str(config.url) == "http://localhost:8321/" -def test_llama_stack_configuration_valid_https_url() -> None: +def test_ogx_cfg_valid_https_url() -> None: """Test that valid HTTPS URLs are accepted.""" config = OgxConfiguration( - url="https://llama-stack.example.com:8321" + url="https://ogx.example.com:8321" ) # pyright: ignore[reportCallIssue] assert config is not None - assert str(config.url) == "https://llama-stack.example.com:8321/" + assert str(config.url) == "https://ogx.example.com:8321/" -def test_llama_stack_configuration_malformed_url_rejected() -> None: +def test_ogx_cfg_malformed_url_rejected() -> None: """Test that malformed URLs are rejected with a ValidationError.""" with pytest.raises(ValidationError, match="Input should be a valid URL"): OgxConfiguration(url="not-a-valid-url") # pyright: ignore[reportCallIssue] -def test_llama_stack_configuration_invalid_scheme_rejected() -> None: +def test_ogx_cfg_invalid_scheme_rejected() -> None: """Test that URLs without http/https scheme are rejected.""" with pytest.raises(ValidationError, match="URL scheme should be 'http' or 'https'"): OgxConfiguration(url="ftp://localhost:8321") # pyright: ignore[reportCallIssue] -def test_llama_stack_configuration_wrong_max_retries_count(subtests: SubTests) -> None: +def test_ogx_cfg_wrong_max_retries_count(subtests: SubTests) -> None: """Test that malformed URLs are rejected with a ValidationError.""" with subtests.test(msg="Configuration with zero max_retries count"): with pytest.raises(ValidationError, match="Input should be greater than 0"): OgxConfiguration( - url="https://llama-stack.example.com:8321", + url="https://ogx.example.com:8321", max_retries=0, ) # pyright: ignore[reportCallIssue] with subtests.test(msg="Configuration with negative max_retries count"): with pytest.raises(ValidationError, match="Input should be greater than 0"): OgxConfiguration( - url="https://llama-stack.example.com:8321", + url="https://ogx.example.com:8321", max_retries=-1, ) # pyright: ignore[reportCallIssue] -def test_llama_stack_configuration_wrong_retry_delay_value(subtests: SubTests) -> None: +def test_ogx_cfg_wrong_retry_delay_value(subtests: SubTests) -> None: """Test that malformed URLs are rejected with a ValidationError.""" with subtests.test(msg="Configuration with zero retry_delay value"): with pytest.raises(ValidationError, match="Input should be greater than 0"): OgxConfiguration( - url="https://llama-stack.example.com:8321", + url="https://ogx.example.com:8321", retry_delay=0, ) # pyright: ignore[reportCallIssue] with subtests.test(msg="Configuration with negative retry_delay value"): with pytest.raises(ValidationError, match="Input should be greater than 0"): OgxConfiguration( - url="https://llama-stack.example.com:8321", + url="https://ogx.example.com:8321", retry_delay=-1, ) # pyright: ignore[reportCallIssue] @@ -214,7 +214,7 @@ def test_llama_stack_configuration_wrong_retry_delay_value(subtests: SubTests) - def test_library_mode_with_unified_config_no_path_is_valid() -> None: - """Library mode driven by llama_stack.config needs no library_client_config_path.""" + """Library mode driven by ogx.config needs no library_client_config_path.""" cfg = OgxConfiguration( use_as_library_client=True, config=UnifiedOgxConfig(), @@ -236,7 +236,7 @@ def test_unified_config_accepts_byo_llm_baseline() -> None: def test_root_rejects_config_and_legacy_path_together() -> None: - """A llama_stack.config block and a legacy path in one file fail at load (R3).""" + """A ogx.config block and a legacy path in one file fail at load (R3).""" config_dict = _base_config_dict() config_dict["ogx"] = { "use_as_library_client": True, @@ -327,7 +327,7 @@ def test_root_accepts_unified_library_config() -> None: def test_root_accepts_inference_providers_only_no_config_block() -> None: """Library mode driven by inference.providers alone is valid (UX: no config:{}). - The minimal unified library config needs no llama_stack.config block — a + The minimal unified library config needs no ogx.config block — a non-empty top-level inference.providers is a sufficient synthesis input. """ config_dict = _base_config_dict() @@ -492,7 +492,7 @@ def test_root_accepts_unified_marker_with_vector_store_providers_body() -> None: def test_root_accepts_unified_marker_with_config_block_body() -> None: - """'unified' agrees with a body whose only synthesis input is llama_stack.config.""" + """'unified' agrees with a body whose only synthesis input is ogx.config.""" config_dict = _clear_synthesis_inputs(_base_config_dict()) config_dict["ogx"] = { "use_as_library_client": True, diff --git a/tests/unit/models/config/test_vector_store.py b/tests/unit/models/config/test_vector_store.py index 4689da0ad..830472d07 100644 --- a/tests/unit/models/config/test_vector_store.py +++ b/tests/unit/models/config/test_vector_store.py @@ -17,7 +17,7 @@ def _base_config_dict() -> dict[str, Any]: """Load the base lightspeed-stack.yaml fixture as a fresh dict.""" - with open(_BASE_CONFIG_PATH, "r", encoding="utf-8") as file: + with open(_BASE_CONFIG_PATH, encoding="utf-8") as file: return copy.deepcopy(yaml.safe_load(file)) diff --git a/tests/unit/pydantic_ai_lightspeed/llamastack/README.md b/tests/unit/pydantic_ai_lightspeed/llamastack/README.md new file mode 100644 index 000000000..7944b07e8 --- /dev/null +++ b/tests/unit/pydantic_ai_lightspeed/llamastack/README.md @@ -0,0 +1,2 @@ +# List of source files stored in `tests/unit/pydantic_ai_lightspeed/llamastack` directory + diff --git a/tests/unit/telemetry/conftest.py b/tests/unit/telemetry/conftest.py index 872f3959e..a13f43647 100644 --- a/tests/unit/telemetry/conftest.py +++ b/tests/unit/telemetry/conftest.py @@ -224,7 +224,7 @@ PII_PROVIDER_API_KEY_ENV, ] -SAMPLE_LLAMA_STACK_CONFIG: dict[str, Any] = { +SAMPLE_OGX_CONFIG: dict[str, Any] = { "version": 2, "image_name": "starter", "container_image": None, @@ -241,8 +241,8 @@ ], "safety": [ { - "provider_id": "llama-guard", - "provider_type": "inline::llama-guard", + "provider_id": "content-filter", + "provider_type": "inline::content-filter", "config": {}, }, ], @@ -264,7 +264,7 @@ }, ], "shields": [ - {"shield_id": "llama-guard", "provider_id": "llama-guard"}, + {"shield_id": "content-filter", "provider_id": "content-filter"}, ], "vector_stores": [], }, @@ -296,7 +296,7 @@ } -LLAMA_STACK_PII_VALUES = [ +OGX_PII_VALUES = [ "sk-openai-secret-key", "/secret/path/kv_store.db", "/secret/path/sql_store.db", @@ -794,7 +794,7 @@ def build_minimal_config() -> Configuration: @pytest.fixture(name="ogx_config_file") def ogx_config_file_fixture(tmp_path: Path) -> str: - """Write SAMPLE_LLAMA_STACK_CONFIG to a temp YAML file and return its path. + """Write SAMPLE_OGX_CONFIG to a temp YAML file and return its path. Parameters: ---------- @@ -805,5 +805,5 @@ def ogx_config_file_fixture(tmp_path: Path) -> str: str: Path to the temporary YAML file. """ path = tmp_path / "ogx_config.yaml" - path.write_text(yaml.dump(SAMPLE_LLAMA_STACK_CONFIG)) + path.write_text(yaml.dump(SAMPLE_OGX_CONFIG)) return str(path) diff --git a/tests/unit/telemetry/test_configuration_snapshot.py b/tests/unit/telemetry/test_configuration_snapshot.py index 57d7c5aae..1d073f7bd 100644 --- a/tests/unit/telemetry/test_configuration_snapshot.py +++ b/tests/unit/telemetry/test_configuration_snapshot.py @@ -36,9 +36,9 @@ from tests.unit.telemetry.conftest import ( ALL_PII_VALUES, BYOK_PORT, - LLAMA_STACK_PII_VALUES, + OGX_PII_VALUES, OKP_CHUNK_FILTER, - SAMPLE_LLAMA_STACK_CONFIG, + SAMPLE_OGX_CONFIG, build_fully_populated_config, build_minimal_config, ) @@ -383,20 +383,20 @@ class TestExtractStoreInfo: def test_inference_store(self) -> None: """Test inference store extraction.""" - result = _extract_store_info(SAMPLE_LLAMA_STACK_CONFIG, "inference") + result = _extract_store_info(SAMPLE_OGX_CONFIG, "inference") assert result["type"] == "sql_sqlite" assert result["db_path"] == CONFIGURED def test_metadata_store_with_namespace(self) -> None: """Test metadata store extraction includes namespace.""" - result = _extract_store_info(SAMPLE_LLAMA_STACK_CONFIG, "metadata") + result = _extract_store_info(SAMPLE_OGX_CONFIG, "metadata") assert result["type"] == "kv_sqlite" assert result["db_path"] == CONFIGURED assert result["namespace"] == "registry" def test_missing_store(self) -> None: """Test missing store returns not_configured.""" - result = _extract_store_info(SAMPLE_LLAMA_STACK_CONFIG, "nonexistent") + result = _extract_store_info(SAMPLE_OGX_CONFIG, "nonexistent") assert result["type"] == NOT_CONFIGURED assert result["db_path"] == NOT_CONFIGURED @@ -407,7 +407,7 @@ def test_no_storage_section(self) -> None: def test_db_path_is_masked(self) -> None: """Test that db_path never leaks the actual path.""" - result = _extract_store_info(SAMPLE_LLAMA_STACK_CONFIG, "inference") + result = _extract_store_info(SAMPLE_OGX_CONFIG, "inference") assert "/secret/path" not in str(result) @@ -553,43 +553,43 @@ def test_service_root_path_masked(self) -> None: snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["service"]["root_path"] == CONFIGURED - def test_llama_stack_timeout_passthrough(self) -> None: - """Test llama_stack timeout passes through.""" + def test_ogx_timeout_passthrough(self) -> None: + """Test ogx timeout passes through.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["timeout"] == 180 - def test_llama_stack_max_retries_passthrough(self) -> None: - """Test llama_stack max_retries passes through.""" + def test_ogx_max_retries_passthrough(self) -> None: + """Test ogx max_retries passes through.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["max_retries"] == 5 - def test_llama_stack_retry_delay_passthrough(self) -> None: - """Test llama_stack retry_delay passes through.""" + def test_ogx_retry_delay_passthrough(self) -> None: + """Test ogx retry_delay passes through.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["retry_delay"] == 2 - def test_llama_stack_allow_degraded_mode_passthrough(self) -> None: - """Test llama_stack allow_degraded_mode passes through.""" + def test_ogx_allow_degraded_mode_passthrough(self) -> None: + """Test ogx allow_degraded_mode passes through.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["allow_degraded_mode"] is True - def test_llama_stack_config_baseline_passthrough(self) -> None: - """Test llama_stack config baseline passes through.""" + def test_ogx_config_baseline_passthrough(self) -> None: + """Test ogx config baseline passes through.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["config"]["baseline"] == "default" - def test_llama_stack_config_profile_masked(self) -> None: - """Test llama_stack config profile is masked as sensitive.""" + def test_ogx_config_profile_masked(self) -> None: + """Test ogx config profile is masked as sensitive.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["config"]["profile"] == CONFIGURED - def test_llama_stack_config_native_override_masked(self) -> None: - """Test llama_stack config native_override is masked as sensitive.""" + def test_ogx_config_native_override_masked(self) -> None: + """Test ogx config native_override is masked as sensitive.""" snapshot = build_lightspeed_stack_snapshot(build_fully_populated_config()) assert snapshot["ogx"]["config"]["native_override"] == CONFIGURED - def test_llama_stack_config_none(self) -> None: - """Test llama_stack config fields when config is None.""" + def test_ogx_config_none(self) -> None: + """Test ogx config fields when config is None.""" snapshot = build_lightspeed_stack_snapshot(build_minimal_config()) assert snapshot["ogx"]["config"]["baseline"] is None assert snapshot["ogx"]["config"]["profile"] == NOT_CONFIGURED @@ -1158,7 +1158,7 @@ class TestBuildConfigurationSnapshot: @pytest.mark.asyncio async def test_combines_both_sources(self) -> None: - """Test that snapshot contains both lightspeed_stack and llama_stack.""" + """Test that snapshot contains both lightspeed_stack and ogx.""" result = await build_configuration_snapshot(build_minimal_config(), None) assert "lightspeed_stack" in result assert "ogx" in result @@ -1197,10 +1197,10 @@ def test_no_pii_in_lightspeed_stack_snapshot(self) -> None: async def test_no_pii_in_ogx_snapshot(self, ogx_config_file: str) -> None: """Verify no PII leaks in OGX snapshot JSON.""" json_str = json.dumps(await build_ogx_snapshot(ogx_config_file)) - for pii_value in LLAMA_STACK_PII_VALUES: + for pii_value in OGX_PII_VALUES: assert ( pii_value not in json_str - ), f"PII leaked in llama-stack snapshot: '{pii_value}'" + ), f"PII leaked in OGX snapshot: '{pii_value}'" @pytest.mark.asyncio async def test_no_pii_in_combined_snapshot(self, ogx_config_file: str) -> None: @@ -1209,7 +1209,7 @@ async def test_no_pii_in_combined_snapshot(self, ogx_config_file: str) -> None: build_fully_populated_config(), ogx_config_file ) json_str = json.dumps(snapshot) - for pii_value in ALL_PII_VALUES + LLAMA_STACK_PII_VALUES: + for pii_value in ALL_PII_VALUES + OGX_PII_VALUES: assert ( pii_value not in json_str ), f"PII leaked in combined snapshot: '{pii_value}'" diff --git a/tests/unit/test_lightspeed_stack.py b/tests/unit/test_lightspeed_stack.py index e2da56084..59931b587 100644 --- a/tests/unit/test_lightspeed_stack.py +++ b/tests/unit/test_lightspeed_stack.py @@ -168,7 +168,7 @@ def test_main_does_not_warn_in_unified_mode( monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, ) -> None: - """A unified-mode config (llama_stack.config) emits no deprecation WARN.""" + """A unified-mode config (ogx.config) emits no deprecation WARN.""" config_yaml = COMMON_CONFIG_SECTIONS + """ ogx: use_as_library_client: true diff --git a/tests/unit/test_ogx_configuration.py b/tests/unit/test_ogx_configuration.py index 19be9278f..a8d9b5035 100644 --- a/tests/unit/test_ogx_configuration.py +++ b/tests/unit/test_ogx_configuration.py @@ -36,7 +36,7 @@ def test_enrich_azure_entra_id_inference_skips_when_not_configured() -> None: """Test enrich_azure_entra_id_inference does nothing without Entra ID config.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "inference": [ { @@ -47,15 +47,15 @@ def test_enrich_azure_entra_id_inference_skips_when_not_configured() -> None: ] } } - enrich_azure_entra_id_inference(ls_config, None) - assert ls_config["providers"]["inference"][0]["config"] == { + enrich_azure_entra_id_inference(ogx_config, None) + assert ogx_config["providers"]["inference"][0]["config"] == { "model_validation": True } def test_enrich_azure_entra_id_inference_sets_model_validation_false() -> None: """Test enrich_azure_entra_id_inference disables startup model validation.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "inference": [ { @@ -66,8 +66,8 @@ def test_enrich_azure_entra_id_inference_sets_model_validation_false() -> None: ] } } - enrich_azure_entra_id_inference(ls_config, {"tenant_id": "t"}) - azure_config = ls_config["providers"]["inference"][0]["config"] + enrich_azure_entra_id_inference(ogx_config, {"tenant_id": "t"}) + azure_config = ogx_config["providers"]["inference"][0]["config"] assert azure_config["model_validation"] is False @@ -115,15 +115,15 @@ def test_generate_configuration_enriches_azure_entra_id(tmp_path: Path) -> None: def test_construct_vector_stores_section_empty() -> None: """Test with no BYOK RAG config.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag: list[dict[str, Any]] = [] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 0 def test_construct_vector_stores_section_preserves_existing() -> None: """Test preserves existing vector_stores entries.""" - ls_config = { + ogx_config = { "registered_resources": { "vector_stores": [ {"vector_store_id": "existing", "provider_id": "existing_provider"}, @@ -131,14 +131,14 @@ def test_construct_vector_stores_section_preserves_existing() -> None: } } byok_rag: list[dict[str, Any]] = [] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["vector_store_id"] == "existing" def test_construct_vector_stores_section_adds_new() -> None: """Test adds new BYOK RAG entries.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rag1", @@ -147,7 +147,7 @@ def test_construct_vector_stores_section_adds_new() -> None: "embedding_dimension": 512, }, ] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["vector_store_id"] == "store1" assert output[0]["provider_id"] == "byok_rag1" @@ -157,17 +157,17 @@ def test_construct_vector_stores_section_adds_new() -> None: def test_construct_vector_stores_section_merge() -> None: """Test merges existing and new entries.""" - ls_config = { + ogx_config = { "registered_resources": {"vector_stores": [{"vector_store_id": "existing"}]} } byok_rag = [{"rag_id": "rag1", "vector_db_id": "new_store"}] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 2 def test_construct_vector_stores_section_skips_duplicate_from_existing() -> None: """Test skips BYOK entry when vector_store_id already exists in config.""" - ls_config = { + ogx_config = { "registered_resources": { "vector_stores": [ {"vector_store_id": "store1", "provider_id": "original_provider"}, @@ -182,7 +182,7 @@ def test_construct_vector_stores_section_skips_duplicate_from_existing() -> None "embedding_dimension": 512, }, ] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_id"] == "original_provider" @@ -192,7 +192,7 @@ def test_construct_vector_stores_section_skips_duplicate_env_var( ) -> None: """Test skips BYOK entry when existing store uses an env var that resolves to the same ID.""" monkeypatch.setenv("FAISS_VECTOR_STORE_ID", "vs_abc123") - ls_config = { + ogx_config = { "registered_resources": { "vector_stores": [ { @@ -210,14 +210,14 @@ def test_construct_vector_stores_section_skips_duplicate_env_var( "embedding_dimension": 768, }, ] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_id"] == "faiss" def test_construct_vector_stores_section_skips_duplicate_within_byok() -> None: """Test skips duplicate vector_db_id entries within the BYOK RAG list.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rag1", @@ -232,7 +232,7 @@ def test_construct_vector_stores_section_skips_duplicate_within_byok() -> None: "embedding_dimension": 768, }, ] - output = construct_vector_stores_section(ls_config, byok_rag) + output = construct_vector_stores_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["embedding_model"] == "sentence-transformers/byok_rag1_embedding" @@ -244,24 +244,24 @@ def test_construct_vector_stores_section_skips_duplicate_within_byok() -> None: def test_construct_vector_io_providers_section_empty() -> None: """Test with no BYOK RAG config.""" - ls_config: dict[str, Any] = {"providers": {}} + ogx_config: dict[str, Any] = {"providers": {}} byok_rag: list[dict[str, Any]] = [] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 0 def test_construct_vector_io_providers_section_preserves_existing() -> None: """Test preserves existing vector_io entries.""" - ls_config = {"providers": {"vector_io": [{"provider_id": "existing"}]}} + ogx_config = {"providers": {"vector_io": [{"provider_id": "existing"}]}} byok_rag: list[dict[str, Any]] = [] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_id"] == "existing" def test_construct_vector_io_providers_section_adds_new() -> None: """Test adds new BYOK RAG entries using rag_id for provider naming.""" - ls_config: dict[str, Any] = {"providers": {}} + ogx_config: dict[str, Any] = {"providers": {}} byok_rag = [ { "rag_id": "rag1", @@ -269,7 +269,7 @@ def test_construct_vector_io_providers_section_adds_new() -> None: "backend": "faiss", }, ] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_id"] == "byok_rag1" assert output[0]["provider_type"] == "inline::faiss" @@ -286,10 +286,10 @@ def test_construct_vector_io_providers_section_idempotent_reenrichment() -> None "backend": "faiss", }, ] - ls_config: dict[str, Any] = {"providers": {}} - first = construct_vector_io_providers_section(ls_config, byok_rag) - ls_config["providers"] = {"vector_io": first} - second = construct_vector_io_providers_section(ls_config, byok_rag) + ogx_config: dict[str, Any] = {"providers": {}} + first = construct_vector_io_providers_section(ogx_config, byok_rag) + ogx_config["providers"] = {"vector_io": first} + second = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(second) == 1 assert second[0]["provider_id"] == "byok_rag1" @@ -306,7 +306,7 @@ def test_construct_vector_io_providers_section_collapses_existing_duplicates() - } }, } - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {"vector_io": [dup, dup, dup]}, } byok_rag = [ @@ -316,14 +316,14 @@ def test_construct_vector_io_providers_section_collapses_existing_duplicates() - "backend": "faiss", }, ] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_id"] == "byok_rag1" def test_construct_vector_io_providers_section_pgvector() -> None: """Test generates correct pgvector provider config.""" - ls_config: dict[str, Any] = {"providers": {}} + ogx_config: dict[str, Any] = {"providers": {}} byok_rag = [ { "rag_id": "pg1", @@ -336,7 +336,7 @@ def test_construct_vector_io_providers_section_pgvector() -> None: "password": "${env.POSTGRES_PASSWORD}", }, ] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 1 provider = output[0] assert provider["provider_id"] == "byok_pg1" @@ -349,7 +349,7 @@ def test_construct_vector_io_providers_section_pgvector() -> None: def test_construct_vector_io_providers_section_mixed() -> None: """Test mixed faiss and pgvector entries generate correct configs.""" - ls_config: dict[str, Any] = {"providers": {}} + ogx_config: dict[str, Any] = {"providers": {}} byok_rag = [ {"rag_id": "f1", "vector_db_id": "vs_f", "backend": "faiss"}, { @@ -363,7 +363,7 @@ def test_construct_vector_io_providers_section_mixed() -> None: "password": "pass", }, ] - output = construct_vector_io_providers_section(ls_config, byok_rag) + output = construct_vector_io_providers_section(ogx_config, byok_rag) assert len(output) == 2 faiss_p = next(p for p in output if p["provider_id"] == "byok_f1") assert faiss_p["provider_type"] == "inline::faiss" @@ -376,20 +376,20 @@ def test_construct_vector_io_providers_section_mixed() -> None: def test_construct_storage_backends_section_skips_pgvector() -> None: """Test pgvector entries are skipped (they use kv_default).""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"rag_id": "pg1", "vector_db_id": "vs_pg", "backend": "pgvector"}] - output = construct_storage_backends_section(ls_config, byok_rag) + output = construct_storage_backends_section(ogx_config, byok_rag) assert len(output) == 0 def test_construct_storage_backends_section_mixed_faiss_pgvector() -> None: """Test only faiss entries get storage backends, pgvector is skipped.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ {"rag_id": "f1", "vector_db_id": "vs_f", "db_path": "/tmp/f.db"}, {"rag_id": "pg1", "vector_db_id": "vs_pg", "backend": "pgvector"}, ] - output = construct_storage_backends_section(ls_config, byok_rag) + output = construct_storage_backends_section(ogx_config, byok_rag) assert len(output) == 1 assert "byok_f1_storage" in output assert "byok_pg1_storage" not in output @@ -397,7 +397,7 @@ def test_construct_storage_backends_section_mixed_faiss_pgvector() -> None: def test_enrich_byok_rag_pgvector_end_to_end() -> None: """Test enrich_byok_rag with a pgvector store entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "pg1", @@ -412,15 +412,16 @@ def test_enrich_byok_rag_pgvector_end_to_end() -> None: "password": "${env.POSTGRES_PASSWORD}", }, ] - enrich_byok_rag(ls_config, byok_rag) - assert "byok_pg1_storage" not in ls_config.get("storage", {}).get("backends", {}) - providers = ls_config["providers"]["vector_io"] + enrich_byok_rag(ogx_config, byok_rag) + assert "byok_pg1_storage" not in ogx_config.get("storage", {}).get("backends", {}) + providers = ogx_config["providers"]["vector_io"] pg_p = next(p for p in providers if p["provider_id"] == "byok_pg1") assert pg_p["provider_type"] == "remote::pgvector" assert pg_p["config"]["persistence"]["backend"] == "kv_default" assert pg_p["config"]["host"] == "${env.POSTGRES_HOST}" store_ids = [ - s["vector_store_id"] for s in ls_config["registered_resources"]["vector_stores"] + s["vector_store_id"] + for s in ogx_config["registered_resources"]["vector_stores"] ] assert "vs_pg" in store_ids @@ -432,19 +433,19 @@ def test_enrich_byok_rag_skipped_still_dedupes_vector_io() -> None: "provider_type": "inline::faiss", "config": {"persistence": {"namespace": "vector_io::faiss", "backend": "b"}}, } - ls_config: dict[str, Any] = {"providers": {"vector_io": [dup, dup, dup]}} - enrich_byok_rag(ls_config, []) - assert len(ls_config["providers"]["vector_io"]) == 1 + ogx_config: dict[str, Any] = {"providers": {"vector_io": [dup, dup, dup]}} + enrich_byok_rag(ogx_config, []) + assert len(ogx_config["providers"]["vector_io"]) == 1 def test_dedupe_providers_vector_io_in_place() -> None: """dedupe_providers_vector_io keeps one entry per provider_id.""" a = {"provider_id": "p1", "provider_type": "inline::faiss", "config": {}} - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {"vector_io": [a, a, {"provider_id": "p2"}]} } - dedupe_providers_vector_io(ls_config) - ids = [p["provider_id"] for p in ls_config["providers"]["vector_io"]] + dedupe_providers_vector_io(ogx_config) + ids = [p["provider_id"] for p in ogx_config["providers"]["vector_io"]] assert ids == ["p1", "p2"] @@ -455,15 +456,15 @@ def test_dedupe_providers_vector_io_in_place() -> None: def test_construct_storage_backends_section_empty() -> None: """Test with no BYOK RAG config.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag: list[dict[str, Any]] = [] - output = construct_storage_backends_section(ls_config, byok_rag) + output = construct_storage_backends_section(ogx_config, byok_rag) assert len(output) == 0 def test_construct_storage_backends_section_preserves_existing() -> None: """Test preserves existing backends.""" - ls_config = { + ogx_config = { "storage": { "backends": { "kv_default": {"type": "kv_sqlite", "db_path": "~/.llama/kv.db"} @@ -471,14 +472,14 @@ def test_construct_storage_backends_section_preserves_existing() -> None: } } byok_rag: list[dict[str, Any]] = [] - output = construct_storage_backends_section(ls_config, byok_rag) + output = construct_storage_backends_section(ogx_config, byok_rag) assert len(output) == 1 assert "kv_default" in output def test_construct_storage_backends_section_adds_new() -> None: """Test adds new BYOK RAG backend entries using rag_id for backend naming.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rag1", @@ -486,7 +487,7 @@ def test_construct_storage_backends_section_adds_new() -> None: "db_path": "/path/to/store1.db", }, ] - output = construct_storage_backends_section(ls_config, byok_rag) + output = construct_storage_backends_section(ogx_config, byok_rag) assert len(output) == 1 assert "byok_rag1_storage" in output assert output["byok_rag1_storage"]["type"] == "kv_sqlite" @@ -500,28 +501,28 @@ def test_construct_storage_backends_section_adds_new() -> None: def test_construct_models_section_empty() -> None: """Test with no BYOK RAG config.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag: list[dict[str, Any]] = [] - output = construct_models_section(ls_config, byok_rag) + output = construct_models_section(ogx_config, byok_rag) assert len(output) == 0 def test_construct_models_section_preserves_existing() -> None: """Test preserves existing models.""" - ls_config = { + ogx_config = { "registered_resources": { "models": [{"model_id": "existing", "model_type": "llm"}] } } byok_rag: list[dict[str, Any]] = [] - output = construct_models_section(ls_config, byok_rag) + output = construct_models_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["model_id"] == "existing" def test_construct_models_section_adds_embedding_model() -> None: """Test adds embedding model from BYOK RAG using rag_id for model naming.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rag1", @@ -530,7 +531,7 @@ def test_construct_models_section_adds_embedding_model() -> None: "embedding_dimension": 768, }, ] - output = construct_models_section(ls_config, byok_rag) + output = construct_models_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["model_id"] == "byok_rag1_embedding" assert output[0]["model_type"] == "embedding" @@ -541,7 +542,7 @@ def test_construct_models_section_adds_embedding_model() -> None: def test_construct_models_section_strips_prefix() -> None: """Test strips sentence-transformers/ prefix from embedding model.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rag1", @@ -550,14 +551,14 @@ def test_construct_models_section_strips_prefix() -> None: "embedding_dimension": 768, }, ] - output = construct_models_section(ls_config, byok_rag) + output = construct_models_section(ogx_config, byok_rag) assert len(output) == 1 assert output[0]["provider_model_id"] == "/usr/path/model" def test_byok_vector_store_uses_registered_embedding_id_not_load_path() -> None: """BYOK store lookup id matches registered model; path stays on provider_model_id.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "rhdh-docs", @@ -566,8 +567,8 @@ def test_byok_vector_store_uses_registered_embedding_id_not_load_path() -> None: "embedding_dimension": 768, }, ] - stores = construct_vector_stores_section(ls_config, byok_rag) - models = construct_models_section(ls_config, byok_rag) + stores = construct_vector_stores_section(ogx_config, byok_rag) + models = construct_models_section(ogx_config, byok_rag) assert stores[0]["embedding_model"] == ( "sentence-transformers/byok_rhdh-docs_embedding" ) @@ -577,7 +578,7 @@ def test_byok_vector_store_uses_registered_embedding_id_not_load_path() -> None: def test_construct_models_section_registers_alias_per_rag_id_for_shared_path() -> None: """Two BYOK entries sharing a load path each get a byok__embedding alias.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [ { "rag_id": "docs-a", @@ -592,8 +593,8 @@ def test_construct_models_section_registers_alias_per_rag_id_for_shared_path() - "embedding_dimension": 768, }, ] - models = construct_models_section(ls_config, byok_rag) - stores = construct_vector_stores_section(ls_config, byok_rag) + models = construct_models_section(ogx_config, byok_rag) + stores = construct_vector_stores_section(ogx_config, byok_rag) assert {m["model_id"] for m in models} == { "byok_docs-a_embedding", "byok_docs-b_embedding", @@ -609,42 +610,42 @@ def test_construct_models_section_registers_alias_per_rag_id_for_shared_path() - def test_construct_storage_backends_section_raises_on_missing_rag_id() -> None: """Test raises ValueError when rag_id is missing from a BYOK RAG entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"vector_db_id": "store1"}] with pytest.raises(ValueError, match="missing required 'rag_id'"): - construct_storage_backends_section(ls_config, byok_rag) + construct_storage_backends_section(ogx_config, byok_rag) def test_construct_vector_stores_section_raises_on_missing_rag_id() -> None: """Test raises ValueError when rag_id is missing from a BYOK RAG entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"vector_db_id": "store1"}] with pytest.raises(ValueError, match="missing required 'rag_id'"): - construct_vector_stores_section(ls_config, byok_rag) + construct_vector_stores_section(ogx_config, byok_rag) def test_construct_vector_stores_section_raises_on_missing_vector_db_id() -> None: """Test raises ValueError when vector_db_id is missing from a BYOK RAG entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"rag_id": "rag1"}] with pytest.raises(ValueError, match="missing required 'vector_db_id'"): - construct_vector_stores_section(ls_config, byok_rag) + construct_vector_stores_section(ogx_config, byok_rag) def test_construct_vector_io_section_raises_on_missing_rag_id() -> None: """Test raises ValueError when rag_id is missing from a BYOK RAG entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"vector_db_id": "store1"}] with pytest.raises(ValueError, match="missing required 'rag_id'"): - construct_vector_io_providers_section(ls_config, byok_rag) + construct_vector_io_providers_section(ogx_config, byok_rag) def test_construct_models_section_raises_on_missing_rag_id() -> None: """Test raises ValueError when rag_id is missing from a BYOK RAG entry.""" - ls_config: dict[str, Any] = {} + ogx_config: dict[str, Any] = {} byok_rag = [{"vector_db_id": "store1", "embedding_model": "some-model"}] with pytest.raises(ValueError, match="missing required 'rag_id'"): - construct_models_section(ls_config, byok_rag) + construct_models_section(ogx_config, byok_rag) # ============================================================================= @@ -825,91 +826,94 @@ def test_generate_configuration_with_pgvector(tmp_path: Path) -> None: def test_enrich_solr_skips_when_not_enabled() -> None: """Test enrich_solr does nothing when OKP is not in rag inline or tool lists.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, {"inline": [], "tool": []}, {}) - assert not ls_config + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, {"inline": [], "tool": []}, {}) + assert not ogx_config def test_enrich_solr_skips_when_empty_config() -> None: """Test enrich_solr does nothing with empty rag config.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, {}, {}) - assert not ls_config + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, {}, {}) + assert not ogx_config def test_enrich_solr_adds_vector_io_provider() -> None: """Test enrich_solr adds Solr provider to vector_io section.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - assert "providers" in ls_config - assert "vector_io" in ls_config["providers"] - provider_ids = [p["provider_id"] for p in ls_config["providers"]["vector_io"]] + assert "providers" in ogx_config + assert "vector_io" in ogx_config["providers"] + provider_ids = [p["provider_id"] for p in ogx_config["providers"]["vector_io"]] assert "okp_solr" in provider_ids def test_enrich_solr_adds_vector_store_registration() -> None: """Test enrich_solr registers the Solr vector store.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - assert "registered_resources" in ls_config + assert "registered_resources" in ogx_config store_ids = [ - s["vector_store_id"] for s in ls_config["registered_resources"]["vector_stores"] + s["vector_store_id"] + for s in ogx_config["registered_resources"]["vector_stores"] ] assert "portal-rag" in store_ids def test_enrich_solr_adds_embedding_model() -> None: """Test enrich_solr registers the Solr embedding model.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - model_ids = [m["model_id"] for m in ls_config["registered_resources"]["models"]] + model_ids = [m["model_id"] for m in ogx_config["registered_resources"]["models"]] assert "sentence-transformers/solr_embedding" in model_ids def test_enrich_solr_skips_duplicate_provider() -> None: """Test enrich_solr does not add duplicate Solr provider.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {"vector_io": [{"provider_id": "okp_solr"}]} } - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - provider_ids = [p["provider_id"] for p in ls_config["providers"]["vector_io"]] + provider_ids = [p["provider_id"] for p in ogx_config["providers"]["vector_io"]] assert provider_ids.count("okp_solr") == 1 def test_enrich_solr_skips_duplicate_vector_store() -> None: """Test enrich_solr does not add duplicate vector store registration.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "registered_resources": {"vector_stores": [{"vector_store_id": "portal-rag"}]} } - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) store_ids = [ - s["vector_store_id"] for s in ls_config["registered_resources"]["vector_stores"] + s["vector_store_id"] + for s in ogx_config["registered_resources"]["vector_stores"] ] assert store_ids.count("portal-rag") == 1 def test_enrich_solr_preserves_existing_config() -> None: """Test enrich_solr preserves existing providers and resources.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {"vector_io": [{"provider_id": "existing_provider"}]}, "registered_resources": { "vector_stores": [{"vector_store_id": "existing_store"}], "models": [{"model_id": "existing_model"}], }, } - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - provider_ids = [p["provider_id"] for p in ls_config["providers"]["vector_io"]] + provider_ids = [p["provider_id"] for p in ogx_config["providers"]["vector_io"]] assert "existing_provider" in provider_ids assert "okp_solr" in provider_ids store_ids = [ - s["vector_store_id"] for s in ls_config["registered_resources"]["vector_stores"] + s["vector_store_id"] + for s in ogx_config["registered_resources"]["vector_stores"] ] assert "existing_store" in store_ids assert "portal-rag" in store_ids @@ -917,11 +921,13 @@ def test_enrich_solr_preserves_existing_config() -> None: def test_enrich_solr_default_chunk_filter_query() -> None: """Test enrich_solr uses the internal chunk filter when no user filter is set.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) provider = next( - p for p in ls_config["providers"]["vector_io"] if p["provider_id"] == "okp_solr" + p + for p in ogx_config["providers"]["vector_io"] + if p["provider_id"] == "okp_solr" ) assert ( provider["config"]["chunk_window_config"]["chunk_filter_query"] @@ -931,11 +937,13 @@ def test_enrich_solr_default_chunk_filter_query() -> None: def test_enrich_solr_user_chunk_filter_query_is_conjoined() -> None: """Test enrich_solr ANDs the user filter with the internal chunk filter.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"chunk_filter_query": "product:ansible"}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"chunk_filter_query": "product:ansible"}) provider = next( - p for p in ls_config["providers"]["vector_io"] if p["provider_id"] == "okp_solr" + p + for p in ogx_config["providers"]["vector_io"] + if p["provider_id"] == "okp_solr" ) assert provider["config"]["chunk_window_config"]["chunk_filter_query"] == ( "is_chunk:true AND product:ansible" @@ -944,64 +952,64 @@ def test_enrich_solr_user_chunk_filter_query_is_conjoined() -> None: def test_enrich_solr_sets_default_search_mode_keyword() -> None: """Test enrich_solr propagates search_mode keyword to vector_stores config.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"search_mode": "keyword"}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"search_mode": "keyword"}) assert ( - ls_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] + ogx_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] == "keyword" ) def test_enrich_solr_sets_default_search_mode_hybrid() -> None: """Test enrich_solr propagates search_mode hybrid to vector_stores config.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"search_mode": "hybrid"}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"search_mode": "hybrid"}) assert ( - ls_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] + ogx_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] == "hybrid" ) def test_enrich_solr_maps_semantic_to_vector() -> None: """Test enrich_solr maps LCORE semantic to OGX vector search mode.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"search_mode": "semantic"}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"search_mode": "semantic"}) assert ( - ls_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] + ogx_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] == "vector" ) def test_enrich_solr_maps_lexical_to_keyword() -> None: """Test enrich_solr maps LCORE lexical to OGX keyword via SOLR_SEARCH_MODE_MAP.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"search_mode": "lexical"}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"search_mode": "lexical"}) assert ( - ls_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] + ogx_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] == "keyword" ) def test_enrich_solr_no_search_mode_skips_vector_stores() -> None: """Test enrich_solr does not set vector_stores when search_mode is absent.""" - ls_config: dict[str, Any] = {} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {}) + ogx_config: dict[str, Any] = {} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {}) - assert "vector_stores" not in ls_config + assert "vector_stores" not in ogx_config def test_enrich_solr_preserves_existing_vector_stores() -> None: """Test enrich_solr preserves existing vector_stores config when adding search_mode.""" - ls_config: dict[str, Any] = {"vector_stores": {"default_provider_id": "faiss"}} - enrich_solr(ls_config, _OKP_RAG_CONFIG, {"search_mode": "keyword"}) + ogx_config: dict[str, Any] = {"vector_stores": {"default_provider_id": "faiss"}} + enrich_solr(ogx_config, _OKP_RAG_CONFIG, {"search_mode": "keyword"}) - assert ls_config["vector_stores"]["default_provider_id"] == "faiss" + assert ogx_config["vector_stores"]["default_provider_id"] == "faiss" assert ( - ls_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] + ogx_config["vector_stores"]["chunk_retrieval_params"]["default_search_mode"] == "keyword" ) @@ -1013,7 +1021,7 @@ def test_enrich_solr_preserves_existing_vector_stores() -> None: def test_enrich_vector_store_faiss_appends() -> None: """Faiss provider appends vector_io, backend, and default_* settings.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "vector_io": [ { @@ -1038,7 +1046,7 @@ def test_enrich_vector_store_faiss_appends() -> None: }, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1052,26 +1060,26 @@ def test_enrich_vector_store_faiss_appends() -> None: ], }, ) - ids = {p["provider_id"] for p in ls_config["providers"]["vector_io"]} + ids = {p["provider_id"] for p in ogx_config["providers"]["vector_io"]} assert ids == {"faiss", "notebooks"} assert ( - ls_config["storage"]["backends"]["vsprov_notebooks_storage"]["db_path"] + ogx_config["storage"]["backends"]["vsprov_notebooks_storage"]["db_path"] == "/var/lib/notebooks.db" ) - assert ls_config["vector_stores"]["default_provider_id"] == "notebooks" - assert ls_config["vector_stores"]["default_embedding_model"]["model_id"] == ( + assert ogx_config["vector_stores"]["default_provider_id"] == "notebooks" + assert ogx_config["vector_stores"]["default_embedding_model"]["model_id"] == ( "vsprov_notebooks_embedding" ) assert ( - ls_config["vector_stores"]["annotation_prompt_params"]["enable_annotations"] + ogx_config["vector_stores"]["annotation_prompt_params"]["enable_annotations"] is False ) - assert not ls_config["registered_resources"]["vector_stores"] + assert not ogx_config["registered_resources"]["vector_stores"] def test_enrich_vector_store_replaces_same_provider_id() -> None: """Same provider_id replaces the baseline entry and leaves orphan backends.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "vector_io": [ { @@ -1098,7 +1106,7 @@ def test_enrich_vector_store_replaces_same_provider_id() -> None: "vector_stores": {}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1112,29 +1120,29 @@ def test_enrich_vector_store_replaces_same_provider_id() -> None: ], }, ) - providers = ls_config["providers"]["vector_io"] + providers = ogx_config["providers"]["vector_io"] assert len(providers) == 1 assert providers[0]["provider_id"] == "notebooks" assert ( providers[0]["config"]["persistence"]["backend"] == "vsprov_notebooks_storage" ) - assert "kv_notebooks" in ls_config["storage"]["backends"] + assert "kv_notebooks" in ogx_config["storage"]["backends"] assert ( - ls_config["storage"]["backends"]["vsprov_notebooks_storage"]["db_path"] + ogx_config["storage"]["backends"]["vsprov_notebooks_storage"]["db_path"] == "/new/notebooks.db" ) def test_enrich_vector_store_pgvector_no_kv_backend() -> None: """Pgvector provider does not create a kv_sqlite storage backend.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {}, "vector_stores": {}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "nb-pg", "providers": [ @@ -1154,7 +1162,7 @@ def test_enrich_vector_store_pgvector_no_kv_backend() -> None: ], }, ) - provider = ls_config["providers"]["vector_io"][0] + provider = ogx_config["providers"]["vector_io"][0] assert provider["provider_type"] == "remote::pgvector" assert provider["config"]["persistence"]["backend"] == "kv_default" assert provider["config"]["host"] == "${env.POSTGRES_HOST}" @@ -1162,12 +1170,12 @@ def test_enrich_vector_store_pgvector_no_kv_backend() -> None: assert provider["config"]["db"] == "${env.POSTGRES_DATABASE}" assert provider["config"]["user"] == "${env.POSTGRES_USER}" assert provider["config"]["password"] == "${env.POSTGRES_PASSWORD}" - assert "vsprov_nb-pg_storage" not in ls_config["storage"]["backends"] + assert "vsprov_nb-pg_storage" not in ogx_config["storage"]["backends"] def test_enrich_vector_store_multiple_entries() -> None: """Multi-entry list: both providers, faiss-only backend, default_provider winner.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {"models": [], "vector_stores": []}, @@ -1176,7 +1184,7 @@ def test_enrich_vector_store_multiple_entries() -> None: }, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1200,27 +1208,27 @@ def test_enrich_vector_store_multiple_entries() -> None: ], }, ) - ids = {p["provider_id"] for p in ls_config["providers"]["vector_io"]} + ids = {p["provider_id"] for p in ogx_config["providers"]["vector_io"]} assert ids == {"notebooks", "nb-pg"} - assert "vsprov_notebooks_storage" in ls_config["storage"]["backends"] - assert "vsprov_nb-pg_storage" not in ls_config["storage"]["backends"] - assert ls_config["vector_stores"]["default_provider_id"] == "notebooks" - assert ls_config["vector_stores"]["default_embedding_model"]["model_id"] == ( + assert "vsprov_notebooks_storage" in ogx_config["storage"]["backends"] + assert "vsprov_nb-pg_storage" not in ogx_config["storage"]["backends"] + assert ogx_config["vector_stores"]["default_provider_id"] == "notebooks" + assert ogx_config["vector_stores"]["default_embedding_model"]["model_id"] == ( "vsprov_notebooks_embedding" ) assert ( - ls_config["vector_stores"]["annotation_prompt_params"]["enable_annotations"] + ogx_config["vector_stores"]["annotation_prompt_params"]["enable_annotations"] is False ) model_ids = { - m["provider_model_id"] for m in ls_config["registered_resources"]["models"] + m["provider_model_id"] for m in ogx_config["registered_resources"]["models"] } assert model_ids == {"/emb-faiss", "/emb-pg"} def test_enrich_vector_store_noop_without_entries() -> None: """Empty list leaves baseline vector_stores defaults unchanged.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "vector_io": [ {"provider_id": "faiss", "provider_type": "inline::faiss", "config": {}} @@ -1228,13 +1236,13 @@ def test_enrich_vector_store_noop_without_entries() -> None: }, "vector_stores": {"default_provider_id": "faiss"}, } - enrich_vector_store(ls_config, {"providers": []}) - assert ls_config["vector_stores"]["default_provider_id"] == "faiss" + enrich_vector_store(ogx_config, {"providers": []}) + assert ogx_config["vector_stores"]["default_provider_id"] == "faiss" def test_enrich_vector_store_registers_alias_when_load_path_shared_with_byok() -> None: """Shared provider_model_id with BYOK still registers vsprov_* for defaults.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": { @@ -1252,7 +1260,7 @@ def test_enrich_vector_store_registers_alias_when_load_path_shared_with_byok() - "vector_stores": {}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1266,19 +1274,19 @@ def test_enrich_vector_store_registers_alias_when_load_path_shared_with_byok() - ], }, ) - model_ids = {m["model_id"] for m in ls_config["registered_resources"]["models"]} + model_ids = {m["model_id"] for m in ogx_config["registered_resources"]["models"]} assert model_ids == { "byok_rhdh-docs_embedding", "vsprov_notebooks_embedding", } - assert ls_config["vector_stores"]["default_embedding_model"]["model_id"] == ( + assert ogx_config["vector_stores"]["default_embedding_model"]["model_id"] == ( "vsprov_notebooks_embedding" ) def test_enrich_vector_store_dedupes_same_vsprov_model_id() -> None: """Re-enriching the same vector_store provider does not duplicate its model.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {"models": [], "vector_stores": []}, @@ -1296,25 +1304,25 @@ def test_enrich_vector_store_dedupes_same_vsprov_model_id() -> None: } ], } - enrich_vector_store(ls_config, vector_store) - enrich_vector_store(ls_config, vector_store) - assert len(ls_config["registered_resources"]["models"]) == 1 + enrich_vector_store(ogx_config, vector_store) + enrich_vector_store(ogx_config, vector_store) + assert len(ogx_config["registered_resources"]["models"]) == 1 assert ( - ls_config["registered_resources"]["models"][0]["model_id"] + ogx_config["registered_resources"]["models"][0]["model_id"] == "vsprov_notebooks_embedding" ) def test_enrich_vector_store_updates_vsprov_alias_on_path_change() -> None: """Re-enrichment with a new embedding path refreshes the vsprov_* model row.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {"models": [], "vector_stores": []}, "vector_stores": {}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1329,7 +1337,7 @@ def test_enrich_vector_store_updates_vsprov_alias_on_path_change() -> None: }, ) enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1343,7 +1351,7 @@ def test_enrich_vector_store_updates_vsprov_alias_on_path_change() -> None: ], }, ) - models = ls_config["registered_resources"]["models"] + models = ogx_config["registered_resources"]["models"] assert len(models) == 1 assert models[0]["model_id"] == "vsprov_notebooks_embedding" assert models[0]["provider_model_id"] == "/new/embeddings_model" @@ -1352,14 +1360,14 @@ def test_enrich_vector_store_updates_vsprov_alias_on_path_change() -> None: def test_enrich_vector_store_skips_embedding_without_dimension() -> None: """embedding_model without embedding_dimension does not register a model.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {"models": []}, "vector_stores": {"default_provider_id": "faiss"}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "notebooks", "providers": [ @@ -1372,22 +1380,22 @@ def test_enrich_vector_store_skips_embedding_without_dimension() -> None: ], }, ) - assert ls_config["providers"]["vector_io"][0]["provider_id"] == "notebooks" - assert not ls_config["registered_resources"]["models"] - assert ls_config["vector_stores"]["default_provider_id"] == "notebooks" - assert "default_embedding_model" in ls_config["vector_stores"] + assert ogx_config["providers"]["vector_io"][0]["provider_id"] == "notebooks" + assert not ogx_config["registered_resources"]["models"] + assert ogx_config["vector_stores"]["default_provider_id"] == "notebooks" + assert "default_embedding_model" in ogx_config["vector_stores"] def test_enrich_vector_store_unmatched_default_provider_skips_defaults() -> None: """Unmatched default_provider still enriches providers but skips default_*.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": {}, "storage": {"backends": {}}, "registered_resources": {"models": []}, "vector_stores": {"default_provider_id": "faiss"}, } enrich_vector_store( - ls_config, + ogx_config, { "default_provider": "missing", "providers": [ @@ -1401,10 +1409,10 @@ def test_enrich_vector_store_unmatched_default_provider_skips_defaults() -> None ], }, ) - assert ls_config["providers"]["vector_io"][0]["provider_id"] == "notebooks" - assert ls_config["vector_stores"]["default_provider_id"] == "faiss" - assert "default_embedding_model" not in ls_config["vector_stores"] - assert len(ls_config["registered_resources"]["models"]) == 1 + assert ogx_config["providers"]["vector_io"][0]["provider_id"] == "notebooks" + assert ogx_config["vector_stores"]["default_provider_id"] == "faiss" + assert "default_embedding_model" not in ogx_config["vector_stores"] + assert len(ogx_config["registered_resources"]["models"]) == 1 # ============================================================================= diff --git a/tests/unit/test_ogx_synthesize.py b/tests/unit/test_ogx_synthesize.py index f58593b44..2cefb8468 100644 --- a/tests/unit/test_ogx_synthesize.py +++ b/tests/unit/test_ogx_synthesize.py @@ -42,9 +42,9 @@ # --------------------------------------------------------------------------- -def _tool_runtime_ids(ls_config: dict[str, Any]) -> list[Optional[str]]: +def _tool_runtime_ids(ogx_config: dict[str, Any]) -> list[Optional[str]]: """Return provider_id values from providers.tool_runtime.""" - providers = ls_config.get("providers") or {} + providers = ogx_config.get("providers") or {} return [ entry.get("provider_id") for entry in providers.get("tool_runtime") or [] @@ -52,19 +52,19 @@ def _tool_runtime_ids(ls_config: dict[str, Any]) -> list[Optional[str]]: ] -def _inference_entries(ls_config: dict[str, Any]) -> list[dict[str, Any]]: +def _inference_entries(ogx_config: dict[str, Any]) -> list[dict[str, Any]]: """Return inference provider dicts from a synthesized or baseline config.""" - providers = ls_config.get("providers") or {} + providers = ogx_config.get("providers") or {} return [ entry for entry in providers.get("inference") or [] if isinstance(entry, dict) ] -def _openai_inference_entries(ls_config: dict[str, Any]) -> list[dict[str, Any]]: +def _openai_inference_entries(ogx_config: dict[str, Any]) -> list[dict[str, Any]]: """Return remote::openai inference rows, including the conditional-id form.""" return [ entry - for entry in _inference_entries(ls_config) + for entry in _inference_entries(ogx_config) if entry.get("provider_type") == "remote::openai" or entry.get("provider_id") in ("openai", OPENAI_CONDITIONAL_PROVIDER_ID) ] @@ -72,7 +72,7 @@ def _openai_inference_entries(ls_config: dict[str, Any]) -> list[dict[str, Any]] def test_ensure_mcp_tool_runtime_appends_and_preserves_rag() -> None: """MCP is appended; existing rag-runtime is untouched.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "apis": ["tool_runtime"], "providers": { "tool_runtime": [ @@ -84,15 +84,15 @@ def test_ensure_mcp_tool_runtime_appends_and_preserves_rag() -> None: ] }, } - ensure_mcp_tool_runtime(ls_config) - assert _tool_runtime_ids(ls_config) == [ + ensure_mcp_tool_runtime(ogx_config) + assert _tool_runtime_ids(ogx_config) == [ "rag-runtime", "model-context-protocol", ] found = False found_entry = {} - for entry in ls_config["providers"]["tool_runtime"]: + for entry in ogx_config["providers"]["tool_runtime"]: if ( isinstance(entry, dict) and entry.get("provider_id") == "model-context-protocol" @@ -112,20 +112,20 @@ def test_ensure_mcp_tool_runtime_idempotent() -> None: "provider_type": "remote::model-context-protocol", "config": {"keep": True}, } - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "apis": ["tool_runtime"], "providers": {"tool_runtime": [existing]}, } - ensure_mcp_tool_runtime(ls_config) - assert ls_config["providers"]["tool_runtime"] == [existing] + ensure_mcp_tool_runtime(ogx_config) + assert ogx_config["providers"]["tool_runtime"] == [existing] def test_ensure_mcp_tool_runtime_adds_api_when_missing() -> None: """Thin baselines get tool_runtime in apis and the MCP provider.""" - ls_config: dict[str, Any] = {} - ensure_mcp_tool_runtime(ls_config) - assert "tool_runtime" in ls_config["apis"] - assert _tool_runtime_ids(ls_config) == ["model-context-protocol"] + ogx_config: dict[str, Any] = {} + ensure_mcp_tool_runtime(ogx_config) + assert "tool_runtime" in ogx_config["apis"] + assert _tool_runtime_ids(ogx_config) == ["model-context-protocol"] # --------------------------------------------------------------------------- @@ -223,9 +223,9 @@ def test_default_baseline_resolves_when_openai_api_key_set( ({"a": 1}, {"a": 2}, {"a": 2}), # maps merge recursively, untouched keys preserved ( - {"safety": {"default_shield_id": "llama-guard", "x": 1}}, + {"safety": {"default_shield_id": "content-filter", "x": 1}}, {"safety": {"x": 2}}, - {"safety": {"default_shield_id": "llama-guard", "x": 2}}, + {"safety": {"default_shield_id": "content-filter", "x": 2}}, ), # lists replace wholesale (no append) ( @@ -267,7 +267,7 @@ def test_deep_merge_list_replace_does_not_mutate_inputs() -> None: def test_apply_high_level_inference_maps_type_and_emits_env_ref() -> None: """A remote provider maps to its provider_type with an ${env} api_key (R6).""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -278,8 +278,8 @@ def test_apply_high_level_inference_maps_type_and_emits_env_ref() -> None: } ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "openai" assert entry["provider_type"] == "remote::openai" assert entry["config"]["api_key"] == "${env.OPENAI_API_KEY}" @@ -288,10 +288,10 @@ def test_apply_high_level_inference_maps_type_and_emits_env_ref() -> None: def test_apply_high_level_inference_hyphenates_provider_id() -> None: """sentence_transformers emits the hyphenated id the ecosystem expects.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = {"providers": [{"type": "sentence_transformers"}]} - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "sentence-transformers" assert entry["provider_type"] == "inline::sentence-transformers" # no api_key / allowed_models -> no config block emitted @@ -302,7 +302,7 @@ def test_apply_high_level_inference_replaces_existing_provider_id( caplog: pytest.LogCaptureFixture, ) -> None: """A high-level provider replaces a baseline entry with the same id.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "inference": [ { @@ -316,17 +316,17 @@ def test_apply_high_level_inference_replaces_existing_provider_id( } inference = {"providers": [{"type": "openai", "api_key_env": "NEW_KEY"}]} with caplog.at_level("INFO", logger="lightspeed_stack.ogx_configuration"): - apply_high_level_inference(ls_config, inference) - ids = [p["provider_id"] for p in ls_config["providers"]["inference"]] + apply_high_level_inference(ogx_config, inference) + ids = [p["provider_id"] for p in ogx_config["providers"]["inference"]] assert ids == ["openai", "other"] # replaced in place, not duplicated - openai = ls_config["providers"]["inference"][0] + openai = ogx_config["providers"]["inference"][0] assert openai["config"]["api_key"] == "${env.NEW_KEY}" assert "provider_id='openai'" in caplog.text def test_apply_high_level_inference_replaces_conditional_provider_id() -> None: """The baseline ${env.OPENAI_API_KEY:+openai} row matches id openai.""" - ls_config: dict[str, Any] = { + ogx_config: dict[str, Any] = { "providers": { "inference": [ { @@ -342,18 +342,18 @@ def test_apply_high_level_inference_replaces_conditional_provider_id() -> None: } } inference = {"providers": [{"type": "openai", "api_key_env": "OPENAI_API_KEY"}]} - apply_high_level_inference(ls_config, inference) - openai_entries = _openai_inference_entries(ls_config) + apply_high_level_inference(ogx_config, inference) + openai_entries = _openai_inference_entries(ogx_config) assert len(openai_entries) == 1 assert openai_entries[0]["provider_id"] == "openai" assert openai_entries[0]["config"]["api_key"] == "${env.OPENAI_API_KEY}" - ids = [entry["provider_id"] for entry in _inference_entries(ls_config)] + ids = [entry["provider_id"] for entry in _inference_entries(ogx_config)] assert ids == ["openai", "sentence-transformers"] def test_apply_high_level_inference_uses_explicit_id() -> None: """An explicit id is emitted as provider_id instead of the type-derived id.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -363,15 +363,15 @@ def test_apply_high_level_inference_uses_explicit_id() -> None: } ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "vllm-prod" assert entry["provider_type"] == "remote::vllm" def test_apply_high_level_inference_same_type_distinct_ids() -> None: """Two providers of the same type with distinct ids both appear.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -388,8 +388,8 @@ def test_apply_high_level_inference_same_type_distinct_ids() -> None: }, ] } - apply_high_level_inference(ls_config, inference) - by_id = {e["provider_id"]: e for e in ls_config["providers"]["inference"]} + apply_high_level_inference(ogx_config, inference) + by_id = {e["provider_id"]: e for e in ogx_config["providers"]["inference"]} assert set(by_id) == {"vllm-prod", "vllm-staging"} assert all(e["provider_type"] == "remote::vllm" for e in by_id.values()) assert by_id["vllm-prod"]["config"]["url"] == "http://prod:8000" @@ -400,7 +400,7 @@ def test_apply_high_level_inference_duplicate_id_last_wins( caplog: pytest.LogCaptureFixture, ) -> None: """Duplicate id keeps the last entry and logs an info message.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -416,8 +416,8 @@ def test_apply_high_level_inference_duplicate_id_last_wins( ] } with caplog.at_level("INFO", logger="lightspeed_stack.ogx_configuration"): - apply_high_level_inference(ls_config, inference) - entries = ls_config["providers"]["inference"] + apply_high_level_inference(ogx_config, inference) + entries = ogx_config["providers"]["inference"] assert len(entries) == 1 assert entries[0]["provider_id"] == "vllm-shared" assert entries[0]["config"]["api_token"] == "${env.SECOND_KEY}" @@ -426,14 +426,14 @@ def test_apply_high_level_inference_duplicate_id_last_wins( def test_apply_high_level_inference_merges_extra() -> None: """The extra mapping is merged verbatim into the provider config block.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ {"type": "vllm_rhaiis", "extra": {"url": "http://x", "tls_verify": False}} ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "vllm-rhaiis" assert entry["provider_type"] == "remote::vllm" assert entry["config"] == {"url": "http://x", "tls_verify": False} @@ -441,16 +441,16 @@ def test_apply_high_level_inference_merges_extra() -> None: def test_apply_high_level_inference_emits_api_token_for_vllm() -> None: """vLLM providers emit api_token from api_key_env, not api_key.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ {"type": "vllm", "api_key_env": "VLLM_API_KEY"}, {"type": "vllm_rhaiis", "api_key_env": "VLLM_API_KEY"}, ] } - apply_high_level_inference(ls_config, inference) - vllm = ls_config["providers"]["inference"][0] - vllm_rhaiis = ls_config["providers"]["inference"][1] + apply_high_level_inference(ogx_config, inference) + vllm = ogx_config["providers"]["inference"][0] + vllm_rhaiis = ogx_config["providers"]["inference"][1] assert vllm["provider_id"] == "vllm" assert vllm["provider_type"] == "remote::vllm" assert vllm["config"]["api_token"] == "${env.VLLM_API_KEY}" @@ -462,14 +462,14 @@ def test_apply_high_level_inference_emits_api_token_for_vllm() -> None: def test_apply_high_level_inference_maps_ollama() -> None: """ollama maps to remote::ollama with extra config merged.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ {"type": "ollama", "extra": {"base_url": "http://localhost:11434"}} ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "ollama" assert entry["provider_type"] == "remote::ollama" assert entry["config"]["base_url"] == "http://localhost:11434" @@ -477,7 +477,7 @@ def test_apply_high_level_inference_maps_ollama() -> None: def test_apply_high_level_inference_maps_vllm() -> None: """vllm maps to remote::vllm with extra config merged.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -487,8 +487,8 @@ def test_apply_high_level_inference_maps_vllm() -> None: } ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["provider_id"] == "vllm" assert entry["provider_type"] == "remote::vllm" assert entry["config"]["api_token"] == "${env.VLLM_API_KEY}" @@ -497,7 +497,7 @@ def test_apply_high_level_inference_maps_vllm() -> None: def test_apply_high_level_inference_extra_cannot_override_api_key_env() -> None: """api_key_env always wins over a conflicting key in extra.""" - ls_config: dict[str, Any] = {"providers": {"inference": []}} + ogx_config: dict[str, Any] = {"providers": {"inference": []}} inference = { "providers": [ { @@ -507,8 +507,8 @@ def test_apply_high_level_inference_extra_cannot_override_api_key_env() -> None: } ] } - apply_high_level_inference(ls_config, inference) - entry = ls_config["providers"]["inference"][0] + apply_high_level_inference(ogx_config, inference) + entry = ogx_config["providers"]["inference"][0] assert entry["config"]["api_token"] == "${env.VLLM_API_KEY}" @@ -522,9 +522,9 @@ def test_unified_inference_provider_accepts_ollama_and_vllm() -> None: def test_apply_high_level_inference_empty_is_noop() -> None: """No providers -> the inference list is left as-is.""" - ls_config: dict[str, Any] = {"providers": {"inference": [{"provider_id": "x"}]}} - apply_high_level_inference(ls_config, {"providers": []}) - assert ls_config["providers"]["inference"] == [{"provider_id": "x"}] + ogx_config: dict[str, Any] = {"providers": {"inference": [{"provider_id": "x"}]}} + apply_high_level_inference(ogx_config, {"providers": []}) + assert ogx_config["providers"]["inference"] == [{"provider_id": "x"}] def test_provider_type_map_covers_every_literal_value() -> None: @@ -1002,7 +1002,7 @@ def test_synthesize_to_file_tightens_perms_on_overwrite(tmp_path: Path) -> None: }, ], }, - "safety": {"default_shield_id": "llama-guard", "excluded_categories": []}, + "safety": {"default_shield_id": "content-filter", "excluded_categories": []}, } diff --git a/tests/unit/transcripts/README.md b/tests/unit/transcripts/README.md new file mode 100644 index 000000000..d1e4b2e24 --- /dev/null +++ b/tests/unit/transcripts/README.md @@ -0,0 +1,10 @@ +# List of source files stored in `tests/unit/transcripts` directory + +## [__init__.py](__init__.py) + +Init of tests/unit/transcripts. + +## [test_transcripts.py](test_transcripts.py) + +Unit tests for functions defined in transcripts module. + diff --git a/tests/unit/transcripts/__init__.py b/tests/unit/transcripts/__init__.py new file mode 100644 index 000000000..4250e0b23 --- /dev/null +++ b/tests/unit/transcripts/__init__.py @@ -0,0 +1 @@ +"""Init of tests/unit/transcripts.""" diff --git a/tests/unit/utils/test_transcripts.py b/tests/unit/transcripts/test_transcripts.py similarity index 94% rename from tests/unit/utils/test_transcripts.py rename to tests/unit/transcripts/test_transcripts.py index fced39ace..1a11a2566 100644 --- a/tests/unit/utils/test_transcripts.py +++ b/tests/unit/transcripts/test_transcripts.py @@ -1,4 +1,4 @@ -"""Unit tests for functions defined in utils.transcripts module.""" +"""Unit tests for functions defined in transcripts module.""" import hashlib @@ -7,7 +7,7 @@ from configuration import AppConfig from models.api.requests import QueryRequest from models.common.turn_summary import ToolCallSummary, ToolResultSummary, TurnSummary -from utils.transcripts import ( +from transcripts.transcripts import ( construct_transcripts_path, create_transcript, create_transcript_metadata, @@ -39,7 +39,7 @@ def test_construct_transcripts_path(mocker: MockerFixture) -> None: cfg = AppConfig() cfg.init_from_dict(config_dict) # Update configuration for this test - mocker.patch("utils.transcripts.configuration", cfg) + mocker.patch("transcripts.transcripts.configuration", cfg) user_id = "user123" conversation_id = "123e4567-e89b-12d3-a456-426614174000" @@ -59,12 +59,12 @@ def test_store_transcript( # pylint: disable=too-many-locals """Test the store_transcript function.""" mocker.patch("builtins.open", mocker.mock_open()) mocker.patch( - "utils.transcripts.construct_transcripts_path", + "transcripts.transcripts.construct_transcripts_path", return_value=mocker.MagicMock(), ) # Mock the JSON to assert the data is stored correctly - mock_json = mocker.patch("utils.transcripts.json") + mock_json = mocker.patch("transcripts.transcripts.json") # Mock parameters user_id = "user123" diff --git a/tests/unit/utils/README.md b/tests/unit/utils/README.md index 34683e80d..3431784c5 100644 --- a/tests/unit/utils/README.md +++ b/tests/unit/utils/README.md @@ -44,29 +44,21 @@ Unit tests for utils/input_sanitization.py. Unit tests for utils/json_schema_updater module. -## [test_ogx_version.py](test_ogx_version.py) - -Unit tests for utility function to check OGX version. - ## [test_markdown_repair.py](test_markdown_repair.py) Unit tests for markdown repair utilities. -## [test_mcp_auth_headers.py](test_mcp_auth_headers.py) - -Unit tests for MCP authorization headers utilities. - -## [test_mcp_headers.py](test_mcp_headers.py) +## [test_model_list.py](test_model_list.py) -Unit tests for MCP headers utility functions. +Unit tests for utils/model_list.py helpers. -## [test_mcp_tools.py](test_mcp_tools.py) +## [test_ogx_serialization.py](test_ogx_serialization.py) -Unit tests for MCP tool discovery utilities. +Unit tests for ogx_client serialization helpers. -## [test_model_list.py](test_model_list.py) +## [test_ogx_version.py](test_ogx_version.py) -Unit tests for utils/model_list.py helpers. +Unit tests for utility function to check OGX version. ## [test_otel_tracing.py](test_otel_tracing.py) @@ -120,10 +112,6 @@ Unit tests for utils/token_estimator. Unit tests for tool_formatter utilities. -## [test_transcripts.py](test_transcripts.py) - -Unit tests for functions defined in utils.transcripts module. - ## [test_types.py](test_types.py) Unit tests for functions and types defined in utils/types.py. diff --git a/tests/unit/utils/agents/test_tool_processor.py b/tests/unit/utils/agents/test_tool_processor.py index bd77cf93d..e077c61f4 100644 --- a/tests/unit/utils/agents/test_tool_processor.py +++ b/tests/unit/utils/agents/test_tool_processor.py @@ -150,6 +150,25 @@ def test_mcp_call(self) -> None: assert summary.args == {"arg": 1} assert summary.type == "mcp_call" + def test_mcp_call_with_json_string_tool_args(self) -> None: + """Test MCP tool call parses OpenAI-style JSON string tool_args.""" + part = NativeToolCallPart( + tool_name=f"{MCPServerTool.kind}:srv", + args={ + "action": "call", + "tool_name": "get_subscriptions", + "tool_args": '{\n "limit": 20\n}', + }, + tool_call_id="mcp-call-json", + ) + + summary = summarize_native_tool_call(part) + + assert summary is not None + assert summary.name == "get_subscriptions" + assert summary.args == {"limit": 20} + assert summary.type == "mcp_call" + def test_unknown_tool_returns_none(self, mocker: MockerFixture) -> None: """Test unknown native tool logs warning and returns None.""" mock_warning = mocker.patch("utils.agents.tool_processor.logger.warning") diff --git a/tests/unit/utils/dumpers/test_config_dumper.py b/tests/unit/utils/dumpers/test_config_dumper.py index c30c2bbd7..1b07914be 100644 --- a/tests/unit/utils/dumpers/test_config_dumper.py +++ b/tests/unit/utils/dumpers/test_config_dumper.py @@ -47,7 +47,7 @@ def test_dump_schema(tmpdir: Path) -> None: filename = tmpdir / "foo.json" dump_schema(str(filename)) - with open(filename, "r", encoding="utf-8") as fin: + with open(filename, encoding="utf-8") as fin: # schema should be stored in JSON format content = load(fin) assert content is not None diff --git a/tests/unit/utils/dumpers/test_models_dumper.py b/tests/unit/utils/dumpers/test_models_dumper.py index 277e2a721..2dce7605a 100644 --- a/tests/unit/utils/dumpers/test_models_dumper.py +++ b/tests/unit/utils/dumpers/test_models_dumper.py @@ -2887,7 +2887,7 @@ def test_dump_models(tmpdir: Path) -> None: }, "OgxConfiguration": { "additionalProperties": false, - "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/)", + "description": "OGX configuration.\n\nOGX is a comprehensive system that provides a uniform set of tools\nfor building, scaling, and deploying generative AI applications, enabling\ndevelopers to create, integrate, and orchestrate multiple AI services and\ncapabilities into an adaptable setup.\n\nUseful resources:\n\n - [OGX](https://ogx-ai.github.io/)\n - [Python OGX client](https://github.com/ogx-ai/ogx-client-python)\n - [Build AI Applications with OGX](https://ogx-ai.github.io/docs/building_applications)", "properties": { "url": { "type": "string", @@ -6756,7 +6756,7 @@ def test_dump_models(tmpdir: Path) -> None: "type": "string" }, "conversation": { - "description": "The conversation ID in llama-stack format", + "description": "The conversation ID in OGX format", "title": "Conversation", "type": "string" }, @@ -9035,7 +9035,7 @@ def test_dump_models(tmpdir: Path) -> None: "type": "object" }, "TurnSummary": { - "description": "Summary of a turn in llama stack.", + "description": "Summary of a turn in OGX.", "properties": { "id": { "default": "", @@ -10030,7 +10030,7 @@ def test_dump_models(tmpdir: Path) -> None: filename = tmpdir / "foo.json" dump_models(str(filename)) - with open(filename, "r", encoding="utf-8") as fin: + with open(filename, encoding="utf-8") as fin: # schema should be stored in JSON format content = load(fin) assert content is not None @@ -10279,7 +10279,7 @@ def test_dump_models(tmpdir: Path) -> None: def check_json_file_content(filename: Path, expected_schemas: list[str]) -> None: """Check the content of provided JSON file with OpenAPI-compatible schema.""" - with open(filename, "r", encoding="utf-8") as fin: + with open(filename, encoding="utf-8") as fin: # schema should be stored in JSON format content = load(fin) assert content is not None diff --git a/tests/unit/utils/mcp/README.md b/tests/unit/utils/mcp/README.md new file mode 100644 index 000000000..118713cd8 --- /dev/null +++ b/tests/unit/utils/mcp/README.md @@ -0,0 +1,14 @@ +# List of source files stored in `tests/unit/utils/mcp` directory + +## [test_mcp_auth_headers.py](test_mcp_auth_headers.py) + +Unit tests for MCP authorization headers utilities. + +## [test_mcp_headers.py](test_mcp_headers.py) + +Unit tests for MCP headers utility functions. + +## [test_mcp_tools.py](test_mcp_tools.py) + +Unit tests for MCP tool discovery utilities. + diff --git a/tests/unit/utils/test_mcp_auth_headers.py b/tests/unit/utils/mcp/test_mcp_auth_headers.py similarity index 98% rename from tests/unit/utils/test_mcp_auth_headers.py rename to tests/unit/utils/mcp/test_mcp_auth_headers.py index 2404d282f..6898ec673 100644 --- a/tests/unit/utils/test_mcp_auth_headers.py +++ b/tests/unit/utils/mcp/test_mcp_auth_headers.py @@ -2,7 +2,7 @@ from pathlib import Path -from utils.mcp_auth_headers import resolve_authorization_headers +from utils.mcp.mcp_auth_headers import resolve_authorization_headers def test_resolve_authorization_headers_empty() -> None: diff --git a/tests/unit/utils/test_mcp_headers.py b/tests/unit/utils/mcp/test_mcp_headers.py similarity index 89% rename from tests/unit/utils/test_mcp_headers.py rename to tests/unit/utils/mcp/test_mcp_headers.py index 0e583b7f3..70dac527f 100644 --- a/tests/unit/utils/test_mcp_headers.py +++ b/tests/unit/utils/mcp/test_mcp_headers.py @@ -8,11 +8,13 @@ import constants from models.config import ModelContextProtocolServer -from utils import mcp_headers -from utils.mcp_headers import ( +from utils.mcp import mcp_headers +from utils.mcp.mcp_headers import ( build_server_headers, + ensure_bearer_prefix, extract_propagated_headers, find_unresolved_auth_headers, + strip_bearer_prefix, ) @@ -335,6 +337,49 @@ def test_empty_resolved_returns_all_configured(self) -> None: assert sorted(result) == ["Authorization", "X-Api-Key"] +class TestStripBearerPrefix: + """Test cases for strip_bearer_prefix function.""" + + def test_strips_bearer_prefix(self) -> None: + """Test that a leading 'Bearer ' scheme is removed.""" + assert strip_bearer_prefix("Bearer abc123") == "abc123" + + def test_does_not_strip_differently_cased_prefix(self) -> None: + """Test that only the exact 'Bearer ' scheme is matched, not other casings.""" + assert strip_bearer_prefix("bearer abc123") == "bearer abc123" + assert strip_bearer_prefix("BEARER abc123") == "BEARER abc123" + assert strip_bearer_prefix("BeArEr abc123") == "BeArEr abc123" + + def test_value_without_prefix_is_unchanged(self) -> None: + """Test that a raw token without a scheme is returned as-is.""" + assert strip_bearer_prefix("abc123") == "abc123" + + def test_empty_string_is_unchanged(self) -> None: + """Test that an empty string is returned unchanged.""" + assert not strip_bearer_prefix("") + + def test_only_strips_leading_prefix(self) -> None: + """Test that only the first 'Bearer ' scheme is stripped, not repeated ones.""" + assert strip_bearer_prefix("Bearer Bearer abc123") == "Bearer abc123" + + +class TestEnsureBearerPrefix: + """Test cases for ensure_bearer_prefix function.""" + + def test_adds_bearer_prefix_to_raw_token(self) -> None: + """Test that a raw token gets the 'Bearer ' scheme added.""" + assert ensure_bearer_prefix("abc123") == "Bearer abc123" + + def test_does_not_double_prefix_already_scoped_value(self) -> None: + """Test that a value already carrying 'Bearer ' is left unchanged.""" + assert ensure_bearer_prefix("Bearer abc123") == "Bearer abc123" + + def test_double_prefixes_differently_cased_scheme(self) -> None: + """Test that a differently-cased scheme is treated as a raw token and prefixed.""" + assert ensure_bearer_prefix("bearer abc123") == "Bearer bearer abc123" + assert ensure_bearer_prefix("BEARER abc123") == "Bearer BEARER abc123" + + class TestBuildServerHeaders: """Test cases for build_server_headers function.""" diff --git a/tests/unit/utils/test_mcp_tools.py b/tests/unit/utils/mcp/test_mcp_tools.py similarity index 88% rename from tests/unit/utils/test_mcp_tools.py rename to tests/unit/utils/mcp/test_mcp_tools.py index 491784e08..bbcac54f2 100644 --- a/tests/unit/utils/test_mcp_tools.py +++ b/tests/unit/utils/mcp/test_mcp_tools.py @@ -4,7 +4,7 @@ import pytest from pytest_mock import MockerFixture -from utils.mcp_tools import _MCP_HTTP_TIMEOUT, list_mcp_tools +from utils.mcp.mcp_tools import _MCP_HTTP_TIMEOUT, list_mcp_tools @pytest.mark.asyncio @@ -17,16 +17,16 @@ async def test_list_mcp_tools_forwards_headers_to_transport( mock_http_client.__aexit__ = mocker.AsyncMock(return_value=None) mock_async_client = mocker.patch( - "utils.mcp_tools.httpx.AsyncClient", + "utils.mcp.mcp_tools.httpx.AsyncClient", return_value=mock_http_client, ) - mock_streamable = mocker.patch("utils.mcp_tools.streamable_http_client") + mock_streamable = mocker.patch("utils.mcp.mcp_tools.streamable_http_client") mock_streamable.return_value.__aenter__ = mocker.AsyncMock( return_value=(mocker.Mock(), mocker.Mock(), mocker.Mock()) ) mock_streamable.return_value.__aexit__ = mocker.AsyncMock(return_value=None) mocker.patch( - "utils.mcp_tools._list_tools_from_session", + "utils.mcp.mcp_tools._list_tools_from_session", new=mocker.AsyncMock(return_value=[]), ) @@ -54,7 +54,7 @@ async def test_list_mcp_tools_returns_empty_list_when_all_transports_fail( ) -> None: """Skip unavailable MCP servers by returning an empty tool list.""" mocker.patch( - "utils.mcp_tools._MCP_TRANSPORTS", + "utils.mcp.mcp_tools._MCP_TRANSPORTS", ( ( "streamable HTTP", @@ -82,7 +82,7 @@ async def test_list_mcp_tools_returns_empty_list_on_http_error( response=response, ) mocker.patch( - "utils.mcp_tools._MCP_TRANSPORTS", + "utils.mcp.mcp_tools._MCP_TRANSPORTS", ( ("streamable HTTP", mocker.AsyncMock(side_effect=http_error)), ("SSE", mocker.AsyncMock(side_effect=http_error)), diff --git a/tests/unit/utils/test_checks.py b/tests/unit/utils/test_checks.py index 0990c66e1..a9dff5e5f 100644 --- a/tests/unit/utils/test_checks.py +++ b/tests/unit/utils/test_checks.py @@ -15,7 +15,7 @@ def input_file_fixture(tmp_path: Path) -> str: """Create file manually using the tmp_path fixture.""" filename = os.path.join(tmp_path, "mydoc.csv") - with open(filename, "wt", encoding="utf-8") as fout: + with open(filename, "w", encoding="utf-8") as fout: fout.write("some content!") return filename diff --git a/tests/unit/utils/test_compaction.py b/tests/unit/utils/test_compaction.py index ab146b4e4..0eda1dcce 100644 --- a/tests/unit/utils/test_compaction.py +++ b/tests/unit/utils/test_compaction.py @@ -74,11 +74,11 @@ def _make_history(num_pairs: int, words_per_message: int = 1) -> list[Any]: class TestIsMessageItem: """Tests for is_message_item.""" - def test_llama_stack_message(self) -> None: + def test_ogx_message(self) -> None: """OGX message item is recognised.""" assert is_message_item(_MessageItem("user", "hi")) is True - def test_llama_stack_tool_call(self) -> None: + def test_ogx_tool_call(self) -> None: """Tool-call item is not a message.""" assert is_message_item(_ToolCallItem()) is False diff --git a/tests/unit/utils/test_endpoints.py b/tests/unit/utils/test_endpoints.py index f7e10e39b..9e88cf489 100644 --- a/tests/unit/utils/test_endpoints.py +++ b/tests/unit/utils/test_endpoints.py @@ -23,7 +23,7 @@ def input_file_fixture(tmp_path: Path) -> str: """Create file manually using the tmp_path fixture.""" filename = os.path.join(tmp_path, "prompt.txt") - with open(filename, "wt", encoding="utf-8") as fout: + with open(filename, "w", encoding="utf-8") as fout: fout.write("this is prompt!") return filename diff --git a/tests/unit/utils/test_otel_tracing.py b/tests/unit/utils/test_otel_tracing.py index 2d6405b84..c9d037bd2 100644 --- a/tests/unit/utils/test_otel_tracing.py +++ b/tests/unit/utils/test_otel_tracing.py @@ -168,7 +168,7 @@ def test_set_single_attribute(self, otel: Generator[Any, Any, Any]) -> None: assert len(spans) == 1 assert spans[0].attributes[SpanAttributes.SESSION_ID] == "test-session-123" - def test_set_multiple_attributes(self, otel): + def test_set_multiple_attributes(self, otel: Generator[Any, Any, Any]) -> None: """Test setting multiple attributes on a span.""" tracer, exporter = otel with tracer.start_as_current_span("test_span") as span: @@ -190,7 +190,7 @@ def test_set_multiple_attributes(self, otel): assert attrs[SpanAttributes.LLM_USAGE_INPUT_TOKENS] == 100 assert attrs[SpanAttributes.LLM_USAGE_OUTPUT_TOKENS] == 50 - def test_set_attributes_with_list(self, otel): + def test_set_attributes_with_list(self, otel: Generator[Any, Any, Any]) -> None: """Test setting attributes with list values.""" tracer, exporter = otel with tracer.start_as_current_span("test_span") as span: @@ -215,7 +215,7 @@ def test_set_attributes_with_list(self, otel): ) assert attrs[SpanAttributes.TOOL_CALLS_NAMES] == ("search", "calculator") - def test_set_empty_attributes(self, otel): + def test_set_empty_attributes(self, otel: Generator[Any, Any, Any]) -> None: """Test setting empty attributes dict.""" tracer, exporter = otel with tracer.start_as_current_span("test_span") as span: diff --git a/tests/unit/utils/test_responses.py b/tests/unit/utils/test_responses.py index ae9b98dac..9adb8ef00 100644 --- a/tests/unit/utils/test_responses.py +++ b/tests/unit/utils/test_responses.py @@ -12,6 +12,7 @@ from ogx_api.openai_responses import ( AllowedToolsFilter, OpenAIResponseInputToolChoiceAllowedTools, + OpenAIResponseReasoning, ) from ogx_api.openai_responses import ApprovalFilter as OgxApprovalFilter from ogx_api.openai_responses import ( @@ -78,6 +79,7 @@ _build_chunk_attributes, _build_okp_doc_url, _merge_tools, + apply_reasoning_for_resolved_tools, build_mcp_tool_call_from_arguments_done, build_tool_call_summary, build_tool_result_from_mcp_output_item_done, @@ -92,6 +94,7 @@ get_topic_summary, is_server_deployed_output, maybe_get_topic_summary, + model_reasoning_enabled_by_default, parse_arguments_string, parse_referenced_documents, prepare_responses_params, @@ -482,7 +485,10 @@ async def test_get_mcp_tools_with_kubernetes_auth( mocker.patch("utils.responses.configuration", mock_config) tools_k8s = await get_mcp_tools(token="user-k8s-token") assert len(tools_k8s) == 1 - assert tools_k8s[0].authorization == "Bearer user-k8s-token" + # The Bearer scheme is stripped here since OGX adds its own "Bearer " + # prefix before forwarding the token to the MCP server; passing the + # scheme through would result in a duplicated "Bearer Bearer ". + assert tools_k8s[0].authorization == "user-k8s-token" @pytest.mark.asyncio async def test_get_mcp_tools_with_mcp_headers(self, mocker: MockerFixture) -> None: @@ -620,7 +626,7 @@ async def test_get_mcp_tools_with_mixed_headers( tools = await get_mcp_tools(token="k8s-token", mcp_headers=mcp_headers) assert len(tools) == 1 - assert tools[0].authorization == "Bearer k8s-token" + assert tools[0].authorization == "k8s-token" assert tools[0].headers == { "X-API-Key": "secret-api-key", "X-Custom": "client-custom-value", @@ -705,6 +711,40 @@ async def test_get_mcp_tools_with_propagated_headers( "x-request-id": "req-456", } + @pytest.mark.asyncio + async def test_get_mcp_tools_propagated_authorization_strips_bearer_scheme( + self, mocker: MockerFixture + ) -> None: + """Regression test for a duplicated 'Bearer Bearer ' MCP header. + + When an MCP server allowlists the incoming ``Authorization`` header + via ``headers: [Authorization]`` (no ``authorization_headers`` + config), the propagated value already includes the ``Bearer`` + scheme. Since OGX's MCP ``authorization`` field expects a raw token + and adds its own ``Bearer `` prefix, the scheme must be stripped + before it is assigned to that field, or the downstream MCP server + receives ``Authorization: Bearer Bearer ``. + """ + servers = [ + ModelContextProtocolServer( + name="subscription-watch", + url="http://subscription-watch:8080", + headers=["Authorization"], + provider_id="provider", + ), + ] + mock_config = mocker.Mock() + mock_config.mcp_servers = servers + mocker.patch("utils.responses.configuration", mock_config) + + request_headers = {"Authorization": "Bearer user-jwt-token"} + tools = await get_mcp_tools( + token=None, mcp_headers=None, request_headers=request_headers + ) + assert len(tools) == 1 + assert tools[0].authorization == "user-jwt-token" + assert tools[0].headers is None + @pytest.mark.asyncio async def test_get_mcp_tools_propagated_headers_do_not_overwrite_auth_headers( self, tmp_path: Path, mocker: MockerFixture @@ -812,7 +852,7 @@ async def test_get_mcp_tools_propagated_headers_additive_with_mcp_headers( token=None, mcp_headers=mcp_hdrs, request_headers=request_headers ) assert len(tools) == 1 - assert tools[0].authorization == "Bearer client-token" + assert tools[0].authorization == "client-token" assert tools[0].headers == {"x-rh-identity": "identity-value"} @pytest.mark.asyncio @@ -931,6 +971,48 @@ async def test_apply_mcp_headers_preserves_type( dumped = out[0].model_dump(exclude_unset=True) assert dumped.get("type") == "mcp" + def test_apply_mcp_headers_strips_bearer_from_propagated_authorization( + self, mocker: MockerFixture + ) -> None: + """apply_mcp_headers_to_explicit_tools must strip the Bearer scheme. + + Same regression as ``test_get_mcp_tools_propagated_authorization_strips_bearer_scheme`` + but for the explicit-tools path: a propagated ``Authorization`` header + must not retain its ``Bearer`` scheme when assigned to OGX's + ``authorization`` field, or the MCP server receives a duplicated + ``Bearer Bearer `` header. + """ + from utils.responses import ( # pylint: disable=import-outside-toplevel + apply_mcp_headers_to_explicit_tools, + ) + + servers = [ + ModelContextProtocolServer( + name="subscription-watch", + url="http://subscription-watch:8080", + headers=["Authorization"], + provider_id="mcp", + ), + ] + mock_config = mocker.Mock() + mock_config.mcp_servers = servers + mocker.patch("utils.responses.configuration", mock_config) + + explicit = InputToolMCP( + server_label="subscription-watch", + server_url="http://subscription-watch:8080", + ) + + out = apply_mcp_headers_to_explicit_tools( + [explicit], + token=None, + mcp_headers=None, + request_headers={"Authorization": "Bearer user-jwt-token"}, + ) + + assert len(out) == 1 + assert out[0].authorization == "user-jwt-token" + class TestGetTopicSummary: """Tests for get_topic_summary function.""" @@ -1093,6 +1175,108 @@ async def test_disabled_emits_no_span( ] +class TestModelReasoningEnabledByDefault: + """Tests for model_reasoning_enabled_by_default.""" + + def test_gpt_5_6_default_on(self) -> None: + """gpt-5.6 reasons by default when reasoning is omitted.""" + assert model_reasoning_enabled_by_default("openai/gpt-5.6-terra") is True + + def test_o_series_default_on(self) -> None: + """o-series models reason by default.""" + assert model_reasoning_enabled_by_default("openai/o3-mini") is True + + def test_gpt_5_4_opt_in(self) -> None: + """gpt-5.4 defaults to reasoning off.""" + assert model_reasoning_enabled_by_default("openai/gpt-5.4") is False + + def test_gpt_5_chat_no_reasoning(self) -> None: + """gpt-5-chat does not use reasoning.""" + assert model_reasoning_enabled_by_default("openai/gpt-5-chat-latest") is False + + def test_gpt_4o_mini(self) -> None: + """gpt-4o-mini does not use reasoning.""" + assert model_reasoning_enabled_by_default("openai/gpt-4o-mini") is False + + +class TestApplyReasoningForResolvedTools: + """Tests for apply_reasoning_for_resolved_tools.""" + + _DEFAULT_ON_MODEL = "openai/gpt-5.6-terra" + _OPT_IN_MODEL = "openai/gpt-5.4" + _NON_REASONING_MODEL = "openai/gpt-4o-mini" + + @staticmethod + def _sample_tools() -> list[InputTool]: + return cast( + list[InputTool], + [InputToolFunction(name="lookup", parameters={"type": "object"})], + ) + + def test_preserves_reasoning_when_no_tools(self) -> None: + """Keep caller reasoning when the resolved request has no tools.""" + reasoning = OpenAIResponseReasoning(effort="high") + assert ( + apply_reasoning_for_resolved_tools(reasoning, None, self._DEFAULT_ON_MODEL) + == reasoning + ) + + def test_preserves_none_reasoning_when_no_tools(self) -> None: + """Leave reasoning unset when no tools are resolved.""" + assert ( + apply_reasoning_for_resolved_tools(None, None, self._DEFAULT_ON_MODEL) + is None + ) + + def test_skips_reasoning_for_non_reasoning_model_with_tools(self) -> None: + """Do not inject reasoning for models that do not use it.""" + assert ( + apply_reasoning_for_resolved_tools( + None, self._sample_tools(), self._NON_REASONING_MODEL + ) + is None + ) + + def test_skips_reasoning_for_opt_in_model_with_tools(self) -> None: + """Do not inject reasoning for opt-in models that default to off.""" + assert ( + apply_reasoning_for_resolved_tools( + None, self._sample_tools(), self._OPT_IN_MODEL + ) + is None + ) + + def test_sets_none_effort_when_tools_present_and_reasoning_missing(self) -> None: + """Default to explicit effort none when tools are present.""" + result = apply_reasoning_for_resolved_tools( + None, self._sample_tools(), self._DEFAULT_ON_MODEL + ) + assert result is not None + assert result.effort == "none" + + def test_forces_none_effort_when_tools_present_and_reasoning_requested( + self, + ) -> None: + """Override non-none reasoning effort when tools are present.""" + reasoning = OpenAIResponseReasoning(effort="medium") + result = apply_reasoning_for_resolved_tools( + reasoning, self._sample_tools(), self._DEFAULT_ON_MODEL + ) + assert result is not None + assert result.effort == "none" + + def test_keeps_none_effort_when_tools_present(self) -> None: + """Preserve other reasoning fields when forcing effort to none.""" + reasoning = OpenAIResponseReasoning(effort="none", summary="concise") + result = apply_reasoning_for_resolved_tools( + reasoning, self._sample_tools(), self._DEFAULT_ON_MODEL + ) + assert result is not None + assert result is not reasoning + assert result.effort == "none" + assert result.summary == "concise" + + class TestResolveToolChoice: """Tests for resolve_tool_choice (ToolChoiceMode, AllowedTools, explicit/implicit tools).""" diff --git a/tests/unit/utils/test_stream_interrupts.py b/tests/unit/utils/test_stream_interrupts.py index 4faea720d..f612ef54d 100644 --- a/tests/unit/utils/test_stream_interrupts.py +++ b/tests/unit/utils/test_stream_interrupts.py @@ -139,6 +139,7 @@ def test_register_interrupt_callback_registers_current_task( context = mocker.Mock(spec=ResponseGeneratorContext) context.request_id = "req-1" context.user_id = "user_1" + context.conversation_id = "conv-1" responses_params = mocker.Mock(spec=ResponsesApiParams) turn_summary = TurnSummary() background_tasks: list[asyncio.Task[None]] = [] @@ -157,6 +158,7 @@ async def run() -> list[bool]: registry.register_stream.assert_called_once() assert registry.register_stream.call_args.kwargs["request_id"] == "req-1" assert registry.register_stream.call_args.kwargs["user_id"] == "user_1" + assert registry.register_stream.call_args.kwargs["conversation_id"] == "conv-1" assert persist_mock.await_count == 0 on_interrupt = registry.register_stream.call_args.kwargs["on_interrupt"] diff --git a/tests/unit/utils/test_token_estimator.py b/tests/unit/utils/test_token_estimator.py index a068c6b7d..fe62157bf 100644 --- a/tests/unit/utils/test_token_estimator.py +++ b/tests/unit/utils/test_token_estimator.py @@ -115,11 +115,11 @@ def test_within_5pct_of_explicit_tiktoken_call(self) -> None: class TestIsMessage: """Tests for the is_message_item duck-type check.""" - def test_llama_stack_message_item(self) -> None: + def test_ogx_message_item(self) -> None: """An OGX-shaped object with type == 'message' is a message.""" assert is_message_item(_MessageItem("user", "hi")) is True - def test_llama_stack_tool_call_item(self) -> None: + def test_ogx_tool_call_item(self) -> None: """A tool-call-shaped object is not a message.""" assert is_message_item(_ToolCallItem()) is False @@ -137,7 +137,7 @@ def test_dict_is_not_a_message(self) -> None: class TestExtractMessageText: """Tests for the extract_message_text duck-type extractor.""" - def test_llama_stack_string_content(self) -> None: + def test_ogx_string_content(self) -> None: """Plain string content is returned as-is.""" assert extract_message_text(_MessageItem("user", "hello")) == "hello" diff --git a/uv.lock b/uv.lock index a601df447..d6c4954de 100644 --- a/uv.lock +++ b/uv.lock @@ -1,14 +1,18 @@ version = 1 -revision = 3 -requires-python = ">=3.12, <3.14" +revision = 2 +requires-python = ">=3.12, <3.15" resolution-markers = [ - "python_full_version >= '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version < '3.13' and sys_platform == 'emscripten'", "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version < '3.13' and sys_platform == 'darwin'", ] @@ -30,7 +34,7 @@ wheels = [ [[package]] name = "accelerate" -version = "1.14.0" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, @@ -42,9 +46,9 @@ dependencies = [ { name = "torch", version = "2.11.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { name = "torch", version = "2.11.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/75/94cd5d389649578aca399e5aa822637eec18319a1dadc400ffe2f9a7493f/accelerate-1.14.0.tar.gz", hash = "sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d", size = 412167, upload-time = "2026-06-11T13:45:52.326Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/b5/1d3ed029ac71d3f2961346829a268da923698e9fd63f218f78841f216bfd/accelerate-1.15.0.tar.gz", hash = "sha256:5654f8c5eaa0d4fa68b33e287a97765da6849bf6d51dcac874e73fbbddfb6134", size = 422615, upload-time = "2026-09-09T13:04:49.078Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/db/253133d7e7cb40d3af384bb2f5c0b4a2b7fdcffbc95c688cc67a20a3c103/accelerate-1.14.0-py3-none-any.whl", hash = "sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6", size = 389246, upload-time = "2026-06-11T13:45:50.477Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/34f0450479d01195027260da68d8a3880683f1640c3ca5adf64acb3185f1/accelerate-1.15.0-py3-none-any.whl", hash = "sha256:97eacca0b73e45cb867dbf8c5d5d4dc32219544300e0c8992c7334dc2ef33cec", size = 394295, upload-time = "2026-09-09T13:04:47.331Z" }, ] [[package]] @@ -125,6 +129,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, ] [[package]] @@ -169,7 +214,7 @@ wheels = [ [[package]] name = "anthropic" -version = "1.2.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -180,22 +225,22 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/1a/b5af41cc1fa14da277ec20ca5554dd2fcbc09b8523ac59b7a97fbb88e452/anthropic-1.2.0.tar.gz", hash = "sha256:12f8eedee7b7fb5685837b1371b7bfae1b281703f62355f4632598ec2fc53b34", size = 1137443, upload-time = "2026-08-27T20:29:12.68Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/3d/4170318539de0c163e1806509b0bb9cd7d611ee15e7885f1d16a11a37d29/anthropic-1.6.0.tar.gz", hash = "sha256:ce3c032f940984f67c516db896375bff36959f8dc589973e386f8e6ea81bbec3", size = 1172375, upload-time = "2026-09-15T15:14:55.826Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/78/3f8b52708b03309e511990700bb8d0ec7a0c9db3d2a1e0d1c3ca417a4604/anthropic-1.2.0-py3-none-any.whl", hash = "sha256:b60642b3e3cd6b8e3e328a2d3f2863ad2b6e743f1037e42cc0143f7df99f63c6", size = 1289535, upload-time = "2026-08-27T20:29:11.01Z" }, + { url = "https://files.pythonhosted.org/packages/c1/32/57a8b6c5441d9e6a1f332f70ee02375af6d9b5b65b42689ba83f7c7d010a/anthropic-1.6.0-py3-none-any.whl", hash = "sha256:049434f013d3a874acdfee696ec626e19166d58f4277e8fa34e5d0a5ec250ad0", size = 1249068, upload-time = "2026-09-15T15:14:54.326Z" }, ] [[package]] name = "anyio" -version = "4.14.2" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, ] [[package]] @@ -218,46 +263,65 @@ wheels = [ [[package]] name = "ast-serialize" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" }, - { url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" }, - { url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" }, - { url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" }, - { url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" }, - { url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" }, - { url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" }, - { url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" }, - { url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" }, - { url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" }, - { url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" }, - { url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" }, - { url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" }, - { url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" }, - { url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" }, - { url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" }, - { url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" }, - { url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" }, - { url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" }, - { url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" }, - { url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" }, - { url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" }, - { url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" }, - { url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" }, - { url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" }, +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/1e/4f6082cdd6e5a29093513e9a3eabc5ed1c5331a9a84386b2fece80a00a48/ast_serialize-0.11.2.tar.gz", hash = "sha256:976a5bd75845d22f4b52905ddf53ab669ef1b14dba7735f5512841a2ef2b5450", size = 954387, upload-time = "2026-09-13T18:48:55.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/2e/beec3364eef4b01793a676d8cd16e9014c42044a5505000ceae3955e33fa/ast_serialize-0.11.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f6a8dfc5ab204a706f6e5d39c6f77c18c27ef084fa2081803a64a9160ce89277", size = 897089, upload-time = "2026-09-13T18:47:22.69Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d7/ef56443df2891c6ba2c4019c2cb3dcaf97c9948da6d963068e04e8dac6ea/ast_serialize-0.11.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cb073bfa15742699d408ac50f60878383b5665ae1791d1b6799ea6f08633cd77", size = 1235218, upload-time = "2026-09-13T18:47:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/42/8d/cff58d17ba1d0272ff0b7ab5d3bdfcf8f47317eb0f47c001d394bffebf95/ast_serialize-0.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1d6ad94edbe93bf1dabc06c9f37d55b898fdabc456aa6d7ced5e23c14f795f32", size = 1216399, upload-time = "2026-09-13T18:47:26.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/d2/a1da7675af5f42335c36e4da6d86ef4fd7168cead18de81df0a2d6faeb1a/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b2801cf2221bd922d9f69d2f0ebc373c3db47207315d525b2d87fa161a2af4", size = 1282064, upload-time = "2026-09-13T18:47:27.787Z" }, + { url = "https://files.pythonhosted.org/packages/97/89/5a400a13b2c9c0152ebb5ad45408a3fe5e4e60e325d3ac4e5cf6e915a0cc/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd666cebd6ab3b3c0fd348a6202c26e18a401ee34293c3804d3472266bc146f6", size = 1285864, upload-time = "2026-09-13T18:47:29.667Z" }, + { url = "https://files.pythonhosted.org/packages/02/b8/80a381c70fd49f0316fb0383c4f9e4c13e81b010b64889bd45898ce8f5f4/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d01f61352c96370febf6c0dbd488dee9183a731fb2702170da9163ae317cded", size = 1554755, upload-time = "2026-09-13T18:47:31.257Z" }, + { url = "https://files.pythonhosted.org/packages/90/97/dcaa34a32d2db789221c125b3eb10feb5089715fe53d9874d627afc26231/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0fd40c668b0fa19b8fdb61d9e63d547e2e19cfbfe053a51ef0b6c37070298a8", size = 1301807, upload-time = "2026-09-13T18:47:32.714Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/84a22420cb312642d7d31547c644d09a3d101418c6d6b9ef2ec30735cf11/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa819d7c14c8e4153dcd84671826331538be7cbe460383fc6386f5eea5bd234", size = 1301941, upload-time = "2026-09-13T18:47:34.418Z" }, + { url = "https://files.pythonhosted.org/packages/20/8a/aa5f3dcf1aed9678c25982f40d366004e3c0cac47bc0c240f6b837dcbb1f/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a9ffa8a197a721f07a352d0be6185f5b3e6f9aaebfdb66169ed652108531ae3b", size = 1307910, upload-time = "2026-09-13T18:47:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/78/79/91a5102797fe3dc992171382d8579bcb33cbd1424b864ad3117ac43fb3fe/ast_serialize-0.11.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00119a8fb8c1dc0f1fab023f4d8071fa49e3b0208ee54d589fd463c16ab0124e", size = 1356258, upload-time = "2026-09-13T18:47:37.984Z" }, + { url = "https://files.pythonhosted.org/packages/51/52/54eeef9918e187ced417c4363eecea66975314cd5b9c91759eef7f7b714b/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0de02520c11391a026e62987a9aa2c3c2ff01545155059ddf0c4bdf2c5ecbe9f", size = 1459057, upload-time = "2026-09-13T18:47:39.891Z" }, + { url = "https://files.pythonhosted.org/packages/e4/cb/fd84b52b15d42f2423319cffd1fb7f1e9df5d5198e69ab0b449c450254cf/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4e4558956b6a0fb35e18fba58f7d1810b1f2c0e6b52352572cd5dfb6b4ef33a", size = 1562447, upload-time = "2026-09-13T18:47:41.727Z" }, + { url = "https://files.pythonhosted.org/packages/be/92/9fb34f2e64b84a63cca92fb86bd0847b995a63b67477f44c20502fb60352/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6061a54f39e82a9f2cbcb9c268fc441890e4818a6636473caa4f4063254e0750", size = 1556423, upload-time = "2026-09-13T18:47:43.357Z" }, + { url = "https://files.pythonhosted.org/packages/75/0f/c43c44449e7ebc4e83ebd48750088fb06234622faa2d62d2a6dc8970d2a3/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:85fbb01e83967a126d71f679f2b9528ef0912cb0854aa1a4657314c34e255b57", size = 1687156, upload-time = "2026-09-13T18:47:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a7/9e520f4a79b639da9ee20c1e747c3d739329e902fc55ac38065f25419f56/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7aaaffc32905159774a107d3cf33dad59bd41b7a0d1bc9885532186753ee7439", size = 1481008, upload-time = "2026-09-13T18:47:46.602Z" }, + { url = "https://files.pythonhosted.org/packages/48/a8/bdd3989f19de09cffcd8179c131f6741a5a8619705fc75b09541ff61530b/ast_serialize-0.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:08eda88a0f290a36c38cab33df8bf7e35eb95bc802ca5beb2c8fcda471a7d10c", size = 1501597, upload-time = "2026-09-13T18:47:48.265Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8a/ca2dce2950875a4ef1d7c298196f803b0adcdb7c15ed0cecc71d84bccd70/ast_serialize-0.11.2-cp314-cp314t-win32.whl", hash = "sha256:76cc294246e60a914326b4ca88c6a5ea89c064906614aaf1537ce82f09e9449f", size = 1119503, upload-time = "2026-09-13T18:47:49.896Z" }, + { url = "https://files.pythonhosted.org/packages/5a/12/3f38e3613d07c46f9f81c5b1352748c6552397cc52825502e2c6ae44c6ea/ast_serialize-0.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:43b51e6ebe6549bf21416c3c78ee886147b80875a87cc6f69e303dde0d75be0b", size = 1156828, upload-time = "2026-09-13T18:47:51.454Z" }, + { url = "https://files.pythonhosted.org/packages/22/90/f89a4f67428a261daafdb69a0d0132c27933268702d1ba47e0b61c51aff1/ast_serialize-0.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:8df32ad4ff7843734a6c2f067ee974f6d3109ee5a2c3e1a9d2f79347bd282a9a", size = 1128298, upload-time = "2026-09-13T18:47:53.008Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/a1962188abf0e62d84d55892bb044347e434711763b9a1d4ad867a70c1be/ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:ab924ba260efd7509492f272d4e236d24564033f20c005d7c63a107c6a76fc85", size = 1235457, upload-time = "2026-09-13T18:47:54.554Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ad/439c2959150718446af76fbe2f4000f35eba9869ef8564f3d9a3d0b1c370/ast_serialize-0.11.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:a586be418eb70a9f1396cea29ddac8f4b9bf277fb73ea2340db31e218bc00f32", size = 1215705, upload-time = "2026-09-13T18:47:56.178Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/2c6542fc3e7c56a0a25d8d12d034d5a2d2e1900e292567b1c1dca8e83124/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8532f20916fa3189d4d785ef2a62d93c4d651ec9c5bffda66d2fc36898351f34", size = 1282530, upload-time = "2026-09-13T18:47:57.619Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ad/6f6755cd0842db46c3b10b1e4735f14aad78d71dea4753eb46933101711b/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ee732ae167e686d1d3c00f98d7d82b23138304694f0441b14d7ddf9c0f8a921c", size = 1287792, upload-time = "2026-09-13T18:47:59.227Z" }, + { url = "https://files.pythonhosted.org/packages/03/40/5da672f5dd23fb7dc0c884c97711e56a3540f2fe3c4355a81f8beb385911/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75a1c7f46b9c19fc0ae01ca6fd076301628faa2ed7a8edbd55c6353c483946a3", size = 1557971, upload-time = "2026-09-13T18:48:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c7/2bb25684f697801eb72866fdb94ed5edbff3867ce878b0e542a4a5b9dab9/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdf31a0bb85ea2575cc91669f005e6647d2efed491231c4dc1497bc9a5b3aa6", size = 1303230, upload-time = "2026-09-13T18:48:02.337Z" }, + { url = "https://files.pythonhosted.org/packages/d8/85/754681846f26e0ff1da729b1ffe3171e93c22f0aa6ec3cea5b14e3703846/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b78e6fdef3b06c86ed263e1962fee5a7b9d2d158e738b212d13b2c605ee12f5", size = 1302271, upload-time = "2026-09-13T18:48:03.915Z" }, + { url = "https://files.pythonhosted.org/packages/fb/dc/f5521d8cb44b69095c3982ae3658a12c403e0efa19e51aeb9c8a79dff60c/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:8d62a47714c8bc432b9fabcc29989c815c5da17327d35151f2fd0d85c2a7a5ff", size = 1309529, upload-time = "2026-09-13T18:48:05.562Z" }, + { url = "https://files.pythonhosted.org/packages/73/0d/649182c7fd7c4f782279bed514de2dd67e48a5afecb605a098d64fdc01fd/ast_serialize-0.11.2-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8a5ffa70e76191dcf240d3c43e20c93b3bfd26f54d89148c762d57837f5bcd2c", size = 1356869, upload-time = "2026-09-13T18:48:07.534Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cc/aff4d84c16afa742d13a75384127c7d24594dc8c304f0558a15924fd51af/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:bfbe47a3a7c368f28836e78b2440a3643ac0ec4c67d9fe53588e1448f0a3d35d", size = 1460006, upload-time = "2026-09-13T18:48:09.162Z" }, + { url = "https://files.pythonhosted.org/packages/94/a7/891cbec2e5e0d7159196159d3ff0646622f3120ff4576c839ac2dd56c719/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:7f1823275b246f9c7d373be6879e4eec09686948895d4ad083f4b27fd7e4da70", size = 1562935, upload-time = "2026-09-13T18:48:10.978Z" }, + { url = "https://files.pythonhosted.org/packages/45/c4/2c8c4498340ea9aff87a9fd408309aa25d56dd51d7bbddfdb46a3c31424a/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:57c0f5cb0021a5beb1e5e4d6e840ae2f23a28909703ef4d256a144cc1ad3d437", size = 1557109, upload-time = "2026-09-13T18:48:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/0d/8b/c5d4e5226fa18885fe17f949aee3ab1aeb8389c384d946ec1b7c9489cc94/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:cd320a5c4f1f2742af97eea22954f776379175c5ef2504801e9a155f2ff9a4d7", size = 1691603, upload-time = "2026-09-13T18:48:14.293Z" }, + { url = "https://files.pythonhosted.org/packages/73/d6/1d2ca472586f9e3416a289a22f36eeb6dd6f47d77b1a4aba358405babbc7/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:13b13afe32e845c86a573497729e1b7ddeb26c572c78bf50ece51da23b8fad5e", size = 1483053, upload-time = "2026-09-13T18:48:15.789Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/d3703a7c1e3c76b144ac9349a54d3926d0749918a8dc13a66cede208b8ec/ast_serialize-0.11.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:9d80a81ec84660422579bdb8e789f656a794b48c7a1ae1261f6bd8bc1897d17d", size = 1502499, upload-time = "2026-09-13T18:48:17.405Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e4/d974e55c2e247ef26ed1df01c74940583db9a5b3a8bcaad5732c6e2047fb/ast_serialize-0.11.2-cp315-abi3.abi3t-win32.whl", hash = "sha256:af8c003ce721b0099dd55cef4ba733500fc3054ea0cc8565d8957aaf7cccdeb4", size = 1119739, upload-time = "2026-09-13T18:48:19.005Z" }, + { url = "https://files.pythonhosted.org/packages/0d/00/d229443488e095054d5e0c0cc20689a2633b899d735849ff1b2c8e4f0cbf/ast_serialize-0.11.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:554d117cb916d8032d85007c654d179efbbfd446174c048062778136a922944f", size = 1158602, upload-time = "2026-09-13T18:48:20.524Z" }, + { url = "https://files.pythonhosted.org/packages/11/75/389fc1a6cfa0c4b2ce522f47d8401329d8bb11732e516d46465960fef1d9/ast_serialize-0.11.2-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:d60515335750d431e462af6e722bb55720a5e7827192777bddfd9c4376065a4d", size = 1128842, upload-time = "2026-09-13T18:48:22.052Z" }, + { url = "https://files.pythonhosted.org/packages/b1/54/f67120006fc73a55b6d057d4662d061fbb4eceafce3047c76ca8b382eb11/ast_serialize-0.11.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:daadf1c3e0224621607ffe16f1379e4bd372271ed2e1db8a67878f0bab3ef7e4", size = 1240734, upload-time = "2026-09-13T18:48:25.287Z" }, + { url = "https://files.pythonhosted.org/packages/9a/7e/8f2ab68bddbe58a66fbbaad87beeae3e7d7edddb17263d1fc423936cf34d/ast_serialize-0.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1844ed9a487fb3de7325c52ddb33f2918b66b65cd54d3f8d83d23785ffe99fa4", size = 1228053, upload-time = "2026-09-13T18:48:26.788Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1b/8a69ab68f4c1603819f0481d756abdd8caf27cec7f1d77caa71007ebe997/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b17869f4ba261a5fa468a753328a548f4dbaf74b4eadae9e28aff66df7f1425b", size = 1292542, upload-time = "2026-09-13T18:48:28.295Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ce/872f2e00f0467c289e483f0a34543463347243a2d0632748d89fcee5e0dc/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feb16d9c2a720e0120c58dd5d6e7b3c7c86b43249b60a3bc212bcb8fa031e2dd", size = 1294791, upload-time = "2026-09-13T18:48:29.969Z" }, + { url = "https://files.pythonhosted.org/packages/3a/82/36277c12af861c64b375c316135d8feffe3f400568463a8d2b2de4c2c4fb/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3109fe4805384effc8d0f8e41fbf875aa8f389af91b4348c1cfb60ea6e4cb82", size = 1567583, upload-time = "2026-09-13T18:48:31.85Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d7/ec643df91cea8bcbcb4e8011d6a8b08e5119b84f9554879f3e3c786d29d1/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abdb3e49ba053c3486ac1263bee9f16cc9a4a8abd9f8c90bfc21e3669f3ad9d1", size = 1312878, upload-time = "2026-09-13T18:48:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/04/6f/4c992cd7841ba589fefb14ddc9aff2f6db7f2a615d4074f9ad04115b5ce0/ast_serialize-0.11.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7004ba572f09be34342ccb98dcd4bad5707d3d81adc8cb4c3f685d2a2c51bbc", size = 1312642, upload-time = "2026-09-13T18:48:35.294Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/22aaa209c231a83cfea004fd67dee7a7a54da3f169c6c460b14b96887385/ast_serialize-0.11.2-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:59c25f47524efa052971b860e128b1add0c94ede7dd16b2962952c85c3582365", size = 1319776, upload-time = "2026-09-13T18:48:36.866Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f7/d4685fb54d10108ce44d3bc893ef670854d61645d47ed96d73524db90c23/ast_serialize-0.11.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f3a367e0e05ed2d1b747ceb07aa728a8c204cc008b589127e9bd4f40053d7575", size = 1365324, upload-time = "2026-09-13T18:48:38.412Z" }, + { url = "https://files.pythonhosted.org/packages/42/3a/250643ffad02bda520c50a9a5f02a5d43259a06f34ce393c91761d134d7e/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:00bbf1f6669f813b48925b759f7ae4591067d456d443924055cab386e7e0a719", size = 1467653, upload-time = "2026-09-13T18:48:40.348Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/af66a646b9b7f8fdec95ce83fc7b1fe538b06864bc79bd554ac4fae2e6ea/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ec1c20f89c3e0d83576e3c06f79375ce936266591fe0d5fd969914af3185cbaa", size = 1571914, upload-time = "2026-09-13T18:48:41.968Z" }, + { url = "https://files.pythonhosted.org/packages/34/82/77a9714564b9e8800087a8afec41527c65c39e49282baae2ac847b9c1c6a/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:c58bb119b73657fdc5569692f316e1e25ca114bd62f7782eb527c6be438ba3a9", size = 1569862, upload-time = "2026-09-13T18:48:43.701Z" }, + { url = "https://files.pythonhosted.org/packages/65/06/fa77b52f46b9bd6dcd8ff2b880e3781f8c1a316bb1342bc3de92907c6f96/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:f739e0b601be7300c5697a2573d9200bd1db74b34ab111ef9537b9d5dcd7f106", size = 1699020, upload-time = "2026-09-13T18:48:45.261Z" }, + { url = "https://files.pythonhosted.org/packages/e1/09/239c83153c7e0798e5867d6909cb06f53dccfef02f6999c8e2e21ecb98c3/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:cae5addfbb54cc1d47fe947ef9138e9d83849ed1cbc72b819cf36d96a2315b07", size = 1492869, upload-time = "2026-09-13T18:48:46.922Z" }, + { url = "https://files.pythonhosted.org/packages/2f/eb/6108fb9a43fc7ab5529856e38e33c6e3e064fbfe375fdcbb208c7cd5438d/ast_serialize-0.11.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2fa3be25f7f5351b1b39c9f8a52779b2dbf21199efbae564b4746422e8edca4e", size = 1511621, upload-time = "2026-09-13T18:48:48.667Z" }, + { url = "https://files.pythonhosted.org/packages/8a/82/60367e58ef346a41ebc90d3f28593c1b8f5c2cb5314c7b2bbd98910ee131/ast_serialize-0.11.2-cp39-abi3-win32.whl", hash = "sha256:d70556a2f9230a44c99a655774cde823f056efc34466eabfb4085f0cb1ea9f99", size = 1125873, upload-time = "2026-09-13T18:48:50.661Z" }, + { url = "https://files.pythonhosted.org/packages/23/bf/b419c3205ce1143ba7c69baef4f0ba43c14d8712113bf34f9e0d27d609be/ast_serialize-0.11.2-cp39-abi3-win_amd64.whl", hash = "sha256:b9065dd23131a23b41f5bab3bf4e9b3c350a3fe8e36e8200eded9b729fcea484", size = 1165434, upload-time = "2026-09-13T18:48:52.169Z" }, + { url = "https://files.pythonhosted.org/packages/91/a7/c8bbb2173f7a7131b3b2412035b2d814ab5ef2ce9799bd06f07c451640e4/ast_serialize-0.11.2-cp39-abi3-win_arm64.whl", hash = "sha256:dab599cbdcb7b45b18c41fad746645580b3a24357082b7f0e8921cd373804f27", size = 1136031, upload-time = "2026-09-13T18:48:54.04Z" }, ] [[package]] @@ -291,6 +355,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, + { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, + { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] [[package]] @@ -424,49 +504,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, ] [[package]] name = "boto3" -version = "1.43.85" +version = "1.43.95" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8d/db/adb561589806195a40e6f5015c884981595eed85ff43a887122165ffd3f4/boto3-1.43.85.tar.gz", hash = "sha256:113b6e1aa3f5722f90c01fc63968c269a9b1fd03ac2594fe16c56a66e6331c5f", size = 112656, upload-time = "2026-08-31T23:23:53.701Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/ed/9d4d4e7d4b874c16f7b3a886efe3e45801e0f9ebb60058adf1bb69cb2073/boto3-1.43.95.tar.gz", hash = "sha256:9d71f299111e1f4e8c28f573a1b7c0555fe40d2147fe9bef852a02bd57cbde60", size = 112666, upload-time = "2026-09-15T19:23:49.395Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/57/a58073f8659d7673b0b629987ab4295b56f44759c1b1f79b3c87330e452a/boto3-1.43.85-py3-none-any.whl", hash = "sha256:f11bdaca18e59f53ec0529f4d6203dd1f0bb7ff165e51559d62fd863024abc9b", size = 140028, upload-time = "2026-08-31T23:23:52.275Z" }, + { url = "https://files.pythonhosted.org/packages/66/6e/9c6fb58b1cdddf13b3e9edc94e0c6e08fff456355f6e097684d6117aa8d4/boto3-1.43.95-py3-none-any.whl", hash = "sha256:c906921c4f9ab41e9587af6586f072c8f2be6979e8bde2ec26aa443bb1745019", size = 140026, upload-time = "2026-09-15T19:23:47.563Z" }, ] [[package]] name = "botocore" -version = "1.43.85" +version = "1.43.95" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/93/a8b3acb06fcbb440704f195dbf14d12bb83c9c9d67b2e699076017f3d5c6/botocore-1.43.85.tar.gz", hash = "sha256:8fc0a3c56078c629320b021edadf7a45d289eea21a4988ada6a02277e5bbbdc0", size = 16040188, upload-time = "2026-08-31T23:23:48.929Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/2c/0031468b521eefed325a3da2d6576e068aab95837af0c96effea6438851c/botocore-1.43.95.tar.gz", hash = "sha256:779588da32bd48a7bb0c097da4bcb747260e86d2bfc507369dca56f1450e0722", size = 16106887, upload-time = "2026-09-15T19:23:44.23Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/4b/bb053aec2a9902df4cdbf9301dd5a41dd197c1cef0c085a32f5d04eeb3e3/botocore-1.43.85-py3-none-any.whl", hash = "sha256:685510e5f4c0f321806c815a60f121a176c0969665f053c4a336209cbe62b1d5", size = 15732946, upload-time = "2026-08-31T23:23:45.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/77/7ce7c937b31903f1402e478fdc97f98e727656fcea3cb54e74f1ca55793e/botocore-1.43.95-py3-none-any.whl", hash = "sha256:0fda26d16c7c7bf7082c421390a817b696f98a43d3da7c43dbbb093f76662876", size = 15800616, upload-time = "2026-09-15T19:23:41.228Z" }, ] [[package]] name = "build" -version = "1.6.0" +version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt' and sys_platform != 'darwin'" }, { name = "packaging" }, { name = "pyproject-hooks" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/b7/1db48a9ce2984842c8c886432ec8a2719613322e868a966ba82a28862f25/build-1.6.0.tar.gz", hash = "sha256:bd2c8afc603e7a2e0ce70e2ea85f0a6d02043bafbd307f5bada0f98669eca5af", size = 113825, upload-time = "2026-08-27T21:01:16.458Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/67/4898a44ea4f3f8e213b0954ec0aa0a16971d62a6212d6ea3931e97115b99/build-1.6.1.tar.gz", hash = "sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53", size = 113427, upload-time = "2026-09-10T07:56:00.417Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/e5/aa1e81b21aea0ce0ba435311837a37d4cb936e7461f9fecac08580073ba9/build-1.6.0-py3-none-any.whl", hash = "sha256:f7aaf1ebbb79178a02ba248bb524f2176b256017e17e8e4bd4289c7b38cc2bad", size = 31187, upload-time = "2026-08-27T21:01:14.957Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9b/9fb3585dabcd73a1b2a6267f63f62649347c9e6d072c9fde365b105abb2c/build-1.6.1-py3-none-any.whl", hash = "sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7", size = 31179, upload-time = "2026-09-10T07:55:59.148Z" }, ] [[package]] @@ -480,22 +565,32 @@ wheels = [ [[package]] name = "caio" -version = "0.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/c8/82b3c760141a1076408164b03e8789b51809add6aecd48aa9d7651cf6b59/caio-0.12.2.tar.gz", hash = "sha256:87a67c0dccc60e432888bd532ec504b66e124a5d8b391aab894583b55abd39ea", size = 80927, upload-time = "2026-08-04T14:43:33.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/bc/b62bf048a6e11870291a24319ed027bdf658df9ba77d1ad762aa138e066b/caio-0.12.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2097cc0d19fa95e8d55aad770597bb0f76e4f70ed48278c965aa7c5b0b8c3bf5", size = 84702, upload-time = "2026-08-04T14:43:03.946Z" }, - { url = "https://files.pythonhosted.org/packages/f7/be/b40d55d793afcfa5bcdb32ade9289d9588e14e3026c2c87522e303cc6e8c/caio-0.12.2-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:2122dccbd1959b922543fc9f8a9d2af47bd5b59190d1ece2445d3d1b4d1be45f", size = 198292, upload-time = "2026-08-04T14:43:05.238Z" }, - { url = "https://files.pythonhosted.org/packages/f8/02/9bd2bca72bfa478337618eae88942c43c891ae225e11baeae275e5e5c6ab/caio-0.12.2-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:107e56554c179749de9440e1b5e5a19813572eebf3166e9dc3e5228b16966beb", size = 196207, upload-time = "2026-08-04T14:43:06.494Z" }, - { url = "https://files.pythonhosted.org/packages/48/9b/65f95efdd68b50b7a9f2555c93d9edc7da7aa5ae5e153163c41cf6fd5cd9/caio-0.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adc7785e61ff7cf372318f67ec65617eaa06975e20da177522665dca8be6ea5d", size = 195748, upload-time = "2026-08-04T14:43:07.893Z" }, - { url = "https://files.pythonhosted.org/packages/3c/16/6a5c010ca435a5184d11ca350874694ac19db249560126dc8df0f25791ce/caio-0.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07942d3b5999127ecb96256c38d5dbf49ed2864c087ed2a80b783901d0aa3ba1", size = 195835, upload-time = "2026-08-04T14:43:09.19Z" }, - { url = "https://files.pythonhosted.org/packages/4f/9b/31f0b49a2542ffa2f9d6140267e2b568e722a1feeb05cfbffea97666c62b/caio-0.12.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:40ebea9ebe3a3a66ae85fa00d4112d163654a33c82dcf9b26a99f7d30de13317", size = 84656, upload-time = "2026-08-04T14:43:10.513Z" }, - { url = "https://files.pythonhosted.org/packages/99/bc/62568d688af9712a34fe3f958d7a98c53bb2017e263260cd5deae67a90e9/caio-0.12.2-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:6003ec389a68d5ec8f089df82b2dc8915293dd630a4d11322d7e3455045981fd", size = 198443, upload-time = "2026-08-04T14:43:11.767Z" }, - { url = "https://files.pythonhosted.org/packages/a3/e4/5ed627860285612e5307f06c109913c5918c947fbc223b55599e484c64b0/caio-0.12.2-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:eee9376d0e2af25b6defc5bce39f6efa90521c803aaf12eba931bd898a397cfc", size = 196356, upload-time = "2026-08-04T14:43:13.206Z" }, - { url = "https://files.pythonhosted.org/packages/81/e2/2a8cfc6ba3ef3f19e7c778e9fb6f98600f0971cca78bbdfc23a413a66349/caio-0.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:78e3ccafc98e009fcb00a97ad441585551e52c0ae7ecc50427a3ccd9b11502fd", size = 195893, upload-time = "2026-08-04T14:43:14.649Z" }, - { url = "https://files.pythonhosted.org/packages/d1/87/77c40fb2301d0b5bb27c2e79ae42fce718ed75396d5fe3e1c09d8e1400b1/caio-0.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f2355db8917f5a0f3638bf332fe0d87549c80e978fca01db84a8a14b9df56a05", size = 195969, upload-time = "2026-08-04T14:43:15.946Z" }, - { url = "https://files.pythonhosted.org/packages/5e/b5/0ceca97eb546fe6bbace3399c8b11dfc503efcc7509d708a7a3f09ab50e9/caio-0.12.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8054cba5e7ee623bea34946e2b59eb7c7c2be8872d0a5d12215d6ff564938d5f", size = 78621, upload-time = "2026-08-04T14:43:17.316Z" }, - { url = "https://files.pythonhosted.org/packages/61/8a/71b0144f783468ba9f1bbf8a2f8e45c7d85ae31ec192f10650aa46f31702/caio-0.12.2-py3-none-any.whl", hash = "sha256:5233e797c9fe2b541914b1bc2e2df82677e2206b537e44e252188f3c2cbb0ea9", size = 62548, upload-time = "2026-08-04T14:43:32.394Z" }, +version = "0.12.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/c9/ac301b7f86ccf6ad02ee78eb953ce8f75175754cc5e76d398e447af42e3e/caio-0.12.4.tar.gz", hash = "sha256:32d8e9f3e2099c8db29446679252766c9bcd806eb88b4fb60ad274f73df2a5e9", size = 81006, upload-time = "2026-09-07T06:52:39.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/35/7ddc8de48cd0142797db186c07e901005b966dc6e8a7852f0273d33e4840/caio-0.12.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:67b725641fea682a2e9b1d3b2a150d21f1a25383e3ec8351bd7677ff857fa982", size = 47927, upload-time = "2026-09-07T06:52:13.58Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b4/37556bde83253cf1ed071a754c8405d02b955febd021c400f0ffa6258d5f/caio-0.12.4-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4854a029e359e5ddfb5589ab66157a34af6a0dda0acdaa43f5e640d60182c1ea", size = 161402, upload-time = "2026-09-07T06:52:14.581Z" }, + { url = "https://files.pythonhosted.org/packages/7a/11/519e40a1da4d18515ab5ddb54e75cdd896532339010f2dcee7e798e4edab/caio-0.12.4-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:f063624a98a64bab387430c5eaea61ef3825a98399104a297b8da4741c15139f", size = 159311, upload-time = "2026-09-07T06:52:15.853Z" }, + { url = "https://files.pythonhosted.org/packages/45/2a/0db30de6c2561e41721d94ac627aa65db61a403545e67914814d8c2b5305/caio-0.12.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a926d562f8c06767774a91239770ebaa93c10a24074e21ea741db208df9cc1d", size = 158910, upload-time = "2026-09-07T06:52:17.27Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e0/5eb43a89aecdf9d6f929b0080e49d92ccdf3f88bbc637353f52843b483af/caio-0.12.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53605636e3b1eaeca368b475f1471ad9774736c6502a4b58b8b6b8d74b531770", size = 158973, upload-time = "2026-09-07T06:52:18.527Z" }, + { url = "https://files.pythonhosted.org/packages/11/ab/12e4896a1e74ff9a65c84894acdfc3b70b8b49e63874a37e0dffcc31f396/caio-0.12.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:134d9d145d75f454de5ece9e87595bad433639b51061b693bafc369f689f8742", size = 47934, upload-time = "2026-09-07T06:52:19.644Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d4/3a96f5537e3fa4e3256f93244fe883a5a4879f2b9e2d59d50151bb417f37/caio-0.12.4-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:27ec5671ac05650abc7ac1fb12e95ad09417ae495ce9be565927786688edd6c5", size = 161559, upload-time = "2026-09-07T06:52:20.637Z" }, + { url = "https://files.pythonhosted.org/packages/05/64/f33b9aaf7bc9bad988211d8e42b4b85c25a1d5dcc0906798aebe49bd421b/caio-0.12.4-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:1e1d5570fd0ec75b1b2c2877c5545ed9f0738b5d23b000e2b6a8fc830dc8b310", size = 159453, upload-time = "2026-09-07T06:52:22.011Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/3fc5e143728d99f8af11453789d755937fbe795a6c5c9c9ea529c91df519/caio-0.12.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e22ce2e69d94e4c80e0c11c871e547b3c2d147f977632894cb2527ddb6e87a8d", size = 159028, upload-time = "2026-09-07T06:52:23.225Z" }, + { url = "https://files.pythonhosted.org/packages/ca/8d/e7a165aa44bb2876bec96f7fb1995346d14e43d46237421382f8af4f37da/caio-0.12.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9dc0fd6f09ef72d18d3f43ca3d1140295222d925c583114ab9b1e8843a109a6e", size = 159088, upload-time = "2026-09-07T06:52:24.362Z" }, + { url = "https://files.pythonhosted.org/packages/6f/31/d31ed073a7f8e02d352b390ff556757ba82afbd747c68238b1bda638ce6f/caio-0.12.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:413565d77dfdf2dd841ac100571ef1cc710f9c56137312f3ec51356350b4f3a5", size = 41919, upload-time = "2026-09-07T06:52:25.5Z" }, + { url = "https://files.pythonhosted.org/packages/6d/07/ab0bc8a481126e6b4f6f56f3bb94e41568fc4604df15420616251b2e28da/caio-0.12.4-cp314-cp314-macosx_26_0_arm64.whl", hash = "sha256:cd1d00ed1867a3b7a4cffb64b1bd8644de4b6f88cb25240cf82a9cc160ace975", size = 41131, upload-time = "2026-09-07T06:52:26.512Z" }, + { url = "https://files.pythonhosted.org/packages/24/3b/49382d9f7b3f65c76bb63a18a657833c4bfa695555b9663139950dec3c11/caio-0.12.4-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:4189104e5579553031340a677762398c130b81ddb7d7c9b69eebffddaecc4f58", size = 162063, upload-time = "2026-09-07T06:52:27.717Z" }, + { url = "https://files.pythonhosted.org/packages/14/7c/eaf41fb7e0c864c3a5ce4c259eba0f505633efe4f789fffb6d3f1eb7e9e1/caio-0.12.4-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:4f2e0b6d393dc0ea78cfd13a9a4321fc35fd6a2db802e5f64651317e859fc929", size = 159514, upload-time = "2026-09-07T06:52:28.914Z" }, + { url = "https://files.pythonhosted.org/packages/41/7e/4041947d8caa760b74a27c8082844c72974da7cbfb96737990b8c6749e53/caio-0.12.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:710ffe4a6f3d69dbfe98e9f856c24a544e4d639c7bd210f9f5503a26e62502f4", size = 159753, upload-time = "2026-09-07T06:52:30.081Z" }, + { url = "https://files.pythonhosted.org/packages/32/76/b6c524a51eca13b861e8f98c927b933fdf5766f0c7c9017a21d889a84e3d/caio-0.12.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddf8d946b795ebd7c75b331b6dcbab4808fdd2c9f16ca76012b4757512861133", size = 159167, upload-time = "2026-09-07T06:52:31.288Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f4/c39ed874d420bb2719d52932d763953c28371c536d6bf264de1febeb27d5/caio-0.12.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:17a41de51203fc4d787d8d577bd56f169167aa9822f14fada015504688791aa4", size = 41920, upload-time = "2026-09-07T06:52:32.432Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/58f4ba2ac9ba69bd30e5c89493d34aa483743f823f79a854f20771fb80ca/caio-0.12.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ee9de68d6ede7ed69e55a1c09073ce146d9f9470088796d55238c485a93fe203", size = 183327, upload-time = "2026-09-07T06:52:33.492Z" }, + { url = "https://files.pythonhosted.org/packages/25/81/6b86722be50975b80a636afebed5af1a6e6588afeff0ae1a0a7f8c262707/caio-0.12.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:1c1a387f8784a86a56a8912ca42e1ad67599f16406f497b5b655b461a1b89426", size = 179368, upload-time = "2026-09-07T06:52:34.66Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a3/19ab810f11d1688bb2009e669b2645410a89b7a35b608239ac4076dc1611/caio-0.12.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ceee64265a55b9aba9c38c1d01a159b99f1e36eb56938022018199d2e7fe1742", size = 181460, upload-time = "2026-09-07T06:52:35.841Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b2/e54e6e445d600e22b107e4005714cad0657bc7dc19d200817ab7a62ac722/caio-0.12.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:456a93868ff007cea65d916d96d192cda8b9992c6eee046cb0159040f9446ab5", size = 179230, upload-time = "2026-09-07T06:52:36.978Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e6/07100a694613344d22958fb6d803fdf914ee9395278fe58ad1b1b9c59e52/caio-0.12.4-py3-none-any.whl", hash = "sha256:7a5e231bbf81eaed269f99afa9ef46457f51f9407952a1795c0795b3a62c23c0", size = 25628, upload-time = "2026-09-07T06:52:38.147Z" }, ] [[package]] @@ -542,6 +637,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, ] [[package]] @@ -562,6 +681,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/eb/93e8036681157f2217a18769927a035984526e6dbd5e91f28a375ca41c14/chardet-7.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b5d31f9b7f793e15e81cca877e7ccd72bffffa2a3443a9d47be9dfee84fad69", size = 1514185, upload-time = "2026-08-14T20:36:39.119Z" }, { url = "https://files.pythonhosted.org/packages/10/04/0066d7ab2c135e404a6fa166bb7fa49d1c7bf7af07b86ed95b1c48a348c7/chardet-7.6.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c54b6a8d3b219560fa5cf4c28df932c37471afe047afdc152067104e741f38c1", size = 1454420, upload-time = "2026-08-14T20:36:40.549Z" }, { url = "https://files.pythonhosted.org/packages/6c/9f/e965f1d9eddfb86cf118f980d329cbee43c4ac4b562448b369a6b6ef36af/chardet-7.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:b3b4c96c4df93899b3c8b9e8159e06b1f55c66d7ca384d91481108e251a06eb0", size = 1159067, upload-time = "2026-08-14T20:36:41.816Z" }, + { url = "https://files.pythonhosted.org/packages/cf/78/e14991c9487277ed7d006fff58f3084ac792ed94cced081788abb95df70c/chardet-7.6.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6061adf247ab5dda173b67010e13904c6071717660c7c8077fb50aca362b264", size = 1087678, upload-time = "2026-08-14T20:36:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/0f95e04cb1820e0a582cd6d86bbf26be8302a94ccf330f8ba5f69735389d/chardet-7.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fc1e1571321baf8927582fe34363ad7f02279f11c8c2839c14b4c76894148db6", size = 1065310, upload-time = "2026-08-14T20:36:44.489Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a1/04404dcc9d6e1253b02583e562d2c7ddca50a7e91f460d62eff7e1d07c92/chardet-7.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8900f6c7cf6b015b17a51767cc6144689059ba1cdceaa383d29eb037ac28579e", size = 1485028, upload-time = "2026-08-14T20:36:45.797Z" }, + { url = "https://files.pythonhosted.org/packages/f4/88/360064c4c7d9d0664561dae03b74c871d2f5332b329f5c99f1c997fb869a/chardet-7.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cedbc584789eb2edfde20fd03669972a833ce6019e60014ae613f9bfc440e8e3", size = 1510242, upload-time = "2026-08-14T20:36:47.206Z" }, + { url = "https://files.pythonhosted.org/packages/43/4c/302869fa1c69a5a41e4e78782680b12552ffd4286c3ae3c839b7b8df53d4/chardet-7.6.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5dc835e40e0e09c2c3eab43731a8b5127834f42786dda09ba2f4b699ccd527a", size = 1456456, upload-time = "2026-08-14T20:36:48.668Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a9/ff4fef15ed25fc3f945a3b981ae0f43c8559b3fbedb40267e59e583d105b/chardet-7.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:0f304de7041afaec0195ad6464937cd112392002e9d72ed15d55f20a9abd3a13", size = 1159103, upload-time = "2026-08-14T20:36:50.107Z" }, + { url = "https://files.pythonhosted.org/packages/31/44/94e6f89485ce6c630e8fec6d388e3fa747e58b7246b0cc8c5c532ba98650/chardet-7.6.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a4f0a368ad04d5def08bdfaa17c7e15e71552f93923dc2aa9b2f7d9dee02fbb6", size = 1120302, upload-time = "2026-08-14T20:36:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/e0/cd/8e68ba12f13aaf4ee82d54ef8a1f83d62942e7a6bc1e579dd2d530a3e26e/chardet-7.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:75d6c3a4d2046d49e83d2d2206eb073a1f390743e856d90c1bbc19949b26acf4", size = 1093157, upload-time = "2026-08-14T20:36:52.987Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f6/6a35342b9efa69dcceab0ea8966571c6442a59c336bf460f0a2af95ed234/chardet-7.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:459e2b1c98f9a86a4698112aa42dffa802bbbff883c1ff144071f87224125862", size = 1521213, upload-time = "2026-08-14T20:36:54.812Z" }, + { url = "https://files.pythonhosted.org/packages/df/8c/8f09bfabbaedae45caa72b996e96d5e3f6436dfddc55c1311ffa2f6bd7d2/chardet-7.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bdb6f03107b7ace3f44e0edd91aa24456ee558787df265cc19daf45785b31c7", size = 1528427, upload-time = "2026-08-14T20:36:56.224Z" }, { url = "https://files.pythonhosted.org/packages/cf/6e/5a0b348fa4cd7847567a28c6e697ccf58391960bfd13a6e7473ee23ca2f2/chardet-7.6.0-py3-none-any.whl", hash = "sha256:4076d795897ce45239825956a1334e134322ecc4bfe84dbb12acd5390de0fbc1", size = 680279, upload-time = "2026-08-14T20:36:57.763Z" }, ] @@ -608,6 +737,43 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, @@ -665,41 +831,71 @@ wheels = [ [[package]] name = "coverage" -version = "7.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/f5/deb1a27aa20746c0278ac998c4179e272004699b2d33959ce020c5ac1615/coverage-7.16.0.tar.gz", hash = "sha256:077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd", size = 945620, upload-time = "2026-08-28T21:54:37.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/9c/8d2688694f53dc0b0f0e4783c7eb3c4bb1e79beaf1411879f6dabedf4607/coverage-7.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1c77c3579ac42798f8b7eed6d3dd258debacca32c8753fc8a1f6eaf1db644f5", size = 223194, upload-time = "2026-08-28T21:51:27.767Z" }, - { url = "https://files.pythonhosted.org/packages/ca/11/f002163dd688aa3fa49ac6a424b7c2705c7fcf80fba18ec9f586d77827ca/coverage-7.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f81cb1554c3712e41649ed5dc98656b50b958e4da12f0f5adb681ce3db92831", size = 223553, upload-time = "2026-08-28T21:51:29.46Z" }, - { url = "https://files.pythonhosted.org/packages/81/65/f9d469e97c4554372a710650a109004a2434dfc56f577142e5d6057fa0cc/coverage-7.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e701938ec9081d3e400a0c9a9a8ae0f7ca44214741daeac4454b1c6ef6dbd19", size = 255054, upload-time = "2026-08-28T21:51:31.54Z" }, - { url = "https://files.pythonhosted.org/packages/95/29/dd89fd39af1a3b6e9a9c3eddeaf03f6376ba517d43d6cbf8b519177e2a10/coverage-7.16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:719a3feb6220dd32ed932d4c3676d17fb8739e2643b29c0e7c3af400ff80ac44", size = 257790, upload-time = "2026-08-28T21:51:33.374Z" }, - { url = "https://files.pythonhosted.org/packages/0a/64/208d26cedc525d6b5db9c492cf9130784c42d9eb08d22badaa7b806005ad/coverage-7.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87771ecf986cff55e87413238cd5e4f54d949c2074bd6fc1657d26a56314ee24", size = 258904, upload-time = "2026-08-28T21:51:35.096Z" }, - { url = "https://files.pythonhosted.org/packages/1f/98/28e2752aa9a8baee5798edade9c95602ca200f4e7eeb503eb64df42e5921/coverage-7.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47d5e1fc0b321c8308a2aacee0497c435b08acaa629b7059798fdf6fc3006352", size = 261165, upload-time = "2026-08-28T21:51:36.744Z" }, - { url = "https://files.pythonhosted.org/packages/eb/77/fa6ae699a0ea2bc12acb38a85d96b786fea0f833c12b5756056350e0e547/coverage-7.16.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01b18b8a6c9cec8d5f45550e2501426ed982cf2c35016b0acd2ba9b5d8b2fb06", size = 255416, upload-time = "2026-08-28T21:51:38.495Z" }, - { url = "https://files.pythonhosted.org/packages/89/c8/5ee46d1de7d34cb00ba08b5c50da1971114dbc09ca9898ccc32975ec74dd/coverage-7.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:32c56b5b47c50635081445ac404dd08c2d591b9c837c22570aa9e182c3b42cd4", size = 256825, upload-time = "2026-08-28T21:51:40.27Z" }, - { url = "https://files.pythonhosted.org/packages/15/f6/d59e1c0693ad48855fe20169fbf6ee5befefe5887a7fabf5f0bcb464a2dc/coverage-7.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ad3bbad240ab937512156bc944fdee63ac4dd34a7558a3094548fd4c1150c02", size = 254970, upload-time = "2026-08-28T21:51:43.136Z" }, - { url = "https://files.pythonhosted.org/packages/df/7b/b51bbe05b3a7565927fccfb1be42b8b3c1f4ab15e53d91b303e9923969aa/coverage-7.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c1f16d5555a195295d0dc9c902612270e3dfed6a11f3bf7bc470b7b6a79ed3c", size = 259039, upload-time = "2026-08-28T21:51:44.983Z" }, - { url = "https://files.pythonhosted.org/packages/fa/04/d513f816456a8a43c1859abe88a37d01d7d2515b6c3e24ebb3c9b1dd44ec/coverage-7.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f6c9c21a8bf0d19788f3c5f3e020c90317a0a63ef60521b376003801e21250fb", size = 254539, upload-time = "2026-08-28T21:51:46.733Z" }, - { url = "https://files.pythonhosted.org/packages/dc/54/5542190ceb97e0d1333a4ce0c8f95b2ef2efe790f1ad018a4b61766f849e/coverage-7.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f20145a9eb5bf1fd1dde3c0bc2af2e7c22135ab07ca6284d6ada7cc3904c4e", size = 256410, upload-time = "2026-08-28T21:51:48.363Z" }, - { url = "https://files.pythonhosted.org/packages/ee/28/78643f361ff6bb5b2ade90f8bfc8395fe9ca367a18c101f8991215b4c65b/coverage-7.16.0-cp312-cp312-win32.whl", hash = "sha256:916cf8d25c1ce148f7eceb1d45afc9724841200110adc4e53250391852debd91", size = 225239, upload-time = "2026-08-28T21:51:50.22Z" }, - { url = "https://files.pythonhosted.org/packages/67/61/8e76b36c36b1a033dc933dd2480db96b04ce3be975793ce3fad122e7174d/coverage-7.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:78f8b56261d608be102c62edd3a60b66bcd0b581f3f86fdcabaf8b8d95adc950", size = 225775, upload-time = "2026-08-28T21:51:51.912Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f3/bb4787a4b81c1792ca69b502f5f730dbbb609f73fed552ab074c6b92cb8b/coverage-7.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:577c2ac8c0036f6f8edd3a7783a9e67302b17771d1abf0fd2ed246e3158be51b", size = 225159, upload-time = "2026-08-28T21:51:53.667Z" }, - { url = "https://files.pythonhosted.org/packages/54/c5/e62c87f4799d1e3647d5b2ae16ea1d12205d72fde1ea8529e13fe050f678/coverage-7.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1545c52ce756b8a97007f439a220297f1cd72a2cbbcdffccdf1c1f70e74f9a42", size = 223215, upload-time = "2026-08-28T21:51:55.628Z" }, - { url = "https://files.pythonhosted.org/packages/89/e9/5e62fda9397175fb206f75368b6e85da06d831c181b6d0f67ca073cd2f89/coverage-7.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0598aadae641f30a0796b75b45c0b9c5de8619bd5cfb251bb0cc254e86e6dd13", size = 223585, upload-time = "2026-08-28T21:51:57.355Z" }, - { url = "https://files.pythonhosted.org/packages/b9/40/bede08621b1ba67e88c4d3336c22b52cb7911ff1fa4ef055344b6670e58a/coverage-7.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4080ad6bad9f14690e6b2104f5e8d137ccc65a4b5427a36662090637d4bd16d5", size = 254575, upload-time = "2026-08-28T21:51:59.233Z" }, - { url = "https://files.pythonhosted.org/packages/12/d8/ab0bdaa45dfd6b8cbf1a3ec548fdf827684b1997f9724375c5b3e89144fb/coverage-7.16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9883a2f8206ce3af59117dc278e5d043fea06912bca3f199816129e5e2de354", size = 257172, upload-time = "2026-08-28T21:52:01.015Z" }, - { url = "https://files.pythonhosted.org/packages/1d/bb/135de81784bbd7dfedcab2b92b03d71d75b09b0815b42d6dabb052def5a6/coverage-7.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984e5430fc6f858385009e92549955157d79335b1f3e13e1031e0f89d1284261", size = 258410, upload-time = "2026-08-28T21:52:02.76Z" }, - { url = "https://files.pythonhosted.org/packages/ad/72/ce44ecc062fb2e43d9447bb76154d091c2139232f20c125297c4b58f4c6a/coverage-7.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1374099dd1ad0d31fbb6c95d00a56a3c5e85fb3343dca14fc12f78323a2b42a", size = 260539, upload-time = "2026-08-28T21:52:04.821Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/9389c36a41e59406ca2bba493807c2294d2e5186a7e9ebcc2e63a0f2a711/coverage-7.16.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d8686bce035c8465b318a8c2890e69ba14a00801a27f4eb6bdc97c23944d87", size = 254756, upload-time = "2026-08-28T21:52:06.68Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0f/7762447b15e01fb84263608540123c4d9941f06303265ee74d801ccbec0e/coverage-7.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:857fceba6ff4b507ee0ad98798a33d544a8473df0c542bf04251ee4ed5ee6292", size = 256540, upload-time = "2026-08-28T21:52:08.529Z" }, - { url = "https://files.pythonhosted.org/packages/e6/fa/c60dc75a8346c1dbebebc7279b19971c88f70dd575f0bc10bc0cb16f92d5/coverage-7.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bbf08d951abaa1ce89e28c998361d56b952413846b459cd017f116ad4c9adbfa", size = 254508, upload-time = "2026-08-28T21:52:10.323Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/4e0834f3a1fccaa8bf625a2a1d73bde0fa32577dc3249853c0dd0e7f2b20/coverage-7.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1a03e78f53e4d2ab13adac19958a89322d1829913e5623d642627bf60b35da21", size = 258659, upload-time = "2026-08-28T21:52:12.124Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ec/fe712d3a11fd6e874565a5fa5497c48b8ece561d9611da040b44cdcf8386/coverage-7.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dcd3dafcdd78305d27c59a1006b53a4990acb89e68d8fbe0992f4f83503c827f", size = 254326, upload-time = "2026-08-28T21:52:14.181Z" }, - { url = "https://files.pythonhosted.org/packages/e7/78/093e12072e01034c65ff380f76c74b79dd83e44fa92b689a2154389be734/coverage-7.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1bcfe470a796fbea6234accd81d258a31574dc0b7bf569e16be757572c4de17", size = 256102, upload-time = "2026-08-28T21:52:16.003Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c0/265176117ca5d06e3f65575842884cdda96cf213350a31e9d41c80d65854/coverage-7.16.0-cp313-cp313-win32.whl", hash = "sha256:1420370276f1694b663207b8245c3628aafb9624fe3cebf313a13d860e55ee67", size = 225250, upload-time = "2026-08-28T21:52:17.82Z" }, - { url = "https://files.pythonhosted.org/packages/1f/01/8a87f2c04fde322430b45d16d8f543693e9894c5b2d2ca238a287c00beca/coverage-7.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:496277c8d7beed695e02c7be53516a0152e4caef8738a0feab6a638546cce449", size = 225790, upload-time = "2026-08-28T21:52:19.641Z" }, - { url = "https://files.pythonhosted.org/packages/23/40/c21feacd9edfe7063195bf9cc84d650e9938fc6a23063e4f027199b160e1/coverage-7.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:181c2906b9b3759955c1c33c51fbb91c754fbd0b82ea49e2c81061f5a052082c", size = 225180, upload-time = "2026-08-28T21:52:21.613Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5a/234e8fadf85c3cc48cb31c247b9e8e0c7f06ece80f5b29f9b8c241f9da4c/coverage-7.16.0-py3-none-any.whl", hash = "sha256:245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73", size = 214977, upload-time = "2026-08-28T21:54:35.189Z" }, +version = "7.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/2d/c738872f477f5687152acae68635790387425d407ae37dd3d3a8a6692307/coverage-7.16.1.tar.gz", hash = "sha256:f83981779bcf9dfa06fa0a8d4cb43e0faec1706328ce07aa3e7b665b4ac0f210", size = 969651, upload-time = "2026-09-13T19:12:21.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/f7/7cd4c9f2a3b7574414222ec425d5eee21cc690d867a013315e3be8ba185c/coverage-7.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b7f2c26ce6ce0b1e0ca0d5fae96ea510e3a2e78b7207f06e76b7f2c87fa3d0af", size = 223475, upload-time = "2026-09-13T19:09:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f2/9ac65f9cedd43f91e2d3657c11f13aef82aebae89b2243dc9e44fe58a740/coverage-7.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070acb9da788dff743a4d36fc015feee12d68f0349959017542017c79f59c21c", size = 223845, upload-time = "2026-09-13T19:09:11.988Z" }, + { url = "https://files.pythonhosted.org/packages/62/4d/f13db452d4367fe1122abfd98ae330596f65a3b40498f8e1a5811f68f123/coverage-7.16.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e366587b370bc9b8b51b7b7272c610c56db5d5b4795b9e4a29d28ff2f440f809", size = 255341, upload-time = "2026-09-13T19:09:13.708Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6b/85ed86e82835a96ddfbe7705c387c28ce1cbbecd1b6d606f5ddfeeb712df/coverage-7.16.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e82e10b9d290f60b63459cfb245a841aec347603997206296b93881463a93dcf", size = 258078, upload-time = "2026-09-13T19:09:15.368Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/f66945853d850b9c05f4e012e37396f1f31d0cd40d062394b229c22e7b56/coverage-7.16.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d73bb1f85c4150ac208fb0755beb04b2e44897bad81414de9380f98dd74729f", size = 259191, upload-time = "2026-09-13T19:09:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/7b907abe62461f289035c7ac60ab3e6334efb8c425151aac4abfa0c97820/coverage-7.16.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3397b9032553d281ad6a9253b12675b65e0cc8cd7a3b0633cf48872c9eb13360", size = 261452, upload-time = "2026-09-13T19:09:18.756Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/e26f4d6b1069a2a2fee3238a132f85c5b4c1dc25aebd88e015451ff0bd68/coverage-7.16.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77890395cf37026a5907d3ad32376aa51f41c0f163b7477fdbd4f94966cc1d08", size = 255698, upload-time = "2026-09-13T19:09:20.786Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ec/99db7450e813050ebce300e31bbd4f997c76a8c6e3a6d168c5dffc104109/coverage-7.16.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b0944dc3bee3091039bf970d73caaf930c906013128a421bdc132e797494d941", size = 257111, upload-time = "2026-09-13T19:09:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9d/500df9d3cd8c541ac84b5e0bcab00646be75654e05aa34e8410ec851282d/coverage-7.16.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b89d22a89d5bc05dd95b64e08295b8394aa96dc88e08f8ba210c9ebfebbe0489", size = 255258, upload-time = "2026-09-13T19:09:24.429Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/53318d96da332bb4946f8ac646fa19fd37fecbd0c49144735c716ac69bf0/coverage-7.16.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:550a2a1faf7559f13d5344f12d1eb886ad87955155d7dfab2a3fe5c8ec8fe776", size = 259326, upload-time = "2026-09-13T19:09:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ce/abc0462b2e6ae96ae22197d2bc30fe33b6b4e52937e782745436ceb2a761/coverage-7.16.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:55eb268e5b81aefac759766c9162625b06c1bedb7b77d936225bafc4f038a6f6", size = 254827, upload-time = "2026-09-13T19:09:28.191Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e1/0a04eedaaf19196b51f0180968134228c7646e469ed29914e48690a7cf3e/coverage-7.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65a8fc80898c9ce59f04349fe8b4849b1f9787f14e52ead990e5f849ff4727a0", size = 256698, upload-time = "2026-09-13T19:09:29.922Z" }, + { url = "https://files.pythonhosted.org/packages/de/1d/d441a55cf22ce9d8e9e34814806c47441ab844bd534e0f4b64e1c6ae8bd1/coverage-7.16.1-cp312-cp312-win32.whl", hash = "sha256:528a61be40977c340cf201d23b69bd6a6bab507da60e9dbda85f8b30e935d70d", size = 225541, upload-time = "2026-09-13T19:09:31.752Z" }, + { url = "https://files.pythonhosted.org/packages/73/27/ec3d032375735dd331477caa051419678079ff90fcb53d0284a6c2bfb757/coverage-7.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:d0f02c633630e2b74522108ee95a84ad6e1204a8016a6cca5297f335ea27147e", size = 226075, upload-time = "2026-09-13T19:09:33.556Z" }, + { url = "https://files.pythonhosted.org/packages/94/00/90e9f5c4434878494306b9c0ee8068ebbd829483737d8cefccc58884d728/coverage-7.16.1-cp312-cp312-win_arm64.whl", hash = "sha256:2959978f9d1d20a2c0c15d0a68baaeccf615ac1aa214cf4a05a10d6f568926c8", size = 225461, upload-time = "2026-09-13T19:09:35.48Z" }, + { url = "https://files.pythonhosted.org/packages/aa/74/c08c0c4dc9fa6bcd1d90728a63660aa1b17b488a806948598456c48f75d1/coverage-7.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ee5465db6e9152a7d09f3215309326878c6aa3ac509195a369f9d264ff4bfbd9", size = 223501, upload-time = "2026-09-13T19:09:37.276Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c8/784986d326663285258a8e39835c46fb730cc85284f0dbcd82078586dd22/coverage-7.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b8256f8b525ba233d2e4cdcdce0d6673c66fc9bf70df1fd5e67c54a74e2d245", size = 223876, upload-time = "2026-09-13T19:09:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/15/76/73fb792928872bbb07e553f920ff55c65ee962c469265feb5d1d4ae5f97a/coverage-7.16.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d57cc400275b9a2892e905fc893f732b21ddb95271bf96406c88e2f6367848b5", size = 254863, upload-time = "2026-09-13T19:09:41.326Z" }, + { url = "https://files.pythonhosted.org/packages/ca/8d/1fd78899513244b065ccecdd1cfc6aa8b8f1bdee796d8c4f2aa8e8e5397d/coverage-7.16.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b3fd0f3435ebb7a7183b32a6062a8b755f08242ced1f3f22761d30b56b3c2a5", size = 257460, upload-time = "2026-09-13T19:09:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/47/1c/b23ddfcb7ef9bd7b3fdb7be9a5dccf9925ae88e556df7aa5b655283c78f2/coverage-7.16.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5eb1762e7eb5fad34ef913e8107c7788a66f19d328e598ce95bf7217f9e5c8f", size = 258697, upload-time = "2026-09-13T19:09:45.161Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/19d0c6ca35778a7e9415c30c46a0c64c9d4374219d004a35563132296896/coverage-7.16.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:46cd3a73e9140410de62cceb66214bce0e08fb3922b9176fbfc1522fec151b41", size = 260827, upload-time = "2026-09-13T19:09:47.061Z" }, + { url = "https://files.pythonhosted.org/packages/97/fc/5862f7344382c62b85df43d483e579168cc062e007f54d895dfa51fe3e7d/coverage-7.16.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d1ba5142d68dd2cb775cbd0ac8601819298152047803c8efe4eec6d7d7aa7878", size = 255038, upload-time = "2026-09-13T19:09:48.945Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9f/5c26583199a68df8d15124b4590520903f8eb7cef17db293fea712bb0783/coverage-7.16.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c389c6f9d1d518e1249ddcb8a7f158135644ce2c508fa6cc17b680777dad5bf2", size = 256827, upload-time = "2026-09-13T19:09:50.738Z" }, + { url = "https://files.pythonhosted.org/packages/95/13/605198bff079b107336710f28a797312ab132587168600d70a290e7ecd2e/coverage-7.16.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cdc57746c7ac0ea063351b4d651c3bb4dd4fd35e64dbb8e90c10e14eb03c4080", size = 254794, upload-time = "2026-09-13T19:09:52.577Z" }, + { url = "https://files.pythonhosted.org/packages/43/b7/0bbb32dc5ccdac766a13763fdfbffd7d73fc80349a69230c46c6b71e7508/coverage-7.16.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5597180ed7670cc94c04c65347418a467d3a43d5f0cf52fcac647f5425f42037", size = 258947, upload-time = "2026-09-13T19:09:54.372Z" }, + { url = "https://files.pythonhosted.org/packages/13/bd/65c31ddd43ff4e61b63721b3dad17cca412dba6e2ce89f92abdf75734339/coverage-7.16.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9647a0ac46255b8fef59a433a2161f03e5483f3a35e1cbd9dfe4600baff0c6b", size = 254613, upload-time = "2026-09-13T19:09:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/25/20/d278115f2ba522b3e3128c6852be265f4e0df7202b2effca4db96cf0217d/coverage-7.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e306e98186b9cd109121f3583aeb7978797ad21d948f22944c5c08845cd554d0", size = 256388, upload-time = "2026-09-13T19:09:58.095Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ca/d43aa396fb3a2f71c9411b99d926475f5acaf5c124f605c592865c80c8d5/coverage-7.16.1-cp313-cp313-win32.whl", hash = "sha256:48a78a66fcce49d7f6156524bf979c0ac633d584199717c68c6ffa949fc14e6a", size = 225550, upload-time = "2026-09-13T19:09:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/df2f6114f6a17cffe94721bd1d298f77f86aad493717fa5bc03cb291a1a6/coverage-7.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:7af03247d598a353bbbbe1b925deb735276e4d845e7197c4073dc89352b236fa", size = 226091, upload-time = "2026-09-13T19:10:02.331Z" }, + { url = "https://files.pythonhosted.org/packages/ee/31/6f90d8aab72a112492bd50e3b5485b53b772b1f5fa70da0a620e723caae6/coverage-7.16.1-cp313-cp313-win_arm64.whl", hash = "sha256:166adae25b05b04c9a84135912066d9c97482115af38df1a419a38aacc6b6f5d", size = 225481, upload-time = "2026-09-13T19:10:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b4/2a7c793965bae9f067aabab793a44d7a2f3ee7fb16b01ce1976bbd4a0218/coverage-7.16.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc0b37fe6f5ce5f1ccc62ad4fa9b1ad201d8e9b6027fd5e0170877beee4b2d15", size = 223546, upload-time = "2026-09-13T19:10:06.019Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e2/633469076a2dbbea036cc15a268a3a5d6b2c7dd5d9a9567b2553dfc5ad61/coverage-7.16.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6618f481053b63fc6121faf8fc676bd9b7163c2a19d9e984a2e850002c28ab57", size = 223881, upload-time = "2026-09-13T19:10:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/de/c3/f06150c13284569d53273b909f31222874276a595637b7852571dfeb2c18/coverage-7.16.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa02d561eb1d8d2f8ba43ba6e3cef4c6c402a3b632a9460fa329fcadcd5df6a3", size = 254919, upload-time = "2026-09-13T19:10:10.254Z" }, + { url = "https://files.pythonhosted.org/packages/d5/40/47e25b215ae18a29010c8e29be8782a6e04d18ba6224be2bf6cebfce6427/coverage-7.16.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bc5354a124799f1f87b7637bbe6f18cd4bc66a1f37f6aa2b5db40f9adad531dc", size = 257428, upload-time = "2026-09-13T19:10:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/27/4b/1e2a4267d14cbd12a8489364a9d40020233e6be836d929b363f0e77209e2/coverage-7.16.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34bafe9f4094315248573e6223e11af0ec1b25f9cbca43bf0e9a26a189ba2751", size = 258771, upload-time = "2026-09-13T19:10:14.031Z" }, + { url = "https://files.pythonhosted.org/packages/be/2e/9aa6146cea929fab9185bb2642ffef7f47520a6e5efe407f75f9b12f4cf0/coverage-7.16.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:29c4d3e32a3b5efa420a3dc627c7e570deb80ef997def52c7686a474f5edc7ab", size = 261086, upload-time = "2026-09-13T19:10:16.213Z" }, + { url = "https://files.pythonhosted.org/packages/13/3c/f9ad8bcd4fb3d21c9d20a16d6d6c6f999eee8f4498ed7659a3dbd2f4b74a/coverage-7.16.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2066c447fdd0bca39a9633a082d8ce67bf9a539a203b85059a364a405dc9fe9", size = 254895, upload-time = "2026-09-13T19:10:18.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d1/47eda9fd1eaeea39fa7b5b13a63b2bed92ab901841fb120b3f9f5e1dc30c/coverage-7.16.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fd8ac10cd2458b3c6343aac082fb9bd0e3fa806cb2c4975f2280153474b88412", size = 256783, upload-time = "2026-09-13T19:10:20.778Z" }, + { url = "https://files.pythonhosted.org/packages/38/c3/565edf044877cb8cd3373c56885347ffc38f0edfd1f1679a487b208c19a8/coverage-7.16.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9d8c54ec32e5c102b9241f75d88ae26538b53662868ca491736611db448d9c7a", size = 254742, upload-time = "2026-09-13T19:10:22.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/88/87d2b2aeaba719192b2089ff1c2cf89a06cf73a6d2e9f1f145626617700c/coverage-7.16.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6dd8dda3402a01a1a8fe8b753a282466f615128574a5590a9108acd07b1f8540", size = 259016, upload-time = "2026-09-13T19:10:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1b/70813185b125768abdcf7899fec4d37edc2e5fc9b60c7045c8f4271ec757/coverage-7.16.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:79afa9726438912e5cddd1fe541815cea9763c92935f594835e4c432565b68a9", size = 254559, upload-time = "2026-09-13T19:10:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/e7aa5af279aafda633a1ede8bfd7d6916b0c8b2082be86759e0b52e73a61/coverage-7.16.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3db3978211c3cead5437a80136ca0556bab8bc7828de15a762884b0598c41361", size = 256215, upload-time = "2026-09-13T19:10:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/38/87/7a894fa4f8c6662d2b6a87a3436950e15b1fa56e01765c9d6634fb2cbeb8/coverage-7.16.1-cp314-cp314-win32.whl", hash = "sha256:49c39c7068a494f8eb427155f5682f44feee43f9b3107fd54b1e52465379c54b", size = 225719, upload-time = "2026-09-13T19:10:30.743Z" }, + { url = "https://files.pythonhosted.org/packages/8b/01/fa7193c8005fb85488f02b0e1cc3c05a233cf2640206dd978af447aeecbf/coverage-7.16.1-cp314-cp314-win_amd64.whl", hash = "sha256:c510dad19552d912058e4c3e3cbec3fb155dbe8d0ce0ceb7e7dbf5c5822bae0b", size = 226208, upload-time = "2026-09-13T19:10:32.698Z" }, + { url = "https://files.pythonhosted.org/packages/da/5c/a08634c714924c3eaef811bb3576c044128aa5e7dfa86c75e52f0761849e/coverage-7.16.1-cp314-cp314-win_arm64.whl", hash = "sha256:b7d4d7e6dcaf33e85f1919f03346403bdcc27437c420a78835f3805bca0ab71f", size = 225633, upload-time = "2026-09-13T19:10:34.79Z" }, + { url = "https://files.pythonhosted.org/packages/43/df/ddb8a4c664046b1a0ee29c9c2d25b993e5dbc8fbde715df3694a64532781/coverage-7.16.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3d0a3681c12d3e0bcdea3d9414b04087828d6c1a482802d6f7f42c37ed530152", size = 224281, upload-time = "2026-09-13T19:10:36.853Z" }, + { url = "https://files.pythonhosted.org/packages/e2/d0/9076e0c762d8afd91182e60a520fa5c92c4a334785eeb9fd6b8ef8fe7e3c/coverage-7.16.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f3b4469d3da3ecced775d1a8c9c5d9fc80f259e30b7b89f9fed0700d6035ecb", size = 224547, upload-time = "2026-09-13T19:10:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/9c59e64b6161704f35fe91549bb19b2bb355e95caf596c26a2065564807c/coverage-7.16.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c08ae35c1be2fe1ce4b4c628df5c6fc0dc9a87f8e5fe8e20238d249678984741", size = 265906, upload-time = "2026-09-13T19:10:41.434Z" }, + { url = "https://files.pythonhosted.org/packages/57/5a/13ccaffb77f766101bf6f38be9dba9e468b02cc92da4552a57877dbf1c1f/coverage-7.16.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8ee71a38c54bb2676bbe762b8b0943a79ccb1c2fd6a52054f66e63eda392f8c1", size = 268023, upload-time = "2026-09-13T19:10:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a1/05cfcf01d3c7c922832698ad46e51d3441d820ce87a943014bb5cf5710dd/coverage-7.16.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76491917771f179f9772efe218c5ccc65950dbdb35f4439298d8a8dfc6ec1f72", size = 270442, upload-time = "2026-09-13T19:10:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/72/15/a2f1544b8e3835d7b769f7dabcc9ac0283e0b646ef3344703ff8f18d83e6/coverage-7.16.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4aa0b0a6f81fa3deb211e643f6954e78b4376b62b9c218271236cfa757664e8", size = 271565, upload-time = "2026-09-13T19:10:48.123Z" }, + { url = "https://files.pythonhosted.org/packages/df/5b/963c2993a82bd313f298d663afe03e164b96ace4d9d4c7561740a559e13d/coverage-7.16.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:756ba2d96d073c5a2a55d67fa22784763710fadbe22c41adde2d9cfa4dd78a8c", size = 264959, upload-time = "2026-09-13T19:10:50.195Z" }, + { url = "https://files.pythonhosted.org/packages/12/59/5eba06d1943735d7cd61d46d8c8a20ffe8ddd2da06b3c94366078dadeb9b/coverage-7.16.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:99bf9ea435cefcefd220f8687c3ddbbf78dc2de0bd11b57c3ae9fbbdf8d5561a", size = 267897, upload-time = "2026-09-13T19:10:52.252Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/af6c30f6ea431bb9b83f9070d268a9cc4fc97490abd32080164177ea999f/coverage-7.16.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:35cbc81f937fc402971df45c897d2df2bfb2014efcd990360032aa0a651635da", size = 265504, upload-time = "2026-09-13T19:10:54.432Z" }, + { url = "https://files.pythonhosted.org/packages/80/f2/6e13852a8656d05fa83284567dd5a5b1e6d89bef79fe3effca2787159eab/coverage-7.16.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:8fae08e85b334ac6ac886002b5041396a31bcf805225bbe19847627203da99e2", size = 269235, upload-time = "2026-09-13T19:10:56.563Z" }, + { url = "https://files.pythonhosted.org/packages/c2/32/b4fe465daa64ece674f83a750dfa4ba0fa3c5c74d6ef5dbb8dfce892cf0d/coverage-7.16.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:83362b64e215ef00b0ba33fcf13655ace6c9fdd144d5ad2ab59ac86c2daf166e", size = 264347, upload-time = "2026-09-13T19:10:58.634Z" }, + { url = "https://files.pythonhosted.org/packages/54/f3/88b5c0e4ca3994c6d5feb7b1bf4c9a62cee205553159184968426930a7b1/coverage-7.16.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:33300f2e140ccf26af3d8152e62bff71993f9310cfc63ba7a20940b0d246a0ae", size = 266660, upload-time = "2026-09-13T19:11:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/97/72/6eff5456d7ba7f1c4678af531c33f9d957cae3201bd229b056fd13a204a3/coverage-7.16.1-cp314-cp314t-win32.whl", hash = "sha256:5539304fdbb2cc144df684d35a33b81145334d23e1c2367b5a923d25107f70b2", size = 226026, upload-time = "2026-09-13T19:11:02.846Z" }, + { url = "https://files.pythonhosted.org/packages/8e/c8/6e5ae3d8d4d0f2c0078985bf4db55fafd90e8107b1bf91ee3547a13f5694/coverage-7.16.1-cp314-cp314t-win_amd64.whl", hash = "sha256:715dcb72c3280c428c3a20134b87e42c29acec9669136e899ab2de69ca86218d", size = 226862, upload-time = "2026-09-13T19:11:04.921Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/68f9f0734afc904a92b974b489545b6a15700f3b1c4bd36eae764561e661/coverage-7.16.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dac8b84c03e6029d272b8249c77018db83de59ca009a9adef7c144b4a62ee5e6", size = 226171, upload-time = "2026-09-13T19:11:06.969Z" }, + { url = "https://files.pythonhosted.org/packages/96/1a/d6d16babd0a5fe4c3fae40702158c570351694e74516d8d81b86c5637448/coverage-7.16.1-py3-none-any.whl", hash = "sha256:3d8bd4e58b6a5c2018d808f297905393c6c61da466a48c3f0596a76a4900ebe4", size = 215264, upload-time = "2026-09-13T19:12:18.895Z" }, ] [[package]] @@ -724,6 +920,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, @@ -914,6 +1123,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9d/8c/0a0f09659c1972aa83b9820cd3dd7f68f6678cfcfebde542e1c23d7d8663/faiss_cpu-1.15.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:88fbe1acac6978869063cb2f9477f85718da596a6e0a17751618f9c756bce255", size = 19470092, upload-time = "2026-08-03T17:49:50.253Z" }, { url = "https://files.pythonhosted.org/packages/b2/76/021398ec5608314124b554bb025878a86f129bcf3576c293826352d9a783/faiss_cpu-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:5b940897b317febaa761088513a3db164fad3ac71a5e1ed7be9a052c9bf1a447", size = 16251530, upload-time = "2026-08-03T17:50:00.166Z" }, { url = "https://files.pythonhosted.org/packages/96/74/4a70395a6e07036628a1bd0b3f709101a6aecfa6a746db13b6e7921cf291/faiss_cpu-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:22dddb013e764aad66dac6cd15b49c7598d60339e0591b73b5e081629419c21b", size = 16251914, upload-time = "2026-08-03T17:50:03.293Z" }, + { url = "https://files.pythonhosted.org/packages/ec/13/0a021b9df16963f839a3f325657656b70f23b5a6dbeb422eaa187d0121b3/faiss_cpu-1.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:37170d5e9ead4b6bfd9c314afc39e17e92064068a0c5a4063dd3f39568c2667e", size = 16535739, upload-time = "2026-08-03T17:50:06.714Z" }, ] [[package]] @@ -988,15 +1198,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, ] [[package]] name = "filelock" -version = "3.32.5" +version = "3.32.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/a0/50c2c0ce5e74d7721bbb1b19a26ebd339aac5878553a6e35308c2f31f935/filelock-3.32.5.tar.gz", hash = "sha256:f6a6a28f743f9b95ce19db5abe0f376f75eb56517dff21e1a4751e2657d3e83d", size = 222838, upload-time = "2026-08-31T18:56:34.729Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/59/e19834834cb01a32febfbb0f8a23a9088088f5d45991824ff2bc3b5e8acb/filelock-3.32.7.tar.gz", hash = "sha256:37b8a3d9811b0f9aef7e5ec5c71bb320de52df51e6ca9bcd6f5ad81187660da7", size = 225154, upload-time = "2026-09-16T00:24:20.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/d2/b70a31e13d04456d28493f31d2aa087e99eeb2767ef0293b2625727ccb8c/filelock-3.32.5-py3-none-any.whl", hash = "sha256:142cd9fa77a872c5e78c62329a0d15278fadc686eb89e760017968961a4fd6b2", size = 100003, upload-time = "2026-08-31T18:56:33.078Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/31098c5aeb4d966b553641472bd55fcf5fdfac953549894b8a765ba44e91/filelock-3.32.7-py3-none-any.whl", hash = "sha256:65ff0d0190ea42038b32bda4b77834fb05be2cad4c5b9b01aa4dfb3614536e52", size = 100157, upload-time = "2026-09-16T00:24:19.543Z" }, ] [[package]] @@ -1065,6 +1286,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] @@ -1084,31 +1337,32 @@ http = [ [[package]] name = "genai-prices" -version = "0.1.5" +version = "0.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx2" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/70/76dcc9c76b416d2df9aa4c65553f88b75d2ba4fbfeb5efb137f078844cc3/genai_prices-0.1.5.tar.gz", hash = "sha256:04c2cbf4444a3b2f5d38c3b6ab8385ea28ab924ac6f9202bde9261f599be8b45", size = 109418, upload-time = "2026-08-31T09:36:22.848Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/62/5f294af24a7ae407e6fa1a7ff36d310bed36b89ea6cea7eb7cfac5c305ff/genai_prices-0.1.7.tar.gz", hash = "sha256:4305395c5891796860c697cc3a49ab50b751ed62c8ff1911e291ea8da77f3a91", size = 114996, upload-time = "2026-09-15T11:24:10.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/14/28f578fa25391bb8983522f3e365e8735310f4b741e54cfed3a614f50384/genai_prices-0.1.5-py3-none-any.whl", hash = "sha256:5215706950decd833ce3527a9e1e745ad0dbdd35ce1b33ac1f7167699640b82a", size = 116330, upload-time = "2026-08-31T09:36:21.364Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/9e04f3dca5ffe74133779ab5e9925ffb6411afbeac55d26fa0e148722599/genai_prices-0.1.7-py3-none-any.whl", hash = "sha256:090ba285549e0d2ea3187d05f091a5f0ae722e652143fa844ac809205aecd023", size = 121970, upload-time = "2026-09-15T11:24:09.54Z" }, ] [[package]] name = "google-api-core" -version = "2.34.0" +version = "2.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, { name = "proto-plus" }, { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7b/7c/9be3903e3d45415e8ca493c75f8990a0f6f579d168015d44c379350d0ab0/google_api_core-2.34.0.tar.gz", hash = "sha256:98a779fe72de956eb1c9c2f47ff4c4432a668ece1a002ec38bed07ec2698ae59", size = 187953, upload-time = "2026-08-06T06:23:58.128Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/19/0a08cdd1198b3b74550464b8108f6976e873f0b4b035e1be3c3872345153/google_api_core-2.37.0.tar.gz", hash = "sha256:cf58f220aa797f1ffdda52194c4c7d72efeced09297c2976529f8135f6d85b9a", size = 203563, upload-time = "2026-09-14T18:52:47.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/c1/a8a92ae1bc4b1a8f804c776d7d3f0c771b78a62c3ad4df1be41b3fd8c767/google_api_core-2.34.0-py3-none-any.whl", hash = "sha256:cdf9c67e7ca2402d86ccbfde5f2503fc83e3cc3f58cc78456ae96cad24a6d2de", size = 180545, upload-time = "2026-08-06T06:22:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/3a/35/6db89f5aeac2f38815105e5dab66f48a0ff035aed6e4a86c81ab0ff483bc/google_api_core-2.37.0-py3-none-any.whl", hash = "sha256:d84042a0034cce9c4304e17d2e5e9966c2fcb1a851b73799291f687a67470c8b", size = 186342, upload-time = "2026-09-14T18:52:24.064Z" }, ] [package.optional-dependencies] @@ -1119,15 +1373,15 @@ grpc = [ [[package]] name = "google-auth" -version = "2.57.0" +version = "2.58.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/64/55f316b729f92a552d26e00aa3b1542b2e149d0a5efe2842afff0cac7af7/google_auth-2.57.0.tar.gz", hash = "sha256:9b4f96d6a1feb5f7201231f47cfb3de08d8f176f8a61f9e461555116e95a8789", size = 370794, upload-time = "2026-08-25T19:18:26.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/ca/f398a483ce5aad18ca2f735646e45ccee2439bd94a41a4ad0cfa646bd495/google_auth-2.58.0.tar.gz", hash = "sha256:55e30cf15e737de92c5323d78cda8a83fcd57e7ffbaf900c4600039fd60a80fd", size = 380018, upload-time = "2026-09-09T20:49:38.043Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/f3/8508a702c094af5f6e89773f4dfdeee74913df0f41a02c21b5e7dc3d75cd/google_auth-2.57.0-py3-none-any.whl", hash = "sha256:180dafe015cfb62193bea26b677500fab5b9fd51a1e825ebf3ad9b182047ae59", size = 259728, upload-time = "2026-08-24T21:55:08.449Z" }, + { url = "https://files.pythonhosted.org/packages/59/13/477d90d09591b3938b45c4e11f4d8a51291682112cb5efcac961e815d562/google_auth-2.58.0-py3-none-any.whl", hash = "sha256:8a9c4645bb4c8e91668fb1934b95ae6a8687084232753639220ba9bf04a1610d", size = 262404, upload-time = "2026-09-09T20:49:33.951Z" }, ] [package.optional-dependencies] @@ -1140,7 +1394,7 @@ requests = [ [[package]] name = "google-cloud-aiplatform" -version = "2.0.1" +version = "2.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1157,14 +1411,14 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/86/d8b154861ab292566d90dbdea276c33ef824b02de4a3fda61f2075d016d7/google_cloud_aiplatform-2.0.1.tar.gz", hash = "sha256:46e051b980baed400c5ea4328c79a8ae25f6f44a034b63bf0041aa5cd248ea84", size = 11325585, upload-time = "2026-08-28T03:41:22.864Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/22/2c0f9ccb918d67e9e34ca857337670c97fe49a84ab31979060eb9112dc64/google_cloud_aiplatform-2.1.3.tar.gz", hash = "sha256:6ccd59e9bd5f313209cfe1bef8c4ab2f57ef2c91b96003dd88053df3a5757dc6", size = 11335191, upload-time = "2026-09-16T02:29:39.991Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/3b/9d7974a7430edf56f009247c8d0d68df1a8fcfc19a27598141bb9e0359a7/google_cloud_aiplatform-2.0.1-py2.py3-none-any.whl", hash = "sha256:a72586889b1eebca0816b34e5914258abe6e76f23fbac40938b47e70103cbeee", size = 9451860, upload-time = "2026-08-28T03:41:19.276Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b3/be1a23d1cbc239f43de9d4a97dd5e2623a04735125aa41a214822b2e8ad6/google_cloud_aiplatform-2.1.3-py2.py3-none-any.whl", hash = "sha256:846d5dcda761832933bac34aeca1faba5d12819299d200e7a215c22c04770843", size = 9455465, upload-time = "2026-09-16T02:29:36.445Z" }, ] [[package]] name = "google-cloud-bigquery" -version = "3.44.0" +version = "3.45.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core", extra = ["grpc"] }, @@ -1175,9 +1429,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/fb/86d5bfdfbd9d810f6a49eb3e9cc591a2b46d33262338acc244cc79d032c1/google_cloud_bigquery-3.44.0.tar.gz", hash = "sha256:30651ae469b419f450b9c96581fd4942e2e060490df1ac0314bf379f16883215", size = 527575, upload-time = "2026-08-25T19:18:36.317Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/f6/e12217ae9518d35e014b2302ddd3a7e047c0854c692e536395c12eb4f81c/google_cloud_bigquery-3.45.1.tar.gz", hash = "sha256:f89bb9d1d546b7495a4c3baaea4f2d56c336a4e06581a19acd8eb8547266e591", size = 528170, upload-time = "2026-09-14T18:52:49.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/84/af193ca97ce72b56fd05f8ad15776bf7446d99cb0ba1cd2f4880dbc52543/google_cloud_bigquery-3.44.0-py3-none-any.whl", hash = "sha256:ac2f0a6ab61a3c742ba4674dc220fb98461c13e1def96fe7a980ef7d5e0c0285", size = 267691, upload-time = "2026-08-24T21:55:19.249Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ce/89915c377eaffca9d509e64631185254fa8e07f0fedd3fb749a31f9f7915/google_cloud_bigquery-3.45.1-py3-none-any.whl", hash = "sha256:d58c1b77b7c5ae1a405be7c5341e78ebf61f352173393cf35ef722c1432e6a73", size = 267691, upload-time = "2026-09-14T18:52:27.798Z" }, ] [[package]] @@ -1212,7 +1466,7 @@ wheels = [ [[package]] name = "google-cloud-storage" -version = "3.13.1" +version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, @@ -1222,9 +1476,9 @@ dependencies = [ { name = "google-resumable-media" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/7e/73bb7512df1d1aad6ce3f9aed847cd40e0cd400ba4a85d86ab8eb412e9cc/google_cloud_storage-3.13.1.tar.gz", hash = "sha256:a80bf8cac2794808aa61c50c5f769ecbbe2d10331bacd0d69d30e59b14b346b2", size = 17341051, upload-time = "2026-08-06T06:24:42.229Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/06/33e124df40437c292c7d666616d890c12cfea5b83842acf5be7dc3fde016/google_cloud_storage-3.14.1.tar.gz", hash = "sha256:b24e74b493c60b19b83462933a83bb831e45b6ca4924b0d75eac8e176b58b3a7", size = 17348933, upload-time = "2026-09-08T17:10:11.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/6f/d69f0e185e08ddb58c323a0a935af2b492907b5de362bc08933b0a3b5644/google_cloud_storage-3.13.1-py3-none-any.whl", hash = "sha256:98208de6c21e85cecd3eb44551894efff33d98365500e178867d4305854a770a", size = 341486, upload-time = "2026-08-06T06:23:36.548Z" }, + { url = "https://files.pythonhosted.org/packages/ec/65/7201c1816ae46e0b4e70a3ce61e5121963315dc9df2e00fcb8897a3d0926/google_cloud_storage-3.14.1-py3-none-any.whl", hash = "sha256:8f0fe2b74bd80ddcbd67247a3b47b81035a0a4f448569ed358fa40a46c940118", size = 343228, upload-time = "2026-09-08T17:10:09.833Z" }, ] [[package]] @@ -1243,11 +1497,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, ] [[package]] name = "google-genai" -version = "2.21.0" +version = "2.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1261,9 +1520,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/a7/a45f64f22ab9302b55fcbeb32acb6f313690a7748629b01e451aad1817a3/google_genai-2.21.0.tar.gz", hash = "sha256:0ecc11c6a5b9f5e3cc58e77ae5fead00c6719f8a1b2b654b803f514a9a6b64c0", size = 677301, upload-time = "2026-08-31T21:49:14.508Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/9a/db14adffb0144584889e05f433a4e8ce540f9ed2928ed8b23cc65ab96178/google_genai-2.23.0.tar.gz", hash = "sha256:1ceebffdcd2af30c039a922ba05de61cd4054bb61c94be7649c4bf9ff8b33c5f", size = 686707, upload-time = "2026-09-10T22:55:31.12Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/d7/c2419dd5fedd5803ce09810e4e39561a0a13a960b3f48d2581c12e42af3e/google_genai-2.21.0-py3-none-any.whl", hash = "sha256:36b575034be46a03acd603a852e22a6359f2cdd6b26bb1d65d9b7e0cc7ab3648", size = 1080223, upload-time = "2026-08-31T21:49:12.699Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b0/f6bc58a6a7c8ad7ad2b6a36b0bf4732b3504dd7d48c6674f848304548869/google_genai-2.23.0-py3-none-any.whl", hash = "sha256:1e63211d44d188b8069c2b354d92b9bde25c1e821513fdbe1948b7c0d9f6b922", size = 1093785, upload-time = "2026-09-10T22:55:29.282Z" }, ] [[package]] @@ -1280,14 +1539,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.75.2" +version = "1.75.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/90/fb8f1c84537fbf210c1f53a53ae473a805f6599c5a40b93c1bbadd211f7a/googleapis_common_protos-1.75.2.tar.gz", hash = "sha256:8829a3d1e4508c5b7b9a6b9525f7fccff611f8531644579a76466c29295d4bb2", size = 154083, upload-time = "2026-08-25T19:19:13.028Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/c5/4353a188e2c335aee33269e8b654af228278cca8e5f0b4b5f11e5d0e9adb/googleapis_common_protos-1.75.3.tar.gz", hash = "sha256:57c435ac2c68b108999b6db075d9053e4d7a936ba57b4a3d45667b1346f1738a", size = 153905, upload-time = "2026-09-03T22:31:21.869Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/5b/1c9e55363c3b1890a98cae813de5b4ea327845756cd8fb7ee690140c7eac/googleapis_common_protos-1.75.2-py3-none-any.whl", hash = "sha256:6b83302f554ea93a0f48409c7fc2050f954bcbcddb7e3a9c76d4a823cb22920e", size = 307002, upload-time = "2026-08-25T19:18:08.927Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7a/7d79170c6ce6f12e109df2b3879d6b934010cf4f99aea8de8b7e5408c174/googleapis_common_protos-1.75.3-py3-none-any.whl", hash = "sha256:a018d2bf098ca9fb6faa08d5bb780e2a2c2f73c566f069761331386c9596d3f2", size = 306984, upload-time = "2026-09-03T22:30:45.133Z" }, ] [package.optional-dependencies] @@ -1297,39 +1556,58 @@ grpc = [ [[package]] name = "greenlet" -version = "3.5.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" }, - { url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" }, - { url = "https://files.pythonhosted.org/packages/9b/30/87c212b5c684d0e72974f1063b7a9687631e8985902c06e1016542c874e7/greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f", size = 629504, upload-time = "2026-08-10T14:30:07.967Z" }, - { url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" }, - { url = "https://files.pythonhosted.org/packages/63/2c/eb487fafc9f50ffff2b1e0b697f70fb34bf150821c08ab225aacf5583a7e/greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759", size = 432462, upload-time = "2026-08-10T14:30:02.309Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" }, - { url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" }, - { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, - { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, - { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, - { url = "https://files.pythonhosted.org/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" }, - { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" }, - { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, - { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, - { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, - { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" }, +version = "3.5.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/6e/0091f175ccd02b02bc8811bbcbcc6ac2e980be116e3b2f7a736ca322bf84/greenlet-3.5.6.tar.gz", hash = "sha256:8e67c43bdfc88d5fee6db0d3e40175b362fc95fb85f0412d233b9b203c53a575", size = 207653, upload-time = "2026-09-14T15:42:51.806Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/18/3fc6d951466ae9a2a688edcddde3b2e388da0a8244e0caf7117bbeb0eb95/greenlet-3.5.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:a5876d0a60355af98d535c47f6cd6eb0f8a432396dab26845d380b92f8412422", size = 295668, upload-time = "2026-09-14T14:22:33.241Z" }, + { url = "https://files.pythonhosted.org/packages/27/89/366d2af5061eeefa5012f510d95a99c8620dcc457609838db4d538820318/greenlet-3.5.6-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e85880b538e59a59f55117b81f208a6660ad5ac328aad9305f812d9b8bc67a0f", size = 611700, upload-time = "2026-09-14T15:12:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/54/1c/07f133f865fd58ae593dd2bbec3144acaee9b04ffe2eb48c6e121747ceef/greenlet-3.5.6-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f0ba7c2a329d650628f4c8572fd1db29f0a59dd70a3e3e0710dcf18a35cce9d8", size = 624223, upload-time = "2026-09-14T15:20:42.459Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f2/844dc823ff2752ad049caa6b59d57e4572f9c445934b02d3518f4c67197c/greenlet-3.5.6-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee7d9da3bf493909cf811a3f038840cb34fab5ae2956b8a263919f6e289ab188", size = 629529, upload-time = "2026-09-14T15:25:06.354Z" }, + { url = "https://files.pythonhosted.org/packages/66/6a/1594f3869c57c149abdb380492529e04d4c0229b5e4d79572c5bd0aaa673/greenlet-3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:975736b002ed080d124cf81a79cb7e05cb26d6b3f5c7a7b651c0fcce70353aa1", size = 621404, upload-time = "2026-09-14T14:35:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/c0/42/b1f8dbc89a53b9e77859fc1ad1627d106fc361daa3ea4bdf43a91ebb4338/greenlet-3.5.6-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:71890d5247020c25c21a6b65202782bfc281d4e6e244842419d30e3492bb6dcc", size = 432385, upload-time = "2026-09-14T15:28:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f5/33e5c9e48178b9259fd000f8f45caa4a65036f65d3d0c06a602f570f025d/greenlet-3.5.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0616b8f878098c5681fd8f0dc92d887551717402342a70f0abcbfea5f5ad8a44", size = 1584998, upload-time = "2026-09-14T15:10:06.653Z" }, + { url = "https://files.pythonhosted.org/packages/ef/31/9b4e140bc24d0ad7927ebd651f5608b0acc2334d061748c3b6ad19085cfa/greenlet-3.5.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb4596a6a4e5d47121a33ff20533a81e60f302d9e67b69909a8bc21a43f0a7", size = 1647568, upload-time = "2026-09-14T14:35:49.787Z" }, + { url = "https://files.pythonhosted.org/packages/c3/71/d79f1791f824f8ff15c2978746640467ae932a2365e0201069f7f272395f/greenlet-3.5.6-cp312-cp312-win_amd64.whl", hash = "sha256:7ac4abb3877c43af320392c664774eef6fa2cc063c79a55fc02d844a3cbe7395", size = 324203, upload-time = "2026-09-14T14:22:54.504Z" }, + { url = "https://files.pythonhosted.org/packages/63/af/42aca4d56e8cb321912203069d8d34734cb288222f10ad2ae102718cc577/greenlet-3.5.6-cp312-cp312-win_arm64.whl", hash = "sha256:301102a49120b095e72a7838792b41233975fc1c155daec6d98f81c00c9280e0", size = 308310, upload-time = "2026-09-14T14:24:03.008Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a1/e720a38852366c589e1a46cf570b886507ad2cf591050c203365638baab0/greenlet-3.5.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f96f0e30b5a95c7631b12bfe214cbc90ec8fe8cfa36920596c10514a65743519", size = 294627, upload-time = "2026-09-14T14:24:40.102Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c3/58187858df41354a11e6a55b421e7af9059798abdab3a384cc51b8567c38/greenlet-3.5.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c75116c9de79949de23006e2d9b35ee82874c594fcf5c0311b439acaa14b8441", size = 614356, upload-time = "2026-09-14T15:12:03.399Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b9/3a7e67d5f05c9760b1ad411fa52264bd69cc08e22a2ebfb4018b90628ced/greenlet-3.5.6-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cad5782f93f7f738b62c6527b6f32a60694d924029f299a8b524758cfa53d815", size = 626756, upload-time = "2026-09-14T15:20:44.269Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7c/40400455f5b5a65bb83e94fde66d1be9e5ec518638113f8083ace746c309/greenlet-3.5.6-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a93ee7c6e8fd0f8a83525a51bd777be57ee17787e91d805bd8d6faf9dcada18e", size = 632632, upload-time = "2026-09-14T15:25:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/85/cb/ab0c123c514ed4e94c0dc9ee2e86362633e6b998cfc05de7fc9ac2eb9690/greenlet-3.5.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f98e8215e172f567ce80eeaed9107fb4d32b6c44f26983d9b8334658136a205a", size = 623779, upload-time = "2026-09-14T14:36:01.104Z" }, + { url = "https://files.pythonhosted.org/packages/f9/67/1f35cff30a6c51c3f23b63d4afcc7313ab4f97490ba3676fa78178984b27/greenlet-3.5.6-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7f731ebac68ea06d628658295cb2d217b10186329fcf9a3b6a149045059bf92e", size = 434933, upload-time = "2026-09-14T15:28:38.858Z" }, + { url = "https://files.pythonhosted.org/packages/a5/26/fda8a5a06e7073333ccb038133c5893b9e0c4fe29d5992a17e83c241bc6e/greenlet-3.5.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df19e2d0b1620039af5102563fbd96e8938c7f5c3f5828528d641d9fc585525e", size = 1584930, upload-time = "2026-09-14T15:10:08.234Z" }, + { url = "https://files.pythonhosted.org/packages/2f/37/50f8813163148d6234e08b23dcad6a9e37f01d148c8ec976e4c44ea2d918/greenlet-3.5.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06c0e933290fba8ffe53ead4ae1b8044b0e9754b75cebf381aa2bc3e50d82fac", size = 1647590, upload-time = "2026-09-14T14:35:51.173Z" }, + { url = "https://files.pythonhosted.org/packages/86/da/b7669b09586365654083a62bd0724cf06cb74bd5085a15cdd161271f992f/greenlet-3.5.6-cp313-cp313-win_amd64.whl", hash = "sha256:5b602b4201b965a8354d74e232364a66ff243dd142e350d035f46169bb36e13d", size = 324086, upload-time = "2026-09-14T14:23:48.428Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5d/c9663cfe84a2a9e0aa96f066f5b0594c227ea4c647511e087e2e11d4ac0a/greenlet-3.5.6-cp313-cp313-win_arm64.whl", hash = "sha256:876077e7ebb8c84ed068e2b23d4c62ebb010d60df84b9591af1be2f39010ffb2", size = 308211, upload-time = "2026-09-14T14:28:01.634Z" }, + { url = "https://files.pythonhosted.org/packages/66/c0/d254544ae2b8bdd311aef000fafc02828c2771b17d994b3075620ea7cc6e/greenlet-3.5.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8cddea1b8339451c2fb3388e138347b6126744f33b611bdb55b7357361cfef46", size = 295221, upload-time = "2026-09-14T14:25:11.583Z" }, + { url = "https://files.pythonhosted.org/packages/18/18/eb54be16b9cc3971e09ca5b73334e1b8c804a4630d9addaaf218a4fe300f/greenlet-3.5.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c59acfa8eb73a1e0d484392dc002bdf001fd4ce73394e0132df3d1ab6093d7cb", size = 660992, upload-time = "2026-09-14T15:12:04.876Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/e193efe65671dcf294bc51fcc59efb52d154adf8612c4ea016da0d2c486c/greenlet-3.5.6-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3b4a01c6da07ef9f80d4fe8933b994bc99747bcea3eab0330a9c34d3c12655b", size = 673428, upload-time = "2026-09-14T15:20:45.756Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/631bb45fafde1dca782152377c0676d182ec924820064047f533a3627b28/greenlet-3.5.6-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dd0b83bed3405b586a3133629f1d1a5bc7bfd64822a3b7ab342bdc68e6dbc61b", size = 677688, upload-time = "2026-09-14T15:25:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/45/ac/28fa7a9e50f2859466214c4ac584d776db52c1604ad4dd158960a5af2a1f/greenlet-3.5.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a09d59bef1db94f384b5bcc2d523694d338f3df6b757aeeaf7baca5d0c0be88", size = 670773, upload-time = "2026-09-14T14:36:02.577Z" }, + { url = "https://files.pythonhosted.org/packages/40/30/2b0a73e68e1e18e30b601d0d183cfdfc2beca4de5a6843c630f0fc9fb90c/greenlet-3.5.6-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:fdacf26402389bdd89857ad3c045a26fe8f3314f9a8b28226f82f88463a65b77", size = 480475, upload-time = "2026-09-14T15:28:40.741Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cd/fb7d6cdd86ff3427c1494854f0e35437eba05142be91f530f6da75e09e19/greenlet-3.5.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8b7c73d1cef3d9ae963e9ff03f6222df43efbb9054ffd2f1969c935b7fc84c02", size = 1631900, upload-time = "2026-09-14T15:10:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/143bdbb20a516628cb15074ae52ed17d850b450292609c7a6fccac6dbece/greenlet-3.5.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8b27df301f56e3b3d2298095c8f7d6b68f2521f6b1693e901fa039bdbae34424", size = 1693740, upload-time = "2026-09-14T14:35:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/019642432e6ae283301df1361227d47610709d2dc69a38f95edef266d713/greenlet-3.5.6-cp314-cp314-win_amd64.whl", hash = "sha256:f8f0bd690e1a41294ac87905e8121c81a3761ec2583c768f13467428606c8c7a", size = 327473, upload-time = "2026-09-14T14:28:12.948Z" }, + { url = "https://files.pythonhosted.org/packages/e9/7f/8aafc7bf70c948786dba7221d0dc0838e5329bebc6d434ef2208b4f0e760/greenlet-3.5.6-cp314-cp314-win_arm64.whl", hash = "sha256:8cda13494d86a4f12429641117cb6ac4bbbc9c30a33f711f7d3a2e5fbe4b0b7e", size = 311095, upload-time = "2026-09-14T14:28:00.7Z" }, + { url = "https://files.pythonhosted.org/packages/14/7e/7a205688a5b3074933b18a906608d46d106e9a79d776bdab5a4abf4b4feb/greenlet-3.5.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:97c5a53e8c1754df58e73f047a99e287d4da1bdfe64b0072fb25c87000897951", size = 305352, upload-time = "2026-09-14T14:21:31.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/cb/9c4a57a9d9dd0256e20b8f7f4f06554c2c92badebf0ab73ce344321b78b9/greenlet-3.5.6-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fea4427d1ffdb3b523d7daa6712038428a4c16c450b9777bdd1221cfee0eab49", size = 672671, upload-time = "2026-09-14T15:12:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/97/52/c6729681ebbd298f4decd28746815acc8a0b0a0fde21d2df33776fd4d042/greenlet-3.5.6-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73a29b5ba642e35433166a03a3e02935e7238c4b3467fbd77523b99edea23e5b", size = 679489, upload-time = "2026-09-14T15:20:47.291Z" }, + { url = "https://files.pythonhosted.org/packages/71/76/3c11c21e0716b1f1dc7c1a4b3d690abb1d3b448c69a9d32049fecb64010a/greenlet-3.5.6-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:61a61b4a95a4f97922c3a6f5606d3e360851584bd47e500a5161373c53810e3d", size = 681303, upload-time = "2026-09-14T15:25:11.088Z" }, + { url = "https://files.pythonhosted.org/packages/58/c5/2b6c721ba8b8963da42d5a0f57f25b8aaeb1fe9bdd156875e57f3be648a2/greenlet-3.5.6-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:460e70b033aba8ed47e2ac9b5d0d2157b05a34fbfa30a241400aef4118902cdc", size = 676608, upload-time = "2026-09-14T14:36:03.959Z" }, + { url = "https://files.pythonhosted.org/packages/3f/26/3ae402202452cd5941bbbd483e5a74297e2397e7aa3182c2a5e3ab7d5666/greenlet-3.5.6-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:fe3170a69fe039b18ad18171e66faa9a75f6fe9d78f968fd9b54e09fbd714d81", size = 510112, upload-time = "2026-09-14T15:28:42.112Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/0d018e0d05bcdde19a0fcb907834155f1fc853a9bedd3f3f5e6acadcae19/greenlet-3.5.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca80a49b53ed1d22f7282da7255f7bb2fd1935fd0f623d8613fda38745f18961", size = 1641479, upload-time = "2026-09-14T15:10:11.216Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/f02ef9073919158f6403fe3701d4ed4403d646720e7201dfc6e9d264bac3/greenlet-3.5.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:916f92f2a8db10508f739d0b5e00b83defe5d1115a997c54532a6d7cf8c95404", size = 1698758, upload-time = "2026-09-14T14:35:54.336Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/1f48fe647473a2dcccfd1839b2ff2c78eb57009be776b4da071e901c9bff/greenlet-3.5.6-cp314-cp314t-win_amd64.whl", hash = "sha256:886bcf1870af74c32bc310fd00a6b803445e17e51b7d5a107c7b35c0f362cc16", size = 331574, upload-time = "2026-09-14T14:27:18.451Z" }, ] [[package]] name = "griffelib" -version = "2.2.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/b4/a767e91c606deefc447a96eaf59edd77397960b1d677dffd833ee8449831/griffelib-2.2.0.tar.gz", hash = "sha256:e1bc36fe9cd21d4b6b659b456346755e4cfdc5676c0a5214083126ee12612b3c", size = 227048, upload-time = "2026-08-16T14:04:58.383Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/af/018c10bc9edd42b6ef6db2e96b09542050d5253f9b195e74bc910b2d13ab/griffelib-2.3.0.tar.gz", hash = "sha256:7b0952caf5bca6afa4bb5ee8c6a2d183fe3f21b62efc5f6c7243cb2b26d2d115", size = 234534, upload-time = "2026-09-04T15:08:17.472Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/b6/f65ac785d4ac90dcf7c831ac6256f5dd4a19780f4e1575b2c0d6eeebe319/griffelib-2.2.0-py3-none-any.whl", hash = "sha256:d71c3bc2bbed9f958488634fe788b843a9f705d6d2838ca32cd6c25eeb64dfc4", size = 166779, upload-time = "2026-08-16T14:04:54.365Z" }, + { url = "https://files.pythonhosted.org/packages/41/63/e876e789525063c840ccfa8857febdabd6523bcef9ce7eb979b9305ea895/griffelib-2.3.0-py3-none-any.whl", hash = "sha256:1b8f9cd525681c26b1d6d574faa1371651e8459ca51d209684f50b8096ae06e0", size = 169423, upload-time = "2026-09-04T15:08:12.956Z" }, ] [[package]] @@ -1348,47 +1626,57 @@ wheels = [ [[package]] name = "grpcio" -version = "1.83.1" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/b1/46539f5050d7c316a13396d185451f95084a74ddc68b12d818595bef0377/grpcio-1.83.1.tar.gz", hash = "sha256:9cee6fcbf2eb57c4b49451787bfa87be8efc1ca02a0b327dd4b54d44502e362b", size = 13445033, upload-time = "2026-08-28T07:09:11.464Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/9e/a3ba13e08bbee5bf6e57597dfe4823961fd7e94c0b8afe3a4bb7dca639f3/grpcio-1.83.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:5acd14c6ddf047de62cbf8745b11103ea91abbf57d1b8edd5395ccd9fcd13abb", size = 6303170, upload-time = "2026-08-28T07:08:08.188Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ae/65ce56a2527faa17d02cba4c2231c74047ad898be339486ba87f093bfb66/grpcio-1.83.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:16138031a47b771860a16a975b53087f4fd5bbdbb2c03a188c5d90ad65d2bdae", size = 12165806, upload-time = "2026-08-28T07:08:10.309Z" }, - { url = "https://files.pythonhosted.org/packages/4e/91/40432480088a2243d360864de072ed5b78c4ebbaabd29c28918f1e1b1454/grpcio-1.83.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5ccc26715fd4defca5e129e280dd883b1737b65045ec50ffe22ce42104089519", size = 6872490, upload-time = "2026-08-28T07:08:12.355Z" }, - { url = "https://files.pythonhosted.org/packages/c8/62/3da2300c8c79fd20a78a8a4bb6251e5068d9af33bc8fd389b98fec35e8a3/grpcio-1.83.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b74f2a1d9ab1dfa3e263ef33d581613679b78d0884babf11671af26e45570ead", size = 7618367, upload-time = "2026-08-28T07:08:14.025Z" }, - { url = "https://files.pythonhosted.org/packages/bc/19/9fc702e31a631262d7a752fa699f6022821e707fefc8bff49b1550a57729/grpcio-1.83.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72578aa07a4008f17521ef52debcc3acfd1e2c5426243bc3ffb56a38bfe610b7", size = 7040936, upload-time = "2026-08-28T07:08:15.963Z" }, - { url = "https://files.pythonhosted.org/packages/ec/56/95933cc44cba2429765fa065c951dd529e5771b119d9d2481b4646f1d6a5/grpcio-1.83.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c12e1fc59c6dc26d10d9144453ddc6cbfe4cd4c31e874ed2d0132f88e685eb8b", size = 7573096, upload-time = "2026-08-28T07:08:17.729Z" }, - { url = "https://files.pythonhosted.org/packages/ac/80/af63359da06b016de48cb111f144703a10043850dafa43ae0a038907b9e8/grpcio-1.83.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4910b62f7d12197160bfb7de06d876d64dd12d43483e8292f98f49ca09b628d9", size = 8609442, upload-time = "2026-08-28T07:08:19.777Z" }, - { url = "https://files.pythonhosted.org/packages/3d/fa/f0586c56bdfb8a7a2adda01e0ac2413447cde3141ab09411a5d5afdcffd3/grpcio-1.83.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e703effe3ae779925c82ac24fdb82cf4105e1096810151ed9501c5f34546b9c", size = 7984321, upload-time = "2026-08-28T07:08:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/25/8a/14ec05669f9eb295801e26c2ea8c561a1b786b0e3557c2c22131165ab010/grpcio-1.83.1-cp312-cp312-win32.whl", hash = "sha256:a2aea8bd6e0a34f12cbaddb7bb70bec836818789fa5c7ab7572c6b745396a2d4", size = 4395604, upload-time = "2026-08-28T07:08:24.08Z" }, - { url = "https://files.pythonhosted.org/packages/e9/37/8c2f7cc16089e36a3fbacaacc7a3d043912aa0d2dfae5556f6450414ea6e/grpcio-1.83.1-cp312-cp312-win_amd64.whl", hash = "sha256:583bf2e8255040a4a312f9572dfe62a05271437b149550e1a536d5c47d2d1e8a", size = 5161512, upload-time = "2026-08-28T07:08:25.81Z" }, - { url = "https://files.pythonhosted.org/packages/7c/fd/d1fc58933bf88c9209f89dc570c810f1aa57cb04b3459cf2b26f61e32112/grpcio-1.83.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:8d228e253b77865efcbdd7b5894ca882c9e0ea98c02b7d20582e61ded8dfd4b5", size = 6305628, upload-time = "2026-08-28T07:08:27.872Z" }, - { url = "https://files.pythonhosted.org/packages/c4/49/0b40bae059c619505c9b751cee6caa208e4904e290aaefa1728c4c2c67a5/grpcio-1.83.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0468b627f2987c9a77f7580030207cbd85457ffe52998beff4f0b5c38c58a72c", size = 12156839, upload-time = "2026-08-28T07:08:30.191Z" }, - { url = "https://files.pythonhosted.org/packages/61/4b/e8c0d635da0ee5ddd9950c8d540f5dcdd0ef1854a382cc55496a487a8d31/grpcio-1.83.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a6a282e81530cead60bbd752cc04950a57f224379e9821495d6a35bd5ce9b1f4", size = 6877036, upload-time = "2026-08-28T07:08:32.285Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d4/760a33f339a7dd3d5f4b3e0e9bec5472d95592a80f887b2e9dab4e41cfbc/grpcio-1.83.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:947d945f52e8ecf3cafd2bb7113502a16ccfda3e12c854443094de32d83ad432", size = 7624404, upload-time = "2026-08-28T07:08:34.194Z" }, - { url = "https://files.pythonhosted.org/packages/54/ec/bd798654b06fb42a92b57d1dc1b530084fa89ed442806fcd0a833a36f9b3/grpcio-1.83.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:55656318d5dd387077396dffb929171ca3966e24bfead9a6c5dba9f889062cb4", size = 7042942, upload-time = "2026-08-28T07:08:36.208Z" }, - { url = "https://files.pythonhosted.org/packages/08/b0/c00f86614566dd0961825cf0f43d4f96a74371d9d95f952bcbc4b86d9a27/grpcio-1.83.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9daf5acf4fc9d5f5627229969c2580a91e511779d76e4ccdeb9f4770f05d8bc2", size = 7576937, upload-time = "2026-08-28T07:08:38.041Z" }, - { url = "https://files.pythonhosted.org/packages/b1/38/85eff43a5c89dc666a252b5c9f8e9ab03f89e11c95b6263d2933f08fdbe7/grpcio-1.83.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7b94174cbca93316888f805efbeb08f1c020f7b7493d2d50cc4f6b64ebb7e8bd", size = 8608391, upload-time = "2026-08-28T07:08:40.092Z" }, - { url = "https://files.pythonhosted.org/packages/35/4e/82835483e2f812494be865e7965c0d626cb9e71ab0d83a420d75aea4ad67/grpcio-1.83.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:65c5a7210911ffe0f67b1cdc5308f9854b6d1f1b345e3e49ab7cac1ba50fa346", size = 7980060, upload-time = "2026-08-28T07:08:42.434Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/68a98bef733fef704fbcfb3957c8dba67e3e38ca7a7fea851195bc97c648/grpcio-1.83.1-cp313-cp313-win32.whl", hash = "sha256:179368d9361854616ce6f397d4716e07480129652752fcbcfc5a7260455ad6f2", size = 4395226, upload-time = "2026-08-28T07:08:44.463Z" }, - { url = "https://files.pythonhosted.org/packages/85/a0/df4de3b51d37ac8fb0320bb9668381ce2bd3b7aa990880bfc56a8a26f665/grpcio-1.83.1-cp313-cp313-win_amd64.whl", hash = "sha256:2e57af456385491a76e13c4aada8c8f43a8e47051e06ea97a9dbe2a49654e6db", size = 5160273, upload-time = "2026-08-28T07:08:46.216Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/c1/4c9a2e0e6b0aaf02781404cad2f79211f989f2c827cf672a4a48d1604d3e/grpcio-1.84.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:b5c6f20d657ae09ae4e30d9d3a21edd13f1219d58cc6f999b9d1bb63be9c1baa", size = 6415756, upload-time = "2026-09-14T06:57:39.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/131e7007bdee9acb77a8dbe8a16fa9fef75f88c1695242d8ee0993ac2d3d/grpcio-1.84.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:406583b4e8fb2282ebd392e12b963e601c1f82e07125a8c2cb5b144e7e024796", size = 12339195, upload-time = "2026-09-14T06:57:42.373Z" }, + { url = "https://files.pythonhosted.org/packages/db/d1/a7b7cda98fcab9b3d2916204a872d87371158a7a34e41768f524584fb64d/grpcio-1.84.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbdbcd06986ede3ce584083b1dc2afe6808e8943e5cf50ad11183c03aceda25a", size = 6984468, upload-time = "2026-09-14T06:57:45.035Z" }, + { url = "https://files.pythonhosted.org/packages/19/81/c5be83e3ac9416f73c4c51fe1ea9c41a0c42fc3509e3505faa46f5046abe/grpcio-1.84.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:23e6e8e8a75cff88e0a793bfd3becea03a13e2763ae90c1ff573bc19ca5b429a", size = 7749432, upload-time = "2026-09-14T06:57:47.395Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bf/258cd7c0a7ed92745dc93c31666d462d05b702807a689744bd49fb833bde/grpcio-1.84.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b44f0a0fc7bc6677d38cc80bca1a32814ce6c8f200fb8b3c1a61c9d77eaefbf3", size = 7156115, upload-time = "2026-09-14T06:57:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/2b/4b/7f829418dbfcf91b875e55e2973f1059a95decb4f081313416317ef04ec1/grpcio-1.84.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:210e4c32f907045eb8158273e60c6ab69a3947697df6245dbda381f26c59485b", size = 7708010, upload-time = "2026-09-14T06:57:52.496Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/9932e2fec6a04205f8bf3f8f4d2020479dcdac88feb6f93822ed31bf0eba/grpcio-1.84.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a71d24f40b0cc6798feaa978c7411dc1135b7018e9fc0442db611c139bf58344", size = 8759980, upload-time = "2026-09-14T06:57:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5c/b67407c6dbc480dfc0715f6eccdb1061e7c88d85f9a330a241d357a538c5/grpcio-1.84.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6c972474ce691aca74e58d17625450cef153dc4760364cadeb167983ea6d589", size = 8124904, upload-time = "2026-09-14T06:57:58.569Z" }, + { url = "https://files.pythonhosted.org/packages/02/37/2bfdae2df8dfcfc0df619b628e0c7153ce703adae827243f44720322ccc1/grpcio-1.84.0-cp312-cp312-win32.whl", hash = "sha256:0d532ade4486dad9b302ffa4d4683d67561051c26d17c4023322845e9fa10140", size = 4478915, upload-time = "2026-09-14T06:58:00.714Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/309268b7b39f6deb2342f634841e105623a0b67982e8b10ec516782ff1c6/grpcio-1.84.0-cp312-cp312-win_amd64.whl", hash = "sha256:49717e857899f4136d7657bf5aded61ac479110a075438290923a4d86af7cd02", size = 5253534, upload-time = "2026-09-14T06:58:03.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, + { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/26/6f/e25ca89ca5b0b7b95464c907a5c21a77c0ac8c4ee1dca164c4dd8f153ddb/grpcio-1.84.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:026d757df86c5b7a41de8200b9a2cda454aaa5004cb0c7e3374c66eb82f61499", size = 6428207, upload-time = "2026-09-14T06:58:34.401Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b4/6b76b429f3f9b901cdbc306c81364d708bc957f847a05cbd1046cd2d05d8/grpcio-1.84.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3de427b05f244ba2c2a9bdc67e7a6731c8340811524ecc4435466549f8af1d17", size = 12342420, upload-time = "2026-09-14T06:58:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/ac86d638ba7f73bee0dccb608ba551d4f63adf75151f00d2c43e46d3979e/grpcio-1.84.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e90e3bdf7b5eac005fef631adae9cafde16f922def207b80a7c46b253c18ad20", size = 6998396, upload-time = "2026-09-14T06:58:40.535Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/fa12e9ec9d7ebf8cc3e81428fa9e1ca0d30d22d546ce2baa4c64bc917cbc/grpcio-1.84.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e88d304f094f4937bc27ec6a435e218a084168f11ec630c8d5d39b431d08d81d", size = 7757538, upload-time = "2026-09-14T06:58:43.297Z" }, + { url = "https://files.pythonhosted.org/packages/21/d7/94240c7fae121ff1f116dcf04a3b7ee0216a06832c704310363f72638d4c/grpcio-1.84.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57dc36a5ab0e676f5f6e171de2917fd0aef73f32a9aaf23956bfe19997a30bd1", size = 7161480, upload-time = "2026-09-14T06:58:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/c9/7033e95d4b344969818b09185721c7608b47fc2498d97b5e4eec4995dbf3/grpcio-1.84.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5deda5b4bf62769eb98c119cca43d40e1231e34846b19db5cdea821d446a2253", size = 7720191, upload-time = "2026-09-14T06:58:48.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/b45df2deba81d55069076859480bae7109c9eec02bce5515c799530cc2aa/grpcio-1.84.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9bab4cf571653a8afffb83ce21aa27b51dfe629b526b7b6adec35491fe1fc2ea", size = 8762792, upload-time = "2026-09-14T06:58:51.068Z" }, + { url = "https://files.pythonhosted.org/packages/de/c4/3e1c3d6155c16b8737cc31d5b477d6cf1fc7cdd10d58320cf0ec9b446f42/grpcio-1.84.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5559b492007dc09b4de9b95dab05f0b5e53547aad230cf07e46c7dd017a3be5", size = 8123299, upload-time = "2026-09-14T06:58:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/f4864de5b815e5ba18858771f99381a398fac14117f89ef5291ed43d3c4e/grpcio-1.84.0-cp314-cp314-win32.whl", hash = "sha256:2c024da73b296f040b8360e60bd73a659b230093684a438da0e1260f34cc724e", size = 4562560, upload-time = "2026-09-14T06:58:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/44/03/640811d4d8c84f5e603995c5a9bab725223aa472cad9ca4286c3bbf1c3e3/grpcio-1.84.0-cp314-cp314-win_amd64.whl", hash = "sha256:800b7e00d92553313c0463c200087930aa78678ec1d528193aeb50906f55989b", size = 5394092, upload-time = "2026-09-14T06:58:59.61Z" }, ] [[package]] name = "grpcio-status" -version = "1.83.1" +version = "1.84.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/03/dee41fe15a9657c60397c1f215622a1f146e174367bbbb67911b62ee0629/grpcio_status-1.83.1.tar.gz", hash = "sha256:c08c8d553d6ab96effae1d923839de22926f5a316a942f48a48040e047c517af", size = 13957, upload-time = "2026-08-28T07:12:25.893Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/45/f80309cdb6a7dbf8f65e2082dd2ddc9797ba7180516a73c54d966ba632c4/grpcio_status-1.84.0.tar.gz", hash = "sha256:5caf28ba7184b81f618b5f7f094859fd2541bf429d2189bbbcd715c9c2cdcee2", size = 14015, upload-time = "2026-09-14T07:10:29.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/34/b03ec688e5c8c6ce283ec8b214d9c171e97059eb55157465f6fd6db1562c/grpcio_status-1.83.1-py3-none-any.whl", hash = "sha256:2cd328ee62ef2b3eb957dd3b75db7dadcdbb76488fdc9ab3aba1ebfbbdc324a4", size = 14636, upload-time = "2026-08-28T07:12:15.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/3a77e4273e866b1b0c412afd80882e95941a37170032b5109d847c501124/grpcio_status-1.84.0-py3-none-any.whl", hash = "sha256:0c182ca0d6e60acbfd0e14499cf39a155e4827a1c3fd9f7638e49af15a74c30a", size = 14639, upload-time = "2026-09-14T07:10:15.175Z" }, ] [[package]] @@ -1406,6 +1694,14 @@ version = "1.6.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/41/62/3c062f593bd92ef4e77a0ef39541e3d82a0a1d3947c8a777a02a13a27828/hf_xet-1.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:70cbb9c896901600128cb9b6f06e132954fbede1db30f31f7c6c63f84cb7c31d", size = 4074584, upload-time = "2026-08-03T22:32:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1e/c0ad437dd267a8e435bef594acf781bbc3874ff0b6435b4962d03ecf7cc4/hf_xet-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23379c2f9ec8696d952b16414a2bae72cad86a52df869b050698ba60f538c675", size = 3867381, upload-time = "2026-08-03T22:32:49.049Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ee/7c0d7b6ab336167531b1c30af2af003f054af4c749becbd7209ae33a77c3/hf_xet-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b", size = 4453982, upload-time = "2026-08-03T22:32:50.568Z" }, + { url = "https://files.pythonhosted.org/packages/63/06/ad8eab1c9525246650cbaa821caa3cdbaca734ab1a5b8c91bea09cbd8d69/hf_xet-1.6.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:948f15d3a9545cfe5932f6bd8b440f6ae630aee108f14b7bd6c561f7c2dcc522", size = 4249445, upload-time = "2026-08-03T22:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/d8/26/1eee8aedb0dafc1ab9717dc9ac602cde33361b232dc06803f1f6ed18b58c/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5153e6bb103ad49d6ea9f1b2e230db5a2ea32551ad09a706d2f61d7c7c80d80e", size = 4451099, upload-time = "2026-08-03T22:32:54.114Z" }, + { url = "https://files.pythonhosted.org/packages/67/57/0b88af1f194ab6c9c650547d9cc06bfeaab836ae4dcdb331676bfb8be95a/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:35cec30d75c6f9eb9c16a77cef68e85a103b72e24d4b473714ec9ff06428bab9", size = 4664712, upload-time = "2026-08-03T22:32:55.547Z" }, + { url = "https://files.pythonhosted.org/packages/53/a0/26b717a9d1840e8abf48dcec64b5ed8fbe472671d38ad28d30e147132b33/hf_xet-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5789835d7c6bc9436962853192082374297fb72d7eff7e7762ec25ceb7e25338", size = 4025906, upload-time = "2026-08-03T22:32:57.391Z" }, + { url = "https://files.pythonhosted.org/packages/49/f6/4a9966633c6fef83af997e2cff68ec1963676d412bdfd096df2a93b8e185/hf_xet-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:75765820ce4700db3750c94acc8fe27c5fae4c9ec000a0dbac3ca082acf97765", size = 3849221, upload-time = "2026-08-03T22:32:59.123Z" }, { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" }, { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" }, { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" }, @@ -1431,15 +1727,15 @@ wheels = [ [[package]] name = "httpcore2" -version = "2.12.0" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11", marker = "sys_platform != 'emscripten'" }, { name = "truststore", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/8c/e925b1c92018abb3a1863ce1549d76d2381e334d21d65d4ac8f65dabd78a/httpcore2-2.13.0.tar.gz", hash = "sha256:2adc8be4fb285fbcd6d894298db3b52c177e74b6674eda3a76bd36be3292a3db", size = 67740, upload-time = "2026-09-14T14:18:04.717Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/117a771a2bb91df334b66bf4da14cd02f21aefbcfe53180f336ce55e8f90/httpcore2-2.13.0-py3-none-any.whl", hash = "sha256:35ae5be347aa40467b4a5dc032ac67ebb6d27189fc97e8cebcf99616f6a1bb9e", size = 83162, upload-time = "2026-09-14T14:18:02.529Z" }, ] [[package]] @@ -1468,7 +1764,7 @@ wheels = [ [[package]] name = "httpx2" -version = "2.12.0" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform != 'emscripten'" }, @@ -1478,9 +1774,9 @@ dependencies = [ { name = "truststore", marker = "sys_platform != 'emscripten'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/a0/e9deef4654132857b5a5dbe4eddd0ac59c2814500e11f2f5044cd81103ee/httpx2-2.13.0.tar.gz", hash = "sha256:81bd07dc67a3701729ef1f777a3c00c915d4539604fdb5afd327f8682f6b7b44", size = 100290, upload-time = "2026-09-14T14:18:05.486Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d1/a0c72b0e006df654709fbc366cc5bcb53e5aee13e1e3395152c6dd293376/httpx2-2.13.0-py3-none-any.whl", hash = "sha256:fc12720cedf72faa26cca6b4ca394e05c894e7d7933fc45cafe767960804e49a", size = 95565, upload-time = "2026-09-14T14:18:03.553Z" }, ] [[package]] @@ -1494,7 +1790,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.29.0" +version = "1.31.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1507,9 +1803,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/35/42316e8f6908b6d21bc8df017cc6efba94fb5edbf99b64e28dd142325e20/huggingface_hub-1.29.0.tar.gz", hash = "sha256:6ebb385a581435325cf6d5c5b233d5d4bc91175834d99fd65dae14379b36e9ad", size = 963121, upload-time = "2026-08-27T12:18:37.432Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/f0/61159db90b5cd275d55516fe27920828e7d3be4053fdbdb27c3f70e5f1ef/huggingface_hub-1.31.0.tar.gz", hash = "sha256:f8e9e710a210613fa5d0f26bba6da05ef4aef9fba5a0f23f508f5ac4d08b6f90", size = 968039, upload-time = "2026-09-10T10:27:22.724Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/a5/47c2ea9b228ccbcba8467e9a64823146e8ebbad29855e591d8f5eedcc9c7/huggingface_hub-1.29.0-py3-none-any.whl", hash = "sha256:b00f7782afc14db4bc6572763810a635bdfbab8623d957bfb553bd18e03852cd", size = 795768, upload-time = "2026-08-27T12:18:35.431Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7f/3f886a625043b77312b80da2f2bf00b5ecbf5a73061af1aa0259cd258c9d/huggingface_hub-1.31.0-py3-none-any.whl", hash = "sha256:9dbb6a503cbe2494ea666695207e7262d410659e09134059deb83e5480864667", size = 798313, upload-time = "2026-09-10T10:27:20.798Z" }, ] [[package]] @@ -1572,6 +1868,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/78/a92b09507565f8bd27eb72a7df3ba59a1ea0a5baacbf155c587d0be51a13/isort-9.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:466b0c3f156a21c10edefba697e641666bc26ffb0122bf08b42caa3d464c20aa", size = 1639624, upload-time = "2026-08-27T20:53:52.22Z" }, { url = "https://files.pythonhosted.org/packages/52/ba/f5efd28563bc751792a7a1f64ae73db57dcbfa0563dc3e0cbbd825a48252/isort-9.0.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:5832683294dd61c59d00cd043a68d42f6ecd7dc7d04b73ac777f7f90a534d6ae", size = 457708, upload-time = "2026-08-27T20:53:53.837Z" }, { url = "https://files.pythonhosted.org/packages/53/d6/a776ea214de5403f038b182e1d5fd2211d0cee41f2c62ac9f2f9a0ecc463/isort-9.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:7281cdf538f682b8d75fa44bcdad1b299036bbc440855f7d61412b3b85d5727d", size = 874790, upload-time = "2026-08-27T20:53:56.154Z" }, + { url = "https://files.pythonhosted.org/packages/db/42/d75a674a7fcbbf4374cd358b8529535d3c59e0ab6acc164b6862acb95058/isort-9.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7ea5f505b152fedd2b990b39d8b76108a48b355da874025aad4982e8ceeb0f3d", size = 1020950, upload-time = "2026-08-27T20:53:57.744Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b6/04d74c7c42b9af1c0d47f34a79722dbbcff71bae3765c9250376ebd44ec5/isort-9.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:873cf1b6371d41e2a74d57d7c0176d311822f0415441abf8251ad074c9fe4a66", size = 1653678, upload-time = "2026-08-27T20:53:59.292Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/621969c0cde272b7a183484589cc9f28132775d78fd708c231fd292c7abd/isort-9.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99b7bc28b1f05f7e3267629043a99c6c479a750df3689327a10324e396827f94", size = 1645589, upload-time = "2026-08-27T20:54:00.911Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/e3db1ae5d5fbe9e2dff0c6a0b2b9dda07a742ede20f8fbc7d78d5d73f472/isort-9.0.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:89ebbcdbdd9d66cc14909bbac36acb9db29f37325606113c9f270242f8a1f896", size = 457029, upload-time = "2026-08-27T20:54:02.551Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f2/b9cf816c8dfd5d9066b65a2f738f8d03fb0f6001bcffa3d436cfdbe1b421/isort-9.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:2057236a764f31c78dac78f7343057621fcc2fd40461ce61061f34fd09066f46", size = 894580, upload-time = "2026-08-27T20:54:03.985Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d7/e5e9e477ff7ec9f75b4c07b25aef10814775200c1e90bc9acbb788def54a/isort-9.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5e72a7063570f1d740f0284c7ae5739dc34c6a2d9f1049b13027a5bdadb56682", size = 1088685, upload-time = "2026-08-27T20:54:05.376Z" }, + { url = "https://files.pythonhosted.org/packages/96/6f/915effa62cdcdc757bd0c567ee4d611c7a73ce7da097c820b3b2f340c2f7/isort-9.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2525606f62742fc4ed9f8ca89043b9522ac3e6f9c9892e6cb16f4870d937f38", size = 1863445, upload-time = "2026-08-27T20:54:06.851Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d4/af84d711cda084ddcb7a01a3d6a2608542c16ae7d418d99169b7deb775a4/isort-9.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e3a2697ebcb54b51af4833de44447dbf31ddf081c5f163772092d21c0267483b", size = 1878639, upload-time = "2026-08-27T20:54:08.618Z" }, + { url = "https://files.pythonhosted.org/packages/58/d0/d77ec7d1c648a0b92ea6a8da3591a342550404af98a338106278a872ea1a/isort-9.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:fd326823ddbe338357ba1823b7f96481d4421d54c83ebd43c92f1b51314a24ae", size = 919565, upload-time = "2026-08-27T20:54:10.094Z" }, { url = "https://files.pythonhosted.org/packages/af/6e/4ec84f19b008864656b9f158bad006aa6346e754694aff47a7fa9ee65a19/isort-9.0.1-py3-none-any.whl", hash = "sha256:5aac7263b7a7f9f647f94fb6df2761ff5b60a7168eb492ff39dd30443207fa19", size = 103487, upload-time = "2026-08-27T20:54:25.395Z" }, ] @@ -1631,42 +1936,70 @@ wheels = [ [[package]] name = "jiter" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, - { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, - { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, - { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, - { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, - { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, - { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, - { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, - { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, - { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, - { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, - { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, - { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, - { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, - { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, - { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, - { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, - { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, - { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, - { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, - { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, - { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, - { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, - { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, - { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, - { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/1f/8176d92e001f86505424b41664032ae26a882bc9ca41a32c803f373f9195/jiter-0.17.0.tar.gz", hash = "sha256:03e432f226a453851079fb84cd17c6da9991eab723e28d716f14ae3d906e0c12", size = 229037, upload-time = "2026-09-12T15:14:14.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/f8/07bd8c3a23f7a8a6875e6a820bbffe1483a18f18f9398a91b5495123176e/jiter-0.17.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ebf918dfd6a74adc1b9ad71f63c4ab00902fcd3b7fd39f2e24d871db8d713b91", size = 291633, upload-time = "2026-09-12T15:11:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5e/0de4c6f84ffefa6809ffc2d550b9a314365acf7e7ec9b6c7375d49047900/jiter-0.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61aed66ee042b3b49ef85fdf75714234d055d89d8496ac1c6e47f89e7a30d5e4", size = 321695, upload-time = "2026-09-12T15:11:52.727Z" }, + { url = "https://files.pythonhosted.org/packages/20/ac/befe2e82065bee37a0252081666ed2f48c1ac5f5c6c318c2de8168ba393d/jiter-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76eb4a5c20e86f9f848286f167024890f2862258a965d254774deb7fc1545ca1", size = 341967, upload-time = "2026-09-12T15:11:54.231Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cd/9797c1e529746750ae589da7c1a8c24373f00d88e11a989f9e5eb1959079/jiter-0.17.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bcc064f99183a9cbe7f26ed648c352031a74145cd61ed75d34632c73eb46a5a8", size = 326546, upload-time = "2026-09-12T15:11:55.41Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fd/e6914c38d6347bab4ebff2b1f0c0f191db276e7a1d5c376176757da42fe3/jiter-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73b64e69c4150748e020356d958af94bec33c70a0a93d665cfa8f6d580fe1a63", size = 340995, upload-time = "2026-09-12T15:11:58.211Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7d/611b3abf6f88945b5474da5cdc6d1a185e805ac9bf446bb7766dcda6ea87/jiter-0.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0bc7f684b65bcda9c20434267577db71bf9905ceddd32b60d1d93278d8c8d3a", size = 352188, upload-time = "2026-09-12T15:11:59.414Z" }, + { url = "https://files.pythonhosted.org/packages/52/f8/b6e513ecbdf3b3cebe587c2279281ecf775b729a58cf4cc7bdf898ded029/jiter-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c21265b251d99bbb40080d178a8953e35601d3a1564e05c4de4c0d2ca616797", size = 345025, upload-time = "2026-09-12T15:12:00.697Z" }, + { url = "https://files.pythonhosted.org/packages/28/a8/fe26d06c5a6c5a4cfe703c5154c8a140da1305671eb3681aba9422d4f393/jiter-0.17.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:f3d7f7b34114f7ddc6d72a8e882d49de636b35d9fd12b4d420d3c5729f6c9812", size = 329180, upload-time = "2026-09-12T15:12:01.831Z" }, + { url = "https://files.pythonhosted.org/packages/e1/58/e6d66a26af40a20e62486feb7e222fd50f6e7aaa4f107abd89675dcc835b/jiter-0.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5078ab00664307fab2019b522a93aeb191122789f085daf5fd9e362154021d4a", size = 335805, upload-time = "2026-09-12T15:12:03.056Z" }, + { url = "https://files.pythonhosted.org/packages/ef/3e/96520aa2fef5ef831d95483a902140bfab83dcac9eaa74f7df61b5e50a1b/jiter-0.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:470e1b1e4c42f1ead2189166a299691871a2df5056c976e7fb96feafaf5f9d44", size = 484121, upload-time = "2026-09-12T15:12:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8f/5d9d92fe538bf36ff481a2278c48147e59c1cf8eb2f7be665260665febe5/jiter-0.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6eb6aedeb7352b8f3b6af9cbd67983840165c00428e63f1b420a85885128ea31", size = 521310, upload-time = "2026-09-12T15:12:05.612Z" }, + { url = "https://files.pythonhosted.org/packages/50/06/a09f979b22e652afbc3de66c709b2ba92edcef555f7535ab937c86b4f21a/jiter-0.17.0-cp312-cp312-win32.whl", hash = "sha256:362bb47423886d45a9f705d2d9d4008c6eedd4e41eb1bab4e96fb6daa06b33fd", size = 185029, upload-time = "2026-09-12T15:12:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d9/98265a005b2473ec2be5a84e2b64c2f65382c673879f1574845cd4bcd77c/jiter-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:9bd3caac219df476dd0cc3fe01d2f1581ed588906feac767abd9614c1c12f8b3", size = 227381, upload-time = "2026-09-12T15:12:08.823Z" }, + { url = "https://files.pythonhosted.org/packages/a8/11/2e05bf5a56e57a543ebb8f585074adf09383e99d7b062dac92eab1f4d57f/jiter-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:36ee6e69027396664e59995b9a635a947a5304ee9837279584a0bb8145c8f6b8", size = 183610, upload-time = "2026-09-12T15:12:10.374Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/2c4a8075ed5ea02b56911e9375d4c8d7784572ff4af32e5a99ae0d071044/jiter-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b18434638228c0c184281609bf3d9459026a0f1ea48fb76c205e3ef72069caa", size = 290991, upload-time = "2026-09-12T15:12:11.641Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b1/34bfa29599d420423baac6ff7cada6674fe63d5a7a2ccb3900b904678783/jiter-0.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ec89771f4272b989487a6364e519db6bbaba323e8bbf949ac89a45ea9c18b7a3", size = 321425, upload-time = "2026-09-12T15:12:13.855Z" }, + { url = "https://files.pythonhosted.org/packages/11/71/a5ac64a62a04aebd556afadab14a6b730001e16df87266ded943a100a1d9/jiter-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e3f052c671d5f425cca5ea5901cf11a831369fba4a55a3862cab93c323b4c3b", size = 343138, upload-time = "2026-09-12T15:12:15.046Z" }, + { url = "https://files.pythonhosted.org/packages/01/dd/f761e320ea473314cb68612bc6a435393464dbd198051399b36848b4ebf3/jiter-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:785a216bbaf8f15fc974e964ced7322cd3d774bb0e86949edd78c6bffd6ba35b", size = 325805, upload-time = "2026-09-12T15:12:16.506Z" }, + { url = "https://files.pythonhosted.org/packages/19/1a/27d8e40f0fb29bbc7a5adf30907144396a115dbe93d5d8976c054a6dfe96/jiter-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d85c558c9f8532bba287a990ac63767c7daf756f0d8c030219f62499b1fa228a", size = 340230, upload-time = "2026-09-12T15:12:17.682Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c0/30bcde78a28155461f965d16b7aca4ffca6d17494d905f7a0bb072e6c64e/jiter-0.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c23849235d2142ce444b2b8c6eceee9f82f4cc0bd5c9081602e4155c6197807", size = 351343, upload-time = "2026-09-12T15:12:19.337Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/91420b156315ae22732f5ee1a7b5725a030aab9dc8fd7dcdacfb4aa588d3/jiter-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58df29268a95e910f17db7ec9178eb7f15aa8619aaca3575275c4e6b3f4fe4c5", size = 344990, upload-time = "2026-09-12T15:12:20.705Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a2/ae6d5672644cc11127970277c9aeb0fa6fae376845587f5b0a8e8828167c/jiter-0.17.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a277f97eba7d66b1ee27eb5dab5b774ff46a10c78d89a1d3dcce04ce1357c8ca", size = 328624, upload-time = "2026-09-12T15:12:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/04/62/45cb1162f6aa586536e4a973fc339d72dc6b08cca030d70a838a307aa778/jiter-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe15ddf316f1f1f643347d3a474e74ce61880c79a11ec5dca53df20c071bd3e8", size = 334731, upload-time = "2026-09-12T15:12:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5f/45c1574b644da7deda0b7591c349520dcf83ce45b24d7ca19922dab1fc27/jiter-0.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:02adebb7ce6413c44d40af9ad59d1c1cd79630ccdcb6f7bdd2d461e48c03d8f9", size = 483649, upload-time = "2026-09-12T15:12:27.557Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d3/ebea1ecb5b241c519f192b30215c79a8e47f42f1621acbcd6f8830728416/jiter-0.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:55d0e0e613a3f9ad600cf436e0e2b8057d1b52bcf1d91b2d36ac53451231e6a8", size = 520758, upload-time = "2026-09-12T15:12:28.99Z" }, + { url = "https://files.pythonhosted.org/packages/64/e6/682b641ff0765ea9bdc349dbc7d223de5c8af8ec1abda0db3406992f92fe/jiter-0.17.0-cp313-cp313-win32.whl", hash = "sha256:2c45ad7c973ef33fe5114a953377b35a95240f4542c0724d9f781e47dc24bac7", size = 184334, upload-time = "2026-09-12T15:12:30.813Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d2/9a49aac2b27af4cc5015e368c0cc3588491a532f717a668ffce1f1ac57da/jiter-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:a3cebb1fe4a1abb00465f3f8a17e09112603e8b7c59e5c3adbcd9f7815a64acd", size = 226601, upload-time = "2026-09-12T15:12:32.096Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ce/9a43e9f614608eafa78de22aedcff54cd21324467b5d442d5c9b00244145/jiter-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:96b8b0c6dc5d78682f54a450785e075aa929cde768304cad363cd4efba5a82ac", size = 183103, upload-time = "2026-09-12T15:12:34.396Z" }, + { url = "https://files.pythonhosted.org/packages/01/9e/23065f8e2c7a4c372c1b6f6622e4cfab4dc786cb5150052b1527e6a6a840/jiter-0.17.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:00d783a779c5664e16dbad5e3a3c3a75e128b07dd5f4765159658d9210a50ca5", size = 292210, upload-time = "2026-09-12T15:12:35.613Z" }, + { url = "https://files.pythonhosted.org/packages/ea/81/67b58647560bc82a4490d722caa8561d7a86a9f45d4fa620b7e5fe282c7a/jiter-0.17.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0619d806e260ecf0c2a64521942c94af5d547c9ec99b55ae4f51b538b5576a76", size = 321512, upload-time = "2026-09-12T15:12:36.907Z" }, + { url = "https://files.pythonhosted.org/packages/c7/07/6658359a25f55927f7f8bf0e16465dee2ccd0b2a1a5208acc0df8972e074/jiter-0.17.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc0288ce39190ee33fe6e4ec73161eed34e7e2da509b525546ca061778d62b64", size = 343897, upload-time = "2026-09-12T15:12:38.189Z" }, + { url = "https://files.pythonhosted.org/packages/46/04/5d50a9f0319cbdc37fd53c27f8c313d46afc34f1b048219ae6d8ea068da4/jiter-0.17.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5a52a430d04225ffde633e6840bf2381d34c019ff98526b5929755b9052fb199", size = 326519, upload-time = "2026-09-12T15:12:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c7/d02517832b29eb8275fdd0f4ce0f17b80f58cc4c3ebecd4d9ace990d633d/jiter-0.17.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37f33d327900bf2879613b3363fd48df97b4232d0c41f54bcf2e790c2fc40a71", size = 341369, upload-time = "2026-09-12T15:12:41.486Z" }, + { url = "https://files.pythonhosted.org/packages/3b/07/499b5f5603501cdd93a73a6a176dfad9c96555a3ae58ca9f8e3acba63dc9/jiter-0.17.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cf564d43c4388149ca58ee571d0f5ccf875e20d1fd4662fd94cc0d1ea3b10ef", size = 352160, upload-time = "2026-09-12T15:12:42.721Z" }, + { url = "https://files.pythonhosted.org/packages/f5/75/b04013c7743269d4533ef4e746fc0ed678a143968dd7448658e3f51daad2/jiter-0.17.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:523c499235fb65add25d4bb01b1c4709ce695efdc7deb6c0a7bc515b5c44e0fb", size = 345018, upload-time = "2026-09-12T15:12:44.192Z" }, + { url = "https://files.pythonhosted.org/packages/1d/96/cbb6fd1e42a77c8412ec4643db95059b30cdfc635e387cc9193e098ce268/jiter-0.17.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:455e4ab35cb2a4a91a8404e08fd3c621bae433922e59bf1c494fe20a426b013b", size = 329244, upload-time = "2026-09-12T15:12:45.491Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/d3be402f398566a379bf40ae65be5c3505b14d9e95e0802a597ddde7ddee/jiter-0.17.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6871973bfbd4408f7f1c632b30bbb5bbd9671c1bc8650af6823e24b7be13709b", size = 335693, upload-time = "2026-09-12T15:12:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8d/98e2c4130b93d64f1d67c89060b928d04102549bf05e64451c9e6024f9ca/jiter-0.17.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:77f6aac0137309b31448c1bdcda4c6c77077664a6d018ece8d94019c68a5a5b9", size = 484329, upload-time = "2026-09-12T15:12:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/78/5e/8da91e49f0fbca37c3489fb4cf3ad6676d4965f00ae5468bca3a2513737a/jiter-0.17.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:93946d89fa04d5ba64dd323a8dd8d901676cb8a3c81d99ae4f6c051a9b4c3f2f", size = 521358, upload-time = "2026-09-12T15:12:49.856Z" }, + { url = "https://files.pythonhosted.org/packages/be/21/5388684a5a38af3557cd9c2424b9827c71809cff24373c75ef9d0d3dfba9/jiter-0.17.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:70f19a2ca8429f91e82eeffb2f51cb87bc2d6e953b009b91a92d29c3a16ccb03", size = 110459, upload-time = "2026-09-12T15:12:51.747Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/58b3a93525d2ffca7f54d9dee441381990082bd1172fbeb8d6a3f72a4dc3/jiter-0.17.0-cp314-cp314-win32.whl", hash = "sha256:71dbd74314c5df52a1bccf7b8bca46d14e943af7a2012e73b23f49977ef194c8", size = 185043, upload-time = "2026-09-12T15:12:54.477Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4a/1aa520eb6c359b262c14ff995ca7283837208ddfb1202082ce9d73cf214d/jiter-0.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:ac3c6ee3264d6f5c44c617f90bc7e8b9e1587e7d6708c9d8f811cb65582ee312", size = 227163, upload-time = "2026-09-12T15:12:55.931Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e4/5997f648794bd9b499491d0ff480b096cc9a9c65bdba29f57568e6aa1705/jiter-0.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:6219adaf59711ba7063a52496e8ec6d3fa3e209d7827d83eee3b2abc780a1744", size = 183505, upload-time = "2026-09-12T15:12:58.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/84a5ec271d09f7590b6073af5ee4abb44eab4ccace453b7e2c5ce45234ca/jiter-0.17.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:59bddbe6f9ffecc68d641e1e2d619ce64cf8a9e9eeb74e5c518f74fc87abf1b0", size = 321527, upload-time = "2026-09-12T15:12:59.394Z" }, + { url = "https://files.pythonhosted.org/packages/39/71/9e1fd0045f5920b4c36be35c3f0f0dfd123668684f8ad352619d7aa44183/jiter-0.17.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6cb41cd1432f1dc19a231cf70b54d42b2c9f05085155859263fce06fa4d41388", size = 340865, upload-time = "2026-09-12T15:13:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2b/14627fd2bc377f3dd09491bcace6b90e34b4d7fea2f1f3295031ff91f528/jiter-0.17.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd7790aa79c8b518e512ebcdfce9f11d8ef5f30efd43720c8a19a548b39fa489", size = 325412, upload-time = "2026-09-12T15:13:02.152Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/8d5808f7bf0f456bde79e6393587183a0cee5f83d179fe1f7f1eff2ba067/jiter-0.17.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dbbfe4e3c21c8166980cddc5bee1a315df082454f007947dfb6fb73800768165", size = 340473, upload-time = "2026-09-12T15:13:03.485Z" }, + { url = "https://files.pythonhosted.org/packages/4f/da/1d8c7c6c4ae6b2423b94a81b6b907d37b28f87664e077427b531bf1b5313/jiter-0.17.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c286860abfe8b100cac1c02e225e5776eb9216edd71ba17cdb237da4af32bc9", size = 350757, upload-time = "2026-09-12T15:13:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/eb/96/c1813dcca15c5a370145a448aaea7d1f83f6f0228a5f1130e79340ee385f/jiter-0.17.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f753eb70b1474a29e635e7542ff7312e6d6b951e0b25e8a2e8c34eeb1ddcd478", size = 345203, upload-time = "2026-09-12T15:13:06.131Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f7/fc61cbcf2992d169ede13648fc3fd8e2d3171a3669dde43cd4db556549ac/jiter-0.17.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:eae86b1f027031e39db2e0e9c4842221edb7b8cd474d23f87a79b3bd4b651768", size = 328322, upload-time = "2026-09-12T15:13:07.392Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/46418a3abbdffb7dc41b314200360f24f75faaeb35573e81c92de322cce9/jiter-0.17.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5bf350452a43173e69e1fc74847c57a60e3d7515807287f29849baa2a85d8718", size = 336570, upload-time = "2026-09-12T15:13:08.666Z" }, + { url = "https://files.pythonhosted.org/packages/f0/28/b8a55b949be6306df8888e365a8df05441de8a7b11289f6957004302e41e/jiter-0.17.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:da139721f4b7cafdbff580a4f511ea24cb91f4909330c6b926a1ca53836c0a59", size = 482879, upload-time = "2026-09-12T15:13:10.037Z" }, + { url = "https://files.pythonhosted.org/packages/75/3b/21d0afa53ba0680962c39f3eb95ed2946f8793369ed44b0c82b490723081/jiter-0.17.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:8079849db9a1371bfd90bad088458a8fb836261879df2233cc9632464ecf64e1", size = 520406, upload-time = "2026-09-12T15:13:11.456Z" }, + { url = "https://files.pythonhosted.org/packages/ef/03/bcbaf8b6b9ea23c2c074411f8ecfbb02d820abac5d0cb8f4e280209174a2/jiter-0.17.0-cp314-cp314t-win32.whl", hash = "sha256:8f770b0c77e5fac482e1ba03ca1a7e18286bfb213d749932a00a7e4cd5de5e06", size = 184434, upload-time = "2026-09-12T15:13:13.037Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b5/5d6ce2c93ef6fe1241b37a9005547f9b6d58db1f07f39fe95807d4b98f51/jiter-0.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c4289293e5278d9314b00f15c37f2120fa51d3d68565292e715524c750e775a9", size = 227392, upload-time = "2026-09-12T15:13:14.933Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4b/1e52baf90187606e33a7b8cfa8f96f5829acd7f01870077eb01059ab76d0/jiter-0.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4dfbfe5a6e1e80a7082af559f66386405025ec278833e0c649f69cbc6e1004cc", size = 182776, upload-time = "2026-09-12T15:13:16.239Z" }, + { url = "https://files.pythonhosted.org/packages/17/31/4bb27f54333d3b9ef1e5bd3312dc0b4bbe59c68bb0885fdb40583a6b1567/jiter-0.17.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:454c4997d73cc466c71fd565d91e603b0274e48ea0c6b0b7a7aee6967e4ceb7c", size = 288415, upload-time = "2026-09-12T15:14:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/879570ecf82574eaea77c5eb10309f4b630dece5f2a556e9814a90ba3f2d/jiter-0.17.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:40d2c240f8f80b5b0f201b29f0ae129c81448c60c772227a41747b5e0026f6a2", size = 279113, upload-time = "2026-09-12T15:14:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/77/7a/1f0b8a35fbd079a4f1752c31a15dc99cf277f863747c459be0af39e900e5/jiter-0.17.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e05f5adbf68c4bd11e1610f394034d984152988e84be6f8314235ce6f2139e5", size = 303708, upload-time = "2026-09-12T15:14:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/d76219ebdbcf3d4209d9d21a0810db4c8d0a6f88e3ee87d30bdea4e90d30/jiter-0.17.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2c0bf24c72fd0491405dce5d40194f2070e9021ce648c1a1d46234b93d848ff", size = 307147, upload-time = "2026-09-12T15:14:12.897Z" }, ] [[package]] @@ -1822,6 +2155,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, + { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, ] [[package]] @@ -1886,6 +2250,8 @@ dev = [ { name = "pip" }, { name = "pybuild-deps" }, { name = "pydocstyle" }, + { name = "pygal" }, + { name = "pygaljs" }, { name = "pylint" }, { name = "pyright" }, { name = "pyroscope-io" }, @@ -1924,6 +2290,7 @@ ogxlibdev = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, { name = "peft" }, + { name = "pgvector" }, { name = "protobuf" }, { name = "psutil" }, { name = "psycopg2-binary" }, @@ -2000,6 +2367,8 @@ dev = [ { name = "pip", specifier = "==26.1" }, { name = "pybuild-deps", specifier = ">=0.5.0" }, { name = "pydocstyle", specifier = ">=6.3.0" }, + { name = "pygal", specifier = ">=3.1.3" }, + { name = "pygaljs", specifier = ">=1.0.2" }, { name = "pylint", specifier = ">=3.3.7" }, { name = "pyright", specifier = ">=1.1.401" }, { name = "pyroscope-io", specifier = ">=0.8.7" }, @@ -2038,6 +2407,7 @@ ogxlibdev = [ { name = "opentelemetry-instrumentation", specifier = ">=0.55b0" }, { name = "opentelemetry-sdk", specifier = ">=1.34.1" }, { name = "peft", specifier = ">=0.15.2" }, + { name = "pgvector", specifier = ">=0.3.6" }, { name = "protobuf", specifier = ">=6.33.5" }, { name = "psutil", specifier = ">=7.0.0" }, { name = "psycopg2-binary", specifier = ">=2.9.10" }, @@ -2064,7 +2434,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.99.0" +version = "1.101.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -2082,20 +2452,20 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6c/1a/76b5f28ba9e07fa0cb807bbdefdf318c554f60623bff699e7b601d0d7b3a/litellm-1.99.0.tar.gz", hash = "sha256:594bf4b6ff6b79c6aa3c3b78c0e939d4afd12687e076ba3fb608d38a5aa7f9c6", size = 16786386, upload-time = "2026-09-01T00:43:07.434Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/c0/08a31c8c5f7fce96e98e2a7f818f446d0468da484228a7450cd2b6f71dcf/litellm-1.101.0.tar.gz", hash = "sha256:734ab2b8cad6a3b582d52d9c9c5fcab759eb382b93935ef808fda0e16d822ac3", size = 17447699, upload-time = "2026-09-14T23:15:36.135Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/ef/4ef1afe95d3a7c52d63b5f2bb32ec5264a4cea198a7dddb8ceff9633f2da/litellm-1.99.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a43e8716da8beed04480e91b4233ff2f1ab1fedd84cad332dbc526b54a9229ca", size = 23350560, upload-time = "2026-09-01T00:42:41.449Z" }, - { url = "https://files.pythonhosted.org/packages/03/8d/f329cf74fc1f929a93210b16523bbcebd679694090df741bb8bee726a473/litellm-1.99.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e2b383070656fdbec4bc44602edaaed2a21e99ceee4ea0a4650c8cb381e67b59", size = 22999186, upload-time = "2026-09-01T00:42:44.978Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4f/0e73fc3f0740249b676d0714bd58c4141f1fc733b88a85b6001f9f2e5e62/litellm-1.99.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e461b7ce53af990e5287cf7ae30d82c956b56dcce886f63ef39a76e678f82a3b", size = 23145753, upload-time = "2026-09-01T00:42:48.19Z" }, - { url = "https://files.pythonhosted.org/packages/db/c4/8e92a6277e28096784616cfda16b2cd839c7bdaabce692ae950e2f0cf72e/litellm-1.99.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1c45097e426fed2ae7fbd38b5404c3addeb203d0e1148c0a59848aabd5fe83c6", size = 23518654, upload-time = "2026-09-01T00:42:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/a9/aa/11c9bd6297e4a9001426ff825bc8bb119d3a9a3de4e5ee9fed22997e6e28/litellm-1.99.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e42f94731665b68e263481efd79f7629e9b97eed7e10c57dc37a890eca058227", size = 23218889, upload-time = "2026-09-01T00:42:54.99Z" }, - { url = "https://files.pythonhosted.org/packages/3f/a9/1fc21c6fb21897867d9c753d651b24903e63955dadffeb2a73517f35003a/litellm-1.99.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:71109c323164b4b6776ff259876523e6e883a465aa1413dd51a1bda8e92efc5f", size = 23617457, upload-time = "2026-09-01T00:42:59.173Z" }, - { url = "https://files.pythonhosted.org/packages/7c/4f/df449e0cfa1f40025e91f3a47b03109e16de44d43149e97fbcbd1ae8c5e2/litellm-1.99.0-cp310-abi3-win_amd64.whl", hash = "sha256:5617804e838499bce8fecb41ad9bc984b7977361e557666fed0fef0c4623ce62", size = 23432066, upload-time = "2026-09-01T00:43:03.177Z" }, + { url = "https://files.pythonhosted.org/packages/ae/cf/6b3a687ab0ab4caacc35faaf695f6862654e21e2a5dc703fa9e1df6d7069/litellm-1.101.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7cc623a224c6f11a04367a682b095a1e08e5f6da75c990a650910db5f9777819", size = 23777389, upload-time = "2026-09-14T23:15:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/42/f6/d5bc3aa1944244fd186e65ca16e62158f0f474b40f99a89e568a5d441739/litellm-1.101.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d4064024151ff2877e542b56c6bb6a39e0c3e6651abf4639af9346a678586e52", size = 23434831, upload-time = "2026-09-14T23:15:15.245Z" }, + { url = "https://files.pythonhosted.org/packages/ba/12/755e4d497b975911449102b8b74d3d4f770137c4467e3e8fe304adf22d66/litellm-1.101.0-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:cdf3351e394206e785bf339f1a39d9358ddf4e4f38129750bfdb5e4f5ceab8ef", size = 23568059, upload-time = "2026-09-14T23:15:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/0c/0e/561d7a314a08940688814be590544907fdfde9225dce35badbd79fc9ad4c/litellm-1.101.0-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:140ee0997324b8fd2405f7f8c26f42c0f364996dc4030187344a90cf27916667", size = 23944856, upload-time = "2026-09-14T23:15:26.417Z" }, + { url = "https://files.pythonhosted.org/packages/f1/df/d640eb6cf4a304be4ec289b72c1b1d9bd66c73e90167211d3a09ac37b2d8/litellm-1.101.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ad013161074676fc91b91d828f696300132cf3936d3f386c4701a41710d70aa3", size = 23643397, upload-time = "2026-09-14T23:15:28.664Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b6/9d69e95de9465cd23651c6623518d5e711d8776a0e58e6ea62066c9d9470/litellm-1.101.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d88053148c5c6e016e495273eeaf8f3a29779b60d18977ec42e2a6bbd2d8eb", size = 24042718, upload-time = "2026-09-14T23:15:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b5/0a54001992c9a8d991f6ddd7532e3c4cb7dbdcc94220a37586513aab6f89/litellm-1.101.0-cp310-abi3-win_amd64.whl", hash = "sha256:e5601ae404b0f1e38ce1e65c75f880254485942d9c824453e3926f2323457fd5", size = 23844988, upload-time = "2026-09-14T23:15:33.632Z" }, ] [[package]] name = "logfire" -version = "4.41.0" +version = "5.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "executing" }, @@ -2106,9 +2476,9 @@ dependencies = [ { name = "rich" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/1a/529f5fd3d0b72eca62737e07b290d38737f104f31891d23e5ed47a8ec7a0/logfire-4.41.0.tar.gz", hash = "sha256:3806fba60389d57c38a12a88135a7c7bf9d0fca09325094517e976b29b5b9d33", size = 1302531, upload-time = "2026-08-20T17:42:23.037Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/da/0c73a4447dffe01e1737580556f5f061ffda8f85e46fbf35ac36f5b64d27/logfire-5.1.0.tar.gz", hash = "sha256:edf6c7d33c00214e11c2cdc567eb53051be36289747091e8bb910527e5970bea", size = 1359738, upload-time = "2026-09-11T10:25:11.674Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/e1/ee33bf0e3f85c00a4235c8c9c4e23f3955154d842f15f0377936461ec6a3/logfire-4.41.0-py3-none-any.whl", hash = "sha256:5bae36637aef81eeee6bfa5d764bf3cff0755af613a4888fd0ae4a656cd2451e", size = 426654, upload-time = "2026-08-20T17:42:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/b32c3d5c298e4a9010694d261fb82c2c672747c0bf2b47bffda484aef349/logfire-5.1.0-py3-none-any.whl", hash = "sha256:c1d54eabf39f4c8f5e18e71f35da13375c8adb144c263f3076e650ba85929108", size = 474841, upload-time = "2026-09-11T10:25:08.131Z" }, ] [package.optional-dependencies] @@ -2118,11 +2488,11 @@ httpx = [ [[package]] name = "logfire-api" -version = "4.41.0" +version = "5.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/83/a2e7de43bb092ffaad904b5756cfc1e0ea4a8d79fdacd24cd55e60790585/logfire_api-4.41.0.tar.gz", hash = "sha256:ec39252acac38b5b50d60cfb9cc62f0ea10c841345fc59692af32dbe3de4a140", size = 92818, upload-time = "2026-08-20T17:42:24.302Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/8a/3d108abf3ad67166fa22742b414ec386de042e5762de3e7b0c644cd6f359/logfire_api-5.1.0.tar.gz", hash = "sha256:16abf9d7d35e08cda2dab66fd0883cabaa4d4206f3dcea0f5967dbc823f01d6d", size = 96091, upload-time = "2026-09-11T10:25:13.116Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/96/97552d0d742866719b3a6e8fd7e68dd2877804f4c3b10c44a19a1f99b0d6/logfire_api-4.41.0-py3-none-any.whl", hash = "sha256:c71010d086c0211b04b4640181e836e8a75cc635fcfa7f712557f7b0676c1413", size = 143003, upload-time = "2026-08-20T17:42:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/2d/51/123dab197fb08c0892e3bea1c2266bff159aa2ee82ad43bef93505859705/logfire_api-5.1.0-py3-none-any.whl", hash = "sha256:f3ad92eed63f7fce45580bf92ff85367c695c4a5bfee52241d71a8bacd3e3697", size = 146617, upload-time = "2026-09-11T10:25:10.054Z" }, ] [[package]] @@ -2181,6 +2551,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] @@ -2194,7 +2586,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.29.1" +version = "1.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2212,9 +2604,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/48/0bb26fdfe7ac16875f534a101ce2405eae192bdef37e7451f2f4507c13ec/mcp-1.29.1.tar.gz", hash = "sha256:1967ba4c315f7a375146209949f45950d18b0efd2f913d7cf3400bc723ee5f04", size = 646823, upload-time = "2026-08-24T18:30:41.161Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/93/0142dc84a666daf8ad51a34268f34c12fd6fda4f3810c4be2504eecc8212/mcp-1.30.0.tar.gz", hash = "sha256:445414625fce5c295faa505bb11bacece661ab6f4028d57c935db57820b7a3e4", size = 680511, upload-time = "2026-09-07T14:34:15.845Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/04/d6b4fb82eefe9e81807aabca1ac98f460ae0883974b83a997aaa20c52545/mcp-1.29.1-py3-none-any.whl", hash = "sha256:b6310eeb59153300c4ab8b9aec4c52f4819a2d6a8e429eb43d908bed7c783648", size = 224653, upload-time = "2026-08-24T18:30:39.573Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f4/e58bc33317c92a0203664daaf00bf6f41166cc0149e5d6870a03f7cd004a/mcp-1.30.0-py3-none-any.whl", hash = "sha256:666edb5009503e1047c9d60346a756f94b261f05cc2625f23d41c728ffc484d0", size = 234581, upload-time = "2026-09-07T14:34:14.266Z" }, ] [[package]] @@ -2263,6 +2655,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/91/1aa88c9f9dda541265aac80bcdddffcc641374b8fe05a8caa6b3f02245bf/memray-1.20.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97539fd71c565c55e208df9ea28ac158cd156b8c33564853685688595c5a991b", size = 9554666, upload-time = "2026-08-07T20:15:06.516Z" }, { url = "https://files.pythonhosted.org/packages/a1/a6/d31b07fa7aa757c3d2cf9bc4850eb4cbfbbd3bff1f6dd710620248b57739/memray-1.20.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c3aec7a4d0b0d0eb55d72e815a492ff060a3a46b014f6ae216ae68a006ea304", size = 9791047, upload-time = "2026-08-07T20:15:08.63Z" }, { url = "https://files.pythonhosted.org/packages/14/0e/65901e28faeb25a27655a43dfdf0e4a36dae5b12f3ee66d7ad849ae8754c/memray-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c18705b70d20161616d3fc9d0b0c7796211b3ab16499dfb29c8f3ab7ece46c2", size = 12431537, upload-time = "2026-08-07T20:15:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/7976092700eecf4e36ae5793020d18f3feab96aabc2f50d8c7669f751326/memray-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:90fc0ebfc6a264fe3b77d5c263678eaf4a47a0f4288e1b6a04ae3439743cd4be", size = 2227009, upload-time = "2026-08-07T20:15:12.733Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/0f44b1b626169d40defaa2ae9286a70d40653ac26865f0604a882a10ffb2/memray-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1503ac18928d2493662af95935b9ccaadbf9f2579d86f6757304c23291a077b", size = 2195287, upload-time = "2026-08-07T20:15:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/09/45b74d8750ddccb9ad76a8ed639b46b2c3990f99dbe77f800f6f524257fe/memray-1.20.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d92692fb5266aca4322e2001e18810207b170dfc7ce2e8f0570c7ee181574063", size = 9871353, upload-time = "2026-08-07T20:15:15.595Z" }, + { url = "https://files.pythonhosted.org/packages/54/b6/d0b4eb7324bf47b52165aea947bb8942807a40684a64f9decdf4b2a34387/memray-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37bc107afee942f162d30160dfd7f28c2b989251c574570f2f920f5bbbb323e4", size = 10112228, upload-time = "2026-08-07T20:15:18.15Z" }, + { url = "https://files.pythonhosted.org/packages/96/18/b70db3a617ba0be54730a0ddf72822df62551d503bba5c131afea1b47da1/memray-1.20.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83cfa504ed4e1061b85af4b3c40228616a852851ebf5968ff1c3d85e363f8d24", size = 9550151, upload-time = "2026-08-07T20:15:20.623Z" }, + { url = "https://files.pythonhosted.org/packages/b9/35/3a972ce61e83648f970dbff8d54dcb0a6a54ab298e283cce139c40c8615b/memray-1.20.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5588174a24ac150100faa7ee60b535b0f09eae82bf672133443425a00704b769", size = 9776012, upload-time = "2026-08-07T20:15:22.822Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d3/7d59a88509a301aa02c033b95460266553a689fc99f5b1b0d0f138675fc9/memray-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2f51218744259983ad0ca66366ca2ff80158513132ca9bd835b1874a453199dd", size = 12425490, upload-time = "2026-08-07T20:15:24.977Z" }, + { url = "https://files.pythonhosted.org/packages/27/71/3560aa204af1771cb25ab91489c48643dfe1ca0ce93783fb70ad677ca058/memray-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:610643cfa186f7958a476e5589f6e8a1b0b7b3008ab10250a0f02575a692ef1d", size = 2239309, upload-time = "2026-08-07T20:15:27.025Z" }, + { url = "https://files.pythonhosted.org/packages/02/e4/f3981abfecc1572fd24274386f3cbbc36b861704a3e3a10aebe2775aa1a0/memray-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:420bc47528fedf1a92832290c7a7b8a940c2e30bd9337a85811da18a27e07220", size = 2213281, upload-time = "2026-08-07T20:15:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/ee/a04a21557e50d76eef59f0fd4d5f293a2334142540a7180bb627d8ef5b73/memray-1.20.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:59e44042f2495698f6a17fe0a831051ea515242e567f08457ee8546283372993", size = 9836577, upload-time = "2026-08-07T20:15:30.102Z" }, + { url = "https://files.pythonhosted.org/packages/31/66/13ec31746d61855bbe05a090b26a0a9aa6fd741bb3a9a2c0122061d3b985/memray-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ffd3c7c53419465922be92654c17ebff577708fa1f74168dbef9eb5efa6bcb02", size = 10087796, upload-time = "2026-08-07T20:15:32.042Z" }, + { url = "https://files.pythonhosted.org/packages/f2/37/27da92dcca4c19ca19fdd5c43de474755eac9d16255a0d8f2104d5976998/memray-1.20.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9a5dd9bef7f44331d98174625167bf0a8f61785ed05440d99f581c93790acf", size = 9602483, upload-time = "2026-08-07T20:15:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/9c/cc/6b682e4287dfb4bba6d0e0fd8e685fc3b1f0b769707f09c6305217cf8e1e/memray-1.20.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70010acd3d07dbf41aaca0787de3c81c5d8e22e07a6c0fdf04d66c4a376c0193", size = 9758476, upload-time = "2026-08-07T20:15:36.179Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/ffbba95b0c468d831c63aa6e7dc9e7bb21074d1899c310604567d10ca204/memray-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3270aad45dfa0bc7d32bb86b352aefabee765a8658aace8fb9dc8112a07f2e3c", size = 12388833, upload-time = "2026-08-07T20:15:38.049Z" }, ] [[package]] @@ -2320,65 +2726,97 @@ wheels = [ [[package]] name = "multidict" -version = "6.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, - { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, - { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, - { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, - { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, - { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, - { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, - { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, - { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, - { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, - { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, - { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, - { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, - { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, - { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, - { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, - { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, - { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, - { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, - { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +version = "6.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/95/989c1b5ca17b72128661530cd6e351a0a83cda9a4d6c036e9ed976c18931/multidict-6.8.0.tar.gz", hash = "sha256:5cd4637ce76312ba1e05eb9c5193fec231f64fee0944e135fa1e951242355b37", size = 122412, upload-time = "2026-09-09T13:57:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/83/a4621577679149ea001806f5963f3fc687c391c1bd5217157be2278863f5/multidict-6.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:22a310ad37672a261e55a8b5e28d0ae08cfb68abb1f46418ccd19835c3b8e836", size = 84146, upload-time = "2026-09-09T13:53:49.163Z" }, + { url = "https://files.pythonhosted.org/packages/09/00/236b063f3e606055a3a9ba8faa5d40e6c688b059a58056b055f213476f46/multidict-6.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bb8c7da8c861391f7ae48e3593762be2dabe405109e01aec520fbe1a6d15d14b", size = 51049, upload-time = "2026-09-09T13:53:50.46Z" }, + { url = "https://files.pythonhosted.org/packages/91/9d/954b139bfa969855f2d4cb5ae7b7d44dd7106f754305b6e21a9068213aa7/multidict-6.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10456943903744ae1249728161c96bd9d2f7eb5ee17fcc2ffda2dc32e1bb36c7", size = 49362, upload-time = "2026-09-09T13:53:51.878Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/8774f5b3f6d5266ecd1117876e04b405f0f1ce19aa750b35a826efe6cfe4/multidict-6.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:658f5a1895b804423d97b22d06fc0d0b171c7c01dcc3aa9c8faf0c0e26a249a5", size = 278619, upload-time = "2026-09-09T13:53:53.44Z" }, + { url = "https://files.pythonhosted.org/packages/db/47/736080fec911ed9f2dd57ccab5a8145e4f17c4987de0bfc27bee20e4d170/multidict-6.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c10b22860dbd09982d0b8993b66231a861bea2993d4a817ff35273f6ea285a", size = 283771, upload-time = "2026-09-09T13:53:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d5/b7f41f59b0583f092602308a5e7c16ec5efd00d60214b22511e89a38dd19/multidict-6.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:088b04a66b3c1fce6fe4d771ec184a0426262d0b86709c908477b4ac7965df40", size = 262108, upload-time = "2026-09-09T13:53:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/a52dc06c6e2598672308e3d392fd85b837b23c25dda459bedaea84985080/multidict-6.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9caef53b20a105c0d66518a34be2f71b2783de8d091767575ef86f6ea422236d", size = 289899, upload-time = "2026-09-09T13:53:58.415Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/00cda7983f37d119b86f1f89d5b4cf771ecb6d0fedeb9a0971758d6d6d4a/multidict-6.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a5e1583c14775580da05641240ce0d93f36ce3ddef3d5083a827468b0bcfe874", size = 293025, upload-time = "2026-09-09T13:53:59.973Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c7/4544cc02e45bbfac4d8788b05379bb360021fd8c53fa74b0f624126ac188/multidict-6.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:003a3bddb32915c3f67096ea41d24e53edf710edb65a1f5d0c70ab40b0e4d20b", size = 287410, upload-time = "2026-09-09T13:54:01.652Z" }, + { url = "https://files.pythonhosted.org/packages/43/1a/7abed90b8eba381842235bfa6f4d730204fd7deb374fc87e3ec9b2c2b4ac/multidict-6.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35977263d9bf506dbc65349f63b3b8c91606d4abc110990945e3b94bc671319c", size = 255878, upload-time = "2026-09-09T13:54:03.366Z" }, + { url = "https://files.pythonhosted.org/packages/25/3e/73fae10e15fc4d711975337caff7e494c87de5d0189afe3518b21b945326/multidict-6.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9dc7b4ff6ef184504b49ef9a4113d49a646653b2ce89f5f48c1f57cdf6ba081", size = 277831, upload-time = "2026-09-09T13:54:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/01cfc81492933331147004861bdff201d8adeba8485ecd8f490e755fe7e8/multidict-6.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:887f9a975996032c686719eb7b3e1e7942fab5079c2b778bbd9afe9a9d78244f", size = 275096, upload-time = "2026-09-09T13:54:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/de/59/e9a3773b17297fa1e38fd4b3c6f5f2f458380796be62eca7d0d77c250618/multidict-6.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f3071e6515cc63714d014da8f738ae9fa3997c476203f3cd46de380c2376ed7b", size = 279803, upload-time = "2026-09-09T13:54:08.389Z" }, + { url = "https://files.pythonhosted.org/packages/64/9d/2d712a2605b3971908e3b4f5eb6f98c353d9991e106f684d0e08ae581814/multidict-6.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5f3a2af441670d80ce5fdf13b6c1b421fc1fc7fc5182d58ac7486738bb2b742", size = 284595, upload-time = "2026-09-09T13:54:10.17Z" }, + { url = "https://files.pythonhosted.org/packages/58/6c/21aded8586e552b29892268c576e5745d1a894c5451c9866ca3c06b7ec50/multidict-6.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:31e8901637e20ccb3cf8f8848b5d0f7a00462bf5b34f7cf3dcbb2753b18e8b39", size = 252641, upload-time = "2026-09-09T13:54:11.811Z" }, + { url = "https://files.pythonhosted.org/packages/2a/70/56a415ae0a45e5eae2ec817d46aeb72a1ae777863621c85f1f39d329275b/multidict-6.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:13967dca8b2f33230a1427b52438326bb1c9101a1df22a3309ed3fcbbb3c96f0", size = 283369, upload-time = "2026-09-09T13:54:13.59Z" }, + { url = "https://files.pythonhosted.org/packages/08/7e/7b7cd611fd94bf2f6bd16244c50495867ba394d5baaf8e6e487d39494ab3/multidict-6.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad474c11d851b6fc97cb625e4822bc0cbd567fc07dc2602e28faec5a36b42bbb", size = 281653, upload-time = "2026-09-09T13:54:15.174Z" }, + { url = "https://files.pythonhosted.org/packages/33/4a/b19a5892ef2ef6c68ae278b4f1504b82e01037baedd92c55d37e55ecad00/multidict-6.8.0-cp312-cp312-win32.whl", hash = "sha256:7bb0dad75068fee80fcb60f88569722c199d8656a16706702dc6e3b786819c90", size = 47936, upload-time = "2026-09-09T13:54:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/29/00/1952f9f282aa71e7c3db3a6b47afb689d0ddf283dbded7e6326a91d421c9/multidict-6.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:7d26dc8f070c0ec5579e987fa615ffd6883086106eefdff9e10d160fc5630630", size = 51723, upload-time = "2026-09-09T13:54:18.05Z" }, + { url = "https://files.pythonhosted.org/packages/49/b5/c9d57dbafe25b8f3460ce2961c968539a81ff7a70160c44dcfd4255cbcd1/multidict-6.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:e6ec7d37841609a691b96a10b4fde386c7cd93ebbb939f59c9f23325ee788395", size = 48492, upload-time = "2026-09-09T13:54:19.42Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/d7112c2dd7db02677097be72fb65542f51a5aa73cb472b87ec211ba9e0dd/multidict-6.8.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:ec0a4d066356054d569a66e0a94691a2058b680be5e710298f61db11a3c4609f", size = 54197, upload-time = "2026-09-09T13:54:20.814Z" }, + { url = "https://files.pythonhosted.org/packages/ae/24/876015abbcb4a179d946579eb77b778eb5a948fc8381bc7928ba895bc051/multidict-6.8.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:714597cb5d5e15a8a449d2ae23c45b486a9e8fa33c462c7a33d7f35b65d92943", size = 47787, upload-time = "2026-09-09T13:54:22.51Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/ceb7d25f8a567599db2eb19b08cac58d67ff553cff42dcadbea9aba56a20/multidict-6.8.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0db3a4d1e264e225037a6023888972c25206a96e016021a5bea41c9a939f2a9", size = 48815, upload-time = "2026-09-09T13:54:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/18/e3/e1c6e9c3818c34b782f23ce5fdba3eaa34ec6750dc53078dfac80fa59be7/multidict-6.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:27747162712e85c84598d364425dbf1714ff335bdb6ba3171c4e5081196e8916", size = 83484, upload-time = "2026-09-09T13:54:25.674Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a0/c23f78a4badee9a5b3e760495c661c62a92c340a1dfd00f829cd16e256bb/multidict-6.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:847d6082ae694dc95e548acb201bc100e1cfa96513bc71fdcb86f709dad6c435", size = 50763, upload-time = "2026-09-09T13:54:27.135Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/db552d402a3f6b650f5d3ae11b82b93833836aebb51bcda22d8691121129/multidict-6.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:88a6df88567680504ae28bfa7a1f2f64243d91e79a40b2c92ef42efc531e23da", size = 49029, upload-time = "2026-09-09T13:54:28.483Z" }, + { url = "https://files.pythonhosted.org/packages/01/b4/546853fba19dcef77cdf91fc173faf0b02284a49106cf250511166b4ec5c/multidict-6.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:560b211fc3bd4a1e1c6de44f6d38113bf5b410dfc89a4c0d2a3c0edbf1a0dfb8", size = 278863, upload-time = "2026-09-09T13:54:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3f/4b52dac7db547936eb762123ac1d99df23f92fdb358bae600e322f611247/multidict-6.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202436df907c15adbb94360296c425ea53cf8968a5d2cff9b5b9790ae1972b33", size = 283915, upload-time = "2026-09-09T13:54:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6e/c0dfbf170e49a91bcb9ce850d51cb98357f3033c5227529200ca7625853e/multidict-6.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c46a08bf070d6849fed483e9d9833f9d06aecb8382ed985be0b38508b3ae958e", size = 260704, upload-time = "2026-09-09T13:54:33.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/56973a060ab8dfc2e80bb6797682f6577aff7123cdb1de1a568670ae3499/multidict-6.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cd560498ae8e1bcc955643c1d78eb8e338226d07a983c656ea8c4443d3eec0f", size = 290243, upload-time = "2026-09-09T13:54:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d5/67/69112989f131bdea4a87b74e82cb0a2daf37880cd92b0e6f0420020adceb/multidict-6.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:758233648ac47b07c575224c4eadd73c8929c3b4c31e2afcfea935fde1cda735", size = 291131, upload-time = "2026-09-09T13:54:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/c2/75/9435f68b0cfc442d4917de85c26f2b2e1292630883414a25576083fa2469/multidict-6.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:122adc7c46ac1e31ecfc7f81b2530533dccafdba70f5d741649f87e336c63384", size = 287551, upload-time = "2026-09-09T13:54:38.835Z" }, + { url = "https://files.pythonhosted.org/packages/13/08/2ee4838081d6587849611aa7ec722c4cb2469e912fd0eaee980e7bac064c/multidict-6.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8125e60f3c70e323ac07dd8b3635f7b3bbc5c3a9ac04ae5988f668ff7ae28a18", size = 254591, upload-time = "2026-09-09T13:54:40.806Z" }, + { url = "https://files.pythonhosted.org/packages/94/f1/05673b51191f77f4198b8e4b35f16ea71c0300c72ca8aa027a66a61b6edc/multidict-6.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83ff054b04915be5c15680da6c6012474a2cc2bf534129a0e8c6a99f17ba7238", size = 278204, upload-time = "2026-09-09T13:54:42.672Z" }, + { url = "https://files.pythonhosted.org/packages/45/4f/b6cf74322b3fbd3e011a1e903730191922291a7779f6d404114c2189b806/multidict-6.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:930c6058047410e3edff445f5a6e4457f2e089042dede00e2d18ce06f3ceae2e", size = 275600, upload-time = "2026-09-09T13:54:44.348Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ab/958bbb04377159ff03c7314cd9d8a48dd6fc4f78c840589c22ab155ee9c7/multidict-6.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13e26f59f0eecfc5f67c663ad550ffdaf62c0f657547cde387f6c86af1c9449e", size = 279793, upload-time = "2026-09-09T13:54:46.086Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3a/706605ab0dfc4179748ee7949829e63c6f14ae28667aceeefaf2c701807f/multidict-6.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd789a294d8e098528be29b2669b83005ce569339f8cef167fc0274c3115c34c", size = 284751, upload-time = "2026-09-09T13:54:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a5/567e36c013ad023546de633079c6b22101dd43226b193cba00e6399703be/multidict-6.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3126f2a96704505aa4e92a72d6e8a5d7f29d40a987ced8bf69e29d71dfc71fbc", size = 250812, upload-time = "2026-09-09T13:54:49.509Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/6b0b64aa0cd346b07831dabaa6ccda0e73014c5df044b68baa763f0f0552/multidict-6.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:23c9ee89967b6a9b4048acb3b93b660ed714ce9c8bf3bbe652959bc120dc02dc", size = 281606, upload-time = "2026-09-09T13:54:51.288Z" }, + { url = "https://files.pythonhosted.org/packages/31/8c/b846b6796f26d496efb07fedef2b69f6de533da32a56f12d236722a96157/multidict-6.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a62e302fc8cd6aa8972207e7e951d1fdee7c1dda18568305041d19f0e2c00f5", size = 281733, upload-time = "2026-09-09T13:54:53.05Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f3/bf14a39d4af5697fd9404baaf70a0aeeb82d258b95de5cb16b1a7f98ae6f/multidict-6.8.0-cp313-cp313-win32.whl", hash = "sha256:093167d22a8c95af30f597b8a5686f20a14512989942d4be804d119899caca20", size = 47738, upload-time = "2026-09-09T13:54:54.676Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/598511a5741a3cb374971b3b02eda8a09896118ba528a54795f7e7e8bfb4/multidict-6.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:f25b61a708bd276e8cbb6afcbbf1b8e793a3be70ba0a842d0b8692020f83b706", size = 51609, upload-time = "2026-09-09T13:54:56.38Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b7/6f5c1bd4ffe42d4a6db0f2f65491d4088e9c25c990358fb31a614621d664/multidict-6.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb36381e1f9f9d06eba2f10bdd438e5d20c07d5b55e1a3eee30b9f44cbf52316", size = 48280, upload-time = "2026-09-09T13:54:58.03Z" }, + { url = "https://files.pythonhosted.org/packages/ab/85/153341590e233a967c1d6791a83402d01693dec0f4c1f695606ef16c7ed2/multidict-6.8.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:f8b09b25e0f4dc2ea9e2adbb1cc3ba11a94d6fa3dd978ae659c8743052e1afbc", size = 53758, upload-time = "2026-09-09T13:54:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ff/44f72d516ece0398683ef52061797d83a74b16b8c1e4587408e97959d783/multidict-6.8.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1f57c414be82490bc0e0305fdb834186229b2d9b6a35fa0afd1eb1a772d125ab", size = 47495, upload-time = "2026-09-09T13:55:01.382Z" }, + { url = "https://files.pythonhosted.org/packages/50/5f/6e118f761b024dd35d26c2fe7ba41572bb0e8ac5f8cfccbbcbc2ff76da4e/multidict-6.8.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:00be37bde741bf60871082cd347a093218c44886e99231b7516671c70f2c280d", size = 48540, upload-time = "2026-09-09T13:55:02.989Z" }, + { url = "https://files.pythonhosted.org/packages/e8/4b/3eed744491b32f0e318e7db89dc06858732362f706e8d045fa9ab51a343a/multidict-6.8.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e37b744849fb631bb52e3dadde35ffeee365a6c41cf71257b5b7acc9cd83fd38", size = 83130, upload-time = "2026-09-09T13:55:04.554Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/95c2c0ddcccb9a41ffbaa5df8ea059a8ff81916b7617a8847ecd89ed8061/multidict-6.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c2b2a96cf1dd99fe7867be4c013314225f4d5786e6685906e29932d42aca6f11", size = 50574, upload-time = "2026-09-09T13:55:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b7/f4f4989594f99bc121ad9277090c4e49819b08ab1a96e132b628a9e10b7d/multidict-6.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bea7df027015856ba5d0a88e3b4777ff8cb5c66b58fc108050fe79d4dd9d4d2d", size = 48786, upload-time = "2026-09-09T13:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/b2/86/f1d86a0222f31fb3df8eef3d6c9abf7e8d65d49edd8d0d7e7afaf23d23cc/multidict-6.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d3da668e903c934ed0b587ecacfed6901f6ae6384a6e975887592b61845e78bc", size = 276670, upload-time = "2026-09-09T13:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/03/50/6945c50f86a978b2bcace9ca344165ff80883be47d984489bbba8fa0ab20/multidict-6.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64eaeda36ee8d88f9e8616a587a8c66a663283cf6e0dcf013c1ddd8c758e4aef", size = 279339, upload-time = "2026-09-09T13:55:11.685Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2c/e649889ba23fd1f4442a85427b99d9e6261226b2ac31914aa7f5b241d947/multidict-6.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ac746cb365bac1c462da9e3e6ab8904a8efe2217a56b0b2e3d9480f41d2b2602", size = 252549, upload-time = "2026-09-09T13:55:13.527Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f8/1023b66e011b1395fb160dabb0f0608ef67e569f0bdb2c1d5ac9b2f2adc6/multidict-6.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18f0e06360c3e451a3ab800355773c8d125a758238d780c800b0ee5e90ee903c", size = 286203, upload-time = "2026-09-09T13:55:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6c/48aea545cbda6d0444848ec23d988c13b86538a00a1b7d3868cc2382ff94/multidict-6.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69708fecaa88bcb2341397b49fc95057a835b02a3670c551b37f95dd79e64e3a", size = 285039, upload-time = "2026-09-09T13:55:16.928Z" }, + { url = "https://files.pythonhosted.org/packages/68/2a/066123b17291671bf67d2a5c65ee81a48de53913bd1b1578791519eacdb0/multidict-6.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9606f583e7acaf61e7b3f56074e14037b9af7cb194590edfc0114b3ae5931ff7", size = 281075, upload-time = "2026-09-09T13:55:19.155Z" }, + { url = "https://files.pythonhosted.org/packages/47/20/4f0b2c485da2e8a659cc677717a3745872918c9c85064491a1ef75d7a3bf/multidict-6.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1f66fe6a021173d0d47968491791966b9f3e6d61115f2491744aa0c07a6e67af", size = 250431, upload-time = "2026-09-09T13:55:21.07Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c7/b9a288901577aa0b82c33c64d52246c88076d260ad7b6c16b021ca0f8e99/multidict-6.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:be007d1aee2cbd530347dcafedb400891a3b5f1bd7135f95cf5d5b330b5219ee", size = 273891, upload-time = "2026-09-09T13:55:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/da/51/0ba50cab2cfd067988de2abb73f23076ac727fe18d03f1368a59def64727/multidict-6.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8457aff3c12a89a8e1c4674de5c777857fbc429f40fe117a3d29538547cbc364", size = 265262, upload-time = "2026-09-09T13:55:24.77Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d6/e5be1117dbca6eb9ce231142b7e20599418bb3500147db51bf844ce8afcb/multidict-6.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:95c27b4f3f04320fc44e338573f40c5c956b504a7fcf081a157fd0b02579311c", size = 278033, upload-time = "2026-09-09T13:55:26.67Z" }, + { url = "https://files.pythonhosted.org/packages/d2/28/cad0afaec3caa56ea2c1ceed43c164d62ad3e83e950daf0d0c87bcf9dca7/multidict-6.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d244cf6b52b5ba1c34c3832f4652a668ebb36d95949b96eed9a1c54d916a90dd", size = 281717, upload-time = "2026-09-09T13:55:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/025702df0b69b856db70a4d66f77622f51c3d99771ec9a07f3ca80f7e098/multidict-6.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5bbbb696c8024475b1877d14ce20d5f1cc05b8f6d786cea0fe3aa7fedc02e891", size = 247124, upload-time = "2026-09-09T13:55:30.497Z" }, + { url = "https://files.pythonhosted.org/packages/b4/96/9dddca563f06a921956389c0bc9b894355b98b0bdf62299e2560c50afb6d/multidict-6.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:cbd86f9787c5e2f5fd27d8b21458222f107347c6731c4e93dde68f554b466a2d", size = 275954, upload-time = "2026-09-09T13:55:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/ec/91/8b2f1f2a774a955665f268340a2b59db7020c5f12baac02ae9ef1b1660cf/multidict-6.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2f8a4b0b4d639d525928c7f30de527bfdf9ead6e44a5e8cb9c50aced5e4590cb", size = 275508, upload-time = "2026-09-09T13:55:34.368Z" }, + { url = "https://files.pythonhosted.org/packages/b6/1a/e2cabdfc0880a61a99d2b8bc361035036fb5a2c6af31ea3fa054ba1065c5/multidict-6.8.0-cp314-cp314-win32.whl", hash = "sha256:8890c89d662560e51c55ac1304d6f919b23942abe9ae1127cb1de9aa6132fa52", size = 46938, upload-time = "2026-09-09T13:55:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7c/11234bcba62c22a58f2ba168499cfe3531f49de3edd5090d04a8c6cdc936/multidict-6.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:45cc39ba50fb0754a4359b90f8229ae08598fe2266abe3521b4e5a9ba916534a", size = 50291, upload-time = "2026-09-09T13:55:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/ab/61/793668439df924752a8137d6db0de97ed1add494779b01e4764dfc60571b/multidict-6.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d0264f8d5cb0a803f650a6a8572dfa0cd1e099a2234c588dc8fb220b415b865f", size = 47622, upload-time = "2026-09-09T13:55:39.335Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b8/3c091b929e6b5b2f6e0eba2232178e76d4503c8b96b92dfc281ff1d823be/multidict-6.8.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1969971900b0871530f9b62280dcc2d75688e74d2a69262bc01faf2b96c78f04", size = 88789, upload-time = "2026-09-09T13:55:41.086Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d7/3df83fab22dd64615db71e3b3cc1346b581d1459719637ce52144f9f6558/multidict-6.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8180b635290a75af8478f1b3e9810135381ae24833293fe77b85c1c21ff842ab", size = 53399, upload-time = "2026-09-09T13:55:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/2d/78/41bd04c04b0aed16540c4856c9e012afc1c254298da154398308df05e26a/multidict-6.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4261863fc8b5ab1b815ede94e592e94c6af5b04616014929057e61859e7382a9", size = 51597, upload-time = "2026-09-09T13:55:44.569Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6b/7bc4cdddf624e1e7e0231734b1331729ea46df10d7c8fd3fce79756e7d0e/multidict-6.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0b143d53590e89f43153d81d505a8448d4d57354354385aef8a51d67ffefa27e", size = 264391, upload-time = "2026-09-09T13:55:46.548Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/d2a946e5938771e92c39354563e535ef6bc6dfe399dd4307c6df8dfea183/multidict-6.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da1c112c5784ccd9d32cd90be6739fee32644e874eff6ae8f0497cba3e352e58", size = 264680, upload-time = "2026-09-09T13:55:49.915Z" }, + { url = "https://files.pythonhosted.org/packages/33/6b/3f9e981c42e7eb9329918523f0f9362ceb0ac3ee0ee1165c28f674249d75/multidict-6.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f7eefd0233a7c33ca980a5cfef26f1e9b5e2137839e752a99963696729f12d91", size = 235420, upload-time = "2026-09-09T13:55:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e1/a3a33a039fb6d381800ae5d1d587b697b8c27fcdfe48819420f08703acba/multidict-6.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:348bb85e2038b40c007383616d73f734869063772372519549ebd7da1723d1a4", size = 270309, upload-time = "2026-09-09T13:55:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/95/5d/8b06724a957f2e480f159b9550988a67810fbe9555a09c5f6a2a4b829607/multidict-6.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:095f62ea4e7a3be2f6c567ab695ce10e950f2adb905c1bec82281593e0b2d2ad", size = 275169, upload-time = "2026-09-09T13:55:55.948Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/8f3dfe2ffa5d0df2a95f71e63c2f11fe3b5e1771f26ef73bb1af84de83f8/multidict-6.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be569fff1d85cd29391c431c5641c8772acb75bbdc61e60a8e82fceb9023d385", size = 264900, upload-time = "2026-09-09T13:55:57.803Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/d5829fc00a055d6ab445e0876346ee9cdee670766cd4190dc0a496188c0f/multidict-6.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3533a03e4e789baf6a286e7b0b1b6da3f3d7c3eab569686ee29ee1d8b52e2cb4", size = 242486, upload-time = "2026-09-09T13:56:00.002Z" }, + { url = "https://files.pythonhosted.org/packages/b7/58/e8d7874038e31e0533182d1c3c5331a856b9c849a71bb26a21850e8c91e1/multidict-6.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1bdb9b8fba5a9aef673ec90db3f55b1ce743f2fbdea4d37dc04d14ccdfc153ff", size = 259916, upload-time = "2026-09-09T13:56:01.802Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f8/1b56a7401acda20efc016440f4fad3bef66c4aee54ca080ec143881ebb0d/multidict-6.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8d7b66c9e09c0bb0add2b5895e646b62a0849e71155066f215523de6b95cbe6", size = 251209, upload-time = "2026-09-09T13:56:03.767Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5b/68d67a9e302b0645a747ba910c30eb41f2834fcdc1d85f53eae2dfceee0a/multidict-6.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:563d6500ca80dac7bba6f48a78e0ffd87e21a7d4d24642c6503a2ddccd70c110", size = 264505, upload-time = "2026-09-09T13:56:05.795Z" }, + { url = "https://files.pythonhosted.org/packages/18/13/4dc304ba2c5f5307b474ab2ce1ed1f6b02b0b4e233c182e3981ed436c2e3/multidict-6.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:346ac52e56bcda320c0dcdfdd081947ed7cada33afea4e2284bef7b0733bff9b", size = 264916, upload-time = "2026-09-09T13:56:09.079Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0f/7b1f729d18369915009185201be5d0b8df0e525340fe6a600d2f8441d6cf/multidict-6.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4ee953a5ebaeed38dc21cc032ed17a9d9782802e00042200497ab4b01b0bf7c0", size = 236839, upload-time = "2026-09-09T13:56:11.273Z" }, + { url = "https://files.pythonhosted.org/packages/22/d1/eba1b88b18b7019d9136303fe77909257c40fabde5aaf138a4d900b6ce3c/multidict-6.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2f79cc3e8039a8cf5c77e0811b0807953fd52d0863b9b76970b20d696dc64a78", size = 265307, upload-time = "2026-09-09T13:56:13.379Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a6/6c1e4106faa27118ac612f4d664eaf909de252634785286262a627108e58/multidict-6.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43a4b56555bbcf8af161e7c7682bd93eec10f068c95844511864c018c8e5e13b", size = 259041, upload-time = "2026-09-09T13:56:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bc/ecfb8b6faa8e158a71b03bdf7f947f30e0bc5d899cc357573a76ab7bb1e5/multidict-6.8.0-cp314-cp314t-win32.whl", hash = "sha256:48ea524a25a1cd5972cf293bc95713918cba0bcd6fa9b992d906c857c546abe2", size = 50628, upload-time = "2026-09-09T13:56:17.837Z" }, + { url = "https://files.pythonhosted.org/packages/30/7f/e27fb699b70ad24dbd02ddee604658acb36f907c03c045baffe4ea774501/multidict-6.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d0be2b832435001bc623ca7f1499ca1a853d4f082fb61221a80ce71132f50b26", size = 55592, upload-time = "2026-09-09T13:56:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/eb/7a/76de70b2f6733696803f1ee56abe44a3757a52777383032c7373d3fea0f4/multidict-6.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:62b8e291a4f7edbf7cde7a43d831d893ba443a1b627498b53581943b0e348feb", size = 50300, upload-time = "2026-09-09T13:56:21.516Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ee/be4e1a4b7a2b27f4fb6936510d4bebcb41b0562c946930ad26916e069cf9/multidict-6.8.0-py3-none-any.whl", hash = "sha256:75daa15ca16d6285eb2e104b2f05ee6f8d9836c68da3ce5c85f615a0450eed0e", size = 16297, upload-time = "2026-09-09T13:57:56.106Z" }, ] [[package]] @@ -2394,6 +2832,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" }, { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" }, { url = "https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl", hash = "sha256:8d5eb4ec5017ba2fab4e34a747c6d2c2b6fecfe9e7236e77988db91580ada952", size = 156414, upload-time = "2026-01-19T06:47:35.915Z" }, + { url = "https://files.pythonhosted.org/packages/a0/61/af9115673a5870fd885247e2f1b68c4f1197737da315b520a91c757a861a/multiprocess-0.70.19-py314-none-any.whl", hash = "sha256:e8cc7fbdff15c0613f0a1f1f8744bef961b0a164c0ca29bdff53e9d2d93c5e5f", size = 160318, upload-time = "2026-01-19T06:47:37.497Z" }, { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" }, ] @@ -2422,6 +2861,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" }, { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" }, { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" }, + { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" }, + { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" }, + { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" }, + { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" }, { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" }, ] @@ -2436,11 +2888,11 @@ wheels = [ [[package]] name = "narwhals" -version = "2.25.0" +version = "2.26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/7b/6248dada39781db1ab3ebf08943080df0796098515a87f6f8696d14ec744/narwhals-2.25.0.tar.gz", hash = "sha256:62c036c810662bf7820b7737077176313bc59350eeeefb808510f388c743e4b2", size = 677076, upload-time = "2026-08-20T18:10:15.454Z" } +sdist = { url = "https://files.pythonhosted.org/packages/35/68/5351e34623d253423240ea7de3f8fc74fa8ab14b1ab3c0ec4ac8997413c9/narwhals-2.26.0.tar.gz", hash = "sha256:6b9cadca82f375c7e4cf584fdc86ca25da54827307a9c58f94547ee6104b82dd", size = 686970, upload-time = "2026-09-08T13:32:08.964Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/dc/55481808fd70ef1567cf13540ffd4702af3f74b112e35427564b03f79c2d/narwhals-2.25.0-py3-none-any.whl", hash = "sha256:1f0f403e8c7e4463cde9bfe78b12fdd809e3ae3dda6d9b2f802934fb9c7a6a8f", size = 467373, upload-time = "2026-08-20T18:10:13.834Z" }, + { url = "https://files.pythonhosted.org/packages/40/b5/1b84b2c784db76d69442334bc8b8748c840f13ca53be086f4f250ad4a0bc/narwhals-2.26.0-py3-none-any.whl", hash = "sha256:29326d74f107c347fd1009bd58e38d9f7c7c5b51e6de97bc93dbc325d9038b54", size = 474034, upload-time = "2026-09-08T13:32:07.159Z" }, ] [[package]] @@ -2458,6 +2910,16 @@ version = "0.3.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz", hash = "sha256:71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848", size = 25662, upload-time = "2026-08-23T14:26:30.728Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/88/b594f0e86856b37e182fb663283da419eea6424972506e640e890885467f/nh3-0.3.7-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:91a4dab4e94d9fc54b9f67b1adfb23e81fab7ab43f33c3b8c97be9aa38f789ba", size = 1471147, upload-time = "2026-08-23T14:25:55.259Z" }, + { url = "https://files.pythonhosted.org/packages/1e/60/847a21339f095c4d4c655af31fa2d18b174585bcc210709facacc7ce205c/nh3-0.3.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae64328e46a25785535afcb6885b6f182ecaf5ee8c88f8c075422db8aacc65b", size = 820463, upload-time = "2026-08-23T14:25:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7f/1a103e00aaf5e59f2dee4c2709aac609bb2d4bb74fddaf0dcfade11ed87b/nh3-0.3.7-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4968fe8d2db97c6f047659bf46a449fd8ec377f44ebf3e0a1b96c0d3a333ae32", size = 861456, upload-time = "2026-08-23T14:25:58.087Z" }, + { url = "https://files.pythonhosted.org/packages/d8/4a/e9c436089a0c80b928011ead0efd156aa7639a19b6064ef58dcedcab8369/nh3-0.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:be53a4825585f701955cb9baf49f478f56eb81e20294329fe4bc689dd5dd81fa", size = 1023930, upload-time = "2026-08-23T14:25:59.465Z" }, + { url = "https://files.pythonhosted.org/packages/04/5c/aa1468e3e281e78d2b3b7d762ccba59f681af355e971dbd255d5903f7b86/nh3-0.3.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:94fd6e59553fbb9ffd8ba71bbd5a54e3126ba01799a097ae30d5341d750bc6ac", size = 1102614, upload-time = "2026-08-23T14:26:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/57d186d9d3dd38905dc12dddb3484406cdf6aa0b1ce33639a2d277d4ee1c/nh3-0.3.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:18f4278ecd157d43cb35acd5aae9f35cfa79f546b4922bd86536adc0f6312102", size = 1059915, upload-time = "2026-08-23T14:26:02.388Z" }, + { url = "https://files.pythonhosted.org/packages/6b/53/097a5ad0b34b15d67a472ef849165a54209fa5fbd3e639801c6fe439ba28/nh3-0.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:808def0c8c07843e6e50dc84f532457bfa2cfd17417b219a5d9e7c773709331a", size = 1047402, upload-time = "2026-08-23T14:26:03.897Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/c57a2c70534418310889a65ccfac3525e62f0bc0a8613225903403755ce7/nh3-0.3.7-cp314-cp314t-win32.whl", hash = "sha256:874b7d67a067bd29a59223f6270fc30da4edd8e6d87fd219fc93bcbaa662c946", size = 619895, upload-time = "2026-08-23T14:26:05.105Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/efda1d0a611d940bdfde6893bde1ea6b7b7d48c31273aea48e35b822fd58/nh3-0.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:614dac4a4c36ad084e78447d16fe898dedd762e354a7ab9cda2984e82f67883d", size = 633456, upload-time = "2026-08-23T14:26:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/1d/18/3ab564595cb88196f50d26e163ed0fd2acc731ab26ac615df91981885887/nh3-0.3.7-cp314-cp314t-win_arm64.whl", hash = "sha256:157ec1eb7a62f3d9a7badb8d82d89aa810e3e24e097eedfa481a25d0c8a99877", size = 611003, upload-time = "2026-08-23T14:26:07.813Z" }, { url = "https://files.pythonhosted.org/packages/94/0d/c257754bf57f829f307aa226bbe136d3a1356b5a0d08324c7b6bd2a8aacd/nh3-0.3.7-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5", size = 1493959, upload-time = "2026-08-23T14:26:09.025Z" }, { url = "https://files.pythonhosted.org/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479", size = 859615, upload-time = "2026-08-23T14:26:10.606Z" }, { url = "https://files.pythonhosted.org/packages/85/05/b0e6bef633549a23347d5462aa288fcc42381e7918482062ca3cb456242a/nh3-0.3.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506", size = 839872, upload-time = "2026-08-23T14:26:12.037Z" }, @@ -2540,6 +3002,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, ] [[package]] @@ -2906,6 +3390,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, ] [[package]] @@ -2941,7 +3441,7 @@ wheels = [ [[package]] name = "peft" -version = "0.20.0" +version = "0.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, @@ -2956,9 +3456,18 @@ dependencies = [ { name = "tqdm" }, { name = "transformers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/08/02541a2c29be7c78698f73d438bc0e733b214f3f35ec5db79fca8da8fc61/peft-0.20.0.tar.gz", hash = "sha256:4769c8093a4ca145fd6fb3fd4dd50449675f5fe46434ad1e98b285a132d4b1d0", size = 880503, upload-time = "2026-07-28T13:46:01.85Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/91/56cc2b1b6824f5a4026750274240f26cd23465fb1a29cd14772d555433f2/peft-0.21.0.tar.gz", hash = "sha256:17f2b5a264439f4cd983c02e95954f2e948e3bcb85f75895ee307f94a55fd28f", size = 978334, upload-time = "2026-09-15T13:34:02.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/79/13bcabb8048126422d5c4b880575d40886c726f354db88cfeed4325525bb/peft-0.20.0-py3-none-any.whl", hash = "sha256:0fbba16ffebfad3de96e06f2da6860fd860292324b85b6141909fa1e26ea9233", size = 775777, upload-time = "2026-07-28T13:45:59.809Z" }, + { url = "https://files.pythonhosted.org/packages/13/f0/29c37002f5ef5cb5be54890114d5c8f726f0cb5072a8ba0880758c912bfd/peft-0.21.0-py3-none-any.whl", hash = "sha256:b64eb75fd9dece7401c70e675b8d9de024993b70483691b41c62876f0c7809b7", size = 832883, upload-time = "2026-09-15T13:33:59.775Z" }, +] + +[[package]] +name = "pgvector" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/ec/6eb80aebc728200f95229219882994c1b0585b956ca47da5edb9d062627a/pgvector-0.5.0.tar.gz", hash = "sha256:07a9dcf735696879406983afc6eba9a787cef7c0cf6c367ca1a5779f036dee74", size = 35170, upload-time = "2026-07-06T18:27:27.767Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e4/a5573f2c579ca9ad133293bfb624148ba0893674ca4a6eeec85ced9a6a09/pgvector-0.5.0-py3-none-any.whl", hash = "sha256:fedc9800894e6da2be51358d7b7c574bf34f247ca741a5a09513622135f5964f", size = 30958, upload-time = "2026-07-06T18:27:26.797Z" }, ] [[package]] @@ -2989,11 +3498,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.11.6" +version = "4.11.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/1d/6e762a6b060e662208951aefc5c39f6a96a272c4a10c0c1f7b6113fc3c09/platformdirs-4.11.6.tar.gz", hash = "sha256:1a4016e373f89f8ec458431fe0e0c5c4285858ac623f3e20efdfcbc0bd862941", size = 35131, upload-time = "2026-09-01T04:41:00.6Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/18/f3bb8ef0d3b930692343da8aa4d3cbcd6749477c053959395ac81965a6e9/platformdirs-4.11.8.tar.gz", hash = "sha256:f23abafea7dd4276d1f29104b83598d7dcc567cafd07c9c951e66665645437fc", size = 37182, upload-time = "2026-09-08T22:20:42.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/d8/2784c6eabb991b5b7494ff9e9888c74a0a72ad613c3ec5adbfcecc0724c7/platformdirs-4.11.6-py3-none-any.whl", hash = "sha256:b22d992e863bc651c26b16242041c7979db6e3286e548f9a76cc91238fac599e", size = 23938, upload-time = "2026-09-01T04:40:58.977Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e1/5b7b8bbb55084d1425bcb9bc823ff519e1b2be05f6ebb0089e2eacc38413/platformdirs-4.11.8-py3-none-any.whl", hash = "sha256:52f2f181bbfde907966932cc8312d967d02976422d66d537ea16092b8e291081", size = 24027, upload-time = "2026-09-08T22:20:41.537Z" }, ] [[package]] @@ -3029,6 +3538,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/3e/6f7fad4fee748ba365cb3e1ba2e061a74e18d987eb554ead4757127df2ab/polyleven-0.11.0-cp313-cp313-win32.whl", hash = "sha256:ce264f6a9daa3265299d8ffcb180d8256517a8d9235613a3b267172da0bc1e06", size = 11629, upload-time = "2026-02-09T09:41:12.652Z" }, { url = "https://files.pythonhosted.org/packages/2f/cc/4877913dec8fb4f968a070c894254db5811b62128d3a69b05bcd1305b5c3/polyleven-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4648732c8ad3955c8d7b1aa015d92936a150475aaa97ce704fe0c8e7fa7e0c4f", size = 10841, upload-time = "2026-02-09T09:41:13.682Z" }, { url = "https://files.pythonhosted.org/packages/59/e2/039cc477ce73d6184e12cf6341ac200bc9f4c5428254c399015ec30392e1/polyleven-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:166f6c9b161c6af92ff201c734d6437bc7ef74a32dab306c5d47a0bdb7a82d9f", size = 9424, upload-time = "2026-02-09T09:41:14.545Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cf/a02d74f965127adb6a8fbd5030e2c98335ef2f8e7452b12a882883b2053a/polyleven-0.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3c18b8e44e5d04f1ffa7d41eb68da553833ab8663b7cfb1a505d85676db5c797", size = 7482, upload-time = "2026-02-09T09:41:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/fe/74/dfa9e9891cd85e679f230c5e740cba11b0bb11bd9fb298657ccf048ff70e/polyleven-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7ab547adc0ac72a2852d37337a4a839d4e2f713940b0e8a944d45c528e5e6538", size = 7508, upload-time = "2026-02-09T09:41:16.365Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ef/399ae8d21f7b348514b7ad3bd7b9d530bf195fb0a8ec63cf7af7d17a4071/polyleven-0.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5808f62874187dfd4e30de5dd5f42a660562ec95a87cc64d5455ba0f4be8f175", size = 21056, upload-time = "2026-02-09T09:41:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/21/60/7eb97286a6171dd794a0e5b261175e8bfeb99a2b566bd9b8848ebc97f6df/polyleven-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9deb75346b4177d5e69496791e6156f705d9059961ce8f9520a0dc96532f10f2", size = 21535, upload-time = "2026-02-09T09:41:18.137Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/6fa59257c2138e33a858f10236a2a6b381b87f61251c1df468be7c666338/polyleven-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ef28c4c6cdc71a32f0478772d2f07b2cd412fe7950182033b1c36c8a481b0834", size = 20560, upload-time = "2026-02-09T09:41:19.04Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2d/85be9c91d05cb0127586640108f3110f6a3a98c9478f84713d4771c49761/polyleven-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94832ff5d04022ba6038c2ca0c9ea6906330cde3a3b1761739d772647d01da33", size = 20814, upload-time = "2026-02-09T09:41:20.001Z" }, + { url = "https://files.pythonhosted.org/packages/da/91/5a99ae6cf16ff55a94c5686871ed20b816ad1690f823494c76dc3ce0f54b/polyleven-0.11.0-cp314-cp314-win32.whl", hash = "sha256:e6182ea6142904ea50cf82e2955d922156b5fcf9a8279925f312961f16710a58", size = 11966, upload-time = "2026-02-09T09:41:20.946Z" }, + { url = "https://files.pythonhosted.org/packages/48/ec/9c6fcdeb1dd436523f8e2275407f588d6a66a524d7a793f554957373769c/polyleven-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:bf82bb8601582da8f2248293c1e6f4cce2025c79fd64fccddf67dd8538655b55", size = 11100, upload-time = "2026-02-09T09:41:21.863Z" }, + { url = "https://files.pythonhosted.org/packages/42/7f/1e59881a56a4963b4546c7b558ab7979daddff586001f18b80f1f66cece9/polyleven-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:45487a1e4a8415e4ed45e6720b2a3ad9d240336f7afa136a625b8f802a1880c2", size = 9624, upload-time = "2026-02-09T09:41:22.749Z" }, + { url = "https://files.pythonhosted.org/packages/47/5a/5eaa75427f17d4cdf8e2139988a3ec6b841b6e077ebc1fccb754c1f8b55e/polyleven-0.11.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c518ced3e7c05de4efbd12fd7b61d6d574eb170f431e0415689d9f143fe552ee", size = 7490, upload-time = "2026-02-09T09:41:23.677Z" }, + { url = "https://files.pythonhosted.org/packages/50/47/5dd5fa13d315e0d5dc3e41bbaa16306ea56e74929ad29df54d5c24a84dcc/polyleven-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa49732cdecd985241db9f78d5fdba7170ba6375d2bf9ad040b05127dc96b877", size = 7514, upload-time = "2026-02-09T09:41:24.55Z" }, + { url = "https://files.pythonhosted.org/packages/75/aa/838f1bc632144f4f5820b9dbd31e0c64de41a7b0970b5cbe6fc02746090f/polyleven-0.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b2aada9dd04e84389d90790f359447447a499d6d86807697d80732ed45547a43", size = 21123, upload-time = "2026-02-09T09:41:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a9/d6f32263b863dfffeed9a67e80b53476cd0089f202b0510a80eb07f7425b/polyleven-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94311ee39e2db957415eacb36b96ae26dcc427c260465324de45fb8c870d4661", size = 21627, upload-time = "2026-02-09T09:41:27.219Z" }, + { url = "https://files.pythonhosted.org/packages/ae/68/4dee05a4217a3eb1f85cbc915f5fa269d79b86d2a8384be68bcd21de37cc/polyleven-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:45cfb234fece0c9df73276788fa529a25f91abf97dd0d9aed4f1b713b6d530e3", size = 20635, upload-time = "2026-02-09T09:41:28.137Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c2/8486bdaebf47e6b764e8be227a7d2898463f2b4d91443ecdeee9ebeca6bc/polyleven-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aaed455f498172769fd88f83c27bb8f43e0583d7b27d6b343154d471ec2145e", size = 20870, upload-time = "2026-02-09T09:41:29.07Z" }, + { url = "https://files.pythonhosted.org/packages/b3/13/b827188b55108bd816110a6f60b78aee0db045a98bf7b1f2e7bfb60f4039/polyleven-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2a59849c327279902e8b396666f6998234aa82aacc47abc103d93babaad46203", size = 11917, upload-time = "2026-02-09T09:41:29.997Z" }, + { url = "https://files.pythonhosted.org/packages/ab/18/c909bde1d1db7ead33329b941b0050c93cab9b811e44b49d04adb8c5f0f8/polyleven-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6ba2dcf3aff2909bbf3bdd9c1749f8de207f023fbb2c0b1d681c6bf3e78ceef1", size = 11073, upload-time = "2026-02-09T09:41:31.371Z" }, + { url = "https://files.pythonhosted.org/packages/78/cf/51f7a0fab2d65c2b6908872f26bb03bb7e2357d195f2a59aec1a27489106/polyleven-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:05207bb66da15a2dc5c530e2f5cb5f0588d0a7e79b3bd542965f9e06e3fb14fe", size = 9601, upload-time = "2026-02-09T09:41:32.235Z" }, ] [[package]] @@ -3054,62 +3581,79 @@ wheels = [ [[package]] name = "propcache" -version = "0.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, - { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, - { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, - { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, - { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, - { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, - { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, - { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, - { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, - { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, - { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, - { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, - { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, - { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, - { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, - { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, - { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, - { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, - { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, - { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, - { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, - { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, - { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, - { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, - { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, - { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, - { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, - { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, - { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, - { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, - { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cd/348d58f142aebc4873345c6b31087629182ca6e0f2b3caeaa528cf882eba/propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429", size = 87285, upload-time = "2026-09-16T00:14:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/df/f4/f3ffaee281b276da854ac1d7a6a506d26cbc62ea2e623756f1d0a4a1ba1a/propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8", size = 50984, upload-time = "2026-09-16T00:14:30.473Z" }, + { url = "https://files.pythonhosted.org/packages/25/88/1d7df7201750b37765ef2b23bc1c526c028dadde80afa0f57a118fc01182/propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21", size = 52460, upload-time = "2026-09-16T00:14:31.692Z" }, + { url = "https://files.pythonhosted.org/packages/83/4f/48865bd02a16ee5236bc46166b2946f37b93e07b0eae355dac0be0b216ca/propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef", size = 251768, upload-time = "2026-09-16T00:14:32.908Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/3742a5eed62317b03b4002ee865dc9fd720308bdd0da1f29a5786c630311/propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f", size = 257723, upload-time = "2026-09-16T00:14:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/ee6350fb0be9122bb6c67082a876d34b90d980d100c106af4b81023e04f4/propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546", size = 265597, upload-time = "2026-09-16T00:14:35.56Z" }, + { url = "https://files.pythonhosted.org/packages/85/9f/83a07b6ec0e043c050cfdd35fb0cf1b7897b91d554d6eea293740309afe7/propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b", size = 250424, upload-time = "2026-09-16T00:14:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/33/2c/a763a8251f50fba042af0fb1f02bfec4b31381e40aff760db2be7b2e1f84/propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab", size = 216748, upload-time = "2026-09-16T00:14:38.369Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e2/4d11bea8fd6a777149c6c20645f873952eab5de3a2497aa11648ec9ab6ab/propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc", size = 246533, upload-time = "2026-09-16T00:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/9f/36/6683597de4907e70c717e3588c541202c66086a72ff3db58be49de66e72c/propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937", size = 238173, upload-time = "2026-09-16T00:14:41.259Z" }, + { url = "https://files.pythonhosted.org/packages/85/84/cb08d79f1762daafeb2b030c470cd0c725c97b8ad67412457c6f35c53e9d/propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526", size = 251128, upload-time = "2026-09-16T00:14:42.652Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0d/41b848036db6621370c1f2e5471a7da8149c730f8552a5257567721f4576/propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944", size = 214821, upload-time = "2026-09-16T00:14:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/adfae4bf9c63bccf12e2d9690a175c6579047a6eec3b5a6a5f51428c15e2/propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031", size = 254793, upload-time = "2026-09-16T00:14:45.429Z" }, + { url = "https://files.pythonhosted.org/packages/51/6f/eeca9647245d5f92e87d53e5f14335bb42fce1a7e6842c8045b364eded8b/propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161", size = 247134, upload-time = "2026-09-16T00:14:46.976Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a9/424e38838793d37160b4379c702f61c74c598fc6cd17204adbe3c554f7a8/propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8", size = 43073, upload-time = "2026-09-16T00:14:48.338Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/6e8ef26f6d510a7916064fec68d55fcbfbdf7eb01e377480d66a122152d8/propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb", size = 46190, upload-time = "2026-09-16T00:14:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/08/b9/72028c5b56ced97f456de6aefa79435ca64d7f77af78ea8cf3c76fc5195f/propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737", size = 43075, upload-time = "2026-09-16T00:14:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" }, + { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" }, + { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" }, + { url = "https://files.pythonhosted.org/packages/33/c9/07e227b930c8ae513b8ef1aae3793499be097bffcdf7aee4fb8b33db4cd1/propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565", size = 85933, upload-time = "2026-09-16T00:15:16.073Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e1/6710bb44510c4e4a8e0f004bbaf3cecfd048141309c77bae56d4e5a6ebc1/propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993", size = 50179, upload-time = "2026-09-16T00:15:17.377Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/b533b493d7025456f44518b33e53e000021a20fe7c27b88cf3d341df7186/propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b", size = 51942, upload-time = "2026-09-16T00:15:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/70ac8430e28f21e442c7bcb964eb46c4363f6881ade4aa0e978bfd8d503a/propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc", size = 232647, upload-time = "2026-09-16T00:15:19.905Z" }, + { url = "https://files.pythonhosted.org/packages/72/95/f222f13b6fe623310be0eb61a673bf26df439ce27e563ca8e422d0818777/propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d", size = 241541, upload-time = "2026-09-16T00:15:21.3Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3e/763e370340db16115c5e63ad46e21ef0770a7f06928b3d3b62d8f8edfca4/propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f", size = 245332, upload-time = "2026-09-16T00:15:22.802Z" }, + { url = "https://files.pythonhosted.org/packages/96/d3/e97cd6f5de2176bd90ed4076c7a9b5e09d0f0b9687d00a576507988bb62c/propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9", size = 232757, upload-time = "2026-09-16T00:15:24.374Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/6766e5f60bcda26d244333aa71d0a702c1c9b21b251d543c7af5953d1eee/propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf", size = 204389, upload-time = "2026-09-16T00:15:25.667Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5e/ec4bb09a70b26ea99d76a8292c3383b960b296de2b347ac9986678f1761c/propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d", size = 228217, upload-time = "2026-09-16T00:15:27.11Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7d/b53922ba7d9e5bf797324e63aa05906ec240871899f779628df068743e2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3", size = 216947, upload-time = "2026-09-16T00:15:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/b62eee45e5ea4de094a258cbb3b01c1e856ca51ddfd95b43135c5effd1eb/propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc", size = 233457, upload-time = "2026-09-16T00:15:29.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a9/feec61ed296d993db9dd097e0f6723e3f576a647722367547495e4c5b05c/propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d", size = 204131, upload-time = "2026-09-16T00:15:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/411ef380cddad28dc001f1c6d75ec72c76cd3817030f68ec1ccfba0ec6c1/propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792", size = 234820, upload-time = "2026-09-16T00:15:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/c988229753629ef1cfd5198337a83e624780ea2b3787efe9e747c05aad2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b", size = 228350, upload-time = "2026-09-16T00:15:34.533Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/5ef1c5cf98591da3c5b952b39e6a298084cc1ce353bc70f85e82397a5036/propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea", size = 43578, upload-time = "2026-09-16T00:15:35.957Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9e/a0ac821a2229186af5e2e3c3635a78abb23cfddca57f38513ab5d70420f3/propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983", size = 46304, upload-time = "2026-09-16T00:15:37.655Z" }, + { url = "https://files.pythonhosted.org/packages/a1/19/c8d0d36a9d16cba5dcee67d389c9333b988c8986a653a61c00a451817a46/propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac", size = 43440, upload-time = "2026-09-16T00:15:39.091Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e9/42f1da77cacfc184e6ec929557ef653b7961bbf6f1da460b9221273948b3/propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0", size = 90672, upload-time = "2026-09-16T00:15:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2f/4b79940908c6ab8c795097c102999d7bc1f7e0b8604dfd1c232f9d99d67a/propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5", size = 52586, upload-time = "2026-09-16T00:15:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/02196ae6320c110235bb343f90dbd34be41f8b8964a3ee30db84ec12579e/propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d", size = 54335, upload-time = "2026-09-16T00:15:43.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/44/f48b9a131985659924df5fa5093f68fe72c7ee375329802989ba3126efc6/propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788", size = 297567, upload-time = "2026-09-16T00:15:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/418d956d2735139f77fc35262179f1f52c23aa666de5a8ab3819c1ae7854/propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56", size = 297477, upload-time = "2026-09-16T00:15:46.048Z" }, + { url = "https://files.pythonhosted.org/packages/69/fd/ff811fdb6d3d3e67fd9bbfb75881675d34a42d0ef29a45d33e3e233dde07/propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574", size = 302669, upload-time = "2026-09-16T00:15:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/fc/57/527910c455b5ec62f6871bef45d4f79fea16cb8c966ba0d4a07f0339ddc4/propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370", size = 287908, upload-time = "2026-09-16T00:15:48.99Z" }, + { url = "https://files.pythonhosted.org/packages/1d/86/f69ab82707534a0cb2057bdca04f9200a71214c7551800f9d34d6ac39e4f/propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320", size = 249804, upload-time = "2026-09-16T00:15:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/27/19/60677af50d93be4256213de7cd487f056944c048b9c0b6f2e45b3a30f666/propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9", size = 282344, upload-time = "2026-09-16T00:15:52.029Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/a0057808a91fb3b6a5f3602b528f0cdcb3d53e0ff8315d73fabdfdf8fec4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7", size = 270167, upload-time = "2026-09-16T00:15:53.466Z" }, + { url = "https://files.pythonhosted.org/packages/83/c8/f4a865490df0dc0c8531d4e59ac411cb6dc24bb255d2396a6f1c60a368f4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4", size = 286551, upload-time = "2026-09-16T00:15:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/b0/67/b4faebde9da4e8173d0e5a30e8cd31335914af7ef350b988f27fec588cfd/propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9", size = 249595, upload-time = "2026-09-16T00:15:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/52e1dd5636e9f5a27f6b5a4b4e2f33c322fd72afe956c397d82523ec4a80/propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e", size = 286700, upload-time = "2026-09-16T00:15:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0e/30b2b324b93ff31a0bab539c102aae59e84e444031b2742150a7646aa1bb/propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57", size = 280500, upload-time = "2026-09-16T00:15:59.487Z" }, + { url = "https://files.pythonhosted.org/packages/64/36/721bb59f682ff060d0c8df64274fca8cd0521b1a54506c2eedaef795b7f5/propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97", size = 46121, upload-time = "2026-09-16T00:16:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/c1/86/0b1b80fa1ac3a0aac44e2922a6964fbe9cd52af5eab8fa933bf9e90b030c/propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12", size = 49154, upload-time = "2026-09-16T00:16:02.901Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/9fe6f05a47cb550c823155052116f710064b6be5c6e8ec4e9faae7e18115/propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827", size = 46005, upload-time = "2026-09-16T00:16:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" }, ] [[package]] @@ -3151,6 +3695,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, @@ -3163,32 +3713,43 @@ wheels = [ [[package]] name = "psycopg2-binary" -version = "2.9.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, - { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, - { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, - { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, - { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, - { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, - { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, - { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" }, - { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, - { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, - { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, - { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, - { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, - { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, - { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, - { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" }, +version = "2.9.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/76/7b4383014be0fcc6c1c0e24292845a14e1672cf17fca62ca0a2bd5f4563d/psycopg2_binary-2.9.13.tar.gz", hash = "sha256:e324ecf60f952d21dd11413b8bbed0951bbd99579a06fd06f28bfc37737cd373", size = 378112, upload-time = "2026-09-10T00:06:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/d1/d0125c56b865e3bc9f318d84930b2df71a729229dbb0ce12de748a82a6d7/psycopg2_binary-2.9.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bf9f97a6df69a5d89d054b8cf5257a0916096c479800715fbfe7974dbcb3a26", size = 3725735, upload-time = "2026-09-09T23:54:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/a5/b5a73d0910555e38ee12c49c1740855f8a1e9776e87d65f0c51e1bab762a/psycopg2_binary-2.9.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:07b7bd9f410650c34c3532162cc329f112368d78a3fc8668cb1ea9df61bc11bf", size = 3818000, upload-time = "2026-09-09T23:54:53.229Z" }, + { url = "https://files.pythonhosted.org/packages/3d/43/3e4783f62ae3f4fc19a5acf8d1c394df54458f1336febe188f317556d2a7/psycopg2_binary-2.9.13-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0463c00f946517f3e69192a59e6601e023ff9de45ad0a875eda3d6b1bebeb7ce", size = 4585760, upload-time = "2026-09-09T23:54:55.313Z" }, + { url = "https://files.pythonhosted.org/packages/8d/c4/a9a67ae65ad3d567eb0fc9cdf9a5a2783b779aecdcdc8945f1807b13d99e/psycopg2_binary-2.9.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e3861eba31f8ea8663fd876166b032fd89179e42aa63764d6feb281f13f9eb60", size = 4282380, upload-time = "2026-09-09T23:54:57.362Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/9d459d3da12e0b841cf1596579455aaa27e9e593e3e9a5a4ded5a55a7c15/psycopg2_binary-2.9.13-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3dc3372b3731b3ef23407fe06b94f640ef87a2bda242fa386033d5589c87514a", size = 5902039, upload-time = "2026-09-09T23:55:01.955Z" }, + { url = "https://files.pythonhosted.org/packages/d6/53/21079c10a581c50b6817498eda7c3481c1b3cdb41482bd08ebfccd3664c4/psycopg2_binary-2.9.13-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0405dd4d97720e7ab177aa02e493f524907c4cb3c445ac173e2627948d3d0528", size = 4119840, upload-time = "2026-09-09T23:55:04.336Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ce/71e8d9e1b4f3e78157b49a5abdff50d915e95f2812550f6c9b4f2e4d5e94/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b6ae51708201f501a171b02419d0c30878a743c369c9054eb1289f0f8d5979e2", size = 3661077, upload-time = "2026-09-09T23:55:06.118Z" }, + { url = "https://files.pythonhosted.org/packages/d9/54/b17616472f09a0fae96f8852692948b7eaa7c971d7629696da0e5932d996/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:81682c227cc1849c4a6adf7b85274229073bb4c9d6ad5697222c695dcea5a8a7", size = 3307202, upload-time = "2026-09-09T23:55:08.061Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/d9737e222a377dac67a0ce0a2c73e7231a57f5cf18bb35a65d5c8d45d5d2/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:13d955f6054a705a19554364fe9888d0a6e8b0746dc7ebc08a447c7b4fd4145c", size = 3052893, upload-time = "2026-09-09T23:55:10.209Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3d/c406c9f698f518c264381192c2bdf8952ee84e469ffa9f82db1411f57385/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e2405196a8cfe6cd3e54172a54452dcf85c241eaf2e9dde7190d7469f7f5ef7", size = 3355210, upload-time = "2026-09-09T23:55:11.883Z" }, + { url = "https://files.pythonhosted.org/packages/27/64/6e3a96699770af2d0d49a2002f722c69b656fc27623ff89281cf2b109644/psycopg2_binary-2.9.13-cp312-cp312-win_amd64.whl", hash = "sha256:376ebf7d8aee4b7386b2bac31fdc27911e7e57cd0a88f1e038b8b149398ac008", size = 2767784, upload-time = "2026-09-09T23:55:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/82/0a/795f2869788373cf7d08410341a444196e8ccebbac07a70a8f9a1f60e72f/psycopg2_binary-2.9.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4d66bfd44a46eb88cff0287929a4193fb45166b6c1f84bb1b233cc17ece0813c", size = 3725723, upload-time = "2026-09-09T23:55:15.887Z" }, + { url = "https://files.pythonhosted.org/packages/b5/63/5a9633f4563a73beba69b20a846ddd14c1c6ac072f5e8aab0da97ffabc2a/psycopg2_binary-2.9.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f818161d2302b3b3e9c75d5a1d0a5c5679e92e45cfec6432b9d5432dde5ff1f1", size = 3817976, upload-time = "2026-09-09T23:55:18.025Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e2/b2e3b3a4331dc8b58e328cda30f3d0cc43a94b7aaf0c8383efd53dd10e95/psycopg2_binary-2.9.13-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:31db6cba66df5231dfd91d9f69188bec3fe6c8baae384e93a0ce792067ee2d98", size = 4585813, upload-time = "2026-09-09T23:55:20.112Z" }, + { url = "https://files.pythonhosted.org/packages/56/5c/87daea77c4132114d1a5da3a4928dd59446c3b3cc73d288cae08cf0b91a6/psycopg2_binary-2.9.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f04ada42bcd537adbaf8b7f3140237a204e452a88d0c1831cfce69f7d2e59f4e", size = 4282438, upload-time = "2026-09-09T23:55:22.329Z" }, + { url = "https://files.pythonhosted.org/packages/91/e5/56f9efdc9337acbd1a75798d97163183b63a1babc17602f7163009506c96/psycopg2_binary-2.9.13-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa37089795bd9701576edc2eb5849ce77a439eda9dfdfa47857449332cfa5292", size = 5902064, upload-time = "2026-09-09T23:55:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e4/15/f7ed0b90b47b73a9087306b42267eccfd919f92c0fb057e46bd2fa2efa4d/psycopg2_binary-2.9.13-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:41c2eb569ebd0e1b02d30d361a46932923b193fe1b5e641fb4d547c75e218955", size = 4119848, upload-time = "2026-09-09T23:55:26.433Z" }, + { url = "https://files.pythonhosted.org/packages/42/08/3091347b9fc5766e979aba6b0756ad14ce867a6bb245f3d69ac71fb768c6/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f699a5225094a5c61402984e2fc1eca20e940223e76767c88189efb0c313f69", size = 3661129, upload-time = "2026-09-09T23:55:28.449Z" }, + { url = "https://files.pythonhosted.org/packages/34/c4/4f9a84d55484c9794b364548eb6e1fe10a57f123afd19729e5a1cc8ad7fc/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5f04ae99c9fbb94c3197ec88599ed7db921f6adcddfe83687a74c7ead4037c22", size = 3307262, upload-time = "2026-09-09T23:55:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6eba8306a61dc890805ae475a9e71790a1c5461ccacbd4f0a1f3f57b40f0/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:81404c37e0344ebcf10aac127d33d35137e5dbab1daf9f3deee46188fd5879c2", size = 3052898, upload-time = "2026-09-09T23:55:32.961Z" }, + { url = "https://files.pythonhosted.org/packages/b3/5d/42a8935ab280e8dcd7c07a655c0c3d25d62e9e242be1961ac14630f1294a/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:feb7b1856f6ca805cc0e08739858f6cdfed8ce903390126af30343c62899a389", size = 3355265, upload-time = "2026-09-09T23:55:35.071Z" }, + { url = "https://files.pythonhosted.org/packages/87/c2/0e0ffb4caeb651631cbc6c8ead83e2a16457750b1d2eb7f5ef111c1f4d36/psycopg2_binary-2.9.13-cp313-cp313-win_amd64.whl", hash = "sha256:691da68ae5dd7c3ac77514357d35ece7b1ba8b5f3e6c92735198aa6159c355c8", size = 2767914, upload-time = "2026-09-09T23:55:37.14Z" }, + { url = "https://files.pythonhosted.org/packages/5f/32/897c074cb99fbdda7d34b0a2546097a59162bb3d04c0d546ae4ec82345e3/psycopg2_binary-2.9.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2ca263643ae37998ae04d18e431df34d0d61f12b47640dab585f14b6dbe00798", size = 3725788, upload-time = "2026-09-09T23:55:39.04Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f4/e3a789de34c9ac25d20b25c2be583da16394a2ba0926da1c863653831f41/psycopg2_binary-2.9.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c0214c7da18a28d108aa7108c8a3cca8035c7911ec97ef9ec0827569c9a2720", size = 3818111, upload-time = "2026-09-09T23:55:40.979Z" }, + { url = "https://files.pythonhosted.org/packages/72/29/647724c43ac510dbc59b80e20e85d439deb94f5d5a024153c32330fa041d/psycopg2_binary-2.9.13-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d89e064bb12b40cad696cf4975e6da86f8c60f14cd06cb6c1bc0a7f5d01761f", size = 4586235, upload-time = "2026-09-09T23:55:43.012Z" }, + { url = "https://files.pythonhosted.org/packages/91/ad/7f52f92cc65c23778daff7eec4ee2099236694a0a4723a5f180d0708b607/psycopg2_binary-2.9.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:190c18b97d9ef72f2e88c451b6588af90d6bd7bf54cb94b963280dc86a2c7076", size = 4282424, upload-time = "2026-09-09T23:55:44.843Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2a/1a472059b198942d99651656e2bc610575584478bfe68d297ecabbd4887f/psycopg2_binary-2.9.13-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c00ebe9a2f31151aade0db233dc1446513a95e92c39ce055ee097af0ae86be1c", size = 5902365, upload-time = "2026-09-09T23:55:46.619Z" }, + { url = "https://files.pythonhosted.org/packages/91/1a/171ea5dac7b3a0fa57b3cb59c2ad6d7b8bc60732368fecfd2ed1f1288392/psycopg2_binary-2.9.13-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5085f7ff7b1e890f279577cedeb8c628957869a340fa34a39f7f406500b3c916", size = 4119938, upload-time = "2026-09-09T23:55:49.381Z" }, + { url = "https://files.pythonhosted.org/packages/41/ce/3c6d4ad71853a59eee6a575fe36df4bb40752a9735a27bd62af66b454ed5/psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4e55357d1943673d491bbabb171c891704fc6a22441fea539e05a5c27a79ea3c", size = 3661557, upload-time = "2026-09-09T23:55:51.269Z" }, + { url = "https://files.pythonhosted.org/packages/10/a3/1819a01bf951eab2afb5ca2a3d11f50500bf536fecff088154372a8d1985/psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3e60b06ec7f9dc3e5f1106d12706514b6d6b92c3dc438fcdf4e43e65cc660d1b", size = 3307770, upload-time = "2026-09-09T23:55:53.196Z" }, + { url = "https://files.pythonhosted.org/packages/4e/df/22f4aec952cd5b2dd02f438399583ed69f7d04b90e7c31659d9571bbe188/psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dde942b46ce20f6c4464cdf551f3293207f803f4e4354454eb1f5599c3eb1fa1", size = 3052935, upload-time = "2026-09-09T23:55:55.117Z" }, + { url = "https://files.pythonhosted.org/packages/95/42/aab651bc22bafa961806ca3b21027bb0739a2730b0e6f7f0778baeb95e67/psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:215777c62ce81c3b487cefdb6a41969944eb982309f91349ff3ca0323d6f17ed", size = 3355387, upload-time = "2026-09-09T23:55:57.366Z" }, + { url = "https://files.pythonhosted.org/packages/bc/af/3b8220633eaf955e95ea7be67d76e81a0d1cd3c76362ea504b91ffa079db/psycopg2_binary-2.9.13-cp314-cp314-win_amd64.whl", hash = "sha256:f3088eb80f58ed933c62d87128741d31e786edc862e23266d3c286763d646de0", size = 2863890, upload-time = "2026-09-09T23:55:59.056Z" }, ] [[package]] @@ -3245,6 +3806,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/ae/b1b97c9ca87f9f9ddbb5230c798df94eccce61bd79b9b45458c69a478588/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f89685964f46e4216103c75483aac0c0692a5f72212d7ca835adba5ede56ce3", size = 49951343, upload-time = "2026-08-10T12:39:11.8Z" }, { url = "https://files.pythonhosted.org/packages/98/9e/a112df5cfd5a68cb1d9fc31cfe38c28d5aec9f10865ce37ecef2e4450873/pyarrow-25.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6943e2fe7954d29d84de45d29d34c8dc36ce96570e67d89aa9976e650a4a9138", size = 53144784, upload-time = "2026-08-10T12:39:20.503Z" }, { url = "https://files.pythonhosted.org/packages/31/24/97e8bd98f1e3b07e2ba08bcdff690674fbe16d69a7d2712cc3884665e615/pyarrow-25.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:31e49a7888fcdf3a835da33ae777f6bb9a866334e5a789282fc26dcf426f7f15", size = 27870159, upload-time = "2026-08-10T12:39:26.161Z" }, + { url = "https://files.pythonhosted.org/packages/36/4c/b525824ad3094076919273cd97db61fb3d78252dee76fa3b8dc8f76774aa/pyarrow-25.0.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bf0b672390cdcb640d7288f96b826d71ff4e9abb254a86c89890baf51a29cee6", size = 35885255, upload-time = "2026-08-10T12:39:32.366Z" }, + { url = "https://files.pythonhosted.org/packages/08/62/448bb0e940de41aec31d1a956e63ad9c54afdf122a103cc3ab20c2a3ce33/pyarrow-25.0.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:38a9a4b4b9613380e200641891495a56c3d5a98a092db4a870af9975e220471d", size = 37644461, upload-time = "2026-08-10T12:39:38.142Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9a/13587e38bd4806fd218f50fd13b8903fab60588a699ff0c406372e5b4043/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b726ad7e7b669be982b0c71c07fe4b037d654354130da79a7902a669e93a66b", size = 46877146, upload-time = "2026-08-10T12:39:43.722Z" }, + { url = "https://files.pythonhosted.org/packages/8d/61/1c5d1229fa21da4cff5365e41e57177aaac57c563c727f35419b8513d1c1/pyarrow-25.0.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:9171748cdf796972d85a4b60157c279913e242992e350c90c7450182a9838b2a", size = 50131616, upload-time = "2026-08-10T12:39:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/43/20/291e1d65cc0b09aa19f03cf25cf51a2f5fa94b5db315178f2d254ed5cad4/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b7a296aac7a71fa0886c08e155ddb6c636a50013f801f6178daafa0f9e726188", size = 50008879, upload-time = "2026-08-10T12:39:56.891Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7c/1b7c9ec28e76576337e4f97b31141c9a181b89b6d1d6221e9d8205621a58/pyarrow-25.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0fe7c8b6c03969b49c8c66182e4a18e3819ab92d07cfab5d8370c531b9369ef0", size = 53170864, upload-time = "2026-08-10T12:40:04.918Z" }, + { url = "https://files.pythonhosted.org/packages/b7/75/f3d789dc06011a765d14d86bda799cf72ac1d715b6a6edecaa0d73d95062/pyarrow-25.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:f729cfdbd36fd99d543b67a914d2de044c84ebe45be8b34902b299b608c15c8f", size = 28620729, upload-time = "2026-08-10T12:40:51.41Z" }, + { url = "https://files.pythonhosted.org/packages/fc/05/647a8ee6f7c2662feb6921315617bc04dcd6034763fb61b1199720bf6162/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:59a2de54c0cbd954da861eee4d1d330f8e909c45b53455baef696380f2c55033", size = 36130288, upload-time = "2026-08-10T12:40:11.014Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/c9ee997554d7bea94520667dd1933f109ac1da3ee3556d2b49381e023484/pyarrow-25.0.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:35935cd5de130aa5cf4dea052a63e6bf2e17006c35c3a468194242b9b2bf5956", size = 37762187, upload-time = "2026-08-10T12:40:16.592Z" }, + { url = "https://files.pythonhosted.org/packages/a2/08/a28c01c7fe9e96e8233ce2d13df1d402f4f999f848f51d2daacd6bb4c036/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f3831aaa25c67a99f99dc8b05873cb9d64560390372e2aa197ce9dd4a3f06a44", size = 46888003, upload-time = "2026-08-10T12:40:23.242Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b9/58612e977d28dc58c878448866838369ee8da2f1e7cc8ed2c84b952aafee/pyarrow-25.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6a1fdfc6659b6b19022f2e50627fb5cf7156a66c46bf4299379955cbe742382a", size = 50079036, upload-time = "2026-08-10T12:40:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/72/13/66e1402dcc860e1dc2760b1e0292c9a569b62b3bccab69def1b3e907d006/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:169d3429d5be7c752125890620f75a60776d38b0035eddae939651640822332e", size = 50040226, upload-time = "2026-08-10T12:40:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/78/10/3f1a5497a7ef732ab0f03ecca3e66d89d9c0f57fdc61b4794c456b781f01/pyarrow-25.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:119297a6dc197e45d9c6d4415f7814a67ffa36c180d26f68c154c58067ae782d", size = 53149035, upload-time = "2026-08-10T12:40:41.454Z" }, + { url = "https://files.pythonhosted.org/packages/93/c0/37d4a7e8e2f7a6076283673d5298018ca26478b934c6ee369e10505ab32c/pyarrow-25.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4288f27577352d608ca08553b0865e4a9b3aa14820c5d95b53337218d609835b", size = 28753071, upload-time = "2026-08-10T12:40:46.623Z" }, ] [[package]] @@ -3432,6 +4007,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, @@ -3497,6 +4102,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/ea/99ddefac41971acad68f14114f38261c1f27dac0b3ec529824ebc739bdaa/pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", size = 38038, upload-time = "2023-01-17T20:29:18.094Z" }, ] +[[package]] +name = "pygal" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b9/58bbfa4d0e2672b0f0baf84047b17d58fda0d9351427e2b6784899924a1b/pygal-3.1.3.tar.gz", hash = "sha256:dd119c14cdeb56beb85282e3e2687ece30561225d410a822e6bb68699aa6e7b1", size = 80887, upload-time = "2026-06-18T20:44:51.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/43/5441ea0f9a35a0f2d30a79712cc21c737cf959a3451565d41e09d2fd90de/pygal-3.1.3-py3-none-any.whl", hash = "sha256:c0b9bc2d31df4094c9f65b0969b62571a47b28197aced081b1a9433c3a760f32", size = 132630, upload-time = "2026-06-18T20:44:49.643Z" }, +] + +[[package]] +name = "pygaljs" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/19/3a53f34232a9e6ddad665e71c83693c5db9a31f71785105905c5bc9fbbba/pygaljs-1.0.2.tar.gz", hash = "sha256:0b71ee32495dcba5fbb4a0476ddbba07658ad65f5675e4ad409baf154dec5111", size = 89711, upload-time = "2020-04-03T07:51:44.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/6f/07dab31ca496feda35cf3455b9e9380c43b5c685bb54ad890831c790da38/pygaljs-1.0.2-py2.py3-none-any.whl", hash = "sha256:d75e18cb21cc2cda40c45c3ee690771e5e3d4652bf57206f20137cf475c0dbe8", size = 91111, upload-time = "2020-04-03T07:51:42.658Z" }, +] + [[package]] name = "pygments" version = "2.21.0" @@ -3508,11 +4134,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.13.0" +version = "2.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c3/8a3b59c25070cc61dc517fbdfa5dc0904670c96f605cc69759dc09166b99/pyjwt-2.14.0.tar.gz", hash = "sha256:77283c83fb56ecf566a886c757a714bc83668e38156de2cce8263302f42e0b86", size = 113177, upload-time = "2026-09-11T13:11:54.638Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/672cb32ce0dfea44b740cb7b4f97038463b9cf7c0ead1aacf595572851d6/pyjwt-2.14.0-py3-none-any.whl", hash = "sha256:ad0cef71c756a56e74863c2919cf0985f72decbcfcb550ee2f422e7c62b5eedc", size = 32896, upload-time = "2026-09-11T13:11:53.409Z" }, ] [package.optional-dependencies] @@ -3540,11 +4166,11 @@ wheels = [ [[package]] name = "pypdf" -version = "6.16.2" +version = "6.18.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/66/54212e75406afd9f3e933d0dda23072f6aecc55c5a273077dc2e0b028b23/pypdf-6.16.2.tar.gz", hash = "sha256:595647f6191de6f402cfde1d0c455d6cbccbd509aac32b34783009c032de5d6e", size = 7008996, upload-time = "2026-08-23T13:50:07.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/c2/e537e78a8282b51ed47031566be118debbc34c04782a9afd8739672eb07d/pypdf-6.18.1.tar.gz", hash = "sha256:2441fc839053745638d9b9ddb38ed66beed8d611f47a3036c9838fc4e3074dea", size = 7027577, upload-time = "2026-09-11T10:10:07.616Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/f1/a2da3b55acd4ab737bf728c97edaaed5ec1d3c1236acb639dcdfa97e42c7/pypdf-6.16.2-py3-none-any.whl", hash = "sha256:c8b09a59399062fb45a1b8156c18a787a10a3dae03ac9674397a226712c94604", size = 385060, upload-time = "2026-08-23T13:50:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/58/13/645df3995075112cb3cce15e8797c205f0f88fb50acc11012b84b071bc22/pypdf-6.18.1-py3-none-any.whl", hash = "sha256:ee93a2665670ecf57ee81d197a4ca548f3dc15f9cefc56e59b8140866aaa3de5", size = 394327, upload-time = "2026-09-11T10:10:06.103Z" }, ] [[package]] @@ -3558,44 +4184,50 @@ wheels = [ [[package]] name = "pyproject-hooks" -version = "1.2.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/94/21931ee768a9544afa6330a2e790e6ef8ce70568e88d79b15560c3054e91/pyproject_hooks-1.3.0.tar.gz", hash = "sha256:26dfd4229ff1820b7964be33bd3eb64367eb7c841215defe11341450c219ce20", size = 20883, upload-time = "2026-09-15T17:43:43.445Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, + { url = "https://files.pythonhosted.org/packages/58/86/3276e42c9349f2349a8a0fa6abf145c867ed7719a56c907e6ef21edf1462/pyproject_hooks-1.3.0-py3-none-any.whl", hash = "sha256:733909c6ac133d222c0b8cfc11364f19d363793d8fbcd8bb7240c5220af8d35d", size = 10723, upload-time = "2026-09-15T17:43:42.11Z" }, ] [[package]] name = "pyright" -version = "1.1.411" +version = "1.1.414" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, ] [[package]] name = "pyroscope-io" -version = "1.2.1" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/d2/5fc44302f861eb2fd19bf7e56423c93e8867199c647337bb9849bc6d2929/pyroscope_io-1.2.1.tar.gz", hash = "sha256:c3236136dc086845d283fbeb434f5e22f5a7ddc0ff5a5b328752335d61ee1aaf", size = 74584, upload-time = "2026-07-27T11:39:44.57Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/2c/cfaddac31c80b471cc896d3d09ee88d99779861a6b6dc3a7f7d43f12e39e/pyroscope_io-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:692091ae7d020ee76a16d9da2e213b69c20367d20032ecb643c23e3a5831f87d", size = 2113014, upload-time = "2026-07-27T11:39:18.237Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d5/188f433e9ab08973948fac72e6aacf2a641a69c45885bed982cc8a768e64/pyroscope_io-1.2.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:ceabd42b2d61876eb81668cb76bb04b911d62369645c1c4eb63c9d4ed5106b3c", size = 2196053, upload-time = "2026-07-27T11:39:19.594Z" }, - { url = "https://files.pythonhosted.org/packages/96/86/fa0bca1756f881b9960c460cef7604e9044bb84a68d9d76cbd3e39372cf3/pyroscope_io-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e64e2f34f340b163013b2942017d98bb0a4584e26ae64dfdba3f85304eea8efe", size = 5579568, upload-time = "2026-07-27T11:39:20.804Z" }, - { url = "https://files.pythonhosted.org/packages/de/1c/a7dac80341fe67ae775a33f9d0da2c04445479be3ce8a96e4b62df210c6e/pyroscope_io-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16a39b80e5032fe978eaac6abf907e7b79128357adc0f0d8e7921e68bd57c4ea", size = 5123015, upload-time = "2026-07-27T11:39:22.185Z" }, - { url = "https://files.pythonhosted.org/packages/6d/90/049e9fda943f6fd01f035783c633d3e83a1a34aa00b1ef1eb325f0b0aa86/pyroscope_io-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:45f5d7ed4a7a158732d3c1634846011c382309720d891200f64aee3a11173418", size = 5510537, upload-time = "2026-07-27T11:39:23.633Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9b/f35ccd7935f87aa67a60e158b45f992e2e08243c5c6b432567cf7b1ef194/pyroscope_io-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7bc01f8b5c49efca59987194c28667559b1fbded965943d020d12aadc40d0c5c", size = 5239529, upload-time = "2026-07-27T11:39:25.532Z" }, - { url = "https://files.pythonhosted.org/packages/fc/11/8a3d16443d157f817975e61f2e97e894a6e2966cfe700cb900a4f16caf9e/pyroscope_io-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2ce28485fc46390c31300abde8c93b7dd6c042960deb1b729c2d001a8795516", size = 2112233, upload-time = "2026-07-27T11:39:26.889Z" }, - { url = "https://files.pythonhosted.org/packages/cd/34/640bcbeb50aec8dac27b4242b349a2a2eeb3c4ab8b62660ae9cb2dbe1cee/pyroscope_io-1.2.1-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:10b0e7a8040ed6b953c9920f2c463d6dddd4f26c01dcb1ffb696879c15cab218", size = 2195244, upload-time = "2026-07-27T11:39:28.397Z" }, - { url = "https://files.pythonhosted.org/packages/03/07/d3e7c279d44b88e94484453574ff837b0fecae3395a979d4def25b548234/pyroscope_io-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:655f8629f3f8c5b8c2bec105f0c19a69be4666ae5e452e4c910b2fa3de3452d6", size = 5578633, upload-time = "2026-07-27T11:39:29.805Z" }, - { url = "https://files.pythonhosted.org/packages/4d/59/9ce4cdafc9a31eda7aeb37c931367c61dab7846b56e396259273efa46e6f/pyroscope_io-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e232ea23cc756f6990a325230c6f22d7c380b012a3bd9dac476a338b1edd7d26", size = 5121946, upload-time = "2026-07-27T11:39:31.478Z" }, - { url = "https://files.pythonhosted.org/packages/19/80/cc8eb25b908e27fc7ef63b007eb226e3c31903fd9886ab18dea1fccda2c6/pyroscope_io-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:35e57ab13d40d8af15821bc30720eb5c74949c169b649379ad114f7e1f75cd8f", size = 5510621, upload-time = "2026-07-27T11:39:32.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/9c/6452ac8356c4f7f11e67a96bc57062b813158e1315fad63ac81008d5ed79/pyroscope_io-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:31a2a694f507280a451fe082789599e68af48389c74b2c15d99715bd0d0f9f77", size = 5238044, upload-time = "2026-07-27T11:39:34.654Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d1/b6/d9e545262fd93c46cc0714c3006c135fcd32e64331efcd391bc4dc8abe13/pyroscope_io-1.2.3.tar.gz", hash = "sha256:d7a2529efeaf8a0b7ac9326e5b8228b16f12171c6e6f07400ed65048b789fdae", size = 75094, upload-time = "2026-09-08T10:16:51.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/79/df054a33fe90cab7d05c05ee0bae3820c5a2ca2f3ee13924caa27e9e421e/pyroscope_io-1.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9dc120c68a1af17dfe55933ee6e89d6ebc6c92c244235f51ee15eab4893efc", size = 2111877, upload-time = "2026-09-08T10:16:24.652Z" }, + { url = "https://files.pythonhosted.org/packages/50/b6/721cd69b0eb2ae2ac9d8c48abf4398b434e068fdf24d6847c2ec169b899a/pyroscope_io-1.2.3-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:1ffbf26ea19b05b6020ad5167802a1a6a8af0d02020a887172cecf74af2bba4b", size = 2195425, upload-time = "2026-09-08T10:16:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/1489788e8fc3cd6c9a14b50cf7d40fe1cee276780893849572c62e908b6b/pyroscope_io-1.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1d06dcf3298e2ad5a3bd3efade9331e50291ad09b41e7643b0bcb2c574a3bc2b", size = 5581228, upload-time = "2026-09-08T10:16:27.222Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/ebccfa22e1fb1b2227df447130b6f8f5388366c2b6e9647b7259e6e9bda0/pyroscope_io-1.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:568a45945a7dedb33cc67657a06bc32085d11bd0b74adb1fc8a5e044cc7bf096", size = 5124659, upload-time = "2026-09-08T10:16:28.737Z" }, + { url = "https://files.pythonhosted.org/packages/ec/52/4d6088f857f00a57b93851a07f137985ce96c24961e1fac16709a6d14ded/pyroscope_io-1.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c47a3f798c83258189173de5332bd92d36e84c05b71fe36c9e3412ac291852d4", size = 5513055, upload-time = "2026-09-08T10:16:30.141Z" }, + { url = "https://files.pythonhosted.org/packages/65/22/dd430812a92c739ec046840e22032eeb40a4ad4a455c2ed1537f6e2aa60a/pyroscope_io-1.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c641d9c1378ac16ce8e4d081aa0d8203a71907540927dddeb17212ea65708820", size = 5243163, upload-time = "2026-09-08T10:16:31.858Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f8/f7ba7b2d35cfbd2666a4d87258e8d5d39f36703eade6fa75f53568b63257/pyroscope_io-1.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dc258d7f9a3eda932bfb802da65dca5ff2599cf2b29c0804c18e3b81e0914820", size = 2111420, upload-time = "2026-09-08T10:16:33.643Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e4/480ddca9a12cfcb81dbfc5b6914de6f6c7d069bbc24dd18421569d2e7c7c/pyroscope_io-1.2.3-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:f85b4150d61d7e1fa1153fed60867ab27606ff9b0aec27b01d3dc29cae65cfbc", size = 2194392, upload-time = "2026-09-08T10:16:34.836Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9c/ee8b3e5546b1c369045eb9912a5f590a9f2a6daafc2f3ba5338cc37f1e5e/pyroscope_io-1.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:69c9f5a06f286edc1728c95b1d45cc868acbb471b6d8b49ee630814a564bbeba", size = 5580928, upload-time = "2026-09-08T10:16:36.261Z" }, + { url = "https://files.pythonhosted.org/packages/d5/23/60abf34c26eb21b02b5b7a3664d1891c50723934a587331acc948d768c4b/pyroscope_io-1.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:703e8fe769b26e41abd728bd3491c2f8adfc1843480dee7d82a94cb36b69dfa2", size = 5124040, upload-time = "2026-09-08T10:16:37.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/22/5d924b6c70a2ee30faaee4f842daf1b6c50676d70b827edc9f2686228aa8/pyroscope_io-1.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7584c53cef3416f75e40ce48b750f106e9a96135dda7ec7ae5441b6ef5fb5e0d", size = 5512810, upload-time = "2026-09-08T10:16:39.129Z" }, + { url = "https://files.pythonhosted.org/packages/88/79/0ddef490331ed7790989bdf8412a2136a382c842cf46fb662249bf7fccf8/pyroscope_io-1.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:06d8d392693452263ec14955322d4e46804136215830fc131fad4a12bb757116", size = 5241779, upload-time = "2026-09-08T10:16:40.631Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d3/58c1726df5e03f733e37daef3e4828425673463f3f3b17cfe8e3b481fe54/pyroscope_io-1.2.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2bffd16e42eae3fad97b268ffeb8f4721cdaedeaa8e836b6be8925975b1d4acd", size = 2112285, upload-time = "2026-09-08T10:16:42.047Z" }, + { url = "https://files.pythonhosted.org/packages/4e/85/0ba10f6c5337b86996b19c27e37e4ab0d02b10662ade4da5dc62109c2a9c/pyroscope_io-1.2.3-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:b7797dcc9906a11cf1204d15e80fc9a188daddc488bf48714cec51592938a0e9", size = 2195807, upload-time = "2026-09-08T10:16:43.523Z" }, + { url = "https://files.pythonhosted.org/packages/fa/6c/594133ab68a208b24eb714f2d63df58e4b65dcab44407a9d489e8efd2831/pyroscope_io-1.2.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5633737a8f5e4734d5c898469564f94af2fc0271fb3280f4e633190a87d6c155", size = 5581416, upload-time = "2026-09-08T10:16:45.05Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/e3928db238fea4ea3a35b62a6fd567995817552a059d2bd79cd1d8a18855/pyroscope_io-1.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d90ab24e2fb030cd2d294dd80b94b6b0accfee0947c22d0734d32447a77d50ba", size = 5125683, upload-time = "2026-09-08T10:16:46.449Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/90f7fa67aa5b791418895233bde7639a42bd4241683f640bd347023e25ec/pyroscope_io-1.2.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f2c36546b814b5d6628b0bfc56cd27990ae1a2663ef2fd2e7f591d31996fa78", size = 5511961, upload-time = "2026-09-08T10:16:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f9/bce53a09243e4ec878fa12ccc9a14a4787d1caf43a1b093e21a7e138fccb/pyroscope_io-1.2.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c32155bbecfce4b2ce93642677b4c8443e0c83dc3da083ab9accd3f594081788", size = 5243402, upload-time = "2026-09-08T10:16:49.68Z" }, ] [[package]] @@ -3749,6 +4381,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] @@ -3763,6 +4405,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, { url = "https://files.pythonhosted.org/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, { url = "https://files.pythonhosted.org/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, ] [[package]] @@ -3800,6 +4445,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { 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", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { 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", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { 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", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { 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", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { 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", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { 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", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] @@ -3832,42 +4495,74 @@ wheels = [ [[package]] name = "regex" -version = "2026.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/c1/6b30b775c7bcc6cf6506a4d4741c2123e8d99cd50f3fe8cbd731f5fef526/regex-2026.9.3.tar.gz", hash = "sha256:aabd43208e335f4c3f0b56de3464b066dd425983a58f6eeb5738bcd7465403db", size = 416720, upload-time = "2026-09-01T00:53:43.821Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/cb/cba530bc3b068fc337f8f455c63ef5ee91a4eb4c76ecf5998e5cef5aaa6b/regex-2026.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5db80d0b1c8238940b5957dd66b5c818ea40a221f6652fb717c027a562d09c77", size = 496699, upload-time = "2026-09-01T00:50:27.98Z" }, - { url = "https://files.pythonhosted.org/packages/81/39/f2e9fb6bbbc80f8bf67ad79d7e2e8866f7837d7c24c692f7faf8f1272e7e/regex-2026.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:35d48ce3dee087b63b15cd0a7a3110d0a76c29edbe1f2ad0520b8c4adb7cb596", size = 297018, upload-time = "2026-09-01T00:50:29.487Z" }, - { url = "https://files.pythonhosted.org/packages/a1/b6/c16ee58840baf7659def27ef6f62f3d9a9909670d3c1b4b98bb8b8ee47e2/regex-2026.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f22e0d21ae7016c77175c139a7fca465b988efc1280df4816c79752068d9e2e", size = 292008, upload-time = "2026-09-01T00:50:30.929Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b4/4987bf0f17604669b4ea5aef219886d0a73188c4716ff3a7d275d4d15c15/regex-2026.9.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:233662cf8cfdfe3c0e58aa8f7bbefc579b5be0ac34546f123c159804179e8687", size = 796101, upload-time = "2026-09-01T00:50:32.486Z" }, - { url = "https://files.pythonhosted.org/packages/0b/95/2a9ab02a68c8a61dc0b4882ed643b1a95740d9dc291dc26c77d19af79691/regex-2026.9.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2eed2e4d231278a2ccab3f4bfa2c1e39855f336475f7756a281d767d2b1753", size = 865435, upload-time = "2026-09-01T00:50:34.171Z" }, - { url = "https://files.pythonhosted.org/packages/04/92/0570d41559b446c97c1148cb9ebc1df09f2949b03c7c9bfee09976b3465f/regex-2026.9.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e674cecb61cb160be392da07fd8a71509ef927f437fbf3215432692ed385151", size = 911828, upload-time = "2026-09-01T00:50:35.72Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8b/9cc6d4123033f7cb82df6cd8ce19eb0fc18a964afe060a03c9b26757c9f3/regex-2026.9.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665207e41bacd435db001099eeab44103197c2c1a729d73ade74688a905ed4ce", size = 801965, upload-time = "2026-09-01T00:50:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/c9/98/39262e91aa87a67c82cbe90a0df4c3d382c7a44811fe80067904085211b4/regex-2026.9.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a7ddc9a8ca1795166a1ca80364b8ce74187fc210e112d3fb048b711b934f36c", size = 776192, upload-time = "2026-09-01T00:50:39.57Z" }, - { url = "https://files.pythonhosted.org/packages/24/e9/3bb93fe4ee4b6f8ce7ba69b527c4a63cfa3393fc425ab26486041fe441c8/regex-2026.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3037d02425863ce9501afbaa04ba967162810004bacde39a53ea9a5b740eb32", size = 785053, upload-time = "2026-09-01T00:50:41.156Z" }, - { url = "https://files.pythonhosted.org/packages/f9/05/31d5bc2553a700c0dfc6b5b6a13c61cdcd1210fde1e304cfa18a33f138b2/regex-2026.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de4eab8c763393b75bbb26f81934ab2cc8794f48f79e90622e3ab7ea57f3d14", size = 860546, upload-time = "2026-09-01T00:50:42.746Z" }, - { url = "https://files.pythonhosted.org/packages/65/a3/2e1e854d80becda0f061093805bbfc037a5849448f46d0a2b71a070d45e2/regex-2026.9.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:98620c9c4c22568ad70f57b80527c780b6f8fd26e36507bf8e2273262a228275", size = 765841, upload-time = "2026-09-01T00:50:44.5Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d6/43d02948cedde2e8476ac893ea02755ee5ee1b21c531fda92d80e114f0bc/regex-2026.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0b1ba3aaaf5776de473ee16625ac60ac195abb0343afb273575a8201d99be089", size = 852147, upload-time = "2026-09-01T00:50:46.474Z" }, - { url = "https://files.pythonhosted.org/packages/21/ff/adb4e2d08afe8f4c6df004d94604257e1f72af7ba328af7715601585aba4/regex-2026.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56d8659c65166641d8f1b5efccc391c62c8a899eff4d528b981cc62b7b402a4b", size = 789761, upload-time = "2026-09-01T00:50:48.749Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e1/1490d1351758e87f6e702cf2025036bdc7bc59182e2ff5c7bec004b19aed/regex-2026.9.3-cp312-cp312-win32.whl", hash = "sha256:837c1859913798d8bebcd98d4a037e113f8d79e81733009bf590e449769eecb3", size = 267150, upload-time = "2026-09-01T00:50:50.414Z" }, - { url = "https://files.pythonhosted.org/packages/d5/49/4c40cf722d84d60e807a08ef4c3f579216bf97df60c4a1b10be49655d302/regex-2026.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ba1dbbb93c5c5629c1861763aec5bfa9f05ad24ef450694130e25029ce7bc36", size = 277773, upload-time = "2026-09-01T00:50:51.963Z" }, - { url = "https://files.pythonhosted.org/packages/aa/af/c48b3b2b4244b4b090554c78d3387e9ae7b859f3dbf7148a27d427e9e5b8/regex-2026.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:d7b3a8a4bbd83ad8b29758f5d24bab10a3f2de87970db36f1e3651c733353136", size = 277122, upload-time = "2026-09-01T00:50:53.778Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d8/1fb6053247efc5b5a1d7b3b7881dcf42861f8ba46bd72a2ff126469d4053/regex-2026.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d9148e47cfa1a067138867996b1d5d825de0132fed8dac3c92eebaaf312d280", size = 496442, upload-time = "2026-09-01T00:50:55.351Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b7/0b9c1c0385365ad12deba0bdf93a70ad9f97d1a919cc5699c33b449ad662/regex-2026.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:33c2860b73ea342c0a42bee9ebe3b3a0de3d68c580c4dcb52241cf4b6663731b", size = 296913, upload-time = "2026-09-01T00:50:57.251Z" }, - { url = "https://files.pythonhosted.org/packages/e6/eb/5750ebabdb010ffb0d31fceae68c7a8f3876c06140c3bd6a7feff6240d6d/regex-2026.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e33dfc13c02d9c4e55bcf3f3b2eb448537823a6f6f30bf737b2974b63a530bc9", size = 291783, upload-time = "2026-09-01T00:50:58.892Z" }, - { url = "https://files.pythonhosted.org/packages/96/12/3ee1542b6428a0955ac5a31562d87966a417c0e0a4c1e5c62ec822fe78de/regex-2026.9.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e84252a16234ee860206a738f9a5084f830a5d0a1370a418d3af4e917f5e08", size = 796114, upload-time = "2026-09-01T00:51:00.5Z" }, - { url = "https://files.pythonhosted.org/packages/0a/24/5ad415b80958d79f2da66a1997743ea8c38a1e9e2f63660f7cbf9303eda4/regex-2026.9.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3077ace9bf59f8513c8471a817a5af63699987dc024f535c1eac3447a4d70211", size = 865524, upload-time = "2026-09-01T00:51:02.25Z" }, - { url = "https://files.pythonhosted.org/packages/2b/8a/c298d469f5dd12b11d4e7b8c7714c808b9edace73118eaf753fc327c9429/regex-2026.9.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:635482cd183a1856da75a39c473a2e222697b7927f1955f586db83fc8a5da17c", size = 911948, upload-time = "2026-09-01T00:51:04.099Z" }, - { url = "https://files.pythonhosted.org/packages/3f/a6/e9a59b507cdf7a9735df4bab92b4ea9d2ca2e665c6de14c112db7e3c0926/regex-2026.9.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f0809798071f56fb1bc536bb93714a95e8ed2ec0dfd869f095deebb30fd11a", size = 801945, upload-time = "2026-09-01T00:51:05.878Z" }, - { url = "https://files.pythonhosted.org/packages/56/1d/443e5541fd23d97c841ccddf90dbc9a964bd00e0ec9219b45a3eb0cde552/regex-2026.9.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d514026ca1c473cc14440e4d7bdf6721c642455b647a74c8143c0f22da358c28", size = 776223, upload-time = "2026-09-01T00:51:07.58Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ae/78d48b14582f5733facaa2e001499b98707d8b8b1b31ab7559053c18f168/regex-2026.9.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b9190d4901d7786af9ab0ec46172e27cf7d72cdba2b82ee38eb40aadd3239a6e", size = 785072, upload-time = "2026-09-01T00:51:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/55/42/2f4ac830de12d264c1486f749988003a7f5a0366d7557a75de2418461466/regex-2026.9.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6c0f60b05cc708e6cdf68dbca86b7a36d99695962db0189bb1b3884ca3b28e90", size = 860644, upload-time = "2026-09-01T00:51:11.558Z" }, - { url = "https://files.pythonhosted.org/packages/47/50/6b44c6cf56f353744e6066a1310177b7b195afb11d33dff4d6cf3ec52720/regex-2026.9.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b7b7e6be82fd6d5256adabb82253c5c307de981cc20c0ce4cff0cbe6de88529b", size = 765862, upload-time = "2026-09-01T00:51:13.371Z" }, - { url = "https://files.pythonhosted.org/packages/64/0f/c5a8023dfc988cddd2c614b1afbe547aedab1ffebd5b5d9cd58f6ab28908/regex-2026.9.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b5f85bffdfe17da7dfff78eb32b261c27f3cd64c060033645079493f4cebc8e9", size = 852081, upload-time = "2026-09-01T00:51:15.324Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f4/ab0ba467ebcaecfddf72ea7b3527a6088863094377c1303a3379284ef621/regex-2026.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10a442c6450ebd35aa89392b8e4b0459ba474df63fc5e6828573d0a31a627ec", size = 789774, upload-time = "2026-09-01T00:51:17.057Z" }, - { url = "https://files.pythonhosted.org/packages/4e/fa/8e2f3021ff5ee3aafc80eed8f16663be5e16c7b663d72e4bd2c9e71e1433/regex-2026.9.3-cp313-cp313-win32.whl", hash = "sha256:63fa79eab192623acb169de1dfe8e733598c4047d06f7712347d1bb810a5ad20", size = 267125, upload-time = "2026-09-01T00:51:18.837Z" }, - { url = "https://files.pythonhosted.org/packages/6d/25/6d20a309c2e4b554cc33579dd55b0bb50d0c2ace7ce6f084be2327c330fc/regex-2026.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:185c1ae881856208dda05708b6c908aff76878e59c998c8548d365c1bbcaf1bd", size = 277741, upload-time = "2026-09-01T00:51:20.583Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b3/0282ae58fc167953809a968850622f89955e0fd70a65df882e5d488138b0/regex-2026.9.3-cp313-cp313-win_arm64.whl", hash = "sha256:db6538d733047f9ce4b74ee29c77643a1f99e4ca36e273495da93fbeedd2f03f", size = 277123, upload-time = "2026-09-01T00:51:22.332Z" }, +version = "2026.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/5c/f403115361de25809e8f785686ec7096e30fef73be9ae35aa51da4e80abb/regex-2026.9.10.tar.gz", hash = "sha256:1e321e2c84f0e52c457f5ea5944f796d6e8e09cb99738ea98dcc1bfe402a128d", size = 417072, upload-time = "2026-09-09T21:00:21.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/c8/bfbe893e90ee0148bd2860dd086f09b5d2080ca2b125f740c2e118c16982/regex-2026.9.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880ac684c27176464c00c3fdc456116364f5ebc70da07aad0c2d4a7ba45e98db", size = 496609, upload-time = "2026-09-09T20:57:09.987Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ac/56d5ae6efb759255c3b3db650a4be25f96a844ea3613b91e1e189a3b7294/regex-2026.9.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9d36b03dc362aa40ffaaec9d9bd75e87763529563ec008c43b0e07782f5be7a", size = 297024, upload-time = "2026-09-09T20:57:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/60/4b/0f2d5f6bbb791cc10f22f0ed16c487e630dde8fa8fa0bd92a2bfe21a4b20/regex-2026.9.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866de9f98df0611d7b62b3a8729d3284a64c0cc6edd90bb95a533e443a4939cb", size = 291905, upload-time = "2026-09-09T20:57:13.127Z" }, + { url = "https://files.pythonhosted.org/packages/89/51/3fb5fe0d32f4cf0bc982286722c729a8d6f522d2fa2d5d14a702d9fc87f8/regex-2026.9.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c66d54042a14a503907d81861b8a5235e6d1f03d4fbc1d8767f652eaf957ac1", size = 800055, upload-time = "2026-09-09T20:57:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/89/46/ee507bd2f9d4420f26a594b35c551d7194b66f5d7897f63730fae6ec05c1/regex-2026.9.10-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:032da15431c890d376f53547f0a6219f4f4cd19f3e4f11bdc321453b5bd207e4", size = 871133, upload-time = "2026-09-09T20:57:16.674Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/cbaa90689684f91b1bc017e7f8c6d9425c6bd299108db02482dc51376d8a/regex-2026.9.10-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:23ac9a28180f274d7dd7651fa131ad5b02d343b75df4b040737f0356223895dd", size = 919627, upload-time = "2026-09-09T20:57:18.402Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/dcdf5e0d898024005cfcce631e3e934d111dfbe177ca0b7f253ae8a735a2/regex-2026.9.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e67f8843f0e4b931f1fa860bf3bbe4134b714c0155cc5c7c0d7ea450230aae0", size = 804587, upload-time = "2026-09-09T20:57:19.859Z" }, + { url = "https://files.pythonhosted.org/packages/73/70/eedfe81c29bae266a06ab4250978361a9bccd474704d88d4f4ef4506dff8/regex-2026.9.10-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bb7774924f8cd69f49cba0b3c2d679a6326f777e0e67d130ad5203e4df53f0d3", size = 777320, upload-time = "2026-09-09T20:57:21.62Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e5/86b207077efbcd91305700488f170b7eb1e1c54721cea74175273aa3b9a4/regex-2026.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c32480f3371b75068decaf9e5da72c224e953830dd71e36e06cf80e30ea39d8", size = 790572, upload-time = "2026-09-09T20:57:23.048Z" }, + { url = "https://files.pythonhosted.org/packages/b2/92/f622c3b2323f4c035b98e80221740a442127ad7993135b814f52057430db/regex-2026.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d2d377fd1cad611b806cdd732d86b65f536c768209890cb442556548daa65a23", size = 865485, upload-time = "2026-09-09T20:57:24.658Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/34bd621d3d6ac906ad67e57ed56c40cd45f7d51b9c0328335e97a7cb8ecb/regex-2026.9.10-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c014641157e9049b0603b8daa5343bd408d9b757b709aaa0f373cd3fab2d7944", size = 767925, upload-time = "2026-09-09T20:57:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/0d/28/ddbf7cba86f2adf5038c6c16aa829636ffc6e437f81bb0cbf302899cea5e/regex-2026.9.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1562aabd9d4eb09bd88a62ad97ed06800094b529ac43419e43020b9cefec79b0", size = 858800, upload-time = "2026-09-09T20:57:27.901Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f1/e8d7656ff3d3bd32e881d32f540b4981c79dee61908d6b790a45966e6895/regex-2026.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2479171edccced52ef02b899558f88ab2c235fe05b93180fdcae1670aacd89e1", size = 791648, upload-time = "2026-09-09T20:57:29.786Z" }, + { url = "https://files.pythonhosted.org/packages/fc/65/eac1a79115c8475d8ff539602eb54031564d719fdea5a599c4b0a1a26d1b/regex-2026.9.10-cp312-cp312-win32.whl", hash = "sha256:239620b0e0681669367c0e218c8eb2551d9f8fe3b9fccfc8d0003377804e8348", size = 267326, upload-time = "2026-09-09T20:57:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d4/4dcd0a05d3e97ca829165df1c39717f899d1d484dac8a6032439f2cb8d6d/regex-2026.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4db7d00c4afbfbb55b8e17b1e371da11418ea9389b030acec63c1fa4c7ad4b86", size = 277933, upload-time = "2026-09-09T20:57:33.648Z" }, + { url = "https://files.pythonhosted.org/packages/55/f8/22617a80dee28f2451011eae36bc26b3d78c4994ba87b5281d60acf9b6c0/regex-2026.9.10-cp312-cp312-win_arm64.whl", hash = "sha256:c25a754bb81a2edcfc3b65eda50f017d736f818112ed43e8aafd595cb00678ae", size = 277447, upload-time = "2026-09-09T20:57:35.156Z" }, + { url = "https://files.pythonhosted.org/packages/20/90/d4452bf1ef7dbe406980e8b921a257024482203c1dafac535eae207611bc/regex-2026.9.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ef5a059ea1c6ee5d1c7e99a2484e628608d010921efe876c6f0e2029d2f35eca", size = 496408, upload-time = "2026-09-09T20:57:36.757Z" }, + { url = "https://files.pythonhosted.org/packages/6a/35/c763c6424a0f99d021d46dc1f9065147bb5a40c2b2cdf28d2ebdbcd96508/regex-2026.9.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dce932f8e3ba936475ea3d0d8b59f7b050a9e206e994f53f8fd80299871e87da", size = 296931, upload-time = "2026-09-09T20:57:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/fa/68/241f88458b17c46ed2f80147a60a03b2ada7fb815c23b6bc76c298abb0a5/regex-2026.9.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8c668af8f7bdb1d18739c27d30cd9f4b371495a883f75a002fb7a39d740fecd", size = 291741, upload-time = "2026-09-09T20:57:40.482Z" }, + { url = "https://files.pythonhosted.org/packages/90/9e/974d6de404c63e2d09525f4ddb99874c7ab8e1f781ccbe0dd3e26fa6f6e5/regex-2026.9.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aebdd9a946de328b3f6f61dbf48dd064a36eb6dddf96e34ae6651d37f6e9383", size = 800088, upload-time = "2026-09-09T20:57:42.098Z" }, + { url = "https://files.pythonhosted.org/packages/9e/fd/3875b73f9e7ba3321dcaa02c19f650c05c61345328acf84599ac6f45ceed/regex-2026.9.10-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f2374c27deb189b282ec7e16106752c22ad39b056bbd8018960b1e4cc95d67a1", size = 871212, upload-time = "2026-09-09T20:57:44.03Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/2358e791c0e171194dd6a8b97b520579098a21397fb79dbe6b7edc9e3fa7/regex-2026.9.10-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e0dc78251154b66dc60211563fc115345da332eaa881e4e2523fb1edae3772f4", size = 919752, upload-time = "2026-09-09T20:57:45.691Z" }, + { url = "https://files.pythonhosted.org/packages/20/3b/000c79c3f9c06b7542225a5d3a7f9a85405da7224b3b9af94a491d07abea/regex-2026.9.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bafa41b0dd63669e5c0f8adf3d24819efeb73c847f492eb011212eb352e69041", size = 804578, upload-time = "2026-09-09T20:57:47.548Z" }, + { url = "https://files.pythonhosted.org/packages/30/6d/195eedb1de87f26639191e7487e41eb81e2ce255bc7563a64f3f5a95eb08/regex-2026.9.10-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ebb2ba68e4641a994061f70bf44ed448fba0b9b1d18c94ffb9efc1cca805b39b", size = 777345, upload-time = "2026-09-09T20:57:49.63Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/11fe2b313fcd92cb75c583648f2746031b9f4da9e9ed4241204a5e8b3721/regex-2026.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:048a89ee797db10160bd2bd519286577a6b43a100279bd4b7d8456a3d69c80a0", size = 790556, upload-time = "2026-09-09T20:57:51.27Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c0/07ec9b4c43b0e16d62454971a5ab3886eccb0bfa161300a02d801ab28620/regex-2026.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:79e9432995e14c749d34209413de5e621ec8e67789bf4f46dbfabea9d06a2406", size = 865572, upload-time = "2026-09-09T20:57:53.163Z" }, + { url = "https://files.pythonhosted.org/packages/19/07/43bc9a9cf9fc8e37d2ba47980dfe4a6e151d2cf3ab969e0031e2a9b21484/regex-2026.9.10-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5847e22bbf959764d776937d791d034cc2d19b787e361c88d97e859e8dc68502", size = 767971, upload-time = "2026-09-09T20:57:54.805Z" }, + { url = "https://files.pythonhosted.org/packages/9c/49/3b9286a3a94f3c89ed4ddbe74e72bdde21c1a5eadd520d5f4ed4a61936cb/regex-2026.9.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c103b3b14e011774af4fb7e4617ad4d72b9171905cd3b231a70a4efd76e477d7", size = 858835, upload-time = "2026-09-09T20:57:56.627Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/e5d27ce9fee8e3ef95f886c7b6ecec211efa4cfc18bd73bd5cf26cca4741/regex-2026.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6b34a778c695d24e77c140e3b4c95da69282e34f2f6b02b55656aa4a0379f643", size = 791793, upload-time = "2026-09-09T20:57:58.313Z" }, + { url = "https://files.pythonhosted.org/packages/63/03/c28a6bebedc3e2d86ee27ec2de16f7ec0419dcd10e771d43dcc9c58a2e99/regex-2026.9.10-cp313-cp313-win32.whl", hash = "sha256:7abb38b8c40f3a235235a44da452c64b7b5c1d650ec6351027db0e090804f2e5", size = 267298, upload-time = "2026-09-09T20:58:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/cd/fd/5c85fa6cfb8e034080bda5a72fa0a4df2b7777a35eb7e73c2799c2adda7a/regex-2026.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:20e8bfb07ad79a282f8b95b56fe67f9750b1b7f775724e4ba1f23cb296115ce4", size = 277894, upload-time = "2026-09-09T20:58:01.731Z" }, + { url = "https://files.pythonhosted.org/packages/c1/28/f5a25f6f65501675977fda35d9f61abb1468c4b87c0f73e536d8b21a60b8/regex-2026.9.10-cp313-cp313-win_arm64.whl", hash = "sha256:3bdeed3318a8eb2bbadc9c56347e0ff651639e934a47e168d05a3b12929fd0e7", size = 277436, upload-time = "2026-09-09T20:58:03.422Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ed/98e9b07d8bb9c765d07774f0b2c19b301b96d51f44630fea48951051c94e/regex-2026.9.10-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd6bd89b9fc06018d35851cab0240adb7dd84d51941b19f6574ac90cd54e3ae5", size = 496662, upload-time = "2026-09-09T20:58:05.118Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a8/9dfe9be48378b47c5a8f04b0f200ea225f9ee0f8e93f010433f661a37878/regex-2026.9.10-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ef4c0a9dfdc90581b90b1b95a8c3d1557f8ff8f5a2a53536d26314de699d1468", size = 297115, upload-time = "2026-09-09T20:58:06.822Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e4/5d1f005a3825ec49842ad349061c1c26e6d42f47ccf105e6e5aa6aeed392/regex-2026.9.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:14caa05ce39ec70437af5aac8814c50ee6628f4a90353871c059692f448a164f", size = 291896, upload-time = "2026-09-09T20:58:08.675Z" }, + { url = "https://files.pythonhosted.org/packages/be/15/44ce83fca50c6058f42b62fa8300a8030eea7e4e5a973a2dd33db0f557fb/regex-2026.9.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3264132d576847ab5f88bb83e7debe67854bf165b3ea613bd467312b6099536a", size = 800534, upload-time = "2026-09-09T20:58:10.339Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/a62e070a5a033643b287489576f02ae6a9c584c337d62349e340a2b4d001/regex-2026.9.10-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cef9f3d14796500ea834c41dbe688f1f6b23c7024dc23e8a794d7ebaf5d71d0", size = 872038, upload-time = "2026-09-09T20:58:12.186Z" }, + { url = "https://files.pythonhosted.org/packages/f3/06/8b8e2483949b1329df10c5b615e85d332066deb426b11332b799629b9201/regex-2026.9.10-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d278ad30ec83b6b9202685b0f80b741a51ea3ca7f0595ebda96e7628b6398876", size = 918927, upload-time = "2026-09-09T20:58:13.903Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/9b56f69100d3afdbc9c4fa6e302764f9cb717fbc06a9d50558d98ca89cd2/regex-2026.9.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb018cfe7144585fc83882405906ff84994a2d154afc2509ecc7752c51f864", size = 803694, upload-time = "2026-09-09T20:58:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/07/43/d00d59a7c8fd0e070ae8457a8743597f45ad9682b100f57b9c9c405fbdfd/regex-2026.9.10-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6fd555fc9abef50c530869690b2daca054c8811a7aff632d11f9a7b2590b2742", size = 777770, upload-time = "2026-09-09T20:58:17.628Z" }, + { url = "https://files.pythonhosted.org/packages/46/6b/a11d0446484efbc9eb67abec133f254c6d66a1568b8f3fb36d39a73a1129/regex-2026.9.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8d5c4518235a2ec1611e57af85fa488d529c1106aacff12adadcedf8687012cd", size = 791234, upload-time = "2026-09-09T20:58:19.476Z" }, + { url = "https://files.pythonhosted.org/packages/12/09/bcd24e78b373fd4f98090caa43eade439223f6703b909be79b9efd9ab0ab/regex-2026.9.10-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:175cf49ce7a994c88b8f15e3cb17cdb66a48ebb2d36de736b8205033db950f89", size = 866259, upload-time = "2026-09-09T20:58:21.683Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/c57ba5e94222a813260af4c17ced92d40cdb44737eb6b50979688310b6a3/regex-2026.9.10-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b71649169a9fcf30b395ee01047fa7ad6654a4c900ca75b23c04dedcce6a1f8c", size = 768219, upload-time = "2026-09-09T20:58:23.842Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a2/3820037587d00901ace96c5864335c2cd1b899d5263ea0dd2261359e0bee/regex-2026.9.10-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8ba1f78bd4fef2d8f84b894ec28ac3481afe6cc07aaa253ad4717ef7b3fe6bcb", size = 858582, upload-time = "2026-09-09T20:58:25.607Z" }, + { url = "https://files.pythonhosted.org/packages/47/f0/f9a838ca6219ae4821de0175e4548db73ef56de5ec08d032fb427732fa07/regex-2026.9.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:217e98ba5fc8908ed8ffd4ebac04753a0c831067cbfb495b9821b94cc61eaa76", size = 791405, upload-time = "2026-09-09T20:58:27.406Z" }, + { url = "https://files.pythonhosted.org/packages/f7/bf/67d71cc4e13ae2e0022d21243ca069e0868701a99eb29cdecd13d356e694/regex-2026.9.10-cp314-cp314-win32.whl", hash = "sha256:b298cdc33c5cc6969ff07f0fba19cc73e0fd8576373c50935feadaca2f6b4405", size = 272702, upload-time = "2026-09-09T20:58:29.116Z" }, + { url = "https://files.pythonhosted.org/packages/c1/38/40a93e72703a741235115ed1b1e5f6b869917677b7643005034ce1611d70/regex-2026.9.10-cp314-cp314-win_amd64.whl", hash = "sha256:c32818b28bcd153b25b63038348a9fe9b9fbcddb60df43f204c3ab55eeb57f77", size = 281170, upload-time = "2026-09-09T20:58:30.899Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ac/e95387f00617c16bb41b786d900bacd17eab88afa81b4f263df532b3a731/regex-2026.9.10-cp314-cp314-win_arm64.whl", hash = "sha256:75242f44a3e283106077be4ab717bc535e4701c9d54ad69e195945c22f137a1d", size = 281511, upload-time = "2026-09-09T20:58:32.594Z" }, + { url = "https://files.pythonhosted.org/packages/39/e5/a4b12262edc488a8a7a95b672db317dd8aa9bf2fab98297f9c91bb11ad4d/regex-2026.9.10-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2dd9286093c71afc8f55ef035c5b9d2776641fd72c6535f1febc92d0b0be9666", size = 501128, upload-time = "2026-09-09T20:58:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/55/c8/9ca31c0fa5197ded8614c8ae0e105bcff2d979025ffa3c75580baa334e2f/regex-2026.9.10-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:71879292c9c7ac67b1680345b16daba1be937cb027362cfa04e68f65db2dcfdd", size = 299428, upload-time = "2026-09-09T20:58:36.296Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d1/ee7662561735f90475443c3ca1977e5cecaeaa8f29620dec75580aebc839/regex-2026.9.10-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ccd139b2061132e7b265cfb4b4721baeb9f8928b81415304abf1ec7e3181c26", size = 294494, upload-time = "2026-09-09T20:58:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b1/333168e45ed6cfe71f6d17e21e5f54725f44d171f84edd12469a2f739227/regex-2026.9.10-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7327795089ddb44912dce1434e1d7244be2e9fb48fcc2d6782936af7a3062db", size = 814925, upload-time = "2026-09-09T20:58:40.438Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a5/df1b38536d0a3b24a030eb4130ce98b403cc925220ff530f5313a7c436eb/regex-2026.9.10-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff4d7b14ea19e50c8d9d6d83f45bd9b45cbb624c07ac1fa54db0a019049abed7", size = 873323, upload-time = "2026-09-09T20:58:42.349Z" }, + { url = "https://files.pythonhosted.org/packages/93/ea/aa71fe62dd63a8336bbdec1ff002a6c53b40ccfca95961c18ee4f09bf03c/regex-2026.9.10-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f0407474ffac8e5e89d93ca41d60891e29f0ab8423eb66ff292d850a86a0843", size = 923080, upload-time = "2026-09-09T20:58:44.488Z" }, + { url = "https://files.pythonhosted.org/packages/86/38/49f8d6fd34fc1a9c75b5b96364ebdde702a8144e5ed63d2213c58d454c27/regex-2026.9.10-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ad10a135fa0b4e4a462a61d07c6654d7518cfdb5cb8da08f9ff7d61384af1fe", size = 821284, upload-time = "2026-09-09T20:58:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/32/45/91a977c96d4be13d1ade8208c260c26841c836d4c91745e1828a30589070/regex-2026.9.10-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9fbd2e5d8002dc49a6129fb321ec51c57a025e752ed525ddce0ba9223c4350a7", size = 789256, upload-time = "2026-09-09T20:58:48.473Z" }, + { url = "https://files.pythonhosted.org/packages/2d/79/4d110bf01bf9651bf9b3f88a6d9fa7e643e0586921a18432b25a478edbfa/regex-2026.9.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4a761ea45f2ad74c575ef5850ea514cef97302a552d3c7c9d1a1a870d4661d6c", size = 803722, upload-time = "2026-09-09T20:58:50.286Z" }, + { url = "https://files.pythonhosted.org/packages/e7/67/b587a0d3bbac2635309ed9c40c197120c39e1ec0afb8813cbed1338fdd75/regex-2026.9.10-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:75aa39d3f4f1650eea84e46b0d8cefe77dd5478c10e3d0aaf0b0f00493475a7a", size = 870085, upload-time = "2026-09-09T20:58:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/07/fc/0827bca20ddba1d70fa5111a2a64e6b6b38bfdab43fc435022b54172a111/regex-2026.9.10-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f5c629df03adec31ee505dda3c8988f106c9390e4cbd343600036eb8b3d6724f", size = 776970, upload-time = "2026-09-09T20:58:54.478Z" }, + { url = "https://files.pythonhosted.org/packages/9b/03/ab9d08d30568ca868791bfb99551db60b947f05e2e65dcd1261e87083c21/regex-2026.9.10-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3a66e40a1a20de96a2fee00ed67e11012b62d85b277688258677fd19997addb7", size = 863611, upload-time = "2026-09-09T20:58:56.432Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6d/8063ae86b543ae878a7d6e7ba21ebf0af4af06161230e0012e2d652320c6/regex-2026.9.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:968c1e33edd9a104d1bf24c8d476c72de7e3839ae7f894b37e9e4f4739fdeeca", size = 804064, upload-time = "2026-09-09T20:58:59.066Z" }, + { url = "https://files.pythonhosted.org/packages/c5/63/b19305c4b8d3d7867699f7d83c43550273d91a2f31793452d87edb1d259f/regex-2026.9.10-cp314-cp314t-win32.whl", hash = "sha256:fbc4e2f3cb7ce8436154e6483079e7d35eeb321a952fa936e180300630d8b873", size = 274607, upload-time = "2026-09-09T20:59:00.942Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/1695072a512a49060294024e23b945eeb87675b02c06e53a92a1b42b0bfd/regex-2026.9.10-cp314-cp314t-win_amd64.whl", hash = "sha256:c37fa93bf18bf4f90b01c0fa9f11ea567ee4b7dd8bf96e63663e5edc37aa38cf", size = 283944, upload-time = "2026-09-09T20:59:02.876Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9f/65bac17f39991a67e22f8b3c849fdc02a56147c97029b5351494341959b4/regex-2026.9.10-cp314-cp314t-win_arm64.whl", hash = "sha256:ffc2da104e43db716ce30cef9f28049a1faa6aca385dd8771b033268d0730b07", size = 283780, upload-time = "2026-09-09T20:59:05.009Z" }, ] [[package]] @@ -3968,31 +4663,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, ] [[package]] name = "ruff" -version = "0.16.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" }, - { url = "https://files.pythonhosted.org/packages/4b/46/6cf67cf6411885a1d6f7f6d801682f155536a85176d10b605e2ceffed8bd/ruff-0.16.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:eda58a5802de40e7ed5b32b64e0b32539338cc6fcd2c78f61e3ad6a0d79f51c3", size = 10204049, upload-time = "2026-08-27T16:33:44.056Z" }, - { url = "https://files.pythonhosted.org/packages/46/fd/c8720ca7a090abf0c2fef4abe8a5ef6e5127ed15196d8886ff75a2b370e2/ruff-0.16.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5ae9a7b9a8875131f40f8fe967cc86abf899779efd663cb7ce3d572d01da7eb", size = 9809037, upload-time = "2026-08-27T16:33:46.257Z" }, - { url = "https://files.pythonhosted.org/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" }, - { url = "https://files.pythonhosted.org/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" }, - { url = "https://files.pythonhosted.org/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" }, - { url = "https://files.pythonhosted.org/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" }, - { url = "https://files.pythonhosted.org/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" }, - { url = "https://files.pythonhosted.org/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" }, - { url = "https://files.pythonhosted.org/packages/ed/20/656d67f5b25ca9bda4e02b1de25867b2954e1d19e03648060f167ad0f4cc/ruff-0.16.5-py3-none-win32.whl", hash = "sha256:288b0a5f080492fe5635db849f9e2e84aa3cce7b7f0e955997d416c507c76a26", size = 10034250, upload-time = "2026-08-27T16:34:11.8Z" }, - { url = "https://files.pythonhosted.org/packages/5b/42/ee8e68a207b9127fcde6c3d7e197def432f346cb1af159e1fa14ca0d1cdc/ruff-0.16.5-py3-none-win_amd64.whl", hash = "sha256:ddc6385fb2137f616357ca03d6c74f4be987f80fed4008566b754f6032b8546f", size = 10516714, upload-time = "2026-08-27T16:34:13.963Z" }, - { url = "https://files.pythonhosted.org/packages/73/e3/7df5a396e445b9ba49ce9a9437439a4d80042c61c0ade199abf8d16de1ac/ruff-0.16.5-py3-none-win_arm64.whl", hash = "sha256:a64abe90968719b851bb7cedffaa8753fbdbdadab483089682db623f3edc587e", size = 10391564, upload-time = "2026-08-27T16:34:16.064Z" }, +version = "0.16.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/bb/5a449b9162e49b139d72f61672bd3ac1d790221f796d3304e2241fff4c58/ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6", size = 4924184, upload-time = "2026-09-10T18:04:06.336Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/b2/c80aeeb7f9e469c0d63a85d2f1ab6e1ebfbe10ea7a8d2438b7e09e3ff09e/ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9", size = 10048917, upload-time = "2026-09-10T18:03:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/7b/96/20bb7bcae008004df52afcb7ac83432d4a467f2c17b672fe46d26be231c5/ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929", size = 10242929, upload-time = "2026-09-10T18:03:32.706Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f184b0d5abec02db69cfd7e49b688ae0237554528ca777136c613bf36bee/ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728", size = 9847245, upload-time = "2026-09-10T18:03:34.509Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2d/db1633a641866ed801e34cc6b60ef236c5e16f9b2124ab1d49cc24a5fe4f/ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0", size = 9961780, upload-time = "2026-09-10T18:03:36.618Z" }, + { url = "https://files.pythonhosted.org/packages/4d/98/edea21e1a3e38dbbc3bf6bb068b863b3b06184cf8533a4c7dbbe208a89d5/ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0", size = 9866337, upload-time = "2026-09-10T18:03:38.805Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/a15e60d4c87b214646f116ca9d204475bf993ee1047459bc9a360fd4d6d1/ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9", size = 10562512, upload-time = "2026-09-10T18:03:40.71Z" }, + { url = "https://files.pythonhosted.org/packages/29/42/eaff4c9b6d0c7cdf56df313a17e89ae854f5bbc0b0c8f9cce19be0ab7a8f/ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1", size = 11302938, upload-time = "2026-09-10T18:03:42.607Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/c75aa59a4ec181fe2ec06cab30e198c1c6d107229a9f008ae3a7c16cabd8/ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9", size = 10840857, upload-time = "2026-09-10T18:03:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/33/81f3da371942ea031105ba679d8d6e28ec1660ccd690a45f42d381161356/ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55", size = 10370001, upload-time = "2026-09-10T18:03:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/fa/0b/6345fb4dbf6dd0ed1cfe5d18391dc9c3f59cc81622a7b0a65b84b3e730ba/ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f", size = 10548735, upload-time = "2026-09-10T18:03:48.658Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4d/c5576adf511f92a328e5569dda190ecdd430da51f1a649f3a4a2fd73e21e/ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656", size = 10108496, upload-time = "2026-09-10T18:03:50.563Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8c/667d83c16199a17a56adc6b0bd4c3beb5b767a2babcd16a56f76f9be7fd6/ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750", size = 9860136, upload-time = "2026-09-10T18:03:52.621Z" }, + { url = "https://files.pythonhosted.org/packages/99/75/78d401106731999a1dd20cc5a6961e37e1eb9397a3b589f73f3a5ce146a3/ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5", size = 10286290, upload-time = "2026-09-10T18:03:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/56f9c3a8b755df93a0ad318b2147bf4ef5dae9a7e5ec61c460109c67957f/ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f", size = 10745048, upload-time = "2026-09-10T18:03:57.299Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ea/7f9b938a63ece4bec677ad7f9f7fa02df3383db1949ed93a382441c09a87/ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7", size = 10059082, upload-time = "2026-09-10T18:03:59.632Z" }, + { url = "https://files.pythonhosted.org/packages/39/11/480a6973a927aa653e1cead6a6416008640e03a99d05b34c0434b8c6c366/ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6", size = 10593368, upload-time = "2026-09-10T18:04:01.999Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4b/51327018d056f0dad2c2238f26d1fb0f53707a9d91b75dea6d1b3039f136/ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b", size = 10412401, upload-time = "2026-09-10T18:04:04.098Z" }, ] [[package]] @@ -4033,7 +4757,7 @@ wheels = [ [[package]] name = "scikit-learn" -version = "1.9.0" +version = "1.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "joblib" }, @@ -4042,20 +4766,32 @@ dependencies = [ { name = "scipy" }, { name = "threadpoolctl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, - { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, - { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, - { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, - { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, - { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, - { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, - { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, - { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, - { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d2/eb/eaf5e07fcc0da7149b0e084f24e54edd7441b9a89ce7e034032ae97fe3a0/scikit_learn-1.9.1.tar.gz", hash = "sha256:629cada3e33e2b9bf376cdc7614a47a4140b8aedc1d836579e359736fbd82977", size = 7786908, upload-time = "2026-09-10T18:34:04.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/a7/25f0a43d2fde306e8ef45f45121192f687b79beaf4bae8c21607c46c5e63/scikit_learn-1.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c0f8b5d09b44101cea2767f300680bada1ea27f976fe4b48b83950a4f55a49a", size = 8775209, upload-time = "2026-09-10T18:32:42.804Z" }, + { url = "https://files.pythonhosted.org/packages/60/ea/57e57539ce175d774fc291ed091b0a6d756854b92cd92554c6bb4d0ae498/scikit_learn-1.9.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:8c14ce41d561f7749f990b41d6703fe02c4669fbc485e598e069e0a1967b488e", size = 8295541, upload-time = "2026-09-10T18:32:45.069Z" }, + { url = "https://files.pythonhosted.org/packages/78/2b/5721a174406bfba49bce20ae997b3b64cf355c3f623a2638284ab6a82156/scikit_learn-1.9.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4c20a6c017d820faa7ac8c783e3d0c6a9a2e297bf9f55332ca17cdf7fd4d04d", size = 8871592, upload-time = "2026-09-10T18:32:46.999Z" }, + { url = "https://files.pythonhosted.org/packages/8e/57/a50162f3d29feb979ab6347c6debda506dfb525bcff3c50dd17606651c7e/scikit_learn-1.9.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5d7b18a5b9dca241a74695f3275fa4c895a9dadc72b3d8df5fa9d1083c9b83e", size = 9166043, upload-time = "2026-09-10T18:32:49.343Z" }, + { url = "https://files.pythonhosted.org/packages/72/8d/27c054166bac671770d1ea0ef7716134fe8119c0df224a58c89fd735a61c/scikit_learn-1.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4b59abb30618121cc46b45972d6bf53a7128b4df4cd346c6ca6f4d5f9031e49c", size = 8262238, upload-time = "2026-09-10T18:32:51.679Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d6/493086006ea0c68ad62c40a8dece1961b61bf503f45400f133d47f56e5be/scikit_learn-1.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:d5945a2908be62350e2978344e62b56c1552c2ca4f844ebf6277c94944d647dd", size = 7902102, upload-time = "2026-09-10T18:32:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8d/b60d5e7354ff0ff5cc9400e60273696589d87a30b8b2235886a76d80d062/scikit_learn-1.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c2b312fd8c02951a364fa120ea08c1cec10d863466bf1701b013152d7537835", size = 8739300, upload-time = "2026-09-10T18:32:55.483Z" }, + { url = "https://files.pythonhosted.org/packages/2f/81/3c6392c03665d2899457a76e535a9a6f597dddddf3220fd2e1d790da88c5/scikit_learn-1.9.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:61cd968ab831a76d0ecbaf0347ab2270268716da28f94fd022497e3d6f205f13", size = 8262364, upload-time = "2026-09-10T18:32:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/0f/35/a15b8653499692879821301d48059376d6e68e8b65cd0f22d19b6ee83cd9/scikit_learn-1.9.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5990f9c69e431bfaddcde1a6d7c5355243e026bc9b9e560c13893b90dab53fb4", size = 8823698, upload-time = "2026-09-10T18:33:00.632Z" }, + { url = "https://files.pythonhosted.org/packages/23/e5/688703d357e5393f708d98eb189fd415ae69e39f6de03c6bd4005aef6118/scikit_learn-1.9.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e79d6e9b0923f1a978179822bd43d7f5543f45e970a00fe861f43486380aba", size = 9121732, upload-time = "2026-09-10T18:33:02.825Z" }, + { url = "https://files.pythonhosted.org/packages/96/45/a10add34c08184d373be9384660c75758128ca881ed27b503b6f6a742478/scikit_learn-1.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:2070f271e5375dc42c6bb93b461ab1c0aa5841d4009267e0cfd95a39dca94a43", size = 8237244, upload-time = "2026-09-10T18:33:05.26Z" }, + { url = "https://files.pythonhosted.org/packages/9e/08/7a89bcdadd1fff0d464d01056417b646c9abcbc54f7297a0a1203bba5ebb/scikit_learn-1.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:613f0a783ca05aa844a4e1ac42d48425058f2c52be73f40f8cd98b7cd111acd6", size = 7875398, upload-time = "2026-09-10T18:33:07.506Z" }, + { url = "https://files.pythonhosted.org/packages/64/e3/b58e45082dcf3dcf0eb1192ee03545ec43d8c98441dfe20e88afca8442ce/scikit_learn-1.9.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d5d117952769b563067656784e03c75a2d8235a7a05cf7fffa78a311e75aac08", size = 8747414, upload-time = "2026-09-10T18:33:10.698Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ed/d68115577c8b42b0442ebd8180945d4008880a33176094640e00b585128e/scikit_learn-1.9.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:8893bc6331f60f18d4ac75e12ed356e2dcf6a564bf767918b5b7ca54c8c8be49", size = 8279175, upload-time = "2026-09-10T18:33:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6c/06c7eb61a438e389cbf3f7210897069bec5a883dbe03c189ae792781e11a/scikit_learn-1.9.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5492cf2df5226691c32611de8734bcf42148c6547ae53c7f4e6b847793addc0", size = 8881706, upload-time = "2026-09-10T18:33:14.741Z" }, + { url = "https://files.pythonhosted.org/packages/86/4e/0bab75490ca4b85fad8388739c7ebc71d9db553f8c69e39943ee8db0aaae/scikit_learn-1.9.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993d332ff80e62efae9e39603b7e872297c418d780f01a01855269a3489c950f", size = 9152030, upload-time = "2026-09-10T18:33:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/9a/13/31c6f8ba1b7eecef9dd9558576c752d2ec5785fd0e456bb9fc59305be23f/scikit_learn-1.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:ca9051447455dae341d4d591eece7deb2d8e3d1020298fc87a81fc51e4da8f53", size = 8377024, upload-time = "2026-09-10T18:33:19.941Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/6d3cb8a45f5228f915acd66b819dd6b8232ccbe24532f51d278e3991df31/scikit_learn-1.9.1-cp314-cp314-win_arm64.whl", hash = "sha256:90de6573f733a9fb79476ff1371af52a397d41c8b35f9146e20923db010d67b6", size = 8014196, upload-time = "2026-09-10T18:33:22.126Z" }, + { url = "https://files.pythonhosted.org/packages/66/6e/6befb2d5961490d18d9dbc16a5df37aa121d08bc9893316a6a363977a903/scikit_learn-1.9.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b5cad1624de8b75e5b9ccb7b0ce1ff1d01306340a3efc56d5529c5ba92392eb", size = 9066066, upload-time = "2026-09-10T18:33:24.396Z" }, + { url = "https://files.pythonhosted.org/packages/cb/18/11271f2f7db337db01f598e358721b1e83989407131272e5dd64214c28a8/scikit_learn-1.9.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:d137ce8a6142029fb5c35bd82f470c40cd9e760e5e2f7694b362c497c4ab3fa2", size = 8647654, upload-time = "2026-09-10T18:33:26.649Z" }, + { url = "https://files.pythonhosted.org/packages/e7/04/9c15d201e1b6a2e81b8215865df7646c5a360f560831769c8dc92ac1ab9a/scikit_learn-1.9.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66f852f7325b5070bc28329005aca76055a2def78faac039548ae889aeaa45a6", size = 8889070, upload-time = "2026-09-10T18:33:28.95Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5a/4cb6c85160af4a639e87a3b7bf8b1c25cfc3b504c5af710ca416a6dcfc5f/scikit_learn-1.9.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:748bcb0a4cc04aec470652c9e5ec68450948e867387e7dfade647107ade68d25", size = 9131227, upload-time = "2026-09-10T18:33:31.008Z" }, + { url = "https://files.pythonhosted.org/packages/00/0e/361440972ae3d19b90ea88a84791138a51de0e8432a770ba741b2c8d9ced/scikit_learn-1.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:38cd925e893e5539be704d5edc64dbe081aacdab6b89d8c2977c1f6a7a453ce5", size = 8683001, upload-time = "2026-09-10T18:33:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8f/a9f405c5c0e2df6f343a871b40c97fb32969e3ccc38e3033dd118f3c261e/scikit_learn-1.9.1-cp314-cp314t-win_arm64.whl", hash = "sha256:b01e5b01735d38474127ca3f49319b592506225a87793b27559816b5c75cea39", size = 8257996, upload-time = "2026-09-10T18:33:35.343Z" }, ] [[package]] @@ -4087,6 +4823,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07", size = 37457323, upload-time = "2026-08-21T23:25:12.483Z" }, { url = "https://files.pythonhosted.org/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28", size = 36622841, upload-time = "2026-08-21T23:25:18.722Z" }, { url = "https://files.pythonhosted.org/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf", size = 24399315, upload-time = "2026-08-21T23:25:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/d8eb4e280ddb56a4ab2c6f02ee49b56b23f6e977cf0802fd6d68dbef14f5/scipy-1.18.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83de5453a7799afc9048b4616bd085cef126e36412f0ea2f6370c36a2a3a51e7", size = 31090936, upload-time = "2026-08-21T23:25:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/59ea385dc3a62ff498ddf3cfff7c2b41b0f9f9d3c4122b3f1dcb6d6327fe/scipy-1.18.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9554bcc6d715ee87a633a3cc8e7703c6628b100dd29cb8a2efc4c0533c7ff729", size = 28725221, upload-time = "2026-08-21T23:25:33.244Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/6b0c288c50942d78193696c9f15f9a0874f5178aa0ddf40f83d9924b3e8d/scipy-1.18.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:011413b7426b75012840e35649e00fe0a2c3bae89fed433876e3a99251572efc", size = 20466839, upload-time = "2026-08-21T23:25:37.516Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/54fd3793c729e3b936782f181b59cbb1205bf250ab605a16cb1ba61cdd5e/scipy-1.18.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:88f0e784020649f88ea48c9f5ddfa403bf9205820667c0914740b392035afb82", size = 23089121, upload-time = "2026-08-21T23:25:42.019Z" }, + { url = "https://files.pythonhosted.org/packages/0b/56/030af62bea3cf878e0028515dff78c123b01633606a879b63f42d2db99cc/scipy-1.18.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d3ab0e8c69a17dd3559eab8cbb88f258e285c94d572c2719033f90f83290c89", size = 34053851, upload-time = "2026-08-21T23:25:47.998Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/2a844506d49651e9aa1af6ef95b6bd8031cb1d5a4375edec6155037e04cf/scipy-1.18.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac0333bdf38309aa3dcbe7e3fa7ea29e7a2c37c6ea306a757b700ded8e4596ad", size = 35329183, upload-time = "2026-08-21T23:25:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/c7370c3640e92ac9613cbf26cb3f729f9b12ddf1727b55b94b53b24d6f48/scipy-1.18.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:911de823097db8b63f034299d12662db93344e6ffa0b881cbb57748974b70168", size = 35672551, upload-time = "2026-08-21T23:25:59.387Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/ec8536f351421f8bf60a1120930638f83790f4710b8230446aca3d6159d4/scipy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:95298364e251be3e60249facbeeca03631d3bb7584f85879516ec55ac717b81f", size = 37469416, upload-time = "2026-08-21T23:26:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/d73da0d28f16c45bb9b0a5691b91610b0275c5ef0eb5e43c87cf2dc1bf31/scipy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:78a0d7c918e74a232394117160e7e3db503377572a45bcef8826e4ab8a35feba", size = 37362755, upload-time = "2026-08-21T23:26:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/e996e4dc74e10e227b1e14db5eaf6608bb6dd33884a64851c38f18dd4249/scipy-1.18.1-cp314-cp314-win_arm64.whl", hash = "sha256:cbf38d043c1aa4ab306e1ada6ab6eddacc3322a20b7af1b30bc93254b366fe09", size = 25036090, upload-time = "2026-08-21T23:26:15.887Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/c00213f92309d753b48903e6a451b87eb52ff5b7a16e789d1568bbf221c4/scipy-1.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0fcb3c93519f27bb4f0c4b0f7802cdcaca7fcf93267b75edda2e9f4e8a55cbd7", size = 31485550, upload-time = "2026-08-21T23:26:20.776Z" }, + { url = "https://files.pythonhosted.org/packages/74/b2/e3067c487982d4eeab2938928529410370c06fea84a4d3f4925e7d96647d/scipy-1.18.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ddef79fb382df40104a19bb7151b3b23e57c1778fcf857c71ceecd9bd264513f", size = 29174642, upload-time = "2026-08-21T23:26:25.395Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ab/374c9fe2d1ec014e576c781a4b5d8e1ba340e8f6b4638c16f711d2b194f0/scipy-1.18.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0e82073ecc7acc6436fac4b31674109c7e1d3e596789767eda01258a8c9e8123", size = 20916357, upload-time = "2026-08-21T23:26:30.112Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/223915c88a17317cafbf8ca2a42b11c265a9fb1e804aa665544132b5fe8a/scipy-1.18.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8bcf3c1ba5d6456e2effd30fcbd3459b044d683fcdac79a2e6830f0bdf7de487", size = 23482611, upload-time = "2026-08-21T23:26:34.846Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d1/db0948da8ca57a80b36520ef0a768b967d99f3af65f4b6f1bf6362ad4dd4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfbf154f2ba187f2ed6cce2639efff7d105f1140573642c0161615b6d91d6a87", size = 34143202, upload-time = "2026-08-21T23:26:40.4Z" }, + { url = "https://files.pythonhosted.org/packages/87/53/39d046cc7574ed6acacb6bd5723e220107ece80bff12faaf3efc4ddeede4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d33a7836f7ddc1993427966a0823468ec41bcbdb1a9f9942d1d7e57f803ba3", size = 35380876, upload-time = "2026-08-21T23:26:46.1Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/32e0e799d875a85ca57d9bde6c78148afcc0e38276df683d95854eadc8c3/scipy-1.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4b8bc363b6d65ee2152bec57568e3c52639bb34c46057b09857a307ed5e21d", size = 35770885, upload-time = "2026-08-21T23:26:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/88/2e/f97a666d362fee68b18f41c9c30ed502ca5c98b549749bfcb52a8b74d1eb/scipy-1.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11c423f1049c5755ad4409af52a9ada1cff96fe9b50795d4af3619f292901239", size = 37525424, upload-time = "2026-08-21T23:26:56.751Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d5/a9e765a84654ebba8479a1fd1b059ced1af72b168a3b2a3a46540ea38d20/scipy-1.18.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c24acac1e18912761c4700239bbc1fd32f615af690f1584d49b35859be51324d", size = 37416961, upload-time = "2026-08-21T23:27:01.546Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/e79e0d1c63ef698879d85439d37e9fb434e3b804e506a6991038d086ebd9/scipy-1.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9f2897bf7737392ad0d5213ea7b6add72a4edf5679b3153106aeb88b6507b3b9", size = 25331848, upload-time = "2026-08-21T23:27:05.884Z" }, ] [[package]] @@ -4104,11 +4860,11 @@ wheels = [ [[package]] name = "semver" -version = "3.0.4" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/d1/d3159231aec234a59dd7d601e9dd9fe96f3afff15efd33c1070019b26132/semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602", size = 269730, upload-time = "2025-01-24T13:19:27.617Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/f5/e1dfe8e1d91c54ce212fd93916eb01fd1c590f413be0a0978c39a97aa1bb/semver-3.1.0.tar.gz", hash = "sha256:14bc073439513d7773662a338f4db9829cf16c12b74b9568e2d2689975fbd7fc", size = 304162, upload-time = "2026-09-12T18:38:14.045Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/24/4d91e05817e92e3a61c8a21e08fd0f390f5301f1c448b137c57c4bc6e543/semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746", size = 17912, upload-time = "2025-01-24T13:19:24.949Z" }, + { url = "https://files.pythonhosted.org/packages/be/02/bce7a1d70e9d0f6ea8623c16386138c251b5855af6cd3a11433d3fba4e66/semver-3.1.0-py3-none-any.whl", hash = "sha256:6e4998f8b4762acdfea85f9bfd5b4032e0bb50038dc04bf82011b42d5d1145c5", size = 18783, upload-time = "2026-09-12T18:38:12.457Z" }, ] [[package]] @@ -4134,15 +4890,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.68.1" +version = "2.69.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/e7/c504a4bd2d95df2e0ab73714a9161ff1cf6ff1486922685e5f46dfd9eba8/sentry_sdk-2.68.1.tar.gz", hash = "sha256:6a97895230b04bc35d4d8d2e51e3b9e21902dfb0086ccf1f131a80c15c7b997a", size = 1019262, upload-time = "2026-08-24T13:09:38.108Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/08/1211e4652384195ef754f8ed570566088dc848f8eda7d005abf13f05f17f/sentry_sdk-2.69.2.tar.gz", hash = "sha256:b4d8915a526e626b0b14a2925907554b722d3b8e3e3781570fc35791712af323", size = 1043564, upload-time = "2026-09-15T15:25:09.018Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/28/465ad9382be98f2172e691f5836cf87f936773913ad7ab85ba1ba1d6706e/sentry_sdk-2.68.1-py3-none-any.whl", hash = "sha256:775b78871783a0ffd758276ad01b3bb2b1ebcdad8f9d2f0a7723f76b73c99b65", size = 520851, upload-time = "2026-08-24T13:09:36.186Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/5d8b79fd60330234317aa827a1f5e445980a6f4e0ea6ed4e1187acb70a0f/sentry_sdk-2.69.2-py3-none-any.whl", hash = "sha256:25bc0a6c55bba0b83398212a7247f4f9f3190f189a1909b68e4807ef4b785895", size = 528765, upload-time = "2026-09-15T15:25:07.362Z" }, ] [package.optional-dependencies] @@ -4197,29 +4953,43 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.52" +version = "2.0.54" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/d5/1b77a026d161f98a08f11af1a5f6c47b98ee7c7e2648af525a1004826c78/sqlalchemy-2.0.52-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727", size = 2170940, upload-time = "2026-08-11T20:58:11.25Z" }, - { url = "https://files.pythonhosted.org/packages/54/bd/f444444adb37b5d53753fb1730ee7a421628e2e3b756c4da461af7e6394a/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee", size = 3383415, upload-time = "2026-08-11T21:02:38.534Z" }, - { url = "https://files.pythonhosted.org/packages/be/57/2eadf93a552568c57e8680b7e58bb5e9770d80942a1bdbaf4f2f63f0d7c8/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b", size = 3398577, upload-time = "2026-08-11T21:16:59.092Z" }, - { url = "https://files.pythonhosted.org/packages/15/c3/2887cf9dd111d1fbf05d22165b404c221ef43e029f7a2695e7302f27a7cc/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee", size = 3328225, upload-time = "2026-08-11T21:02:40.183Z" }, - { url = "https://files.pythonhosted.org/packages/02/0f/466bdf9e1feeeef5587f868c187d8687e21ff8c85b1775e9041130181132/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf", size = 3357374, upload-time = "2026-08-11T21:17:01.076Z" }, - { url = "https://files.pythonhosted.org/packages/22/20/5c2b4583904af4173076dda1c9e53c9e2ffc7a702d2efde0216bbacbf7cb/sqlalchemy-2.0.52-cp312-cp312-win32.whl", hash = "sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e", size = 2129366, upload-time = "2026-08-11T21:14:50.991Z" }, - { url = "https://files.pythonhosted.org/packages/ed/06/543dab8ef62d4e9fb96fb31a30c2b8b14a8763bccf48d428294d6b3041c0/sqlalchemy-2.0.52-cp312-cp312-win_amd64.whl", hash = "sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca", size = 2157344, upload-time = "2026-08-11T21:14:52.453Z" }, - { url = "https://files.pythonhosted.org/packages/7f/18/e30c6fe1eca1bf34a39fbdd6066121cc9974c850faf6f349eac563697a26/sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43", size = 2167724, upload-time = "2026-08-11T20:58:12.679Z" }, - { url = "https://files.pythonhosted.org/packages/d0/56/2e17d161a4f7ecc1c2ffb93e607b4e1898bb551b451b283235acb8f6ce47/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1", size = 3321189, upload-time = "2026-08-11T21:02:41.932Z" }, - { url = "https://files.pythonhosted.org/packages/cf/b8/8490916e893f3f8d74dc9cc54c078619364999dee37047a188e73abbc852/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c", size = 3338185, upload-time = "2026-08-11T21:17:02.597Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f7/752cc8ee453da222829b3f5c4613614bf750d97429363b70414fa10478e4/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15", size = 3271698, upload-time = "2026-08-11T21:02:43.963Z" }, - { url = "https://files.pythonhosted.org/packages/51/e6/074ade0c07b9e4c8e8bca46820320ed94df9702afdb6f2af06623068d2e6/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057", size = 3308936, upload-time = "2026-08-11T21:17:04.172Z" }, - { url = "https://files.pythonhosted.org/packages/66/07/557c0d04716705599227945ac14e0a17ad0338e899f37d8c2ddff4dcc663/sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d", size = 2127308, upload-time = "2026-08-11T21:14:53.879Z" }, - { url = "https://files.pythonhosted.org/packages/96/4e/226eda27654318ce525d043025221f689abef883da2c7126f9065121618c/sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751", size = 2153876, upload-time = "2026-08-11T21:14:55.527Z" }, - { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/271aa905cf2964f841371a97f3e63ab692bf51b4423d0491e67bc7f64037/sqlalchemy-2.0.54.tar.gz", hash = "sha256:baa8521e8ee9f24e75dfc7aaabc08020e551ef0d48d7c3e3536f5cddf277586b", size = 9969559, upload-time = "2026-09-15T21:06:57.337Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/30/75504fd1d70458000e85a3e772333dd0fbf80254b0dc4d41c99922e4d112/sqlalchemy-2.0.54-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffba7eb2d67c7505e82a0902aa854d8824b74c28a183820d6a8bd3cfd0f812c2", size = 2187596, upload-time = "2026-09-15T22:32:37.031Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3d/5dfbb9528a391186a99986daecc5cbe003f34408dee91db8f5cdcf917040/sqlalchemy-2.0.54-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63cae7210fea9899e0bf35c1f1ae55d3ddd9c6d47cae8b6b43d945afa79dd65b", size = 3448745, upload-time = "2026-09-15T22:40:18.168Z" }, + { url = "https://files.pythonhosted.org/packages/d9/93/34fdc4a4faced77037a6b3ba1db1acd92e9bd12c21e229b327edd1d3e881/sqlalchemy-2.0.54-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68d994e9b0d0423a02a20039631fa6fcbb7fa829a992f7605025774940305d19", size = 3466644, upload-time = "2026-09-15T22:35:37.7Z" }, + { url = "https://files.pythonhosted.org/packages/66/68/4beab40ae60ac3d679dbbb46bc0d2bb277013264a0bde37ecaf4b6780e98/sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3de32cc6721eb42c3aad35bcfb244bb7a18f66c00f3582aae6281d6287a339b5", size = 3401519, upload-time = "2026-09-15T22:40:19.907Z" }, + { url = "https://files.pythonhosted.org/packages/e5/df/a24757e3249b1c7c1c5f0317666d16a8ab901ba239819ecef29eb4ed172e/sqlalchemy-2.0.54-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d31a2bc06a854ee52dd86b455be4df7c750b28817e2d1b884e31fff126c4fd7b", size = 3432590, upload-time = "2026-09-15T22:35:39.434Z" }, + { url = "https://files.pythonhosted.org/packages/d9/0c/69559e3d90d200fbfe9c44aff39a9ff4e506b1a3e9557a203d69df721244/sqlalchemy-2.0.54-cp312-cp312-win32.whl", hash = "sha256:32de6deded25e8b9b11d07428d496ff24dfbc882b8e990c177266948cb5f3d9e", size = 2140825, upload-time = "2026-09-15T21:25:22.444Z" }, + { url = "https://files.pythonhosted.org/packages/d9/10/4a0f7113664c2877906db143709c52ddbd28a88e4d51f0b520bc30048ba7/sqlalchemy-2.0.54-cp312-cp312-win_amd64.whl", hash = "sha256:d65f8ca742ef1e1e14bc417ef59dc2ddf207a7b66b30cfdc6152447314e030cf", size = 2170005, upload-time = "2026-09-15T21:25:23.885Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/021ccd7a838425f52e67c02edc541e8d53be8a27a103e684e14b18aac17b/sqlalchemy-2.0.54-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b374e3bc91e246a942592a98ba6a23be76fff21358b00546ac8c0ebc0fd0e00b", size = 2184259, upload-time = "2026-09-15T22:28:59.153Z" }, + { url = "https://files.pythonhosted.org/packages/74/8f/f95de908a4af7ac3a6925cdcb837a93274f608925a0fbc786df38811bccf/sqlalchemy-2.0.54-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d5458672a6f72db2c087f4a5098b3c8503ea0254186ff29205d63afa9401a4", size = 3398792, upload-time = "2026-09-15T22:29:35.218Z" }, + { url = "https://files.pythonhosted.org/packages/84/26/bd327a1a6be223e438c98ac8faf299307d83797225a81f3b209607a9fd98/sqlalchemy-2.0.54-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cad78d04254967bdbcccbed5e631d88fe4868530946ab0929aa45e9032849518", size = 3417562, upload-time = "2026-09-15T22:40:32.637Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b1/f13a8fe8e8e167d8e65484b91b827897b8a5ca1efd7ba6e87a5f63412fed/sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:48611087a75d26d798003645c688c7d3cfc26b89dbe4a2c568d6b378d330deae", size = 3349138, upload-time = "2026-09-15T22:29:36.729Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/5fd58f03281a74c57f02f509433e1be58a79d9f5b0e14020e665e0dcb614/sqlalchemy-2.0.54-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d6adf80277372a89910a0f3ccfe960b846d279dc55b366dd5c5ec07f41c84758", size = 3382943, upload-time = "2026-09-15T22:40:34.587Z" }, + { url = "https://files.pythonhosted.org/packages/4b/73/e29fa88dfc809857a55e6523911d345d00cab4adf87032c21ade8874223c/sqlalchemy-2.0.54-cp313-cp313-win32.whl", hash = "sha256:264460333ed0b177cbb1956355d0ee4e0cab83fb415c934ce12a25db2e7be39c", size = 2138931, upload-time = "2026-09-15T22:42:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/89/9d/885e7491836f3dad368c74f0d5b93551d37b0ec66c9e8753240ee988308f/sqlalchemy-2.0.54-cp313-cp313-win_amd64.whl", hash = "sha256:cf89e92bf0d4204a6afcc17af27b9271ed9c7e34e17d6f80c085d431ea4a1747", size = 2166496, upload-time = "2026-09-15T22:42:53.603Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c0/4a6503c9d22d6d00a5631082ab1484222ecf7d573db791e0f53161bf7745/sqlalchemy-2.0.54-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:abd6b21bc58e91c1932eb5d6d7f1bd44a551dfec7b6a7f517c3638ccd67233a0", size = 2185899, upload-time = "2026-09-15T22:29:00.581Z" }, + { url = "https://files.pythonhosted.org/packages/12/28/f4424f618bd1f373761a32a821d53ce2c257350e9894bae9b968cb03d8fd/sqlalchemy-2.0.54-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5417322b3c025dd82918725d3bf09ec105fac95efc195722b8b06e1d9c381139", size = 3394763, upload-time = "2026-09-15T22:29:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/59/d2/7f0c77f8e042cb5f28275fea29c3080b4ac6fd4b3fdd7f59ff1ef3e28c11/sqlalchemy-2.0.54-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f84099e4b04a5c2d44500a2a8302eee5af4bc6fee63e8c6e9cf6786e747280e", size = 3402800, upload-time = "2026-09-15T22:40:36.509Z" }, + { url = "https://files.pythonhosted.org/packages/af/32/3eaa930bcf71d17a72587081d2706a5fb97ab3f11a7e0fb838f581f7cff1/sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a0956dc754d3884da7fe60097110ec7a8a105d26afa2f0844468f4b1598c6912", size = 3341469, upload-time = "2026-09-15T22:29:39.682Z" }, + { url = "https://files.pythonhosted.org/packages/eb/cc/cddb6cbd4408e5c55b3bf722be26b9d3b54d42509f901b7ecc15debd3d1f/sqlalchemy-2.0.54-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:87ba8834318b0d8dc94fc6f405d071b5c08be32a6c3fd68107fd6952ee949615", size = 3373232, upload-time = "2026-09-15T22:40:39.181Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/9c2aa5efc642b7f3b985d13565cd1a5e78856e079ef3022796fea5180498/sqlalchemy-2.0.54-cp314-cp314-win32.whl", hash = "sha256:842540e4382472f23c79589995752648d14696a8200d0807ed8c5c59c92ade44", size = 2142018, upload-time = "2026-09-15T22:42:55.118Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0b/3594f1f51769feb3022d686135dc5d8682a12345ed15ae61d0c0ca42cbee/sqlalchemy-2.0.54-cp314-cp314-win_amd64.whl", hash = "sha256:f4e8f955d13af83fb4e35c3472e5377ee22d3445eada1e5e48199588edb69835", size = 2169220, upload-time = "2026-09-15T22:42:56.727Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a5/c211a9a7af83222509519407e16a4db760c6df3d03be69ebc5414d465321/sqlalchemy-2.0.54-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ca05f4e7852cf48083b0cf157e4f9504b7068780422a50fa82f45353b8c5e14a", size = 2208458, upload-time = "2026-09-15T22:30:05.718Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2e/490ad7b3731116cb48ba170f7722eaa99a89707193e54389ca84b7ad55af/sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18a8b6417cbb7b735cf91c2b59453c2a554cefa0a8d7bd15aa35740739410d77", size = 3660585, upload-time = "2026-09-15T22:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/eb/25/15dfe6814847eeda773bd58ab6cf42a94b0176e5cf25a578fc1165777160/sqlalchemy-2.0.54-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e55a0b96a1577a1e108c91ccdeeb9cd92768f28ce206597311c3bf6d6423abd", size = 3624442, upload-time = "2026-09-15T22:36:38.377Z" }, + { url = "https://files.pythonhosted.org/packages/aa/19/724d0a6a2fb2a86ff2d6008e581c258f722d9b7d8e52adc7b79085febdd4/sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:69cab115c40fd02c5a22c68e4ee630fa6ef9a1650f1de944419aab1f7096fc4f", size = 3562972, upload-time = "2026-09-15T22:36:08.581Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/9df1bd81c2f2d000cf5e7a1b1a9b331468a3aab939ad983355e701fa42b2/sqlalchemy-2.0.54-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e08397c6c42f53b2488acde9108b8bfefd52d7afd1bf2f03d2ffcab7a204aceb", size = 3576479, upload-time = "2026-09-15T22:36:40.272Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/b5775465d3b89061d7c46057c31c56ff8fb6c509550b2b0c6570ffc248b3/sqlalchemy-2.0.54-cp314-cp314t-win32.whl", hash = "sha256:b9086b8ad48280ef6a7ba68262d5e44f7db1c4cb1973e8cdae8a9f467ae66f51", size = 2174794, upload-time = "2026-09-15T22:31:45.925Z" }, + { url = "https://files.pythonhosted.org/packages/77/f8/296c2e46b4ccd3f29b00b954ef2f195dde32f98e352ed21de1d292cedc0d/sqlalchemy-2.0.54-cp314-cp314t-win_amd64.whl", hash = "sha256:b67c1744e453af833667fc1b84de07adb4a64f3536ef52a8ec5ac2b941d43970", size = 2211942, upload-time = "2026-09-15T22:31:47.368Z" }, + { url = "https://files.pythonhosted.org/packages/24/a1/bd5e3e99bc9c8863b51ac5b9b03008a7f2da8c6b59695992f5c654e1265b/sqlalchemy-2.0.54-py3-none-any.whl", hash = "sha256:7e33a631ab1474f8fe6b910bd1a07b7b8009c4c78cdd3fb18001b03e3bc2e1d2", size = 1958015, upload-time = "2026-09-15T22:24:22.95Z" }, ] [package.optional-dependencies] @@ -4229,15 +4999,15 @@ asyncio = [ [[package]] name = "sse-starlette" -version = "3.4.8" +version = "3.4.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/00/b42a44342a054d58cb1115d7c8aa9cb4290dd9442f9c1b91a4b8173dba22/sse_starlette-3.4.8.tar.gz", hash = "sha256:ed89ffbb75cbf78a5fe2f2109cd584792ee7f9dfac96f791db546df8f15f3f9c", size = 32548, upload-time = "2026-08-05T11:19:49.982Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/54/6767bb789b2f2fed6e0f953df949cd39dc263a384c1b65a95232598621d6/sse_starlette-3.4.11.tar.gz", hash = "sha256:1bae716c02f3e6f294be41ff333220692dae7c3cbab077c900f159676719dade", size = 34972, upload-time = "2026-09-05T12:11:04.607Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/3a/764912c58293d95b6dcdf4cc255f9d10de310580ced547b082eb9d72018c/sse_starlette-3.4.8-py3-none-any.whl", hash = "sha256:6e82314c786709a3cd9520f2285cf9fff90e181e598e8a357b0cf80f66afba0d", size = 16516, upload-time = "2026-08-05T11:19:48.748Z" }, + { url = "https://files.pythonhosted.org/packages/98/6a/2ba3ed4a69babf3afdddf7d8314a48d87562c0a442206bbc2a1b50d5efc0/sse_starlette-3.4.11-py3-none-any.whl", hash = "sha256:c7b2244bdff016fe7f64e10075e89a3e6bbf899649cc89b0fe884b5545042453", size = 17122, upload-time = "2026-09-05T12:11:03.195Z" }, ] [[package]] @@ -4320,11 +5090,11 @@ wheels = [ [[package]] name = "threadpoolctl" -version = "3.6.0" +version = "3.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/dc/6c58154c1c65f758ea979e7139cb76993a9cfc662d14e9be3c4a667cfb77/threadpoolctl-3.7.0.tar.gz", hash = "sha256:61348cfb77d53b9242e0017029244b559b810c142ced65b4e21eeca1843959a7", size = 31961, upload-time = "2026-09-15T15:46:20.263Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, + { url = "https://files.pythonhosted.org/packages/43/3f/f88a53f60a472b46f4023f56d204dd7de33d34c5d2acbfa0d70a674e639e/threadpoolctl-3.7.0-py3-none-any.whl", hash = "sha256:cd8b60b5641b45c67bbf73c64c843235fc2d8a480c87389f52f5dbee893b86be", size = 26362, upload-time = "2026-09-15T15:46:19.168Z" }, ] [[package]] @@ -4351,33 +5121,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b9/1e/633e30237b94e383cf814145499079f3bb9cdd4aeafc1bc42e01b0f810a6/tiktoken-0.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:380873f330b741c4435574f37edb20813d04603ace2d53e0a63560e1fec83010", size = 1250980, upload-time = "2026-08-17T19:49:02.274Z" }, { url = "https://files.pythonhosted.org/packages/cb/56/4c12f07b812f84206f38d723eb1ebfdd34bad9309b5dbc0bee6bbcff4cbf/tiktoken-0.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3fd7c14b1cb45b486c39fc9b3443bb341f3e2fc7e6f31247f3435a5836651632", size = 1315434, upload-time = "2026-08-17T19:49:03.434Z" }, { url = "https://files.pythonhosted.org/packages/c9/e0/c65603f0c44811def666d3fbf611bf2af3b5e1ef613e06c19411419830b3/tiktoken-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:90a762670c7f968184723769a06ed51f5cf5ce5dcd1e30164f25c72d85c2d1f1", size = 940883, upload-time = "2026-08-17T19:49:04.583Z" }, + { url = "https://files.pythonhosted.org/packages/59/b0/1cf129f4af8fc513931f931023def596b7c4bfc77026513cd9d851da9e88/tiktoken-0.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e067f4cbcc5d036e8aff7fe7a6b530a8f4de2e4616ad9005a24a1879e24e6450", size = 1096273, upload-time = "2026-08-17T19:49:05.807Z" }, + { url = "https://files.pythonhosted.org/packages/62/85/2ae74575e321148484147e10b53c3b1717c59ebaa9edb4fe18b1f5c055f8/tiktoken-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f2af4a336ea56d6c14f27741a0e1d8294a35dd0b038bcf990d232ebb54eb994b", size = 1040269, upload-time = "2026-08-17T19:49:06.943Z" }, + { url = "https://files.pythonhosted.org/packages/89/29/92a1120a12e4bcf2d5464350d1a91b68a433d63ce656bb7f806c27aec09c/tiktoken-0.14.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f702e0aeeb6506e57687e881c59e844ebe8f0a6a097ddafe20e3ab25f387be4e", size = 1186101, upload-time = "2026-08-17T19:49:08.102Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7d/144af98dc5ad68108451a82e2f5a17f80e2663f5115058b8dfd215c1ad02/tiktoken-0.14.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e3442bbb2f0c588cec876061e37ae67b455b9df9978b003c8fe30e45f2ef5b42", size = 1204457, upload-time = "2026-08-17T19:49:09.28Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1f/be7cb06ab2108f612f3e92e7b76cf391e192db0db37a984616f0cc32aafc/tiktoken-0.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:979c1524f753b662b0f3cd261b135afe6659cce33caaa7a5ea00dd1756b3055c", size = 1251716, upload-time = "2026-08-17T19:49:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6b/81f158d0f90adb826cd704069c2129a046cb784a2a09861009519fc41cf4/tiktoken-0.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2cc19ac87b41c9493c9778ff5847f0c8bbcf5bd0ec6b87ce06c1c802adc8a771", size = 1315432, upload-time = "2026-08-17T19:49:11.844Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ec/f5fa35ec13f07279fdcaf3cc9c04bbb154ea591d23978651f2b672593e8a/tiktoken-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:eceeff0c62419bc78d4b6e70a4762a4d25df3ae8f2d5946e3853ce93e7a57098", size = 988046, upload-time = "2026-08-17T19:49:13.282Z" }, + { url = "https://files.pythonhosted.org/packages/68/c9/7756717408d3d0dfea3f046c9466144b28afde39ff69d5808f2475dcd7f5/tiktoken-0.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6eb94895c45f26bb8f5546e5fd8a069efcf6e3f108ea9d5cbe3bf6f7f3983438", size = 1096261, upload-time = "2026-08-17T19:49:14.351Z" }, + { url = "https://files.pythonhosted.org/packages/79/29/46ad8061f57bd9f8b2ea0aa82bf574e0f2aa040b0857a1582adba9957899/tiktoken-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:86951a971c53979ec857bd8c4a32dc227ab0fd33f6c12a3bd62d3fbf5f0bfcaa", size = 1040183, upload-time = "2026-08-17T19:49:15.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7c/3184d17b868456f17b60b1a75f5ec0405618a43aa753336df341d8f11781/tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e2eca764c53490f8930dbce329e0769f11108d87d908282a80c5c130e26e7037", size = 1186719, upload-time = "2026-08-17T19:49:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e8/46de4400d5bf859f640feee85bd7e32235f68ddf25db53c63be78e581e3a/tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:26cc4b4840fa0e9f4b72ed489883e12f57e00d1021ca794720e3c29a12f0edef", size = 1204660, upload-time = "2026-08-17T19:49:17.987Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/af8964c38bc8226dd8950305b7a255fa33345d5572f78af7275a313d28e0/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2fc834fbe3f6a0736905c36ab709537e6840dbd63b982dc9e0216ae7d305ba1a", size = 1250932, upload-time = "2026-08-17T19:49:19.28Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4b/323631116fc986d9cc5bbeb2b8223c7c85e61a8bb94ea5ab4951023b149b/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ca4db6ff5c5bf600f9b7761a0070ed44dfe5797a76bd432fb978bc480ef40c58", size = 1315190, upload-time = "2026-08-17T19:49:20.467Z" }, + { url = "https://files.pythonhosted.org/packages/18/8b/ba48a73729c9270989b36f37ab2ed5525e52690d715097c9fa791aaa5d05/tiktoken-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7aab286a020660a039097912a088236b985d18a3090d73f136c4413d29d37ca0", size = 987717, upload-time = "2026-08-17T19:49:21.704Z" }, ] [[package]] name = "tokenizers" -version = "0.23.1" +version = "0.23.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/60/21f715d9faba5f5407ff759472ade058ec4a507ad62bcea47cb847239a73/tokenizers-0.23.1.tar.gz", hash = "sha256:1feeeadf865a7915adc25445dea30e9933e593c31bb96c277cee36de227c8bfa", size = 365748, upload-time = "2026-04-27T14:43:25.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/1e/bc6587c5ab643b2e17776cace9070a2ae73549c86bffac9934a600bf3c31/tokenizers-0.23.2.tar.gz", hash = "sha256:7f0f085686b9de0d0079e6f874ae053600db64c5d13049e0bbc0119926d25aac", size = 385745, upload-time = "2026-09-03T08:55:42.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/39/b87a87d5bb9470610b80a2d31df42fcffeaf35118b8b97952b2aff598cc7/tokenizers-0.23.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e03d6ffcbe0d56ee9c1ccd070e70a13fa750727c0277e138152acbc0252c2224", size = 3146732, upload-time = "2026-04-27T14:43:15.427Z" }, - { url = "https://files.pythonhosted.org/packages/e2/6a/068ed9f6e444c9d7e9d55ce134181325700f3d7f30410721bdc8f848d727/tokenizers-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0948bbb1ac1d7cdfc9fb6d62c596e3b7550036ad60ecd654a66ad273326324e", size = 3054954, upload-time = "2026-04-27T14:43:13.745Z" }, - { url = "https://files.pythonhosted.org/packages/6c/36/e006edf031154cba92b8416057d92c3abe3635e4c4b0aa0b5b9bb39dde70/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bf13402aff9bc533c89cb849ec3b412dc3fbeacc9744840e423d7bf3f7dc0e3", size = 3374081, upload-time = "2026-04-27T14:43:01.241Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ef/7735d226f9c7f874a6bee5e3f27fb25ecabdf207d37b8cf45286d0795893/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f836ca703b89ae07919a309f9651f7a88fd5a33d5f718ba5ad0870ec0256bad6", size = 3247641, upload-time = "2026-04-27T14:43:03.856Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d9/24827036f6e21297bfffda0768e58eb6096a4f411e932964a01707857931/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae848657742035523fdf261773630cb819a26995fcd3d9ecae0c1daf6e5a4959", size = 3585624, upload-time = "2026-04-27T14:43:10.664Z" }, - { url = "https://files.pythonhosted.org/packages/0c/9a/22f3582b3a4f49358293a5206e25317621ee4526bfe9cdaa0f07a12e770e/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53b09e85775d5187941e7bab30e941b4134ab4a7dd8c68e783d231fb7ca27c51", size = 3844062, upload-time = "2026-04-27T14:43:05.643Z" }, - { url = "https://files.pythonhosted.org/packages/7e/65/b8f8814eef95800f20721384136d9a1d22241d50b2874357cb70542c392f/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea5a0ce170074329faaa8ea3f6400ecde604b6678192688533af80980daae71a", size = 3460098, upload-time = "2026-04-27T14:43:08.854Z" }, - { url = "https://files.pythonhosted.org/packages/0d/d5/1353e5f677ec27c2494fb6a6725e82d56c985f53e90ec511369e7e4f02c6/tokenizers-0.23.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b405006415ea148a992d093699c66eb01952bf59f4d5727089a98bda45a4", size = 3346235, upload-time = "2026-04-27T14:43:12.377Z" }, - { url = "https://files.pythonhosted.org/packages/71/89/39b6b8fc073fb6d413d0147aa333dc7eff7be65639ac9d19930a0b21bf33/tokenizers-0.23.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:56f3a77de629917652f876294dc9fe6bad4a0c43bc229dc72e59bb23a0f4729a", size = 3426398, upload-time = "2026-04-27T14:43:07.264Z" }, - { url = "https://files.pythonhosted.org/packages/0f/80/127c854da64827e5b79264ce524993a90dddcb320e5cd42412c5c02f9e8a/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9d10a6d957ef01896dc274e890eee27d41bd0e74ef31e60616f0fc311345184e", size = 9823279, upload-time = "2026-04-27T14:43:17.222Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ba/44c2502feb1a058f096ddfb4e0996ef3225a01a388e1a9b094e91689fe93/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1974288a609c343774f1b897c8b482c791ab17b75ab5c8c2b1737565c1d82288", size = 9644986, upload-time = "2026-04-27T14:43:19.45Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c1/464019a9fb059870bfe4eebb4ba12208f3042035e258bf5e782906bd3847/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:120468fb4c24faf0543c835a4fabafa4deb3f20a035c9b6e83d0b553a97615d4", size = 9976181, upload-time = "2026-04-27T14:43:21.463Z" }, - { url = "https://files.pythonhosted.org/packages/79/94/3ac1432bda31626071e9b6a12709b97ae05131c804b94c8f3ac622c5da32/tokenizers-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e3d8f40ea6268047de7046906326abed5134f27d4e8447b23763afe5808c8a96", size = 10113853, upload-time = "2026-04-27T14:43:23.617Z" }, - { url = "https://files.pythonhosted.org/packages/6a/dd/631b21433c771b1382535326f0eca80b9c9cee2e64961dd993bc9ac4669e/tokenizers-0.23.1-cp310-abi3-win32.whl", hash = "sha256:93120a930b919416da7cd10a2f606ac9919cc69cacae7980fa2140e277660948", size = 2536263, upload-time = "2026-04-27T14:43:29.888Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/2553f72aaf65a2797d4229e37fa7fbe38ffbf3e32912d31bdd78b3323e59/tokenizers-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:e7bfaf995c1bdbbd21d13539decb6650967013759318627d85daeb7881af16b7", size = 2798223, upload-time = "2026-04-27T14:43:28.51Z" }, - { url = "https://files.pythonhosted.org/packages/cd/2b/2be299bab55fc595e3d38567edb1a87f86e594842968fa9515a07bdcf422/tokenizers-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:a26197957d8e4425dfba746315f3c425ea00cfa8367c5fbc4ec73447893dcea9", size = 2664127, upload-time = "2026-04-27T14:43:26.949Z" }, + { url = "https://files.pythonhosted.org/packages/4d/ed/8a443528baa6fac8dfe8c3b75b038c63ac92bb539bcabe311e227c718173/tokenizers-0.23.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:85a9a357a3764aecc904ee76bdaf8cf1ad8e5a67a1b929a487c4a39b49ed0e90", size = 3148852, upload-time = "2026-09-03T08:55:30.874Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/22da045a91732384d3a3771816bf188dc5a1f702c32e635afa7c679c0bef/tokenizers-0.23.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:986670e43691469dcee610ea0f846f91a8f84e91fc6f7a48d4c064414c0ec2bf", size = 3101593, upload-time = "2026-09-03T08:55:28.587Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4d/8f569ed49372a3ed8e57099bd515055fd48d7c95912c4307cda6973c2168/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a37039b5dfc4af84eb3ef0a92f4307e28936c8f9adccba2629d36f652e9bf7a2", size = 3516830, upload-time = "2026-09-03T08:55:14.741Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/e2f14c8919d5bf51874051d00d6c7b7e0e8bde6c6a2dbeddda7f642896ff/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b7e37ba198f24150f523e1242e83c4970de4a525480586be5dcc24d9add32c5", size = 3407975, upload-time = "2026-09-03T08:55:16.842Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bd/93c69152d02ef06ce47aed8b2bf4952dcf733c935a62791873932b2934d9/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43e4f2071e3cc8d5d86421c874aebc82659bb51a68bcdef5a0da75ee89511ccb", size = 3748165, upload-time = "2026-09-03T08:55:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b7/56b84b80bc96942bba8eb23751a9e8a1fce4faaf4390425e7083f721c98c/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325fee2e0418a9dc6c9ecf736a5f5f0db7875183ace9549ae339da76f7a1fbb7", size = 4024165, upload-time = "2026-09-03T08:55:18.806Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8a/0175e216f005c2fe08238292663aa41e4c802b216e71047a69a0e9fc6fa3/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:950d7c9426fa72406a0ffeacdbc0bb9985f5db20eb8b263f29c79aaf83105703", size = 3591899, upload-time = "2026-09-03T08:55:22.752Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/ca6b93c7820df123b2662a9469e8facc826ccc94e98fdd0d615f6431e73a/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41c2f84d172449b4dadb9cdc508e3e364076613c35b16e76ecfe47a60d1e3305", size = 3386843, upload-time = "2026-09-03T08:55:26.584Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a4/4f9106d317b14a80aefea9f0e3a8d07ef25f856a7607eb7f5ab894281fcb/tokenizers-0.23.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:12f0835dc2ee694746a76adf7b1567d4346a4a502ebe93fb1f5f80ea49799b78", size = 3577314, upload-time = "2026-09-03T08:55:20.825Z" }, + { url = "https://files.pythonhosted.org/packages/8d/6a/1552b70fb0d9ab074fd3fc961435d01364e79c9058481822c3af6e8d402c/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb2f9c8a24da020ea8c11a01a19c1c2547912d92121ae4a01cfbca46125dee40", size = 9967367, upload-time = "2026-09-03T08:55:33.188Z" }, + { url = "https://files.pythonhosted.org/packages/06/01/3ccb3a956c7528b2507b8a9714155c4baf86af593039db6ea375dd0c96c3/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f486f402f6f9abee5bb032553736813af0c710a86b2e0ca592634c55cea1f835", size = 9811886, upload-time = "2026-09-03T08:55:35.642Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/7038e612d48bda1599457f712f6bd3854eae1a9dc9c13aa47f835349db48/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:bef235815a067b2648caf6dcc7a71091b0b0fff9ee8057f6451eb9335fae52ef", size = 10146224, upload-time = "2026-09-03T08:55:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d8/8e9e4e0b287a338d8f88976729628c9d22e8a54cfaf9777018a7f7cb58a0/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5c56bda1511921587789163e524d196ed8284174ac23abd7685d5ea8da6c4718", size = 10256304, upload-time = "2026-09-03T08:55:40.977Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1f/c79a01f671a49728ebb0b61f7ff9ea45663b66cab40bc0858e9859b25c16/tokenizers-0.23.2-cp310-abi3-win32.whl", hash = "sha256:debf978920d93ba9c219bd67cc4bbfaf912c9039e41e7a28b91ec15e3728c95a", size = 2592809, upload-time = "2026-09-03T08:55:48.02Z" }, + { url = "https://files.pythonhosted.org/packages/db/f7/0a69ac6b82dbccf3f71add938a161c497952749294b8dd6dfe03a819dc40/tokenizers-0.23.2-cp310-abi3-win_amd64.whl", hash = "sha256:2e96f5699d5249c9c64aa8412e044f727aae3a4098cf830f9901ec1afc361cde", size = 2863236, upload-time = "2026-09-03T08:55:46.193Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/dee84cb44175be1b4c35bd2f770727494e78f0bb38e571a623ade94dbebb/tokenizers-0.23.2-cp310-abi3-win_arm64.whl", hash = "sha256:e49c394456dd9985787fec76132438ba3fb8911f857b1bf3d40119f9292d41aa", size = 2729352, upload-time = "2026-09-03T08:55:44.345Z" }, ] [[package]] @@ -4394,7 +5178,8 @@ name = "torch" version = "2.11.0" source = { registry = "https://download.pytorch.org/whl/cpu" } resolution-markers = [ - "python_full_version >= '3.13' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version < '3.13' and sys_platform == 'darwin'", ] dependencies = [ @@ -4410,6 +5195,8 @@ wheels = [ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43b35116802c85fb88d99f4a396b8bd4472bfca1dd82e69499e5a4f9b8b4e252", upload-time = "2026-03-23T15:16:58Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:442ec9dc78592564fdad69cf0beaa9da2f82ab810ccb4f13903869a90bf3f15d", upload-time = "2026-03-23T15:17:02Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cc3a195701bba2239c313ee311487f80f8aaebe9e89b9073dddbcf2f93b5a0ba", upload-time = "2026-03-23T15:17:06Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a0d6e4865e8b0dc0dbfe6ebed68fae235124222835ef03e5814d414d8c012", upload-time = "2026-03-23T15:17:10Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23ec7789017da9d95b6d543d790814785e6f30905c5443efa8257d1490d73f79", upload-time = "2026-03-23T15:17:14Z" }, ] [[package]] @@ -4417,9 +5204,12 @@ name = "torch" version = "2.11.0+cpu" source = { registry = "https://download.pytorch.org/whl/cpu" } resolution-markers = [ - "python_full_version >= '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", "python_full_version < '3.13' and sys_platform == 'win32'", "python_full_version < '3.13' and sys_platform == 'emscripten'", "python_full_version < '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", @@ -4446,23 +5236,31 @@ wheels = [ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8f13dc7075ae04ca5f876a9f40b4e47522a04c23e30824b4409f42a3f3e57aa4", upload-time = "2026-04-28T00:07:27Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8713bb8679376ea0ec25742100b6cfb8447e0904c48bddefb9eb0ac1abbfa60a", upload-time = "2026-04-28T00:07:37Z" }, { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:62ec1f1694c185f601eab74eb7fc0e8e10c64c06ae82f13c3592774c231c4877", upload-time = "2026-04-28T00:07:47Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-linux_s390x.whl", hash = "sha256:c9a14c367f470623b978e273a4e1915995b4ba7a0ae999178b06c273eea3536f", upload-time = "2026-04-28T00:07:54Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:71676f6a9a84bbd385e010198b51fa1c2324fb8f3c512a32d2c81af65f68f4c9", upload-time = "2026-04-28T00:08:02Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:f8481ea9088e4e5b81178a75aabdbb658bde8639bc1a15fd5d8f930abc966735", upload-time = "2026-04-28T00:08:11Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:7575af4c9f7f7500ed62b1dafeb069aa0ba35b368a5f09793b3976b3d50f4fe4", upload-time = "2026-04-28T00:08:20Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-linux_s390x.whl", hash = "sha256:825f1596878280a3a4c861441674888bc2d792e4ab7b045cb35feeab3f4f5dd7", upload-time = "2026-04-28T00:08:27Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c8a0bdfb2fd915b6c2cd27c856f63f729c366a4917772eba6b2b02aa3bce70d5", upload-time = "2026-04-28T00:08:36Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:768f22924a25cad2adeb9c6cbac5159e71067c8d4019b1511960d7435a5ca652", upload-time = "2026-04-28T00:08:47Z" }, + { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:6db45e7b2526d996fbf47c3d08737807a60a4e17996a6d91a97027fe260832c8", upload-time = "2026-04-28T00:08:57Z" }, ] [[package]] name = "tqdm" -version = "4.70.0" +version = "4.70.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/ea/b2a5bd54b28a324dae8211928b2d730b6547500342c7e6c6dea08bd0a485/tqdm-4.70.1.tar.gz", hash = "sha256:cefd0eca11b2a37a3aee776544d4f4ae913f02688135b5556b8788dfa474afc4", size = 171846, upload-time = "2026-09-11T07:25:16.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, + { url = "https://files.pythonhosted.org/packages/a7/03/921a3d3c75785aca9ebfbfcabfbc3a1be12e2ab5265deb026d55a5a3f83e/tqdm-4.70.1-py3-none-any.whl", hash = "sha256:c293e525e6fef9c20e8728fd4612df02a0aa31bb5fe91ecd93e123b1b7bffa73", size = 80199, upload-time = "2026-09-11T07:25:14.599Z" }, ] [[package]] name = "transformers" -version = "5.16.1" +version = "5.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, @@ -4475,9 +5273,9 @@ dependencies = [ { name = "tqdm" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/2e/ba418680ab901dae269360bb8642485eae04f1af91ee2ebb8bd6f3607305/transformers-5.16.1.tar.gz", hash = "sha256:17b0eac726ddc55e84ac58946063e0c6d37fd000c456b581f050ea0f4e822869", size = 9650542, upload-time = "2026-08-26T14:48:58.789Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/9e/750649904a065007a838981785b2bd8d9ff26154c6c341ac67d0b7f82c68/transformers-5.17.0.tar.gz", hash = "sha256:a153be279169b55b92d8000bf4af294aed684503d091cca7804da2dd8a9de000", size = 9817878, upload-time = "2026-09-09T15:39:56.886Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/4d/ee3728674c0bbc637bb4af88ccf0be697f92e4e90b55f5dc110c44d61b61/transformers-5.16.1-py3-none-any.whl", hash = "sha256:2f2d5b98a5ad3718713653734298fa620754ed683702a635ebb587df3ed29c7e", size = 12080592, upload-time = "2026-08-26T14:48:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d0/c502b60d684adbd98a8dc7d5bb866842772b816ac4354e4608be240041ae/transformers-5.17.0-py3-none-any.whl", hash = "sha256:78ec1ce21579b38dfb83950a0658cd119f87212a2fcfdff478096ce9d6c03801", size = 12295140, upload-time = "2026-09-09T15:39:53.746Z" }, ] [[package]] @@ -4502,11 +5300,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cd/e6/0fe05ba396e9623b0ae40ccf34171336b8701ec8d7bd0ee9f5224d638665/tree_sitter-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00289bfe7978f3e0dc0ce69813a20fa9f44ea4c100b3ec62043e5eb74ccfc3a2", size = 665121, upload-time = "2026-06-30T12:14:16.403Z" }, { url = "https://files.pythonhosted.org/packages/eb/d2/a944b1ca35bed6068dc84a9967aaf3049d8cc0b7a36179eea8787270a6ab/tree_sitter-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:93e220cab7e6a823efeb2046c49171427de92ef71c7c681c01820d14d8d3721f", size = 129615, upload-time = "2026-06-30T12:14:17.463Z" }, { url = "https://files.pythonhosted.org/packages/09/ef/c7ca48293580d2249f36940c4eed5b4ddeb9ce75baf9a4ef30621987e0c7/tree_sitter-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:b31a8195d2f224224c530ac814632d98c1dcc123d227442c07c736e86b70d564", size = 116525, upload-time = "2026-06-30T12:14:18.53Z" }, + { url = "https://files.pythonhosted.org/packages/c5/7a/4d84e6f6ae2c3e757490dd84de251712c31e293dfe31f28da1ec019cefa2/tree_sitter-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5a3c93a352b7e6f70f73e121bbfa2d0117ba7478bd51114ed35c91b0b78814fa", size = 148901, upload-time = "2026-06-30T12:14:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d9/efe62ec65dc9d096e834d27b8c058127e2146e42ff3380b822a233f016a6/tree_sitter-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5fc2f41bf246ff2f70a9cc3690be35ec7580a4923151873d898c8bcb1a4503d3", size = 140805, upload-time = "2026-06-30T12:14:20.478Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2c/c82326b7b97e3c485c18679883b16f89e5e913c639d3b219d3da70c9e67e/tree_sitter-0.26.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8ea92a255c91671a7ec4625aba3ab7bb5220c423630ffbf83c45d7312abe084", size = 640586, upload-time = "2026-06-30T12:14:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7a/f56e7d8282859452611024c7cbc623bfba5b24b8cb9b8f8bc88c5219fe9a/tree_sitter-0.26.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f665510f0fcf4636fb9696f1f7853bed7a3bd764b7bb0cb8494e619c14ed5a0c", size = 668300, upload-time = "2026-06-30T12:14:22.728Z" }, + { url = "https://files.pythonhosted.org/packages/91/51/240ee81b9d5e9ca0a6cb1528e8605ffa70ab58c89ce126631be96d3e4bae/tree_sitter-0.26.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:253df7ab82cc0a9d311cd65f06e9f99fb3eac55996ae9fc94da22f123a861b90", size = 649627, upload-time = "2026-06-30T12:14:23.819Z" }, + { url = "https://files.pythonhosted.org/packages/6a/54/760035cefedf9eb44f0f84c4ac22f1322e73155853e272576ee876336312/tree_sitter-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ff80d4833d330a73184a3ac5132abe93c575d2dea31975c6f15c0d21fef238aa", size = 664885, upload-time = "2026-06-30T12:14:25.064Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1b/0b36fe2a984ecedc4ce6aefd5d56447a6626a8e9b595c4e48658510ce8f8/tree_sitter-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:a4033fecc8f606c7f2e8b8014d0057b74668a7f0152763606f7bc25c5f9ec64c", size = 132688, upload-time = "2026-06-30T12:14:26.106Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/ebc041a13fbf40144afdb0d4b447e48e0b4012ca866c63de8b48f801f0c1/tree_sitter-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:823251c4b6725a7c03ed497a339135ede7ae4bdde75bb8be7ef5e305aeb4ff52", size = 120287, upload-time = "2026-06-30T12:14:26.991Z" }, ] [[package]] name = "trl" -version = "1.12.0" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "accelerate" }, @@ -4515,9 +5321,9 @@ dependencies = [ { name = "packaging" }, { name = "transformers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/e8/2e131fd15224433c48b0c6afd80a2926a815e070dd2d9b528cc07c337056/trl-1.12.0.tar.gz", hash = "sha256:494ec6cfd07097bb8116cc7bec299b067dd765d1c19ed3eca2d874a29979f229", size = 818314, upload-time = "2026-08-26T19:46:16.456Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/ec/2ea0856273ec5a5414555c81a2b9a9cdc9dc91e518ce2da656912ccb01f2/trl-1.13.0.tar.gz", hash = "sha256:0905a7813e246c8449fc8021492134cdf80402ec9ad9ae7f8a72876f77eea9d7", size = 825648, upload-time = "2026-09-10T00:40:37.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/21/b29125ab2c1b324de93f863b9ab58237ccf87990dbd7ef7ef066bb1284aa/trl-1.12.0-py3-none-any.whl", hash = "sha256:9c8f117b21a941edbc85744f5a341c9453d38e81f24ae81d3c6e9b9ca3b96409", size = 992576, upload-time = "2026-08-26T19:46:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/d4/66/94f4f8f2967a5ec02fdfa61c08423ffeba652d9407fb6e4f5e8eaf1c8e34/trl-1.13.0-py3-none-any.whl", hash = "sha256:7c7ae0c72e969450221cf4c7cfe69c532c5a83c727204f1c8ced38a9b79134ec", size = 1008465, upload-time = "2026-09-10T00:40:36.301Z" }, ] [[package]] @@ -4588,23 +5394,23 @@ wheels = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20260815" +version = "6.0.12.20260906" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/72/b56089aeee6c496d969bac42376bedb6e3eeab4682e1018fa3137122f94b/types_pyyaml-6.0.12.20260815.tar.gz", hash = "sha256:28764110c9cf35846e733da32d8d734df7473c5dde9ef67c3b7332ec0e819858", size = 18545, upload-time = "2026-08-15T02:41:51.532Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/6e/abec85b9013db5b934b0280a6dd104904d84f7bcbaab2e2f3def87ac7463/types_pyyaml-6.0.12.20260906.tar.gz", hash = "sha256:f59c1cc05010b833d2d72287bbaa72610106b28d42d89a907313117faba85212", size = 18649, upload-time = "2026-09-06T06:35:35.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/52/eefeba09be4ef2a1eb989eb92934561e8e502a6ee3c32654996e4be7e399/types_pyyaml-6.0.12.20260815-py3-none-any.whl", hash = "sha256:6f332212b7e191f3afd5016a713c510b6340593b7ebec573c7d5d20aa5386d3b", size = 21148, upload-time = "2026-08-15T02:41:50.555Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/fc0644b7ddcfb969e95845837143cb5173ddd6e06ee4ba5fc493cd9329b7/types_pyyaml-6.0.12.20260906-py3-none-any.whl", hash = "sha256:bca893ff0d51df5c9053137d5d0e6ccd36e939a196356f1d5c16372422f5137b", size = 21282, upload-time = "2026-09-06T06:35:34.372Z" }, ] [[package]] name = "types-requests" -version = "2.33.0.20260712" +version = "2.33.0.20260906" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/51/703318f7b7be8bee126ec13bf615050f932d0179b8784420f3a0199cc769/types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb", size = 25084, upload-time = "2026-07-12T05:14:20.455Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/18/4c2c0290953f8b3b9612adfcb07b57f144ade3ad32a76764fca42b77c5f3/types_requests-2.33.0.20260906.tar.gz", hash = "sha256:76ab8a0fb736744a0c3deee7aa57b2927e301f078d9e61f5391b3e92002416b9", size = 25263, upload-time = "2026-09-06T06:35:47.707Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/e7/010c87f559e216d83f9dc51e939633fd0d0ead3377340181ab0e223cd3b5/types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb", size = 21392, upload-time = "2026-07-12T05:14:19.616Z" }, + { url = "https://files.pythonhosted.org/packages/60/4c/51ec821d22a45b4162fa3f3e9e94ea4c0f8c49e82363b10955baa5438391/types_requests-2.33.0.20260906-py3-none-any.whl", hash = "sha256:9f53622652bd921ead7a54d665be1b8d518165c8b51cc9d68d944cd6b6bfa8fb", size = 21461, upload-time = "2026-09-06T06:35:45.856Z" }, ] [[package]] @@ -4630,20 +5436,20 @@ wheels = [ [[package]] name = "tzdata" -version = "2026.3" +version = "2026.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/31/3d74fa778a63b98b7374323befcc0be5ab3bd94afd4096a0124e7379152c/tzdata-2026.4.tar.gz", hash = "sha256:f1b8bd365d8d210c55353f4d7f8d6d8561c0ba50d704b700d195a9424bba0d79", size = 199350, upload-time = "2026-09-12T12:56:03.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bc/8737e8d54cf51106118039b83f485a4783112fab49ea9d044b234978a46e/tzdata-2026.4-py2.py3-none-any.whl", hash = "sha256:c2169a8b0a7a5e9674da5a135ccdfb2b3e671b333ed9fed17b41f73c34476e81", size = 347494, upload-time = "2026-09-12T12:56:01.67Z" }, ] [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]] @@ -4674,15 +5480,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.52.4" +version = "0.53.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/ad/04bbb797c84fc1f26cb171f7394716f4865ffb8d8c5e1eef42565c2dfa6b/uvicorn-0.53.0.tar.gz", hash = "sha256:a9356f0cb89b3b8621529c5d5eebd69bfe154f4c3f68b4cf2de47e45fa855c2e", size = 110881, upload-time = "2026-09-14T07:44:23.815Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/0eea75741ee812e9f598b687619ce2454f6c3a1c5cd21ea990ec6bd26f45/uvicorn-0.53.0-py3-none-any.whl", hash = "sha256:e8dca71ec86dce5f04e333f0d56cdedf942446e6643b9cea1af0d6d3a02cb03e", size = 87081, upload-time = "2026-09-14T07:44:22.179Z" }, ] [[package]] @@ -4743,6 +5549,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" }, + { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" }, + { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" }, + { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" }, + { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" }, + { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" }, + { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" }, { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, ] @@ -4760,33 +5600,55 @@ wheels = [ [[package]] name = "wrapt" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ba/8dc25478ed234dacc7d83c671634f347d0bdfb65bf0502f41879cf2f15a9/wrapt-2.4.0.tar.gz", hash = "sha256:7082fc1f94b020ac275870c4af71b09cff22876fe6e9c4c0ad01ea21d217b288", size = 161179, upload-time = "2026-08-30T04:41:51.424Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/22/581a0b44349d5babe526c958f365b8126e0fbd8fc2810e80446c47358050/wrapt-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef4e2d6e399ce6eecc80179a6b9ef6544f121288f95fc132bc36c9d9503903af", size = 96374, upload-time = "2026-08-30T04:39:42.335Z" }, - { url = "https://files.pythonhosted.org/packages/5d/90/095984648cec62a786bb27c0b50f6cfa5856d1e073ba1006fe148d190084/wrapt-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9b32d5e4f0a179cef5075cc79b79d6d3482c44c434c12969e48c6719e06d95", size = 96178, upload-time = "2026-08-30T04:39:43.789Z" }, - { url = "https://files.pythonhosted.org/packages/f8/fd/b20e3cb3cab35131b515edf18e8cd777dff680fc76fc00919481f4e536af/wrapt-2.4.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7dbbdbfdacb85c2d962fa52db791c77943fd777d600d74c95af2d53b32f5a94", size = 227806, upload-time = "2026-08-30T04:39:45.264Z" }, - { url = "https://files.pythonhosted.org/packages/08/75/c8dfba5e0caf17cd0718a0cbbe76cb85e637a2d65183fb728232419f6fca/wrapt-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39cd68df4dff79f5336f9c745c06259d204bcb42d504040c9c91eac9e2abb39c", size = 229004, upload-time = "2026-08-30T04:39:47.068Z" }, - { url = "https://files.pythonhosted.org/packages/42/05/d4853fbd33e5860b10d5aec690f563547a92a82e61fb8bb2d4ece1ce3570/wrapt-2.4.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2a9f1a2f75bb95257cc5744e255e10a5a86e923f328b40ad3dbf9d8d03430013", size = 208934, upload-time = "2026-08-30T04:39:48.73Z" }, - { url = "https://files.pythonhosted.org/packages/a3/66/23d0e8de9b411fd198af5121627587563657370c8d509fbe5ea8adb3df79/wrapt-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8763ad01e3725b7751a4575f38bbcc19c0aa0822fec91c5c5bd21ce3ce7e1d2b", size = 225709, upload-time = "2026-08-30T04:39:50.287Z" }, - { url = "https://files.pythonhosted.org/packages/01/37/3b357bc90530d510ae59ae7ac48265c482ae899e47637ca4436645688b40/wrapt-2.4.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9125c6dbe8b88c00dd8ef4fc1e55757e8eb4720b6b2b2cc610a45bd32bd28c57", size = 207090, upload-time = "2026-08-30T04:39:51.78Z" }, - { url = "https://files.pythonhosted.org/packages/6d/0c/d8a5c6dbcc2d221308223bcea4130c6332454a855cb4dbd5dcb2360b13b2/wrapt-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:28f5de1526831b8f173889a436e289fe181ede8c66c9feb669d1aca8fd602eaf", size = 216269, upload-time = "2026-08-30T04:39:53.641Z" }, - { url = "https://files.pythonhosted.org/packages/92/93/cc9fc8fef1d3d25edaa1c2dc2337b556dc1d0613ddc1c4a6fe9ee08ad705/wrapt-2.4.0-cp312-cp312-win32.whl", hash = "sha256:a9ca1cdb3f7facb4990c7739ea5afbaceeb6728d066feedde03a4cfe83b29b03", size = 91187, upload-time = "2026-08-30T04:39:55.38Z" }, - { url = "https://files.pythonhosted.org/packages/ad/ec/a7b10705172bdb669b9687a8ff68bbe5f566437d2a49ad6d976af48b6d10/wrapt-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:8b464316489fb2fca0669ea0f8f07290054a0f26fc72982d3e4cf95469628ba9", size = 96423, upload-time = "2026-08-30T04:39:56.81Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/e838ac6463a1a1a1817b2f184ee2aa20c54692b80368c5063403c8d2461c/wrapt-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:db1285071ea09a7767fac608e7b5c7b03c09833b06186875a359905fbc659d29", size = 93003, upload-time = "2026-08-30T04:39:58.237Z" }, - { url = "https://files.pythonhosted.org/packages/19/86/f9de4e11582ff96ad2199eeeceaa17faa27bbdc599243f520070c4f3de07/wrapt-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5c5c4c728cd22a36e4b8bb5df4a7d3bccaa865d27725b36eeb3b6f18fb2e1bc2", size = 96041, upload-time = "2026-08-30T04:39:59.575Z" }, - { url = "https://files.pythonhosted.org/packages/c3/ab/1dbf50802bea3b46192fd0dc39bb0eb2e77a064c813b2bbd88d2888ad49f/wrapt-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7de5b8d94417e55c02be50cc226e0ae1209bbc73813bf691dff3979c94438115", size = 96269, upload-time = "2026-08-30T04:40:01.182Z" }, - { url = "https://files.pythonhosted.org/packages/cb/a3/a3b5cde1cd06e04b6e95134eb3187a0a7da607a530e7795b221d4e4fa819/wrapt-2.4.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6436e2bda993a3eb69a1b317fc831c8ebcafb5704c390859ebd49f81218c4bbb", size = 225787, upload-time = "2026-08-30T04:40:02.715Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f7/d100f6c348b7669f19119cf890dcd4764623e2233af065586d110e0cd99e/wrapt-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e084558fbd112d2e1e34b0f5c71e45a3405bdad51a17150368a959bcf6697964", size = 226649, upload-time = "2026-08-30T04:40:04.647Z" }, - { url = "https://files.pythonhosted.org/packages/52/c6/3af8df515d5d7e92306957536f3468c6bdfecbe3659f99dbf09a468c2c4c/wrapt-2.4.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e78c947e18fadfd690c9420c30a96d221feeb93fc8f1cc00509b370ac16c3114", size = 206760, upload-time = "2026-08-30T04:40:06.332Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/40d355552bd3eb6c5186e26051c19b573d24d7896de42caa7937d6b5ca9f/wrapt-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08d8378c4514ac8dcc0ace76044cf87a873e6a52b5e6109834c8fb9037f4441b", size = 223467, upload-time = "2026-08-30T04:40:07.829Z" }, - { url = "https://files.pythonhosted.org/packages/40/ab/d198eebdb39f0d7e182e771e590a36673489cd58cebdad8aa273dcf28e04/wrapt-2.4.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:93180c2199784dd6a1075b33f9ed636bd0966821edbece6b3d5379b1c4f0bb7d", size = 205358, upload-time = "2026-08-30T04:40:09.344Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0e/974a60672ad507d39a3d8a1c6351ef37fe65b07240d000ceba5d2b83e9e9/wrapt-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d5e5eb76fb87e62752af751d2dcd9d1cd986b12037d2e1363d109ba716029e8", size = 214654, upload-time = "2026-08-30T04:40:10.923Z" }, - { url = "https://files.pythonhosted.org/packages/cc/5a/8b2db70206db0a4246758e0472ce344cb9636217113ef70640fc8d2ce874/wrapt-2.4.0-cp313-cp313-win32.whl", hash = "sha256:49bb5a572469e0e18163a8ec2aa972135a0929899ecbe627665f274506e1b5b4", size = 91171, upload-time = "2026-08-30T04:40:12.895Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1e/e782b511c680dbe7369c92e7d981484aacca0cda584da1f28a84cd9a8e1a/wrapt-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:b1737f46b1e4a81eb93500a7f2854319e1c7a86e8863fb050b7b4daadd5a4178", size = 96178, upload-time = "2026-08-30T04:40:14.336Z" }, - { url = "https://files.pythonhosted.org/packages/9f/62/095ba31123fa5dd482d6183c05200b061314aabbd5442c010aba4b03ff1c/wrapt-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:f1e9e088094f4895f84ab043e7d59401df137d663efbf1e80c82144882960830", size = 92949, upload-time = "2026-08-30T04:40:15.935Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/fafe0002f572ced999c792cfe8b05d39269c63d8193d15d25bd828bcad7a/wrapt-2.4.0-py3-none-any.whl", hash = "sha256:18aabd9301d06026f5900538051773d6f87f65ae02cdc60de482df978513dc0a", size = 73713, upload-time = "2026-08-30T04:41:49.805Z" }, +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/42/a6/6375d56c44d590ef24acf0f8f5bf7ed768ff7a510b959306ec412611e90f/wrapt-2.4.1.tar.gz", hash = "sha256:fd6390aab9e8aa40c52eff3c180f098e8d9f5894b1fd4c4fd2c207067b33ed16", size = 164597, upload-time = "2026-09-10T23:12:16.811Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/1f/a2f3225c5ecf522684c1d051aea8ce8253f240e55be75826b787777afd6c/wrapt-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7e86fbc2ac8a363ea04abf631fad82720e16b17a25020f32dbe9b24a2ed2b0e3", size = 98890, upload-time = "2026-09-10T23:10:07.876Z" }, + { url = "https://files.pythonhosted.org/packages/68/6c/eb45660fd4d92cce11ec923f55bb2e647a6c18d30e53734eb07a3c530e31/wrapt-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24389748f0b9d5b67e478fad4fc8b3f1108422ef80716e48eead6cebcebbff08", size = 98742, upload-time = "2026-09-10T23:10:09.236Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4f/17a89a580cb0082e61b8375074d5c9e5d38e4aa83ee19b6174ee472d17c2/wrapt-2.4.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30d11c289b013bf384ff1a1a6553f150d0b855901708a9bef667a5680f8247c9", size = 236301, upload-time = "2026-09-10T23:10:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/01/ca/4700eb008a34bf02de328806ddde15fc84c8d1e65d3dcafb92a935a50319/wrapt-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9356dbb59199a0e4709de35fa4a1ac1a88ef6da99711a397f5b009233faff326", size = 237805, upload-time = "2026-09-10T23:10:12.594Z" }, + { url = "https://files.pythonhosted.org/packages/75/5d/26c1740299b29e190d5f4b4a99eb001401042a9d9ab338e3f8e1ce140ecb/wrapt-2.4.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8342f332dada211f64b74609e332d727b13315e9a83177f7918bf68c59f815f2", size = 217037, upload-time = "2026-09-10T23:10:14.028Z" }, + { url = "https://files.pythonhosted.org/packages/3a/55/ec72991153a2ae8b40238bc44cec7c3ddf7706ef6e2d314b0c6f5c7febce/wrapt-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2f86e328c482bc5383b4eda5094be0bed3617fc3076aa9225ff1a9eb6372de9b", size = 234659, upload-time = "2026-09-10T23:10:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/7cfa1e070dcda76ea56a3e252341cba1cd1e9412baf23cd7adfebf1114e2/wrapt-2.4.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4dc92697444ee380544fbb43c86612d8486529aadf917c22b5524141d4af074c", size = 214590, upload-time = "2026-09-10T23:10:16.744Z" }, + { url = "https://files.pythonhosted.org/packages/79/10/248841cb30107f6f32c53a02662e1c3e0c7c06bea0b8ebfaaee94885dcee/wrapt-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edd03758a7578526642508b8833d43496fdfba0f64e0025dfca153a7c1777735", size = 225103, upload-time = "2026-09-10T23:10:18.346Z" }, + { url = "https://files.pythonhosted.org/packages/90/02/5b2bf7b35b008a39939a2908e85dba3b867596e535fc9f12ddf3ba1fcaf6/wrapt-2.4.1-cp312-cp312-win32.whl", hash = "sha256:5d83e412665aeb1e854eefbf1564d0d67872d9994b502a0bce96e6ff7f4970b7", size = 93441, upload-time = "2026-09-10T23:10:19.81Z" }, + { url = "https://files.pythonhosted.org/packages/d4/2a/47be56772bfb07ef242d6e924049688e38384af2b2fc99b0f31180988448/wrapt-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:b4e7efdd476ac631a0181551fd9aace844765ea3ce2b5133b194fae4421e8ad0", size = 98808, upload-time = "2026-09-10T23:10:21.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ec/40e4c9735626afd1dc0eeb310310278361f192800503cda0f5b3d8d24db4/wrapt-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:38819761401baa2d11916d7265b82f23265f8fe5a31c431dd7c24a8863c65f88", size = 95240, upload-time = "2026-09-10T23:10:22.39Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1a/9b5aa3c2391aa6d00fffa085c2219e8fc91cd4d2b9b080d2b4e4b6b93f42/wrapt-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55f36bb1461f93beaf18d818e568b5de343dd8fade7456773d201a38ee723bd3", size = 98597, upload-time = "2026-09-10T23:10:23.723Z" }, + { url = "https://files.pythonhosted.org/packages/fe/07/dc98150c2f9ee5b5fcbd841765e178ea6cc6c43733ab8d1f5181a4fb9f3d/wrapt-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:53e15cd74bd6b84d7fa90b93dda7334d85f4641fae97632de8aa61d268dfd145", size = 98842, upload-time = "2026-09-10T23:10:25.109Z" }, + { url = "https://files.pythonhosted.org/packages/08/c2/0e772a570e8d75c1b3ec45930a3023492fa68223f8f5e3485af4844c10c3/wrapt-2.4.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:be6cdd7121adc89a6f52e3c2f4e26a2d4dcdc1c0fde47e3156234db8939e4cdc", size = 234642, upload-time = "2026-09-10T23:10:26.644Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/4ce4d02dd95ff9ed972a2fc396d04fec636daa5a4ac18cc73a3dc20aa6c3/wrapt-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03b5598edd435373278731d0d53449ce7a9626bc48d5548e4a71124ee3e526a1", size = 235484, upload-time = "2026-09-10T23:10:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/436ef1df620ef8edd9ef057b4d55a0cd704435ff66852a0ef26cbaa02a58/wrapt-2.4.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fdc997819a6df4c65bdb0a1c5601c98b479ee34cc2f94ffa98a767034ad6366d", size = 214371, upload-time = "2026-09-10T23:10:29.841Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2c/cc5b7503843399087db3106a7cf60d60d0900f69539e96148b9fff116291/wrapt-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3bfc6907ebed560d2d3f677c3b17bf6199679163b6c6e475035c9dca497d1697", size = 232347, upload-time = "2026-09-10T23:10:31.511Z" }, + { url = "https://files.pythonhosted.org/packages/86/fb/17668b1ca572c44b458c09d76064d35f5f57d5ac7629248871604bef816c/wrapt-2.4.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a0c3b217332cf0c4df085fec41c126bf7507d6c1ca0efb3ba8d2b3fd234d4e73", size = 212792, upload-time = "2026-09-10T23:10:32.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2b/0c5de10d7259e07c478c44bf2fbe179c6878727d09edf0632b97884801db/wrapt-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a23b89621cfeb3329b1a290596bf402e61d7d5a647a65ca8ea735e48771b71d4", size = 223585, upload-time = "2026-09-10T23:10:34.43Z" }, + { url = "https://files.pythonhosted.org/packages/aa/36/013dce1c687f8f1c87b1e78f403d04c80ae9b2ae77de4ddba16069a3e7d1/wrapt-2.4.1-cp313-cp313-win32.whl", hash = "sha256:bc67d4872af5ab2dc1b88904097b92ac00e7658fdf010dee36807807fe882ac4", size = 93425, upload-time = "2026-09-10T23:10:35.901Z" }, + { url = "https://files.pythonhosted.org/packages/b7/1d/d53bcf5910209a45191c8bc173ff0e00830416547d8038772dc444932ee0/wrapt-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:1fe758b9c2d49138231ec3efabd106fec665f86fec50b3d02d7edd75f08a69ca", size = 98569, upload-time = "2026-09-10T23:10:37.316Z" }, + { url = "https://files.pythonhosted.org/packages/e8/1f/759d0c522918f9dfb620136622d8e1ce619570adda0de8fa2cfbb55cbb86/wrapt-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c974c36e8205255a3947dad9c2fe000431b57dd522946e56c192174a7f92a0f", size = 95272, upload-time = "2026-09-10T23:10:38.657Z" }, + { url = "https://files.pythonhosted.org/packages/08/05/ed5aa8991c5e9969e2ca17f0e44eb324a9757f44fa982bfc13844cfe9e1d/wrapt-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6c35541cc729964c65c2b9b1f9cf317811039abc2da13b4557f20f21cdc292b", size = 98876, upload-time = "2026-09-10T23:10:40.078Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5f/f8bf07c3b9a2ad01d28d5ca419e28819bb16937d129167d97c446c05875d/wrapt-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6b9df84f0a96763159cccb8e3b0ed83cc950c7c8bf82d6e45428372a805b3224", size = 99054, upload-time = "2026-09-10T23:10:41.484Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/518e5f3ebd18472652e5332bf37728cd6634ff81a0aa568969d05cc66099/wrapt-2.4.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78af62413095d0a57077606654ce85e273deda8e2bfa28fdb04257b962d94ef2", size = 237430, upload-time = "2026-09-10T23:10:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2b/503835d183c1ca99268e0b5a98af8f487d528ebabb124f4003cd96e73b1f/wrapt-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d60702ebc914d0bb01aa48f5c1785ceaaaa505e0a841b444a69d6ceb5de4097e", size = 237988, upload-time = "2026-09-10T23:10:44.466Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9d/a11a3e1eb18ae1d9adef1c39f8e8e36fd5b86db33938bebf8e6bc5fd9b06/wrapt-2.4.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8c4b44e4be7680fc496816e824b6ed134d781c4da296e44b75399196e5c248f1", size = 218554, upload-time = "2026-09-10T23:10:45.969Z" }, + { url = "https://files.pythonhosted.org/packages/e1/61/61a6f983737ba81008561ab634373bba25b986761d3a0f0aded8352ae3e8/wrapt-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fb5b3f94258bcf71db902795f4a71151c7a74d9fe21fffa00752d2bd286866f8", size = 235476, upload-time = "2026-09-10T23:10:47.528Z" }, + { url = "https://files.pythonhosted.org/packages/42/68/acb7cba46e0f662eaae421487807d2fed28ba52df9a00a3f04db16675e38/wrapt-2.4.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f1556a96b20d5bfdc9cb5dcd0a3ec65cbbac8a4eebcd3cd34efdc91c9519f660", size = 216454, upload-time = "2026-09-10T23:10:49.126Z" }, + { url = "https://files.pythonhosted.org/packages/8b/52/3a4dab8d4803df595de82f19775535d28c036746340c1b498fc8ecba39aa/wrapt-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:094a606d0bf1c4b847b3a743d22fc69cb164015b8c70cf6f20a534d30889ed47", size = 225346, upload-time = "2026-09-10T23:10:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/d72d051757fb7955021d8174014679f951fd5067187d2faed95520e1f8d6/wrapt-2.4.1-cp314-cp314-win32.whl", hash = "sha256:b0ee076be124406a7f97ca663c4a3ba32b6bcdd9102ca82497feaca79e9cb33c", size = 93870, upload-time = "2026-09-10T23:10:52.495Z" }, + { url = "https://files.pythonhosted.org/packages/a1/00/1a1bf4d8b60b9e7ac009969595438549ae6e33b2e3e174b78d26a833aad3/wrapt-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:d2d6f9abaa52de05090a2b4a4c1d0e858a268c4de69eec277506af9fbcccff91", size = 98910, upload-time = "2026-09-10T23:10:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/1d/59/b3169c3bcdcff70a6d02ed2b6366097b083d31391d616c407efe9a943a82/wrapt-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:3152b2e94d733a9bd70dbd1c95f148266f079a70e9ffca2a9c5dc421ade1b4e6", size = 96013, upload-time = "2026-09-10T23:10:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/d29a063c2ca6e779305cd444dd5acaac60833d73b2d94c7b1d0820c27e3e/wrapt-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:37ff91b390400463ecd4d080ea823314510b6843ac53e6e35cc09bba1805d466", size = 102069, upload-time = "2026-09-10T23:10:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/7435de516099b528e2232770b459d6706002736f41d8f092bbb11ea01575/wrapt-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e0a518cac3e789443af54fc77f23e66f17d80192c281b160b42058f11fabccc5", size = 102647, upload-time = "2026-09-10T23:10:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a7/cca7fe0f26aa11cbbf09b2e6f4f774db167feeca43ad22fa1772afdccbad/wrapt-2.4.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ecdeb8a1ec13397421e6f925412186bd87ab86d63517e6825b6d7bccf781f26", size = 275614, upload-time = "2026-09-10T23:11:00.464Z" }, + { url = "https://files.pythonhosted.org/packages/77/22/f3b5f4428ae679b1e2c0d4833519045d8983a9df0ad24df96b195d44dd1b/wrapt-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c15c4ede3fde08723cabab0a892d4b75d35b5a6f0a51c84e6542ac0bdc0507aa", size = 287064, upload-time = "2026-09-10T23:11:02.111Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b8/24274ec01b6ab6d0672f4312bfa29b5f1ffd9d56f16c8da67d5683883956/wrapt-2.4.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e32c5951c36fed88b6c603dc0bab209a62dc3217bd8762413634725fe28f2f3c", size = 255440, upload-time = "2026-09-10T23:11:03.746Z" }, + { url = "https://files.pythonhosted.org/packages/8b/df/c63a36f0f03d70c7b92f60ec1ad3757088167914e1a262f5dcc7df233d51/wrapt-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:898513db90d55a4ed3009312d41c12932b8163edbae3535280046d47aaff774f", size = 281681, upload-time = "2026-09-10T23:11:05.41Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c7/4930b6a369d9b3da59f5ada4ae9e659e2d4a68fe72c5d1ce15e5bead2d24/wrapt-2.4.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:42d01574bd4bcafc3476e77a95c6c0dd6167101991401325fc5591e2db21a91d", size = 252409, upload-time = "2026-09-10T23:11:06.867Z" }, + { url = "https://files.pythonhosted.org/packages/68/d7/f6bc5703061598c4a2ad4f7a2efce696782be6e9d5afed714a59f5902ead/wrapt-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:68a403adbeb4dd2654d6d108e43e69f0f90e6d34cb7588e0e6589109f6985e67", size = 270559, upload-time = "2026-09-10T23:11:08.385Z" }, + { url = "https://files.pythonhosted.org/packages/68/a3/952337a153e634f530079334c47dcc935b20f8ea22f364729fc2a8cf1821/wrapt-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:7cb3035b332bc9d21478600ba82c7c71e2d074ed9b4e76364297f54598792227", size = 96241, upload-time = "2026-09-10T23:11:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/48f0651547a125dd84e312b86268d4150c565a29f81ff9ca6fd81eba4be4/wrapt-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:707d2bef68deddd0fc74a81103d91b286a69ac5a9ff0f0a6dad66f8787e86697", size = 102655, upload-time = "2026-09-10T23:11:11.388Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/977441bed5e5afbffe7a789067ac2f5fd2e9f848884122a23db9ccae6a70/wrapt-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:28cb1c2713b4377bf03ddcb3e76d8216e4bb334199066c6122be7eed2f72de8c", size = 98495, upload-time = "2026-09-10T23:11:12.952Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a2/edcfc8d9a30375791b775715f8501364588f65b494ec4f6930568a19e765/wrapt-2.4.1-py3-none-any.whl", hash = "sha256:1e84ec5d89a0a07a0ef6bcd343f5c8ecdc95601d71de3058cdc63274e86c193c", size = 75317, upload-time = "2026-09-10T23:12:14.82Z" }, ] [[package]] @@ -4849,6 +5711,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/96/926f7335a0a1647952c00421e8da877f658094f61336306c7cadc335c94d/xxhash-4.0.1-cp313-cp313-win32.whl", hash = "sha256:cba763d84b06bda2c38d5185dee76f1b9dfdc0789e96e476d9e10005526d0788", size = 34449, upload-time = "2026-08-17T08:22:29.362Z" }, { url = "https://files.pythonhosted.org/packages/ea/61/8a5aeb811de093bab3434e77eff0e9461624a1a56a6a93d315d080aab2aa/xxhash-4.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:97b94fb29abf21f5f0bde15f7dbdd3a4aa2dc59f37026adc7b4bee8563b84375", size = 36520, upload-time = "2026-08-17T08:35:34.852Z" }, { url = "https://files.pythonhosted.org/packages/04/14/97f3c74000ca36955e9cb86f6d270dcd5848b5c65afa623453f5cf2d83d6/xxhash-4.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:08ed8da18cd4fd0a6a5d6a444852d8fbd0e565388a74a4937085451b5f1a312a", size = 33428, upload-time = "2026-08-17T08:21:31.713Z" }, + { url = "https://files.pythonhosted.org/packages/81/0e/ea406a02b561d3275232ccfdb3e29df80f7a65414940e3a15721c7bea40f/xxhash-4.0.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:af05a3f650220a6c59fa0ad2410249f2d2470a05225807c378fb67458693f8df", size = 43747, upload-time = "2026-08-17T08:22:31.37Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f0/b0c94d61ccf6b5d1f8847b58ef8f923125ac4919ed5bd0eb082750ca7cbd/xxhash-4.0.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:a6e3653df1a70b8ac4191216324242e4be2bca18c9a7c10934e1bd56dc7ca15e", size = 40749, upload-time = "2026-08-17T08:22:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c5/8085881a538983be0fd1c865d5df236242fea496044e2c8ca32b9f2ba39c/xxhash-4.0.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4528cf80ebbbf57d40edfb31521ae265daa6dd636d615b1cf0ac86209579e59d", size = 34734, upload-time = "2026-08-17T08:35:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/d3/94/8803d13c968fc75ca434eea991d29ac5fd8a36b4afc9a6a9803c53933db4/xxhash-4.0.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:90cb2a1c9cc503a054a19612b48ff6e8e47805f618bdb3224a07568aad03a37e", size = 35671, upload-time = "2026-08-17T08:21:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/85/d5/ad91d7f0fd294190d37c08236fe661f5c4e3f83dcd1a121877a2e64681ce/xxhash-4.0.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a949b072ea59c6eca0811ccd9e95133cc50d2afda8d464b5b077c78f78efa269", size = 38094, upload-time = "2026-08-17T08:22:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/89/f4/2b7ebdc1869caca5f02c4cba8379b631050d3c3d4adb9187e4dc1a6b8d3c/xxhash-4.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:79a3203aadf39637869dfea1185227d8452844d78b837e54fb1117b4d34ba5c3", size = 38244, upload-time = "2026-08-17T08:35:38.081Z" }, + { url = "https://files.pythonhosted.org/packages/90/9d/f66cf6935f528e575f1ae4d6560d376e7587569747186f4fae8777cadc1b/xxhash-4.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d9f3848ffaf010bdbabdbf4c25641fa258b6227ff27bc74a4d06edef521a4873", size = 35904, upload-time = "2026-08-17T08:21:37.358Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/34569d7b482f0dc060074faafd163c588f915cbc3e3e218f1ffd8a3ad340/xxhash-4.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9283d9dd6b44acad35118e2976fc763a065509e4118debdb61916ec322ed17b9", size = 259595, upload-time = "2026-08-17T08:22:38.153Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/a2370acfcd48732cf5c2b87f06cfbf7fa51c0ce0dd736bde42939eb9ebf7/xxhash-4.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c7c642a0f79c3e3cf2965475507574d3d1a50ec71060039d60cb87358667cb2", size = 284279, upload-time = "2026-08-17T08:22:36.396Z" }, + { url = "https://files.pythonhosted.org/packages/08/15/17d33c24e6c4a1c0b9ddc5584f0c25d51d48b34bacde1416a2235a19db4b/xxhash-4.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96dedccfb09a73a25751053a183159b88f4ee75f388df8166040c152ac0531c6", size = 303973, upload-time = "2026-08-17T08:35:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e0/4ec0d69ad5738729098a61e631b7ed2df22a922b0e03014b597c72bd863d/xxhash-4.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81664268dba92e037b740ecf37fa02f1cab4a391f93f28e35792b3341c60648f", size = 287535, upload-time = "2026-08-17T08:21:52.158Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/4f9b17e7a9eb71c65548ecddd9c18b84e3c18ca41c4d436ad2a3000d3f7b/xxhash-4.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:839f58c5bd9989875be0fd28446dbf32cace2c2cd8bf2f6762acdc38a95cd1aa", size = 519257, upload-time = "2026-08-17T08:22:43.272Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/3276b3e743b8ddbed9c3f71c76d9dd6a75d72aa4e678b1447b635cfd92e0/xxhash-4.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa44b4c7c5d0ffa31356b4428659516c0e47647825c74079a296b3857b6d99d", size = 268190, upload-time = "2026-08-17T08:35:44.985Z" }, + { url = "https://files.pythonhosted.org/packages/08/d4/f1555de3c96721320930dbb7988c8482d82b85970076aba1a8d40e83ad43/xxhash-4.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e681a6fc7e4f715252b9b5acfb30536ec7dd1f75033a32dc617e6fa95af1a3fd", size = 345553, upload-time = "2026-08-17T08:21:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/ac/98/c28908f27007087b61139d290f908dd827ffd40b88af0c43f9e1a1a7ffd5/xxhash-4.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6301d92545c591ad31c3e050aa40a5f8a4c16413f1f9e6f9322c6f0f9d2b736", size = 280499, upload-time = "2026-08-17T08:22:52.236Z" }, + { url = "https://files.pythonhosted.org/packages/a9/76/3ef57622c65816348f8196273485baab4752aae064959901e85cd867e067/xxhash-4.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6efb8f21cc136c79b3e5bb747c8682d37916fb202cdbbc32182de5c4e47f821f", size = 307211, upload-time = "2026-08-17T08:22:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/5804504bbc808968e57d6a50286dd8f8cc06e0ddd6e4ab4b1dc89ae42f35/xxhash-4.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760de77279e9cf9c81d012ce0705cba13afccee9b09c480f17d778c8c5cefae8", size = 265865, upload-time = "2026-08-17T08:35:42.727Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ee/8572fdfd70e7aaaf150af899871c2cc0bb88c3295ca82172a31e04ca5168/xxhash-4.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a16a3fa6936e36bb1414d16a6bd012c9033e5161b68b426805b61d895392437d", size = 284545, upload-time = "2026-08-17T08:21:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f8/6eadcca0904660c848b466524e82a233d16c9d2d5258433aaf3546142d86/xxhash-4.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9c3c4b9aa9a27196b921197f7daf9e6c1412739df06a99cfa6e923879362eff6", size = 336022, upload-time = "2026-08-17T08:22:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/27/df/4aa107b81602d6d6d09ab5a607c530d2d3a6b28e2e9a59b01875bd877c54/xxhash-4.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:863f3d3b44110f7243e86cf994aa5c5d88f2348b6e84ab4402fadadfbf9f7da7", size = 486671, upload-time = "2026-08-17T08:35:49.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/b7/b2bf9b5301e9cd5f2e335fea8da0f5cf209a6594cb1fe77754774ad4a6fd/xxhash-4.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63aa52659bc32bb9bd7cb5caf523b4d14429a477762cfac886132d687c1f80fc", size = 263744, upload-time = "2026-08-17T08:21:56.165Z" }, + { url = "https://files.pythonhosted.org/packages/0b/96/35b1c02177ae26234892c2310fb4822ba62411acccbf425ab8f9fd99354a/xxhash-4.0.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:67e57b834e07ed973cee7b6da1548ff28a56458d77696fd2a5f397f340694848", size = 20563, upload-time = "2026-08-17T08:35:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/51/c2/a06300b165fbd6b0cb4a9742987f2e997a9f447ce3bf7c6ac97b862ce62a/xxhash-4.0.1-cp314-cp314-win32.whl", hash = "sha256:b6c1f9c59bbe593f88a0aad30be4150f15bd57bd64efb95feeabcb8e563f1ecd", size = 35151, upload-time = "2026-08-17T08:22:44.283Z" }, + { url = "https://files.pythonhosted.org/packages/06/96/c5b37296b78f80fc97124c0fee0c7bbd1bdb6f3b18bcd8748bb113b2d8fc/xxhash-4.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:da544672efd9ad76077928a3e6c5d894e52ce82d3bf14002db4a1bf17d1a36a2", size = 37156, upload-time = "2026-08-17T08:35:46.551Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5e/248f9cd169c2fb62236bedfba246d213bce728f74901e99047e3f3c55875/xxhash-4.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:d0d24a4f3fb63852cd09af46ae4b7a4d00cc8b8615a046dca543786e728d1056", size = 34379, upload-time = "2026-08-17T08:21:59.446Z" }, + { url = "https://files.pythonhosted.org/packages/58/c8/db1d37c0da0324d0298f6abd931ca1d4736e049d9f2081230a8421da74d2/xxhash-4.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:349775ac30372b344d2338b2a168c0a1312a644194da25b8bec476d55761a128", size = 38656, upload-time = "2026-08-17T08:22:49.119Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/e18998ec465fb977bc74272e5bf3c2e886c13b014cbef916cd607802c709/xxhash-4.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:43e5f9169e73d0f0db33b5f6b8554bcce69ac278c966daf83d5eb4eb2f13829f", size = 36306, upload-time = "2026-08-17T08:35:52.853Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1a/b83f86f8a987a3cbcb7e005a6824ff64aecae35abc1395a0d44ee16c3319/xxhash-4.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4a252fb862b0ae2590587e625f47a0e03da05cf0205e8830b67b6596c06038b1", size = 273729, upload-time = "2026-08-17T08:21:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/02/4e/2db15aa8508e0cd5b632927a53b98234f24039ea65377e6cf996c06d2d4f/xxhash-4.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2df3ca8757dc381e75e90a4d7995a6324f58a923c7145220a7b2c0231f66fddc", size = 301083, upload-time = "2026-08-17T08:35:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/ed759787ffe802bd8e31cfcdad3755cbeca2dcdafd2f790cd6f25d195199/xxhash-4.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bfed61996d618eb90d6eaae0178002e3466a28b06bfc557a7a3a7266378d8c5a", size = 312745, upload-time = "2026-08-17T08:22:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/f64b4a4cc8b51e950709207f55f7f56ae9c5af6631dd31d7fb443312418c/xxhash-4.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9761ff4a0ffa583fe850731ad24fe82c88cccb7a2294727db0955f3279a4cb3f", size = 301419, upload-time = "2026-08-17T08:35:50.143Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/bac313b8de073569b8db3152044a7cfcce87a3fa9698c18fe9f914dee6b1/xxhash-4.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edccc2ec58435a580f96a48a3ccae8cd0a480824119165dd90108718ad81ae6e", size = 534485, upload-time = "2026-08-17T08:22:11.515Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/16b5e419f24e59507ee05626d2bb0deafdb03f9f27783bc0785a9849602e/xxhash-4.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4741d42d59e4e5fa1a86c17ab9c27dc8ea459c700d91b6742fdb9138d9a516cb", size = 279605, upload-time = "2026-08-17T08:22:52.934Z" }, + { url = "https://files.pythonhosted.org/packages/5f/55/5787dd6e2d8d5b61256a5039f6b18c2193c7c1de4a2fd2413288d0d9c604/xxhash-4.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:440c401e146ce64bdb3beb8ff0c84677b6f21307c28a34779071cecee5d4d70c", size = 358924, upload-time = "2026-08-17T08:35:58.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/68/89be41991f3b0a2e91f940bdf3128852c3ed571cf560d98ad0f67024afe4/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5b7979f71d06ae45a769de0699900a246d8cb632db1e8bfdc79ec019063a503c", size = 295305, upload-time = "2026-08-17T08:22:13.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5a/52ff0a0cc361aad393ff9a46ffe3aabbcf9c03d6c8f2612da7d553048276/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:62198213fc3e0c56e567894b318ba45834e007d065f84ba6dc9165d21546fc56", size = 320228, upload-time = "2026-08-17T08:35:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b5/91c60ff22c7f6cd5f6d7a5bad5a2cdcb4c33987dfa50bf13f0d856279b2e/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3bece52127ac20044311ee73567f9f0893b5de64f9028aecc90cc740cfd525a", size = 279414, upload-time = "2026-08-17T08:23:03.212Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/9685954804d47d0390871a64bec606a0d536406382d71a784df3a5883fb4/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a865d2d470220e659220fdb59d5b6c4422802d8d6098e1324bc4d12444798914", size = 297594, upload-time = "2026-08-17T08:35:57.881Z" }, + { url = "https://files.pythonhosted.org/packages/89/62/b67ac9412907b7a07a2a0c08c3440b9e4480231a7b3de0767e87011e4564/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8580aab306888224074c7edeec734de0c3c5ccde65b2da4e6c9a5e28f7c0a1bd", size = 348526, upload-time = "2026-08-17T08:22:18.571Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/6723cc49a9f567d52d01fd7c1741b0f2e3a13e71d15f7ac49d753a20c115/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2d52dc7c33c1b83082b707f6b7814dc76d2faaa2ea62bd9c5fab4b36f83c087f", size = 499307, upload-time = "2026-08-17T08:22:56.52Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/7b10e101ab988d93b791023be7191d7661271d6ab31ac082276b9091042a/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a9f98af872355e0c02439e48583958eee00e60b928bb20476460d9d40cb7b4e", size = 274989, upload-time = "2026-08-17T08:36:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/7eabcc8d29cce40621443cff24c07d7306ef574b8956c47ac59f21098005/xxhash-4.0.1-cp314-cp314t-win32.whl", hash = "sha256:a14578102a6081465aec9cf73c76c3cd3f79f0709bdb3b8ae7ab0b54c9d8b089", size = 35482, upload-time = "2026-08-17T08:22:32.336Z" }, + { url = "https://files.pythonhosted.org/packages/ca/89/2a4268e1971f63038b79fb75e3b9c8de942cd77acabbb0c5625352a31940/xxhash-4.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c57963970d359a72262f7fe6be88f945e2334d4bc41462b7f08c37b0abf35ca6", size = 37490, upload-time = "2026-08-17T08:35:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/90/7b/950ecab1fe4cf421d0a6211ddd9a0ac82e39e55c45a111ceb90953dc6c9a/xxhash-4.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:b659fad79c99b0238c7ad7e9d7dbf4eebfea9097c2dba65fa0a4d18a25b29a2f", size = 34596, upload-time = "2026-08-17T08:23:10.001Z" }, { url = "https://files.pythonhosted.org/packages/86/79/9127ff42a887a348dc4ce3211cf1a962836887adee6f57078132bfba78b4/xxhash-4.0.1-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:ff48915bf1871a1f19f74c11834c6329443d306cedc0c05fe7fe617810422a80", size = 31836, upload-time = "2026-08-17T08:36:28.261Z" }, { url = "https://files.pythonhosted.org/packages/0a/e6/f238693bfdd642adb59c99683964d46d9947fe721ff44d3bd850ae675407/xxhash-4.0.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a76345f5aceb4ec404918edf9c7f2b5507db864dc0d7455982009ac0890b57b", size = 34453, upload-time = "2026-08-17T08:23:49.795Z" }, { url = "https://files.pythonhosted.org/packages/40/4b/796ace33cdfb75c91ba6d11615c3bd436355b9f3103e05865bbee9abce57/xxhash-4.0.1-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d86f9e81f3e84e00131ac7c54caf5119ae4ddd82c09c31cff597c813ce1ee2", size = 38488, upload-time = "2026-08-17T08:23:59.901Z" }, @@ -4858,50 +5764,84 @@ wheels = [ [[package]] name = "yarl" -version = "1.24.5" +version = "1.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, - { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, - { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, - { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, - { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, - { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, - { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, - { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, - { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, - { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, - { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, - { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, - { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, - { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, - { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, - { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, - { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, - { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, - { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, - { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, - { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, - { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, - { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, - { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, - { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" }, + { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, + { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, + { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, + { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, + { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, + { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, ] [[package]] @@ -4953,4 +5893,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, ]