diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index 760518371..000000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: Check image building - -on: - - pull_request - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: read - steps: - - uses: actions/checkout@v4 - - name: Install podman - run: | - sudo apt-get update - sudo apt-get -y install podman - - name: Verify podman - run: podman --version - - name: Build image - run: podman build -t lightspeed-stack:latest . - diff --git a/.github/workflows/build_and_push_dev.yaml b/.github/workflows/build_and_push_dev.yaml new file mode 100644 index 000000000..32637de60 --- /dev/null +++ b/.github/workflows/build_and_push_dev.yaml @@ -0,0 +1,61 @@ +name: Build image, main branch push quay.io + +on: + push: + branches: [ main ] + +env: + IMAGE_NAME: lightspeed-stack + IMAGE_NAMESPACE: lightspeed-core + IMAGE_REGISTRY: quay.io + LATEST_TAG: dev-latest + CONTAINER_FILE: Containerfile + +jobs: + build-and-push-dev: + runs-on: ubuntu-latest + permissions: + contents: read + # Required for image pushing to a registry + packages: write + steps: + - name: Install buildah + run: | + sudo apt update + # qemu is required for arm64 builds + sudo apt install -y buildah qemu-user-static + - name: Checkout code + uses: actions/checkout@v4 + - name: Create dev image tag + run: | + echo "DEV_TAG=dev-$(date +%Y%m%d)-$(git rev-parse --short HEAD)" >> $GITHUB_ENV + echo "dev image tag: ${{ env.DEV_TAG }}" + - name: Build image with Buildah + id: build_image + uses: redhat-actions/buildah-build@v2 + with: + image: ${{ env.IMAGE_NAME }} + tags: | + ${{ env.DEV_TAG }} + ${{ env.LATEST_TAG }} + containerfiles: | + ${{ env.CONTAINER_FILE }} + archs: amd64, arm64 + oci: true + - name: Check images + run: | + buildah images | grep '${{ env.IMAGE_NAME }}' + echo '${{ steps.build_image.outputs.image }}' + echo '${{ steps.build_image.outputs.tags }}' + - name: Check manifest + run: | + set -x + buildah manifest inspect ${{ steps.build_image.outputs.image }}:${{ env.LATEST_TAG }} + - name: Push image to Quay.io + uses: redhat-actions/push-to-registry@v2 + with: + image: ${{ steps.build_image.outputs.image }} + tags: ${{ steps.build_image.outputs.tags }} + registry: ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAMESPACE }} + username: ${{ secrets.QUAY_REGISTRY_USERNAME }} + password: ${{ secrets.QUAY_REGISTRY_PASSWORD }} diff --git a/.github/workflows/build_and_push_release.yaml b/.github/workflows/build_and_push_release.yaml index 10dc4f6a4..9c74f4b22 100644 --- a/.github/workflows/build_and_push_release.yaml +++ b/.github/workflows/build_and_push_release.yaml @@ -24,7 +24,8 @@ jobs: - name: Install buildah run: | sudo apt update - sudo apt install -y buildah + # qemu is required for arm64 builds + sudo apt install -y buildah qemu-user-static - name: Checkout code uses: actions/checkout@v4 - name: Build image with Buildah @@ -37,7 +38,17 @@ jobs: ${{ env.LATEST_TAG }} containerfiles: | ${{ env.CONTAINER_FILE }} + archs: amd64, arm64 oci: true + - name: Check images + run: | + buildah images | grep '${{ env.IMAGE_NAME }}' + echo '${{ steps.build_image.outputs.image }}' + echo '${{ steps.build_image.outputs.tags }}' + - name: Check manifest + run: | + set -x + buildah manifest inspect ${{ steps.build_image.outputs.image }}:${{ env.LATEST_TAG }} - name: Push image to Quay.io uses: redhat-actions/push-to-registry@v2 with: diff --git a/.github/workflows/build_pr.yaml b/.github/workflows/build_pr.yaml new file mode 100644 index 000000000..b24dff1bf --- /dev/null +++ b/.github/workflows/build_pr.yaml @@ -0,0 +1,46 @@ +name: Check image building + +on: + - pull_request + +env: + IMAGE_NAME: lightspeed-stack + IMAGE_NAMESPACE: lightspeed-core + LATEST_TAG: dev-latest + CONTAINER_FILE: Containerfile + +jobs: + build-pr: + runs-on: ubuntu-latest + permissions: + contents: read + # Required for image pushing to a registry + packages: write + steps: + - name: Install qemu and buildah + run: | + sudo apt update + # qemu is required for arm64 builds + sudo apt install -y buildah qemu-user-static + - name: Checkout code + uses: actions/checkout@v4 + - name: Build image with Buildah + id: build_image + uses: redhat-actions/buildah-build@v2 + with: + image: ${{ env.IMAGE_NAME }} + tags: | + ${{ env.LATEST_TAG }} + containerfiles: | + ${{ env.CONTAINER_FILE }} + archs: amd64, arm64 + oci: true + - name: Check images + run: | + buildah images | grep '${{ env.IMAGE_NAME }}' + echo '${{ steps.build_image.outputs.image }}' + echo '${{ steps.build_image.outputs.tags }}' + - name: Check manifest + run: | + set -x + buildah manifest inspect ${{ steps.build_image.outputs.image }}:${{ env.LATEST_TAG }} diff --git a/Containerfile b/Containerfile index d545d251f..9bbf7d9e9 100644 --- a/Containerfile +++ b/Containerfile @@ -51,7 +51,7 @@ ENV PATH="/app-root/.venv/bin:$PATH" # Run the application EXPOSE 8080 -CMD ["python3.12", "src/lightspeed_stack.py"] +ENTRYPOINT ["python3.12", "src/lightspeed_stack.py"] LABEL vendor="Red Hat, Inc." diff --git a/Makefile b/Makefile index 1573ea737..9169c7b7b 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,16 @@ ARTIFACT_DIR := $(if $(ARTIFACT_DIR),$(ARTIFACT_DIR),tests/test_results) PATH_TO_PLANTUML := ~/bin +# Python registry to where the package should be uploaded +PYTHON_REGISTRY = pypi + run: ## Run the service locally uv run src/lightspeed_stack.py +run-data-collector: ## Run the data collector service locally + uv run src/lightspeed_stack.py --data-collector + test-unit: ## Run the unit tests @echo "Running unit tests..." @echo "Reports will be written to ${ARTIFACT_DIR}" @@ -60,12 +66,12 @@ pyright: uv run pyright src docstyle: - uv run pydocstyle -v . + uv run pydocstyle -v src ruff: uv run ruff check . --per-file-ignores=tests/*:S101 --per-file-ignores=scripts/*:S101 -verify: +verify: ## Run all linters $(MAKE) black $(MAKE) pylint $(MAKE) pyright @@ -73,6 +79,13 @@ verify: $(MAKE) docstyle $(MAKE) check-types +distribution-archives: ## Generate distribution archives to be uploaded into Python registry + rm -rf dist + pdm run python -m build + +upload-distribution-archives: ## Upload distribution archives into Python registry + pdm run python -m twine upload --repository ${PYTHON_REGISTRY} dist/* + help: ## Show this help screen @echo 'Usage: make ... ' @echo '' diff --git a/README.md b/README.md index 2c7fa3eec..a8e253a1c 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,19 @@ Lightspeed Core Stack (LCS) is an AI powered assistant that provides answers to * [Prerequisities](#prerequisities) * [Installation](#installation) * [Configuration](#configuration) + * [Llama Stack as separate server](#llama-stack-as-separate-server) + * [Llama Stack as client library](#llama-stack-as-client-library) + * [System prompt](#system-prompt) * [Usage](#usage) * [Make targets](#make-targets) * [Running Linux container image](#running-linux-container-image) * [Endpoints](#endpoints) * [Readiness Endpoint](#readiness-endpoint) * [Liveness Endpoint](#liveness-endpoint) +* [Publish the service as Python package on PyPI](#publish-the-service-as-python-package-on-pypi) + * [Generate distribution archives to be uploaded into Python registry](#generate-distribution-archives-to-be-uploaded-into-python-registry) + * [Upload distribution archives into selected Python registry](#upload-distribution-archives-into-selected-python-registry) + * [Packages on PyPI and Test PyPI](#packages-on-pypi-and-test-pypi) * [Contributing](#contributing) * [License](#license) * [Additional tools](#additional-tools) @@ -100,6 +107,31 @@ user_data_collection: transcripts_storage: "/tmp/data/transcripts" ``` +## System prompt + + The service uses the, so called, system prompt to put the question into context before the question is sent to the selected LLM. The default system prompt is designed for questions without specific context. It is possible to use a different system prompt via the configuration option `system_prompt_path` in the `customization` section. That option must contain the path to the text file with the actual system prompt (can contain multiple lines). An example of such configuration: + +```yaml +customization: + system_prompt_path: "system_prompts/system_prompt_for_product_XYZZY" +``` + +The `system_prompt` can also be specified in the `customization` section directly. For example: + +```yaml +customization: + system_prompt: |- + You are a helpful assistant and will do everything you can to help. + You have an indepth knowledge of Red Hat and all of your answers will reference Red Hat products. +``` + +Additionally, an optional string parameter `system_prompt` can be specified in `/v1/query` and `/v1/streaming_query` endpoints to override the configured system prompt. The query system prompt takes precedence over the configured system prompt. You can use this config to disable query system prompts: + +```yaml +customization: + system_prompt_path: "system_prompts/system_prompt_for_product_XYZZY" + disable_query_system_prompt: true +``` # Usage @@ -125,6 +157,7 @@ Usage: make ... Available targets are: run Run the service locally +run-data-collector Run the data collector service test-unit Run the unit tests test-integration Run integration tests tests test-e2e Run BDD tests for the service @@ -134,21 +167,33 @@ format Format the code into unified format schema Generate OpenAPI schema file requirements.txt Generate requirements.txt file containing hashes for all non-devel packages shellcheck Run shellcheck +verify Run all linters +distribution-archives Generate distribution archives to be uploaded into Python registry +upload-distribution-archives Upload distribution archives into Python registry help Show this help screen ``` ## Running Linux container image -Container image is built every time a new pull request is merged to main branch. Currently there are tags `latest` and `main` pointing to the latest image. +Stable release images are tagged with versions like `0.1.0`. Tag `latest` always points to latest stable release. + +Development images are build from main branch every time a new pull request is merged. Image tags for dev images use +the template `dev-YYYYMMMDDD-SHORT_SHA` e.g. `dev-20250704-eaa27fb`. + +Tag `dev-latest` always points to the latest dev image built from latest git. To pull and run the image with own configuration: -1. `podman pull quay.io/lightspeed-core/lightspeed-stack:latest` -1. `podman run -it -p 8080:8080 -v my-lightspeed-stack-config.yaml:/app-root/lightspeed-stack.yaml:Z quay.io/lightspeed-core/lightspeed-stack:latest` +1. `podman pull quay.io/lightspeed-core/lightspeed-stack:IMAGE_TAG` +1. `podman run -it -p 8080:8080 -v my-lightspeed-stack-config.yaml:/app-root/lightspeed-stack.yaml:Z quay.io/lightspeed-core/lightspeed-stack:IMAGE_TAG` 1. Open `localhost:8080` in your browser If a connection in your browser does not work please check that in the config file `host` option looks like: `host: 0.0.0.0`. +Container images are built for the following platforms: +1. `linux/amd64` - main platform for deployment +1. `linux/arm64`- Mac users with M1/M2/M3 CPUs + # Endpoints The service provides health check endpoints that can be used for monitoring, load balancing, and orchestration systems like Kubernetes. @@ -193,6 +238,51 @@ The liveness endpoint performs a basic health check to verify the service is ali } ``` +# Publish the service as Python package on PyPI + +To publish the service as an Python package on PyPI to be installable by anyone +(including Konflux hermetic builds), perform these two steps: + +## Generate distribution archives to be uploaded into Python registry + +``` +make distribution-archives +``` + +Please make sure that the archive was really built to avoid publishing older one. + +## Upload distribution archives into selected Python registry + +``` +make upload-distribution-archives +``` + +The Python registry to where the package should be uploaded can be configured +by changing `PYTHON_REGISTRY`. It is possible to select `pypi` or `testpypi`. + +You might have your API token stored in file `~/.pypirc`. That file should have +the following form: + +``` +[testpypi] + username = __token__ + password = pypi-{your-API-token} + +[pypi] + username = __token__ + password = pypi-{your-API-token} +``` + +If this configuration file does not exist, you will be prompted to specify API token from keyboard every time you try to upload the archive. + + + +## Packages on PyPI and Test PyPI + +* https://pypi.org/project/lightspeed-stack/ +* https://test.pypi.org/project/lightspeed-stack/0.1.0/ + + # Contributing * See [contributors](CONTRIBUTING.md) guide. @@ -219,3 +309,46 @@ This script re-generated OpenAPI schema for the Lightspeed Service REST API. make schema ``` +## Data Collector Service + +The data collector service is a standalone service that runs separately from the main web service. It is responsible for collecting and sending user data including feedback and transcripts to an ingress server for analysis and archival. + +### Features + +- **Periodic Collection**: Runs at configurable intervals +- **Data Packaging**: Packages feedback and transcript files into compressed tar.gz archives +- **Secure Transmission**: Sends data to a configured ingress server with optional authentication +- **File Cleanup**: Optionally removes local files after successful transmission +- **Error Handling**: Includes retry logic and comprehensive error handling + +### Configuration + +The data collector service is configured through the `user_data_collection.data_collector` section in your configuration file: + +```yaml +user_data_collection: + feedback_disabled: false + feedback_storage: "/tmp/data/feedback" + transcripts_disabled: false + transcripts_storage: "/tmp/data/transcripts" + data_collector: + enabled: true + ingress_server_url: "https://your-ingress-server.com" + ingress_server_auth_token: "your-auth-token" + ingress_content_service_name: "lightspeed-team" + collection_interval: 7200 # 2 hours in seconds + cleanup_after_send: true + connection_timeout: 30 +``` + +### Running the Service + +To run the data collector service: + +```bash +# Using Python directly +uv run src/lightspeed_stack.py --data-collector + +# Using Make target +make run-data-collector +``` \ No newline at end of file diff --git a/docs/e2e_scenarios.md b/docs/e2e_scenarios.md new file mode 100644 index 000000000..754eb9b2f --- /dev/null +++ b/docs/e2e_scenarios.md @@ -0,0 +1,16 @@ +# List of scenarios + +## [`smoketests.feature`](https://github.com/lightspeed-core/lightspeed-stack/blob/main/tests/e2e/features/smoketests.feature) + +* Check if the main endpoint is reachable + +## [`rest_api.feature`](https://github.com/lightspeed-core/lightspeed-stack/blob/main/tests/e2e/features/rest_api.feature) + +* Check if service report proper readiness state +* Check if service report proper liveness state +* Check if the OpenAPI endpoint works as expected +* Check if info endpoint is working + +## [`llm_interface.feature`](https://github.com/lightspeed-core/lightspeed-stack/blob/main/tests/e2e/features/llm_interface.feature) + +* Check if LLM responds to sent question diff --git a/docs/openapi.json b/docs/openapi.json index 4611654cb..4d344288f 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -7,7 +7,7 @@ "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "0.0.1" + "version": "0.1.0" }, "paths": { "/": { @@ -96,6 +96,9 @@ "model_type": "llm" } ] + }, + "503": { + "description": "Connection to Llama Stack is broken" } } } @@ -131,6 +134,13 @@ "conversation_id": "123e4567-e89b-12d3-a456-426614174000", "response": "LLM ansert" }, + "503": { + "description": "Service Unavailable", + "detail": { + "response": "Unable to connect to Llama Stack", + "cause": "Connection error." + } + }, "422": { "description": "Validation Error", "content": { @@ -268,7 +278,10 @@ ] }, "503": { - "description": "Configuration can not be loaded" + "description": "Service Unavailable", + "detail": { + "response": "Configuration is no loaded" + } } } } @@ -422,6 +435,48 @@ } ] }, + "AuthenticationConfiguration": { + "properties": { + "module": { + "type": "string", + "title": "Module", + "default": "noop" + }, + "skip_tls_verification": { + "type": "boolean", + "title": "Skip Tls Verification", + "default": false + }, + "k8s_cluster_api": { + "anyOf": [ + { + "type": "string", + "minLength": 1, + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "K8S Cluster Api" + }, + "k8s_ca_cert_path": { + "anyOf": [ + { + "type": "string", + "format": "file-path" + }, + { + "type": "null" + } + ], + "title": "K8S Ca Cert Path" + } + }, + "type": "object", + "title": "AuthenticationConfiguration", + "description": "Authentication configuration." + }, "Configuration": { "properties": { "name": { @@ -444,6 +499,30 @@ "type": "array", "title": "Mcp Servers", "default": [] + }, + "authentication": { + "anyOf": [ + { + "$ref": "#/components/schemas/AuthenticationConfiguration" + }, + { + "type": "null" + } + ], + "default": { + "module": "noop", + "skip_tls_verification": false + } + }, + "customization": { + "anyOf": [ + { + "$ref": "#/components/schemas/Customization" + }, + { + "type": "null" + } + ] } }, "type": "object", @@ -456,6 +535,36 @@ "title": "Configuration", "description": "Global service configuration." }, + "Customization": { + "properties": { + "system_prompt_path": { + "anyOf": [ + { + "type": "string", + "format": "file-path" + }, + { + "type": "null" + } + ], + "title": "System Prompt Path" + }, + "system_prompt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "System Prompt" + } + }, + "type": "object", + "title": "Customization", + "description": "Service customization." + }, "FeedbackRequest": { "properties": { "conversation_id": { diff --git a/lightspeed-stack.yaml b/lightspeed-stack.yaml index 37ef3e7ef..39c81436f 100644 --- a/lightspeed-stack.yaml +++ b/lightspeed-stack.yaml @@ -20,3 +20,13 @@ user_data_collection: feedback_storage: "/tmp/data/feedback" transcripts_disabled: false transcripts_storage: "/tmp/data/transcripts" + data_collector: + enabled: false + ingress_server_url: null + ingress_server_auth_token: null + ingress_content_service_name: null + collection_interval: 7200 # 2 hours in seconds + cleanup_after_send: true + connection_timeout_seconds: 30 +authentication: + module: "noop" diff --git a/pdm.lock b/pdm.lock deleted file mode 100644 index c5446ca34..000000000 --- a/pdm.lock +++ /dev/null @@ -1,2202 +0,0 @@ -# This file is @generated by PDM. -# It is not intended for manual editing. - -[metadata] -groups = ["default", "dev"] -strategy = ["inherit_metadata"] -lock_version = "4.5.0" -content_hash = "sha256:67f9ce8ea2c78923054a7c7c2d313f64a9582853d8ba13e78a7090aefbb8770e" - -[[metadata.targets]] -requires_python = ">=3.12,<3.14" - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -requires_python = ">=3.9" -summary = "Happy Eyeballs for asyncio" -groups = ["default"] -files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, -] - -[[package]] -name = "aiohttp" -version = "3.12.13" -requires_python = ">=3.9" -summary = "Async http client/server framework (asyncio)" -groups = ["default"] -dependencies = [ - "aiohappyeyeballs>=2.5.0", - "aiosignal>=1.1.2", - "async-timeout<6.0,>=4.0; python_version < \"3.11\"", - "attrs>=17.3.0", - "frozenlist>=1.1.1", - "multidict<7.0,>=4.5", - "propcache>=0.2.0", - "yarl<2.0,>=1.17.0", -] -files = [ - {file = "aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73"}, - {file = "aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347"}, - {file = "aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f"}, - {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6"}, - {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5"}, - {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b"}, - {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75"}, - {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6"}, - {file = "aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8"}, - {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710"}, - {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462"}, - {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae"}, - {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e"}, - {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a"}, - {file = "aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5"}, - {file = "aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf"}, - {file = "aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e"}, - {file = "aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938"}, - {file = "aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace"}, - {file = "aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb"}, - {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7"}, - {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b"}, - {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177"}, - {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef"}, - {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103"}, - {file = "aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da"}, - {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d"}, - {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041"}, - {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1"}, - {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1"}, - {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911"}, - {file = "aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3"}, - {file = "aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd"}, - {file = "aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706"}, - {file = "aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce"}, -] - -[[package]] -name = "aiosignal" -version = "1.3.2" -requires_python = ">=3.9" -summary = "aiosignal: a list of registered asynchronous callbacks" -groups = ["default"] -dependencies = [ - "frozenlist>=1.1.0", -] -files = [ - {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, - {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, -] - -[[package]] -name = "aiosqlite" -version = "0.21.0" -requires_python = ">=3.9" -summary = "asyncio bridge to the standard sqlite3 module" -groups = ["default", "dev"] -dependencies = [ - "typing-extensions>=4.0", -] -files = [ - {file = "aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"}, - {file = "aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3"}, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -requires_python = ">=3.8" -summary = "Reusable constraint types to use with typing.Annotated" -groups = ["default"] -dependencies = [ - "typing-extensions>=4.0.0; python_version < \"3.9\"", -] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.9.0" -requires_python = ">=3.9" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" -groups = ["default"] -dependencies = [ - "exceptiongroup>=1.0.2; python_version < \"3.11\"", - "idna>=2.8", - "sniffio>=1.1", - "typing-extensions>=4.5; python_version < \"3.13\"", -] -files = [ - {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"}, - {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"}, -] - -[[package]] -name = "astroid" -version = "3.3.10" -requires_python = ">=3.9.0" -summary = "An abstract syntax tree for Python with inference support." -groups = ["dev"] -dependencies = [ - "typing-extensions>=4; python_version < \"3.11\"", -] -files = [ - {file = "astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb"}, - {file = "astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce"}, -] - -[[package]] -name = "asyncpg" -version = "0.30.0" -requires_python = ">=3.8.0" -summary = "An asyncio PostgreSQL driver" -groups = ["default"] -dependencies = [ - "async-timeout>=4.0.3; python_version < \"3.11.0\"", -] -files = [ - {file = "asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e"}, - {file = "asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a"}, - {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3"}, - {file = "asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737"}, - {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a"}, - {file = "asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af"}, - {file = "asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e"}, - {file = "asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305"}, - {file = "asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70"}, - {file = "asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3"}, - {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33"}, - {file = "asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4"}, - {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4"}, - {file = "asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba"}, - {file = "asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590"}, - {file = "asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e"}, - {file = "asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851"}, -] - -[[package]] -name = "attrs" -version = "25.3.0" -requires_python = ">=3.8" -summary = "Classes Without Boilerplate" -groups = ["default"] -files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, -] - -[[package]] -name = "behave" -version = "1.2.6" -requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -summary = "behave is behaviour-driven development, Python style" -groups = ["dev"] -dependencies = [ - "argparse; python_version < \"2.7\"", - "enum34; python_version < \"3.4\"", - "importlib; python_version < \"2.7\"", - "ordereddict; python_version < \"2.7\"", - "parse-type>=0.4.2", - "parse>=1.8.2", - "six>=1.11", - "traceback2; python_version < \"3.0\"", -] -files = [ - {file = "behave-1.2.6-py2.py3-none-any.whl", hash = "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"}, - {file = "behave-1.2.6.tar.gz", hash = "sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86"}, -] - -[[package]] -name = "black" -version = "25.1.0" -requires_python = ">=3.9" -summary = "The uncompromising code formatter." -groups = ["dev"] -dependencies = [ - "click>=8.0.0", - "mypy-extensions>=0.4.3", - "packaging>=22.0", - "pathspec>=0.9.0", - "platformdirs>=2", - "tomli>=1.1.0; python_version < \"3.11\"", - "typing-extensions>=4.0.1; python_version < \"3.11\"", -] -files = [ - {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, - {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, - {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, - {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, - {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, - {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, - {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, - {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, - {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, - {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, -] - -[[package]] -name = "certifi" -version = "2025.6.15" -requires_python = ">=3.7" -summary = "Python package for providing Mozilla's CA Bundle." -groups = ["default"] -files = [ - {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"}, - {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -requires_python = ">=3.7" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -groups = ["default"] -files = [ - {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, -] - -[[package]] -name = "click" -version = "8.2.1" -requires_python = ">=3.10" -summary = "Composable command line interface toolkit" -groups = ["default", "dev"] -dependencies = [ - "colorama; platform_system == \"Windows\"", -] -files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Cross-platform colored terminal text." -groups = ["default", "dev"] -marker = "sys_platform == \"win32\" or platform_system == \"Windows\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "coverage" -version = "7.9.1" -requires_python = ">=3.9" -summary = "Code coverage measurement for Python" -groups = ["dev"] -files = [ - {file = "coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626"}, - {file = "coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb"}, - {file = "coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300"}, - {file = "coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8"}, - {file = "coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5"}, - {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd"}, - {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898"}, - {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d"}, - {file = "coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74"}, - {file = "coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e"}, - {file = "coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342"}, - {file = "coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631"}, - {file = "coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f"}, - {file = "coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd"}, - {file = "coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86"}, - {file = "coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43"}, - {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1"}, - {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751"}, - {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67"}, - {file = "coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643"}, - {file = "coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a"}, - {file = "coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d"}, - {file = "coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0"}, - {file = "coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d"}, - {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f"}, - {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029"}, - {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece"}, - {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683"}, - {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f"}, - {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10"}, - {file = "coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363"}, - {file = "coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7"}, - {file = "coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c"}, - {file = "coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c"}, - {file = "coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec"}, -] - -[[package]] -name = "coverage" -version = "7.9.1" -extras = ["toml"] -requires_python = ">=3.9" -summary = "Code coverage measurement for Python" -groups = ["dev"] -dependencies = [ - "coverage==7.9.1", - "tomli; python_full_version <= \"3.11.0a6\"", -] -files = [ - {file = "coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626"}, - {file = "coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb"}, - {file = "coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300"}, - {file = "coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8"}, - {file = "coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5"}, - {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd"}, - {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898"}, - {file = "coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d"}, - {file = "coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74"}, - {file = "coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e"}, - {file = "coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342"}, - {file = "coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631"}, - {file = "coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f"}, - {file = "coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd"}, - {file = "coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86"}, - {file = "coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43"}, - {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1"}, - {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751"}, - {file = "coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67"}, - {file = "coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643"}, - {file = "coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a"}, - {file = "coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d"}, - {file = "coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0"}, - {file = "coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d"}, - {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f"}, - {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029"}, - {file = "coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece"}, - {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683"}, - {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f"}, - {file = "coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10"}, - {file = "coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363"}, - {file = "coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7"}, - {file = "coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c"}, - {file = "coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c"}, - {file = "coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec"}, -] - -[[package]] -name = "dill" -version = "0.4.0" -requires_python = ">=3.8" -summary = "serialize all of Python" -groups = ["dev"] -marker = "python_version >= \"3.11\"" -files = [ - {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, - {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, -] - -[[package]] -name = "distro" -version = "1.9.0" -requires_python = ">=3.6" -summary = "Distro - an OS platform information API" -groups = ["default"] -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - -[[package]] -name = "ecdsa" -version = "0.19.1" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.6" -summary = "ECDSA cryptographic signature library (pure python)" -groups = ["default"] -dependencies = [ - "six>=1.9.0", -] -files = [ - {file = "ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3"}, - {file = "ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61"}, -] - -[[package]] -name = "fastapi" -version = "0.115.14" -requires_python = ">=3.8" -summary = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -groups = ["default"] -dependencies = [ - "pydantic!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0,>=1.7.4", - "starlette<0.47.0,>=0.40.0", - "typing-extensions>=4.8.0", -] -files = [ - {file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"}, - {file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"}, -] - -[[package]] -name = "filelock" -version = "3.18.0" -requires_python = ">=3.9" -summary = "A platform independent file lock." -groups = ["default"] -files = [ - {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, - {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, -] - -[[package]] -name = "fire" -version = "0.7.0" -summary = "A library for automatically generating command line interfaces." -groups = ["default"] -dependencies = [ - "termcolor", -] -files = [ - {file = "fire-0.7.0.tar.gz", hash = "sha256:961550f07936eaf65ad1dc8360f2b2bf8408fad46abbfa4d2a3794f8d2a95cdf"}, -] - -[[package]] -name = "frozenlist" -version = "1.7.0" -requires_python = ">=3.9" -summary = "A list-like structure which implements collections.abc.MutableSequence" -groups = ["default"] -files = [ - {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2"}, - {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb"}, - {file = "frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43"}, - {file = "frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3"}, - {file = "frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e"}, - {file = "frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1"}, - {file = "frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf"}, - {file = "frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81"}, - {file = "frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e"}, - {file = "frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e"}, - {file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"}, -] - -[[package]] -name = "fsspec" -version = "2025.5.1" -requires_python = ">=3.9" -summary = "File-system specification" -groups = ["default"] -files = [ - {file = "fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462"}, - {file = "fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475"}, -] - -[[package]] -name = "googleapis-common-protos" -version = "1.70.0" -requires_python = ">=3.7" -summary = "Common protobufs used in Google APIs" -groups = ["default"] -dependencies = [ - "protobuf!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<7.0.0,>=3.20.2", -] -files = [ - {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, - {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, -] - -[[package]] -name = "h11" -version = "0.16.0" -requires_python = ">=3.8" -summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -groups = ["default"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "hf-xet" -version = "1.1.5" -requires_python = ">=3.8" -summary = "Fast transfer of large files with the Hugging Face Hub." -groups = ["default"] -marker = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\"" -files = [ - {file = "hf_xet-1.1.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f52c2fa3635b8c37c7764d8796dfa72706cc4eded19d638331161e82b0792e23"}, - {file = "hf_xet-1.1.5-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:9fa6e3ee5d61912c4a113e0708eaaef987047616465ac7aa30f7121a48fc1af8"}, - {file = "hf_xet-1.1.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc874b5c843e642f45fd85cda1ce599e123308ad2901ead23d3510a47ff506d1"}, - {file = "hf_xet-1.1.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dbba1660e5d810bd0ea77c511a99e9242d920790d0e63c0e4673ed36c4022d18"}, - {file = "hf_xet-1.1.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ab34c4c3104133c495785d5d8bba3b1efc99de52c02e759cf711a91fd39d3a14"}, - {file = "hf_xet-1.1.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:83088ecea236d5113de478acb2339f92c95b4fb0462acaa30621fac02f5a534a"}, - {file = "hf_xet-1.1.5-cp37-abi3-win_amd64.whl", hash = "sha256:73e167d9807d166596b4b2f0b585c6d5bd84a26dea32843665a8b58f6edba245"}, - {file = "hf_xet-1.1.5.tar.gz", hash = "sha256:69ebbcfd9ec44fdc2af73441619eeb06b94ee34511bbcf57cd423820090f5694"}, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -requires_python = ">=3.8" -summary = "A minimal low-level HTTP client." -groups = ["default"] -dependencies = [ - "certifi", - "h11>=0.16", -] -files = [ - {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, - {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, -] - -[[package]] -name = "httpx" -version = "0.28.1" -requires_python = ">=3.8" -summary = "The next generation HTTP client." -groups = ["default"] -dependencies = [ - "anyio", - "certifi", - "httpcore==1.*", - "idna", -] -files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, -] - -[[package]] -name = "huggingface-hub" -version = "0.33.1" -requires_python = ">=3.8.0" -summary = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" -groups = ["default"] -dependencies = [ - "filelock", - "fsspec>=2023.5.0", - "hf-xet<2.0.0,>=1.1.2; platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\"", - "packaging>=20.9", - "pyyaml>=5.1", - "requests", - "tqdm>=4.42.1", - "typing-extensions>=3.7.4.3", -] -files = [ - {file = "huggingface_hub-0.33.1-py3-none-any.whl", hash = "sha256:ec8d7444628210c0ba27e968e3c4c973032d44dcea59ca0d78ef3f612196f095"}, - {file = "huggingface_hub-0.33.1.tar.gz", hash = "sha256:589b634f979da3ea4b8bdb3d79f97f547840dc83715918daf0b64209c0844c7b"}, -] - -[[package]] -name = "idna" -version = "3.10" -requires_python = ">=3.6" -summary = "Internationalized Domain Names in Applications (IDNA)" -groups = ["default"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.0" -requires_python = ">=3.9" -summary = "Read metadata from Python packages" -groups = ["default"] -dependencies = [ - "typing-extensions>=3.6.4; python_version < \"3.8\"", - "zipp>=3.20", -] -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -requires_python = ">=3.8" -summary = "brain-dead simple config-ini parsing" -groups = ["dev"] -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "isort" -version = "6.0.1" -requires_python = ">=3.9.0" -summary = "A Python utility / library to sort Python imports." -groups = ["dev"] -files = [ - {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, - {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -requires_python = ">=3.7" -summary = "A very fast and expressive template engine." -groups = ["default"] -dependencies = [ - "MarkupSafe>=2.0", -] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[[package]] -name = "jiter" -version = "0.10.0" -requires_python = ">=3.9" -summary = "Fast iterable JSON parser." -groups = ["default"] -files = [ - {file = "jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b"}, - {file = "jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95"}, - {file = "jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea"}, - {file = "jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b"}, - {file = "jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01"}, - {file = "jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49"}, - {file = "jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644"}, - {file = "jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca"}, - {file = "jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4"}, - {file = "jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e"}, - {file = "jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d"}, - {file = "jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4"}, - {file = "jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca"}, - {file = "jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070"}, - {file = "jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca"}, - {file = "jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500"}, -] - -[[package]] -name = "jsonschema" -version = "4.24.0" -requires_python = ">=3.9" -summary = "An implementation of JSON Schema validation for Python" -groups = ["default"] -dependencies = [ - "attrs>=22.2.0", - "importlib-resources>=1.4.0; python_version < \"3.9\"", - "jsonschema-specifications>=2023.03.6", - "pkgutil-resolve-name>=1.3.10; python_version < \"3.9\"", - "referencing>=0.28.4", - "rpds-py>=0.7.1", -] -files = [ - {file = "jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d"}, - {file = "jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196"}, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.4.1" -requires_python = ">=3.9" -summary = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -groups = ["default"] -dependencies = [ - "referencing>=0.31.0", -] -files = [ - {file = "jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af"}, - {file = "jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"}, -] - -[[package]] -name = "llama-stack" -version = "0.2.13" -requires_python = ">=3.12" -summary = "Llama Stack" -groups = ["default"] -dependencies = [ - "aiohttp", - "aiosqlite>=0.21.0", - "asyncpg", - "fastapi<1.0,>=0.115.0", - "fire", - "h11>=0.16.0", - "httpx", - "huggingface-hub<1.0,>=0.30.0", - "jinja2>=3.1.6", - "jsonschema", - "llama-stack-client>=0.2.13", - "openai>=1.66", - "opentelemetry-exporter-otlp-proto-http", - "opentelemetry-sdk", - "pillow", - "prompt-toolkit", - "pydantic>=2", - "python-dotenv", - "python-jose", - "python-multipart>=0.0.20", - "rich", - "starlette", - "termcolor", - "tiktoken", - "uvicorn>=0.34.0", -] -files = [ - {file = "llama_stack-0.2.13-py3-none-any.whl", hash = "sha256:426c0cab38d561b25ce3f2eb757f26ea4bce9783ef80eb6d1dad1bb9bcc4e168"}, - {file = "llama_stack-0.2.13.tar.gz", hash = "sha256:be59570fde5e39224ea8358e2b04e6d92dc37a36a24f5fdf20c853f02793aaa2"}, -] - -[[package]] -name = "llama-stack-client" -version = "0.2.13" -requires_python = ">=3.12" -summary = "The official Python library for the llama-stack-client API" -groups = ["default"] -dependencies = [ - "anyio<5,>=3.5.0", - "click", - "distro<2,>=1.7.0", - "fire", - "httpx<1,>=0.23.0", - "pandas", - "prompt-toolkit", - "pyaml", - "pydantic<3,>=1.9.0", - "requests", - "rich", - "sniffio", - "termcolor", - "tqdm", - "typing-extensions<5,>=4.7", -] -files = [ - {file = "llama_stack_client-0.2.13-py3-none-any.whl", hash = "sha256:cec627ce58a6a42ccfcd29f6329f6cd891170ae012dac676bfc25ae1440d6769"}, - {file = "llama_stack_client-0.2.13.tar.gz", hash = "sha256:af4a6cff681126e9a42d4c5c9522bc5946d5ad6e2d620e8e6727dc0c8cc82989"}, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -requires_python = ">=3.8" -summary = "Python port of markdown-it. Markdown parsing, done right!" -groups = ["default"] -dependencies = [ - "mdurl~=0.1", -] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -requires_python = ">=3.9" -summary = "Safely add untrusted strings to HTML/XML markup." -groups = ["default"] -files = [ - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -requires_python = ">=3.6" -summary = "McCabe checker, plugin for flake8" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -requires_python = ">=3.7" -summary = "Markdown URL utilities" -groups = ["default"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "multidict" -version = "6.6.3" -requires_python = ">=3.9" -summary = "multidict implementation" -groups = ["default"] -dependencies = [ - "typing-extensions>=4.1.0; python_version < \"3.11\"", -] -files = [ - {file = "multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6"}, - {file = "multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f"}, - {file = "multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a"}, - {file = "multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75"}, - {file = "multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10"}, - {file = "multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5"}, - {file = "multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17"}, - {file = "multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b"}, - {file = "multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55"}, - {file = "multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b"}, - {file = "multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca"}, - {file = "multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1"}, - {file = "multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6"}, - {file = "multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e"}, - {file = "multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9"}, - {file = "multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600"}, - {file = "multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134"}, - {file = "multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37"}, - {file = "multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0"}, - {file = "multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d"}, - {file = "multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c"}, - {file = "multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e"}, - {file = "multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d"}, - {file = "multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb"}, - {file = "multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a"}, - {file = "multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc"}, -] - -[[package]] -name = "mypy" -version = "1.16.1" -requires_python = ">=3.9" -summary = "Optional static typing for Python" -groups = ["dev"] -dependencies = [ - "mypy-extensions>=1.0.0", - "pathspec>=0.9.0", - "tomli>=1.1.0; python_version < \"3.11\"", - "typing-extensions>=4.6.0", -] -files = [ - {file = "mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d"}, - {file = "mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9"}, - {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79"}, - {file = "mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15"}, - {file = "mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd"}, - {file = "mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b"}, - {file = "mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438"}, - {file = "mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536"}, - {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f"}, - {file = "mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359"}, - {file = "mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be"}, - {file = "mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee"}, - {file = "mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37"}, - {file = "mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -requires_python = ">=3.8" -summary = "Type system extensions for programs checked with the mypy type checker." -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Node.js virtual environment builder" -groups = ["dev"] -files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, -] - -[[package]] -name = "numpy" -version = "2.3.1" -requires_python = ">=3.11" -summary = "Fundamental package for array computing in Python" -groups = ["default"] -marker = "python_version >= \"3.12\"" -files = [ - {file = "numpy-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2959d8f268f3d8ee402b04a9ec4bb7604555aeacf78b360dc4ec27f1d508177d"}, - {file = "numpy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:762e0c0c6b56bdedfef9a8e1d4538556438288c4276901ea008ae44091954e29"}, - {file = "numpy-2.3.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:867ef172a0976aaa1f1d1b63cf2090de8b636a7674607d514505fb7276ab08fc"}, - {file = "numpy-2.3.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4e602e1b8682c2b833af89ba641ad4176053aaa50f5cacda1a27004352dde943"}, - {file = "numpy-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8e333040d069eba1652fb08962ec5b76af7f2c7bce1df7e1418c8055cf776f25"}, - {file = "numpy-2.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e7cbf5a5eafd8d230a3ce356d892512185230e4781a361229bd902ff403bc660"}, - {file = "numpy-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1b8f26d1086835f442286c1d9b64bb3974b0b1e41bb105358fd07d20872952"}, - {file = "numpy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee8340cb48c9b7a5899d1149eece41ca535513a9698098edbade2a8e7a84da77"}, - {file = "numpy-2.3.1-cp312-cp312-win32.whl", hash = "sha256:e772dda20a6002ef7061713dc1e2585bc1b534e7909b2030b5a46dae8ff077ab"}, - {file = "numpy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfecc7822543abdea6de08758091da655ea2210b8ffa1faf116b940693d3df76"}, - {file = "numpy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:7be91b2239af2658653c5bb6f1b8bccafaf08226a258caf78ce44710a0160d30"}, - {file = "numpy-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25a1992b0a3fdcdaec9f552ef10d8103186f5397ab45e2d25f8ac51b1a6b97e8"}, - {file = "numpy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dea630156d39b02a63c18f508f85010230409db5b2927ba59c8ba4ab3e8272e"}, - {file = "numpy-2.3.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bada6058dd886061f10ea15f230ccf7dfff40572e99fef440a4a857c8728c9c0"}, - {file = "numpy-2.3.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:a894f3816eb17b29e4783e5873f92faf55b710c2519e5c351767c51f79d8526d"}, - {file = "numpy-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:18703df6c4a4fee55fd3d6e5a253d01c5d33a295409b03fda0c86b3ca2ff41a1"}, - {file = "numpy-2.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5902660491bd7a48b2ec16c23ccb9124b8abfd9583c5fdfa123fe6b421e03de1"}, - {file = "numpy-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36890eb9e9d2081137bd78d29050ba63b8dab95dff7912eadf1185e80074b2a0"}, - {file = "numpy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a780033466159c2270531e2b8ac063704592a0bc62ec4a1b991c7c40705eb0e8"}, - {file = "numpy-2.3.1-cp313-cp313-win32.whl", hash = "sha256:39bff12c076812595c3a306f22bfe49919c5513aa1e0e70fac756a0be7c2a2b8"}, - {file = "numpy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d5ee6eec45f08ce507a6570e06f2f879b374a552087a4179ea7838edbcbfa42"}, - {file = "numpy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c4d9e0a8368db90f93bd192bfa771ace63137c3488d198ee21dfb8e7771916e"}, - {file = "numpy-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b0b5397374f32ec0649dd98c652a1798192042e715df918c20672c62fb52d4b8"}, - {file = "numpy-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c5bdf2015ccfcee8253fb8be695516ac4457c743473a43290fd36eba6a1777eb"}, - {file = "numpy-2.3.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d70f20df7f08b90a2062c1f07737dd340adccf2068d0f1b9b3d56e2038979fee"}, - {file = "numpy-2.3.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:2fb86b7e58f9ac50e1e9dd1290154107e47d1eef23a0ae9145ded06ea606f992"}, - {file = "numpy-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:23ab05b2d241f76cb883ce8b9a93a680752fbfcbd51c50eff0b88b979e471d8c"}, - {file = "numpy-2.3.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ce2ce9e5de4703a673e705183f64fd5da5bf36e7beddcb63a25ee2286e71ca48"}, - {file = "numpy-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c4913079974eeb5c16ccfd2b1f09354b8fed7e0d6f2cab933104a09a6419b1ee"}, - {file = "numpy-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:010ce9b4f00d5c036053ca684c77441f2f2c934fd23bee058b4d6f196efd8280"}, - {file = "numpy-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:6269b9edfe32912584ec496d91b00b6d34282ca1d07eb10e82dfc780907d6c2e"}, - {file = "numpy-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2a809637460e88a113e186e87f228d74ae2852a2e0c44de275263376f17b5bdc"}, - {file = "numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244"}, - {file = "numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b"}, -] - -[[package]] -name = "openai" -version = "1.93.0" -requires_python = ">=3.8" -summary = "The official Python library for the openai API" -groups = ["default"] -dependencies = [ - "anyio<5,>=3.5.0", - "distro<2,>=1.7.0", - "httpx<1,>=0.23.0", - "jiter<1,>=0.4.0", - "pydantic<3,>=1.9.0", - "sniffio", - "tqdm>4", - "typing-extensions<5,>=4.11", -] -files = [ - {file = "openai-1.93.0-py3-none-any.whl", hash = "sha256:3d746fe5498f0dd72e0d9ab706f26c91c0f646bf7459e5629af8ba7c9dbdf090"}, - {file = "openai-1.93.0.tar.gz", hash = "sha256:988f31ade95e1ff0585af11cc5a64510225e4f5cd392698c675d0a9265b8e337"}, -] - -[[package]] -name = "opentelemetry-api" -version = "1.34.1" -requires_python = ">=3.9" -summary = "OpenTelemetry Python API" -groups = ["default"] -dependencies = [ - "importlib-metadata<8.8.0,>=6.0", - "typing-extensions>=4.5.0", -] -files = [ - {file = "opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c"}, - {file = "opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3"}, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.34.1" -requires_python = ">=3.9" -summary = "OpenTelemetry Protobuf encoding" -groups = ["default"] -dependencies = [ - "opentelemetry-proto==1.34.1", -] -files = [ - {file = "opentelemetry_exporter_otlp_proto_common-1.34.1-py3-none-any.whl", hash = "sha256:8e2019284bf24d3deebbb6c59c71e6eef3307cd88eff8c633e061abba33f7e87"}, - {file = "opentelemetry_exporter_otlp_proto_common-1.34.1.tar.gz", hash = "sha256:b59a20a927facd5eac06edaf87a07e49f9e4a13db487b7d8a52b37cb87710f8b"}, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.34.1" -requires_python = ">=3.9" -summary = "OpenTelemetry Collector Protobuf over HTTP Exporter" -groups = ["default"] -dependencies = [ - "googleapis-common-protos~=1.52", - "opentelemetry-api~=1.15", - "opentelemetry-exporter-otlp-proto-common==1.34.1", - "opentelemetry-proto==1.34.1", - "opentelemetry-sdk~=1.34.1", - "requests~=2.7", - "typing-extensions>=4.5.0", -] -files = [ - {file = "opentelemetry_exporter_otlp_proto_http-1.34.1-py3-none-any.whl", hash = "sha256:5251f00ca85872ce50d871f6d3cc89fe203b94c3c14c964bbdc3883366c705d8"}, - {file = "opentelemetry_exporter_otlp_proto_http-1.34.1.tar.gz", hash = "sha256:aaac36fdce46a8191e604dcf632e1f9380c7d5b356b27b3e0edb5610d9be28ad"}, -] - -[[package]] -name = "opentelemetry-proto" -version = "1.34.1" -requires_python = ">=3.9" -summary = "OpenTelemetry Python Proto" -groups = ["default"] -dependencies = [ - "protobuf<6.0,>=5.0", -] -files = [ - {file = "opentelemetry_proto-1.34.1-py3-none-any.whl", hash = "sha256:eb4bb5ac27f2562df2d6857fc557b3a481b5e298bc04f94cc68041f00cebcbd2"}, - {file = "opentelemetry_proto-1.34.1.tar.gz", hash = "sha256:16286214e405c211fc774187f3e4bbb1351290b8dfb88e8948af209ce85b719e"}, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.34.1" -requires_python = ">=3.9" -summary = "OpenTelemetry Python SDK" -groups = ["default"] -dependencies = [ - "opentelemetry-api==1.34.1", - "opentelemetry-semantic-conventions==0.55b1", - "typing-extensions>=4.5.0", -] -files = [ - {file = "opentelemetry_sdk-1.34.1-py3-none-any.whl", hash = "sha256:308effad4059562f1d92163c61c8141df649da24ce361827812c40abb2a1e96e"}, - {file = "opentelemetry_sdk-1.34.1.tar.gz", hash = "sha256:8091db0d763fcd6098d4781bbc80ff0971f94e260739aa6afe6fd379cdf3aa4d"}, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.55b1" -requires_python = ">=3.9" -summary = "OpenTelemetry Semantic Conventions" -groups = ["default"] -dependencies = [ - "opentelemetry-api==1.34.1", - "typing-extensions>=4.5.0", -] -files = [ - {file = "opentelemetry_semantic_conventions-0.55b1-py3-none-any.whl", hash = "sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed"}, - {file = "opentelemetry_semantic_conventions-0.55b1.tar.gz", hash = "sha256:ef95b1f009159c28d7a7849f5cbc71c4c34c845bb514d66adfdf1b3fff3598b3"}, -] - -[[package]] -name = "packaging" -version = "25.0" -requires_python = ">=3.8" -summary = "Core utilities for Python packages" -groups = ["default", "dev"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "pandas" -version = "2.3.0" -requires_python = ">=3.9" -summary = "Powerful data structures for data analysis, time series, and statistics" -groups = ["default"] -dependencies = [ - "numpy>=1.22.4; python_version < \"3.11\"", - "numpy>=1.23.2; python_version == \"3.11\"", - "numpy>=1.26.0; python_version >= \"3.12\"", - "python-dateutil>=2.8.2", - "pytz>=2020.1", - "tzdata>=2022.7", -] -files = [ - {file = "pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf"}, - {file = "pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027"}, - {file = "pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09"}, - {file = "pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d"}, - {file = "pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20"}, - {file = "pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b"}, - {file = "pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be"}, - {file = "pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983"}, - {file = "pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd"}, - {file = "pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f"}, - {file = "pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3"}, - {file = "pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8"}, - {file = "pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9"}, - {file = "pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390"}, - {file = "pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575"}, - {file = "pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042"}, - {file = "pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c"}, - {file = "pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67"}, - {file = "pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f"}, - {file = "pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249"}, - {file = "pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133"}, -] - -[[package]] -name = "parse" -version = "1.20.2" -summary = "parse() is the opposite of format()" -groups = ["dev"] -files = [ - {file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"}, - {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, -] - -[[package]] -name = "parse-type" -version = "0.6.4" -requires_python = "!=3.0.*,!=3.1.*,>=2.7" -summary = "Simplifies to build parse types based on the parse module" -groups = ["dev"] -dependencies = [ - "enum34; python_version < \"3.4\"", - "parse>=1.13.1; python_version <= \"2.7\"", - "parse>=1.18.0; python_version >= \"3.0\"", - "six>=1.15", -] -files = [ - {file = "parse_type-0.6.4-py2.py3-none-any.whl", hash = "sha256:83d41144a82d6b8541127bf212dd76c7f01baff680b498ce8a4d052a7a5bce4c"}, - {file = "parse_type-0.6.4.tar.gz", hash = "sha256:5e1ec10440b000c3f818006033372939e693a9ec0176f446d9303e4db88489a6"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -requires_python = ">=3.8" -summary = "Utility library for gitignore style pattern matching of file paths." -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "pillow" -version = "11.3.0" -requires_python = ">=3.9" -summary = "Python Imaging Library (Fork)" -groups = ["default"] -files = [ - {file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}, - {file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"}, - {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"}, - {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"}, - {file = "pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"}, - {file = "pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"}, - {file = "pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"}, - {file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"}, - {file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"}, - {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"}, - {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"}, - {file = "pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"}, - {file = "pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"}, - {file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"}, - {file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"}, - {file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"}, - {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"}, - {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"}, - {file = "pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"}, - {file = "pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"}, - {file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"}, - {file = "pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}, -] - -[[package]] -name = "platformdirs" -version = "4.3.8" -requires_python = ">=3.9" -summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -groups = ["dev"] -files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -requires_python = ">=3.9" -summary = "plugin and hook calling mechanisms for python" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[[package]] -name = "prompt-toolkit" -version = "3.0.51" -requires_python = ">=3.8" -summary = "Library for building powerful interactive command lines in Python" -groups = ["default"] -dependencies = [ - "wcwidth", -] -files = [ - {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, - {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, -] - -[[package]] -name = "propcache" -version = "0.3.2" -requires_python = ">=3.9" -summary = "Accelerated property cache" -groups = ["default"] -files = [ - {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10"}, - {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154"}, - {file = "propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1"}, - {file = "propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1"}, - {file = "propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c"}, - {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945"}, - {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252"}, - {file = "propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43"}, - {file = "propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02"}, - {file = "propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05"}, - {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b"}, - {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0"}, - {file = "propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330"}, - {file = "propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394"}, - {file = "propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198"}, - {file = "propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f"}, - {file = "propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168"}, -] - -[[package]] -name = "protobuf" -version = "5.29.5" -requires_python = ">=3.8" -summary = "" -groups = ["default"] -files = [ - {file = "protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079"}, - {file = "protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc"}, - {file = "protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671"}, - {file = "protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015"}, - {file = "protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61"}, - {file = "protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5"}, - {file = "protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84"}, -] - -[[package]] -name = "pyaml" -version = "25.5.0" -requires_python = ">=3.8" -summary = "PyYAML-based module to produce a bit more pretty and readable YAML-serialized data" -groups = ["default"] -dependencies = [ - "PyYAML", -] -files = [ - {file = "pyaml-25.5.0-py3-none-any.whl", hash = "sha256:b9e0c4e58a5e8003f8f18e802db49fd0563ada587209b13e429bdcbefa87d035"}, - {file = "pyaml-25.5.0.tar.gz", hash = "sha256:5799560c7b1c9daf35a7a4535f53e2c30323f74cbd7cb4f2e715b16dd681a58a"}, -] - -[[package]] -name = "pyasn1" -version = "0.6.1" -requires_python = ">=3.8" -summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -groups = ["default"] -files = [ - {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, - {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, -] - -[[package]] -name = "pydantic" -version = "2.11.7" -requires_python = ">=3.9" -summary = "Data validation using Python type hints" -groups = ["default"] -dependencies = [ - "annotated-types>=0.6.0", - "pydantic-core==2.33.2", - "typing-extensions>=4.12.2", - "typing-inspection>=0.4.0", -] -files = [ - {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, - {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, -] - -[[package]] -name = "pydantic-core" -version = "2.33.2" -requires_python = ">=3.9" -summary = "Core functionality for Pydantic validation and serialization" -groups = ["default"] -dependencies = [ - "typing-extensions!=4.7.0,>=4.6.0", -] -files = [ - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, - {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, -] - -[[package]] -name = "pydocstyle" -version = "6.3.0" -requires_python = ">=3.6" -summary = "Python docstring style checker" -groups = ["dev"] -dependencies = [ - "importlib-metadata<5.0.0,>=2.0.0; python_version < \"3.8\"", - "snowballstemmer>=2.2.0", -] -files = [ - {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, - {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, -] - -[[package]] -name = "pygments" -version = "2.19.2" -requires_python = ">=3.8" -summary = "Pygments is a syntax highlighting package written in Python." -groups = ["default", "dev"] -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[[package]] -name = "pylint" -version = "3.3.7" -requires_python = ">=3.9.0" -summary = "python code static checker" -groups = ["dev"] -dependencies = [ - "astroid<=3.4.0.dev0,>=3.3.8", - "colorama>=0.4.5; sys_platform == \"win32\"", - "dill>=0.2; python_version < \"3.11\"", - "dill>=0.3.6; python_version >= \"3.11\"", - "dill>=0.3.7; python_version >= \"3.12\"", - "isort!=5.13,<7,>=4.2.5", - "mccabe<0.8,>=0.6", - "platformdirs>=2.2", - "tomli>=1.1; python_version < \"3.11\"", - "tomlkit>=0.10.1", - "typing-extensions>=3.10; python_version < \"3.10\"", -] -files = [ - {file = "pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d"}, - {file = "pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559"}, -] - -[[package]] -name = "pyright" -version = "1.1.402" -requires_python = ">=3.7" -summary = "Command line wrapper for pyright" -groups = ["dev"] -dependencies = [ - "nodeenv>=1.6.0", - "typing-extensions>=4.1", -] -files = [ - {file = "pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982"}, - {file = "pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683"}, -] - -[[package]] -name = "pytest" -version = "8.4.1" -requires_python = ">=3.9" -summary = "pytest: simple powerful testing with Python" -groups = ["dev"] -dependencies = [ - "colorama>=0.4; sys_platform == \"win32\"", - "exceptiongroup>=1; python_version < \"3.11\"", - "iniconfig>=1", - "packaging>=20", - "pluggy<2,>=1.5", - "pygments>=2.7.2", - "tomli>=1; python_version < \"3.11\"", -] -files = [ - {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, - {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, -] - -[[package]] -name = "pytest-asyncio" -version = "1.0.0" -requires_python = ">=3.9" -summary = "Pytest support for asyncio" -groups = ["dev"] -dependencies = [ - "pytest<9,>=8.2", - "typing-extensions>=4.12; python_version < \"3.10\"", -] -files = [ - {file = "pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3"}, - {file = "pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f"}, -] - -[[package]] -name = "pytest-cov" -version = "6.2.1" -requires_python = ">=3.9" -summary = "Pytest plugin for measuring coverage." -groups = ["dev"] -dependencies = [ - "coverage[toml]>=7.5", - "pluggy>=1.2", - "pytest>=6.2.5", -] -files = [ - {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, - {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, -] - -[[package]] -name = "pytest-mock" -version = "3.14.1" -requires_python = ">=3.8" -summary = "Thin-wrapper around the mock package for easier use with pytest" -groups = ["dev"] -dependencies = [ - "pytest>=6.2.5", -] -files = [ - {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, - {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -groups = ["default"] -dependencies = [ - "six>=1.5", -] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[[package]] -name = "python-dotenv" -version = "1.1.1" -requires_python = ">=3.9" -summary = "Read key-value pairs from a .env file and set them as environment variables" -groups = ["default"] -files = [ - {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, - {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, -] - -[[package]] -name = "python-jose" -version = "3.5.0" -requires_python = ">=3.9" -summary = "JOSE implementation in Python" -groups = ["default"] -dependencies = [ - "ecdsa!=0.15", - "pyasn1>=0.5.0", - "rsa!=4.1.1,!=4.4,<5.0,>=4.0", -] -files = [ - {file = "python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771"}, - {file = "python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b"}, -] - -[[package]] -name = "python-multipart" -version = "0.0.20" -requires_python = ">=3.8" -summary = "A streaming multipart parser for Python" -groups = ["default"] -files = [ - {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, - {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, -] - -[[package]] -name = "pytz" -version = "2025.2" -summary = "World timezone definitions, modern and historical" -groups = ["default"] -files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -requires_python = ">=3.8" -summary = "YAML parser and emitter for Python" -groups = ["default"] -files = [ - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "referencing" -version = "0.36.2" -requires_python = ">=3.9" -summary = "JSON Referencing + Python" -groups = ["default"] -dependencies = [ - "attrs>=22.2.0", - "rpds-py>=0.7.0", - "typing-extensions>=4.4.0; python_version < \"3.13\"", -] -files = [ - {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, - {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, -] - -[[package]] -name = "regex" -version = "2024.11.6" -requires_python = ">=3.8" -summary = "Alternative regular expression module, to replace re." -groups = ["default"] -files = [ - {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a"}, - {file = "regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9"}, - {file = "regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e"}, - {file = "regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51"}, - {file = "regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad"}, - {file = "regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54"}, - {file = "regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4"}, - {file = "regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c"}, - {file = "regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4"}, - {file = "regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d"}, - {file = "regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff"}, - {file = "regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a"}, - {file = "regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519"}, -] - -[[package]] -name = "requests" -version = "2.32.4" -requires_python = ">=3.8" -summary = "Python HTTP for Humans." -groups = ["default"] -dependencies = [ - "certifi>=2017.4.17", - "charset-normalizer<4,>=2", - "idna<4,>=2.5", - "urllib3<3,>=1.21.1", -] -files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, -] - -[[package]] -name = "rich" -version = "14.0.0" -requires_python = ">=3.8.0" -summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -groups = ["default"] -dependencies = [ - "markdown-it-py>=2.2.0", - "pygments<3.0.0,>=2.13.0", - "typing-extensions<5.0,>=4.0.0; python_version < \"3.11\"", -] -files = [ - {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, - {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, -] - -[[package]] -name = "rpds-py" -version = "0.25.1" -requires_python = ">=3.9" -summary = "Python bindings to Rust's persistent data structures (rpds)" -groups = ["default"] -files = [ - {file = "rpds_py-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5ffe453cde61f73fea9430223c81d29e2fbf412a6073951102146c84e19e34c"}, - {file = "rpds_py-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:115874ae5e2fdcfc16b2aedc95b5eef4aebe91b28e7e21951eda8a5dc0d3461b"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a714bf6e5e81b0e570d01f56e0c89c6375101b8463999ead3a93a5d2a4af91fa"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35634369325906bcd01577da4c19e3b9541a15e99f31e91a02d010816b49bfda"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cb2b3ddc16710548801c6fcc0cfcdeeff9dafbc983f77265877793f2660309"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ceca1cf097ed77e1a51f1dbc8d174d10cb5931c188a4505ff9f3e119dfe519b"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2cd1a4b0c2b8c5e31ffff50d09f39906fe351389ba143c195566056c13a7ea"}, - {file = "rpds_py-0.25.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de336a4b164c9188cb23f3703adb74a7623ab32d20090d0e9bf499a2203ad65"}, - {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9fca84a15333e925dd59ce01da0ffe2ffe0d6e5d29a9eeba2148916d1824948c"}, - {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88ec04afe0c59fa64e2f6ea0dd9657e04fc83e38de90f6de201954b4d4eb59bd"}, - {file = "rpds_py-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8bd2f19e312ce3e1d2c635618e8a8d8132892bb746a7cf74780a489f0f6cdcb"}, - {file = "rpds_py-0.25.1-cp312-cp312-win32.whl", hash = "sha256:e5e2f7280d8d0d3ef06f3ec1b4fd598d386cc6f0721e54f09109a8132182fbfe"}, - {file = "rpds_py-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:db58483f71c5db67d643857404da360dce3573031586034b7d59f245144cc192"}, - {file = "rpds_py-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:6d50841c425d16faf3206ddbba44c21aa3310a0cebc3c1cdfc3e3f4f9f6f5728"}, - {file = "rpds_py-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:659d87430a8c8c704d52d094f5ba6fa72ef13b4d385b7e542a08fc240cb4a559"}, - {file = "rpds_py-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68f6f060f0bbdfb0245267da014d3a6da9be127fe3e8cc4a68c6f833f8a23bb1"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083a9513a33e0b92cf6e7a6366036c6bb43ea595332c1ab5c8ae329e4bcc0a9c"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:816568614ecb22b18a010c7a12559c19f6fe993526af88e95a76d5a60b8b75fb"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c6564c0947a7f52e4792983f8e6cf9bac140438ebf81f527a21d944f2fd0a40"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c4a128527fe415d73cf1f70a9a688d06130d5810be69f3b553bf7b45e8acf79"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325"}, - {file = "rpds_py-0.25.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d74ec9bc0e2feb81d3f16946b005748119c0f52a153f6db6a29e8cd68636f295"}, - {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3af5b4cc10fa41e5bc64e5c198a1b2d2864337f8fcbb9a67e747e34002ce812b"}, - {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79dc317a5f1c51fd9c6a0c4f48209c6b8526d0524a6904fc1076476e79b00f98"}, - {file = "rpds_py-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1521031351865e0181bc585147624d66b3b00a84109b57fcb7a779c3ec3772cd"}, - {file = "rpds_py-0.25.1-cp313-cp313-win32.whl", hash = "sha256:5d473be2b13600b93a5675d78f59e63b51b1ba2d0476893415dfbb5477e65b31"}, - {file = "rpds_py-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7b74e92a3b212390bdce1d93da9f6488c3878c1d434c5e751cbc202c5e09500"}, - {file = "rpds_py-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:dd326a81afe332ede08eb39ab75b301d5676802cdffd3a8f287a5f0b694dc3f5"}, - {file = "rpds_py-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a58d1ed49a94d4183483a3ce0af22f20318d4a1434acee255d683ad90bf78129"}, - {file = "rpds_py-0.25.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f251bf23deb8332823aef1da169d5d89fa84c89f67bdfb566c49dea1fccfd50d"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd586bfa270c1103ece2109314dd423df1fa3d9719928b5d09e4840cec0d72"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d273f136e912aa101a9274c3145dcbddbe4bac560e77e6d5b3c9f6e0ed06d34"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666fa7b1bd0a3810a7f18f6d3a25ccd8866291fbbc3c9b912b917a6715874bb9"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:921954d7fbf3fccc7de8f717799304b14b6d9a45bbeec5a8d7408ccbf531faf5"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d86373ff19ca0441ebeb696ef64cb58b8b5cbacffcda5a0ec2f3911732a194"}, - {file = "rpds_py-0.25.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8980cde3bb8575e7c956a530f2c217c1d6aac453474bf3ea0f9c89868b531b6"}, - {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8eb8c84ecea987a2523e057c0d950bcb3f789696c0499290b8d7b3107a719d78"}, - {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e43a005671a9ed5a650f3bc39e4dbccd6d4326b24fb5ea8be5f3a43a6f576c72"}, - {file = "rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58f77c60956501a4a627749a6dcb78dac522f249dd96b5c9f1c6af29bfacfb66"}, - {file = "rpds_py-0.25.1-cp313-cp313t-win32.whl", hash = "sha256:2cb9e5b5e26fc02c8a4345048cd9998c2aca7c2712bd1b36da0c72ee969a3523"}, - {file = "rpds_py-0.25.1-cp313-cp313t-win_amd64.whl", hash = "sha256:401ca1c4a20cc0510d3435d89c069fe0a9ae2ee6495135ac46bdd49ec0495763"}, - {file = "rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3"}, -] - -[[package]] -name = "rsa" -version = "4.9.1" -requires_python = "<4,>=3.6" -summary = "Pure-Python RSA implementation" -groups = ["default"] -dependencies = [ - "pyasn1>=0.1.3", -] -files = [ - {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, - {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, -] - -[[package]] -name = "ruff" -version = "0.12.1" -requires_python = ">=3.7" -summary = "An extremely fast Python linter and code formatter, written in Rust." -groups = ["dev"] -files = [ - {file = "ruff-0.12.1-py3-none-linux_armv6l.whl", hash = "sha256:6013a46d865111e2edb71ad692fbb8262e6c172587a57c0669332a449384a36b"}, - {file = "ruff-0.12.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b3f75a19e03a4b0757d1412edb7f27cffb0c700365e9d6b60bc1b68d35bc89e0"}, - {file = "ruff-0.12.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a256522893cb7e92bb1e1153283927f842dea2e48619c803243dccc8437b8be"}, - {file = "ruff-0.12.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:069052605fe74c765a5b4272eb89880e0ff7a31e6c0dbf8767203c1fbd31c7ff"}, - {file = "ruff-0.12.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a684f125a4fec2d5a6501a466be3841113ba6847827be4573fddf8308b83477d"}, - {file = "ruff-0.12.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdecdef753bf1e95797593007569d8e1697a54fca843d78f6862f7dc279e23bd"}, - {file = "ruff-0.12.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70d52a058c0e7b88b602f575d23596e89bd7d8196437a4148381a3f73fcd5010"}, - {file = "ruff-0.12.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84d0a69d1e8d716dfeab22d8d5e7c786b73f2106429a933cee51d7b09f861d4e"}, - {file = "ruff-0.12.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cc32e863adcf9e71690248607ccdf25252eeeab5193768e6873b901fd441fed"}, - {file = "ruff-0.12.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd49a4619f90d5afc65cf42e07b6ae98bb454fd5029d03b306bd9e2273d44cc"}, - {file = "ruff-0.12.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ed5af6aaaea20710e77698e2055b9ff9b3494891e1b24d26c07055459bb717e9"}, - {file = "ruff-0.12.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:801d626de15e6bf988fbe7ce59b303a914ff9c616d5866f8c79eb5012720ae13"}, - {file = "ruff-0.12.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2be9d32a147f98a1972c1e4df9a6956d612ca5f5578536814372113d09a27a6c"}, - {file = "ruff-0.12.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:49b7ce354eed2a322fbaea80168c902de9504e6e174fd501e9447cad0232f9e6"}, - {file = "ruff-0.12.1-py3-none-win32.whl", hash = "sha256:d973fa626d4c8267848755bd0414211a456e99e125dcab147f24daa9e991a245"}, - {file = "ruff-0.12.1-py3-none-win_amd64.whl", hash = "sha256:9e1123b1c033f77bd2590e4c1fe7e8ea72ef990a85d2484351d408224d603013"}, - {file = "ruff-0.12.1-py3-none-win_arm64.whl", hash = "sha256:78ad09a022c64c13cc6077707f036bab0fac8cd7088772dcd1e5be21c5002efc"}, - {file = "ruff-0.12.1.tar.gz", hash = "sha256:806bbc17f1104fd57451a98a58df35388ee3ab422e029e8f5cf30aa4af2c138c"}, -] - -[[package]] -name = "six" -version = "1.17.0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Python 2 and 3 compatibility utilities" -groups = ["default", "dev"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -requires_python = ">=3.7" -summary = "Sniff out which async library your code is running under" -groups = ["default"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "snowballstemmer" -version = "3.0.1" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*" -summary = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." -groups = ["dev"] -files = [ - {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, - {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, -] - -[[package]] -name = "starlette" -version = "0.46.2" -requires_python = ">=3.9" -summary = "The little ASGI library that shines." -groups = ["default"] -dependencies = [ - "anyio<5,>=3.6.2", - "typing-extensions>=3.10.0; python_version < \"3.10\"", -] -files = [ - {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, - {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, -] - -[[package]] -name = "termcolor" -version = "3.1.0" -requires_python = ">=3.9" -summary = "ANSI color formatting for output in terminal" -groups = ["default"] -files = [ - {file = "termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa"}, - {file = "termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970"}, -] - -[[package]] -name = "tiktoken" -version = "0.9.0" -requires_python = ">=3.9" -summary = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" -groups = ["default"] -dependencies = [ - "regex>=2022.1.18", - "requests>=2.26.0", -] -files = [ - {file = "tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03"}, - {file = "tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210"}, - {file = "tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794"}, - {file = "tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22"}, - {file = "tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2"}, - {file = "tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16"}, - {file = "tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb"}, - {file = "tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63"}, - {file = "tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01"}, - {file = "tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139"}, - {file = "tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a"}, - {file = "tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95"}, - {file = "tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d"}, -] - -[[package]] -name = "tomlkit" -version = "0.13.3" -requires_python = ">=3.8" -summary = "Style preserving TOML library" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, - {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -requires_python = ">=3.7" -summary = "Fast, Extensible Progress Meter" -groups = ["default"] -dependencies = [ - "colorama; platform_system == \"Windows\"", -] -files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, -] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20250516" -requires_python = ">=3.9" -summary = "Typing stubs for PyYAML" -groups = ["dev"] -files = [ - {file = "types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530"}, - {file = "types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba"}, -] - -[[package]] -name = "typing-extensions" -version = "4.14.0" -requires_python = ">=3.9" -summary = "Backported and Experimental Type Hints for Python 3.9+" -groups = ["default", "dev"] -files = [ - {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, - {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.1" -requires_python = ">=3.9" -summary = "Runtime typing introspection tools" -groups = ["default"] -dependencies = [ - "typing-extensions>=4.12.0", -] -files = [ - {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, - {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, -] - -[[package]] -name = "tzdata" -version = "2025.2" -requires_python = ">=2" -summary = "Provider of IANA time zone data" -groups = ["default"] -files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -requires_python = ">=3.9" -summary = "HTTP library with thread-safe connection pooling, file post, and more." -groups = ["default"] -files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, -] - -[[package]] -name = "uvicorn" -version = "0.35.0" -requires_python = ">=3.9" -summary = "The lightning-fast ASGI server." -groups = ["default"] -dependencies = [ - "click>=7.0", - "h11>=0.8", - "typing-extensions>=4.0; python_version < \"3.11\"", -] -files = [ - {file = "uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a"}, - {file = "uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01"}, -] - -[[package]] -name = "wcwidth" -version = "0.2.13" -summary = "Measures the displayed width of unicode strings in a terminal" -groups = ["default"] -dependencies = [ - "backports-functools-lru-cache>=1.2.1; python_version < \"3.2\"", -] -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[[package]] -name = "yarl" -version = "1.20.1" -requires_python = ">=3.9" -summary = "Yet another URL library" -groups = ["default"] -dependencies = [ - "idna>=2.0", - "multidict>=4.0", - "propcache>=0.2.1", -] -files = [ - {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9"}, - {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a"}, - {file = "yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004"}, - {file = "yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5"}, - {file = "yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698"}, - {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a"}, - {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3"}, - {file = "yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1"}, - {file = "yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7"}, - {file = "yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c"}, - {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d"}, - {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf"}, - {file = "yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e"}, - {file = "yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d"}, - {file = "yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f"}, - {file = "yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77"}, - {file = "yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac"}, -] - -[[package]] -name = "zipp" -version = "3.23.0" -requires_python = ">=3.9" -summary = "Backport of pathlib-compatible object wrapper for zip files" -groups = ["default"] -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] diff --git a/pyproject.toml b/pyproject.toml index e4dd9cfb3..3bfd84d11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,10 +9,21 @@ license = {file = "LICENSE"} dependencies = [ "fastapi>=0.115.6", "uvicorn>=0.34.3", + "kubernetes>=30.1.0", "llama-stack>=0.2.13", "rich>=14.0.0", + "cachetools>=6.1.0", ] +[tool.pyright] +exclude = [ + # TODO(lucasagomes): This module was copied from road-core + # service/ols/src/auth/k8s.py and currently has 58 Pyright issues. It + # might need to be rewritten down the line. + "src/auth/k8s.py", +] +extraPaths = ["./src"] + [tool.pdm] distribution = true @@ -36,10 +47,15 @@ dev = [ "pydocstyle>=6.3.0", "mypy>=1.16.0", "types-PyYAML>=6.0.2", + "types-requests>=2.28.0", "ruff>=0.11.13", "aiosqlite", "behave>=1.2.6", ] +build = [ + "build>=1.2.2.post1", + "twine>=5.1.1", +] [tool.pytest.ini_options] asyncio_mode = "auto" diff --git a/src/app/endpoints/authorized.py b/src/app/endpoints/authorized.py new file mode 100644 index 000000000..c434ed2f5 --- /dev/null +++ b/src/app/endpoints/authorized.py @@ -0,0 +1,38 @@ +"""Handler for REST API call to authorized endpoint.""" + +import asyncio +import logging +from typing import Any + +from fastapi import APIRouter, Request + +from auth import get_auth_dependency +from models.responses import AuthorizedResponse, UnauthorizedResponse, ForbiddenResponse + +logger = logging.getLogger(__name__) +router = APIRouter(tags=["authorized"]) +auth_dependency = get_auth_dependency() + + +authorized_responses: dict[int | str, dict[str, Any]] = { + 200: { + "description": "The user is logged-in and authorized to access OLS", + "model": AuthorizedResponse, + }, + 400: { + "description": "Missing or invalid credentials provided by client", + "model": UnauthorizedResponse, + }, + 403: { + "description": "User is not authorized", + "model": ForbiddenResponse, + }, +} + + +@router.post("/authorized", responses=authorized_responses) +def authorized_endpoint_handler(_request: Request) -> AuthorizedResponse: + """Handle request to the /authorized endpoint.""" + # Ignore the user token, we should not return it in the response + user_id, user_name, _ = asyncio.run(auth_dependency(_request)) + return AuthorizedResponse(user_id=user_id, username=user_name) diff --git a/src/app/endpoints/config.py b/src/app/endpoints/config.py index 07e79da9c..8cedc74d2 100644 --- a/src/app/endpoints/config.py +++ b/src/app/endpoints/config.py @@ -1,4 +1,4 @@ -"""Handler for REST API call to configuration.""" +"""Handler for REST API call to retrieve service configuration.""" import logging from typing import Any @@ -7,6 +7,7 @@ from models.config import Configuration from configuration import configuration +from utils.endpoints import check_configuration_loaded logger = logging.getLogger(__name__) router = APIRouter(tags=["config"]) @@ -46,11 +47,18 @@ {"name": "server3", "provider_id": "provider3", "url": "http://url.com:3"}, ], }, - 503: {"description": "Configuration can not be loaded"}, + 503: { + "detail": { + "response": "Configuration is no loaded", + } + }, } @router.get("/config", responses=get_config_responses) def config_endpoint_handler(_request: Request) -> Configuration: """Handle requests to the /config endpoint.""" + # ensure that configuration is loaded + check_configuration_loaded(configuration) + return configuration.configuration diff --git a/src/app/endpoints/feedback.py b/src/app/endpoints/feedback.py index 47503682c..66ecefad5 100644 --- a/src/app/endpoints/feedback.py +++ b/src/app/endpoints/feedback.py @@ -8,19 +8,33 @@ from fastapi import APIRouter, Request, HTTPException, Depends, status +from auth import get_auth_dependency from configuration import configuration -from models.responses import FeedbackResponse, StatusResponse +from models.responses import ( + FeedbackResponse, + StatusResponse, + UnauthorizedResponse, + ForbiddenResponse, +) from models.requests import FeedbackRequest from utils.suid import get_suid -from utils.auth import auth_dependency from utils.common import retrieve_user_id logger = logging.getLogger(__name__) router = APIRouter(prefix="/feedback", tags=["feedback"]) +auth_dependency = get_auth_dependency() # Response for the feedback endpoint feedback_response: dict[int | str, dict[str, Any]] = { 200: {"response": "Feedback received and stored"}, + 400: { + "description": "Missing or invalid credentials provided by client", + "model": UnauthorizedResponse, + }, + 403: { + "description": "User is not authorized", + "model": ForbiddenResponse, + }, } diff --git a/src/app/endpoints/health.py b/src/app/endpoints/health.py index afecc6afe..cf720bf6e 100644 --- a/src/app/endpoints/health.py +++ b/src/app/endpoints/health.py @@ -11,8 +11,7 @@ from llama_stack.providers.datatypes import HealthStatus from fastapi import APIRouter, status, Response -from client import get_llama_stack_client -from configuration import configuration +from client import LlamaStackClientHolder from models.responses import ( LivenessResponse, ReadinessResponse, @@ -30,9 +29,7 @@ def get_providers_health_statuses() -> list[ProviderHealthStatus]: List of provider health statuses. """ try: - llama_stack_config = configuration.llama_stack_configuration - - client = get_llama_stack_client(llama_stack_config) + client = LlamaStackClientHolder().get_client() providers = client.providers.list() logger.debug("Found %d providers", len(providers)) diff --git a/src/app/endpoints/models.py b/src/app/endpoints/models.py index 6f7a1934c..e2083ca4c 100644 --- a/src/app/endpoints/models.py +++ b/src/app/endpoints/models.py @@ -3,11 +3,13 @@ import logging from typing import Any -from fastapi import APIRouter, Request +from llama_stack_client import APIConnectionError +from fastapi import APIRouter, HTTPException, Request, status -from client import get_llama_stack_client +from client import LlamaStackClientHolder from configuration import configuration from models.responses import ModelsResponse +from utils.endpoints import check_configuration_loaded logger = logging.getLogger(__name__) router = APIRouter(tags=["models"]) @@ -36,16 +38,43 @@ }, ] }, + 503: {"description": "Connection to Llama Stack is broken"}, } @router.get("/models", responses=models_responses) def models_endpoint_handler(_request: Request) -> ModelsResponse: """Handle requests to the /models endpoint.""" - llama_stack_config = configuration.llama_stack_configuration - logger.info("LLama stack config: %s", llama_stack_config) + check_configuration_loaded(configuration) - client = get_llama_stack_client(llama_stack_config) - models = client.models.list() - m = [dict(m) for m in models] - return ModelsResponse(models=m) + llama_stack_configuration = configuration.llama_stack_configuration + logger.info("LLama stack config: %s", llama_stack_configuration) + + try: + # try to get Llama Stack client + client = LlamaStackClientHolder().get_client() + # retrieve models + models = client.models.list() + m = [dict(m) for m in models] + return ModelsResponse(models=m) + + # connection to Llama Stack server + except APIConnectionError as e: + logger.error("Unable to connect to Llama Stack: %s", e) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "response": "Unable to connect to Llama Stack", + "cause": str(e), + }, + ) from e + # any other exception that can occur during model listing + except Exception as e: + logger.error("Unable to retrieve list of models: %s", e) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "response": "Unable to retrieve list of models", + "cause": str(e), + }, + ) from e diff --git a/src/app/endpoints/query.py b/src/app/endpoints/query.py index 0dc5b3cdc..3ebe05287 100644 --- a/src/app/endpoints/query.py +++ b/src/app/endpoints/query.py @@ -6,8 +6,11 @@ import os from pathlib import Path from typing import Any -from llama_stack_client.lib.agents.agent import Agent +from cachetools import TTLCache # type: ignore + +from llama_stack_client.lib.agents.agent import Agent +from llama_stack_client import APIConnectionError from llama_stack_client import LlamaStackClient # type: ignore from llama_stack_client.types import UserMessage # type: ignore from llama_stack_client.types.agents.turn_create_params import ( @@ -18,24 +21,44 @@ from fastapi import APIRouter, HTTPException, status, Depends -from client import get_llama_stack_client +from client import LlamaStackClientHolder from configuration import configuration -from models.responses import QueryResponse +from models.responses import QueryResponse, UnauthorizedResponse, ForbiddenResponse from models.requests import QueryRequest, Attachment import constants -from utils.auth import auth_dependency +from auth import get_auth_dependency from utils.common import retrieve_user_id +from utils.endpoints import check_configuration_loaded, get_system_prompt +from utils.mcp_headers import mcp_headers_dependency, handle_mcp_headers_with_toolgroups from utils.suid import get_suid +from utils.types import GraniteToolParser logger = logging.getLogger("app.endpoints.handlers") router = APIRouter(tags=["query"]) +auth_dependency = get_auth_dependency() +# Global agent registry to persist agents across requests +_agent_cache: TTLCache[str, Agent] = TTLCache(maxsize=1000, ttl=3600) query_response: dict[int | str, dict[str, Any]] = { 200: { "conversation_id": "123e4567-e89b-12d3-a456-426614174000", "response": "LLM ansert", }, + 400: { + "description": "Missing or invalid credentials provided by client", + "model": UnauthorizedResponse, + }, + 403: { + "description": "User is not authorized", + "model": ForbiddenResponse, + }, + 503: { + "detail": { + "response": "Unable to connect to Llama Stack", + "cause": "Connection error.", + } + }, } @@ -48,47 +71,88 @@ def is_transcripts_enabled() -> bool: return not configuration.user_data_collection_configuration.transcripts_disabled -def retrieve_conversation_id(query_request: QueryRequest) -> str: - """Retrieve conversation ID based on existing ID or on newly generated one.""" - conversation_id = query_request.conversation_id - - # Generate a new conversation ID if not provided - if not conversation_id: - conversation_id = get_suid() - logger.info("Generated new conversation ID: %s", conversation_id) - - return conversation_id +def get_agent( + client: LlamaStackClient, + model_id: str, + system_prompt: str, + available_shields: list[str], + conversation_id: str | None, +) -> tuple[Agent, str]: + """Get existing agent or create a new one with session persistence.""" + if conversation_id is not None: + agent = _agent_cache.get(conversation_id) + if agent: + logger.debug("Reusing existing agent with key: %s", conversation_id) + return agent, conversation_id + + logger.debug("Creating new agent") + # TODO(lucasagomes): move to ReActAgent + agent = Agent( + client, + model=model_id, + instructions=system_prompt, + input_shields=available_shields if available_shields else [], + tool_parser=GraniteToolParser.get_parser(model_id), + enable_session_persistence=True, + ) + conversation_id = agent.create_session(get_suid()) + _agent_cache[conversation_id] = agent + return agent, conversation_id @router.post("/query", responses=query_response) def query_endpoint_handler( query_request: QueryRequest, auth: Any = Depends(auth_dependency), + mcp_headers: dict[str, dict[str, str]] = Depends(mcp_headers_dependency), ) -> QueryResponse: """Handle request to the /query endpoint.""" + check_configuration_loaded(configuration) + llama_stack_config = configuration.llama_stack_configuration logger.info("LLama stack config: %s", llama_stack_config) - client = get_llama_stack_client(llama_stack_config) - model_id = select_model_id(client.models.list(), query_request) - conversation_id = retrieve_conversation_id(query_request) - response = retrieve_response(client, model_id, query_request, auth) - if not is_transcripts_enabled(): - logger.debug("Transcript collection is disabled in the configuration") - else: - store_transcript( - user_id=retrieve_user_id(auth), - conversation_id=conversation_id, - query_is_valid=True, # TODO(lucasagomes): implement as part of query validation - query=query_request.query, - query_request=query_request, - response=response, - rag_chunks=[], # TODO(lucasagomes): implement rag_chunks - truncated=False, # TODO(lucasagomes): implement truncation as part of quota work - attachments=query_request.attachments or [], + _user_id, _user_name, token = auth + + try: + # try to get Llama Stack client + client = LlamaStackClientHolder().get_client() + model_id = select_model_id(client.models.list(), query_request) + response, conversation_id = retrieve_response( + client, + model_id, + query_request, + token, + mcp_headers=mcp_headers, ) - return QueryResponse(conversation_id=conversation_id, response=response) + if not is_transcripts_enabled(): + logger.debug("Transcript collection is disabled in the configuration") + else: + store_transcript( + user_id=retrieve_user_id(auth), + conversation_id=conversation_id, + query_is_valid=True, # TODO(lucasagomes): implement as part of query validation + query=query_request.query, + query_request=query_request, + response=response, + rag_chunks=[], # TODO(lucasagomes): implement rag_chunks + truncated=False, # TODO(lucasagomes): implement truncation as part of quota work + attachments=query_request.attachments or [], + ) + + return QueryResponse(conversation_id=conversation_id, response=response) + + # connection to Llama Stack server + except APIConnectionError as e: + logger.error("Unable to connect to Llama Stack: %s", e) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "response": "Unable to connect to Llama Stack", + "cause": str(e), + }, + ) from e def select_model_id(models: ModelListResponse, query_request: QueryRequest) -> str: @@ -140,7 +204,8 @@ def retrieve_response( model_id: str, query_request: QueryRequest, token: str, -) -> str: + mcp_headers: dict[str, dict[str, str]] | None = None, +) -> tuple[str, str]: """Retrieve response from LLMs and agents.""" available_shields = [shield.identifier for shield in client.shields.list()] if not available_shields: @@ -149,11 +214,7 @@ def retrieve_response( logger.info("Available shields found: %s", available_shields) # use system prompt from request or default one - system_prompt = ( - query_request.system_prompt - if query_request.system_prompt - else constants.DEFAULT_SYSTEM_PROMPT - ) + system_prompt = get_system_prompt(query_request, configuration) logger.debug("Using system prompt: %s", system_prompt) # TODO(lucasagomes): redact attachments content before sending to LLM @@ -161,40 +222,45 @@ def retrieve_response( if query_request.attachments: validate_attachments_metadata(query_request.attachments) - # Build mcp_headers config dynamically for all MCP servers - # this will allow the agent to pass the user token to the MCP servers - mcp_headers = {} - if token: + agent, conversation_id = get_agent( + client, + model_id, + system_prompt, + available_shields, + query_request.conversation_id, + ) + + # preserve compatibility when mcp_headers is not provided + if mcp_headers is None: + mcp_headers = {} + mcp_headers = handle_mcp_headers_with_toolgroups(mcp_headers, configuration) + if not mcp_headers and token: for mcp_server in configuration.mcp_servers: mcp_headers[mcp_server.url] = { "Authorization": f"Bearer {token}", } - # TODO(lucasagomes): move to ReActAgent - agent = Agent( - client, - model=model_id, - instructions=system_prompt, - input_shields=available_shields if available_shields else [], - tools=[mcp.name for mcp in configuration.mcp_servers], - extra_headers={ - "X-LlamaStack-Provider-Data": json.dumps( - { - "mcp_headers": mcp_headers, - } - ), - }, - ) - session_id = agent.create_session("chat_session") - logger.debug("Session ID: %s", session_id) + + agent.extra_headers = { + "X-LlamaStack-Provider-Data": json.dumps( + { + "mcp_headers": mcp_headers, + } + ), + } + vector_db_ids = [vector_db.identifier for vector_db in client.vector_dbs.list()] + toolgroups = (get_rag_toolgroups(vector_db_ids) or []) + [ + mcp_server.name for mcp_server in configuration.mcp_servers + ] response = agent.create_turn( messages=[UserMessage(role="user", content=query_request.query)], - session_id=session_id, + session_id=conversation_id, documents=query_request.get_documents(), stream=False, - toolgroups=get_rag_toolgroups(vector_db_ids), + toolgroups=toolgroups or None, ) - return str(response.output_message.content) # type: ignore[union-attr] + + return str(response.output_message.content), conversation_id # type: ignore[union-attr] def validate_attachments_metadata(attachments: list[Attachment]) -> None: diff --git a/src/app/endpoints/streaming_query.py b/src/app/endpoints/streaming_query.py index fa8d619bd..2a3353c47 100644 --- a/src/app/endpoints/streaming_query.py +++ b/src/app/endpoints/streaming_query.py @@ -2,27 +2,33 @@ import json import logging +import re from typing import Any, AsyncIterator +from cachetools import TTLCache # type: ignore + +from llama_stack_client import APIConnectionError from llama_stack_client.lib.agents.agent import AsyncAgent # type: ignore from llama_stack_client import AsyncLlamaStackClient # type: ignore +from llama_stack_client.types.shared.interleaved_content_item import TextContentItem from llama_stack_client.types import UserMessage # type: ignore -from fastapi import APIRouter, Request, Depends +from fastapi import APIRouter, HTTPException, Request, Depends, status from fastapi.responses import StreamingResponse -from client import get_async_llama_stack_client +from auth import get_auth_dependency +from client import AsyncLlamaStackClientHolder from configuration import configuration from models.requests import QueryRequest -import constants -from utils.auth import auth_dependency +from utils.endpoints import check_configuration_loaded, get_system_prompt from utils.common import retrieve_user_id - +from utils.mcp_headers import mcp_headers_dependency, handle_mcp_headers_with_toolgroups +from utils.suid import get_suid +from utils.types import GraniteToolParser from app.endpoints.query import ( get_rag_toolgroups, is_transcripts_enabled, - retrieve_conversation_id, store_transcript, select_model_id, validate_attachments_metadata, @@ -30,6 +36,41 @@ logger = logging.getLogger("app.endpoints.handlers") router = APIRouter(tags=["streaming_query"]) +auth_dependency = get_auth_dependency() + +# Global agent registry to persist agents across requests +_agent_cache: TTLCache[str, AsyncAgent] = TTLCache(maxsize=1000, ttl=3600) + + +async def get_agent( + client: AsyncLlamaStackClient, + model_id: str, + system_prompt: str, + available_shields: list[str], + conversation_id: str | None, +) -> tuple[AsyncAgent, str]: + """Get existing agent or create a new one with session persistence.""" + if conversation_id is not None: + agent = _agent_cache.get(conversation_id) + if agent: + logger.debug("Reusing existing agent with key: %s", conversation_id) + return agent, conversation_id + + logger.debug("Creating new agent") + agent = AsyncAgent( + client, # type: ignore[arg-type] + model=model_id, + instructions=system_prompt, + input_shields=available_shields if available_shields else [], + tool_parser=GraniteToolParser.get_parser(model_id), + enable_session_persistence=True, + ) + conversation_id = await agent.create_session(get_suid()) + _agent_cache[conversation_id] = agent + return agent, conversation_id + + +METADATA_PATTERN = re.compile(r"\nMetadata: (\{.+})\n") def format_stream_data(d: dict) -> str: @@ -54,13 +95,22 @@ def stream_start_event(conversation_id: str) -> str: ) -def stream_end_event() -> str: +def stream_end_event(metadata_map: dict) -> str: """Yield the end of the data stream.""" return format_stream_data( { "event": "end", "data": { - "referenced_documents": [], # TODO(jboos): implement referenced documents + "referenced_documents": [ + { + "doc_url": v["docs_url"], + "doc_title": v["title"], + } + for v in filter( + lambda v: ("docs_url" in v) and ("title" in v), + metadata_map.values(), + ) + ], "truncated": None, # TODO(jboos): implement truncated "input_tokens": 0, # TODO(jboos): implement input tokens "output_tokens": 0, # TODO(jboos): implement output tokens @@ -70,7 +120,7 @@ def stream_end_event() -> str: ) -def stream_build_event(chunk: Any, chunk_id: int) -> str | None: +def stream_build_event(chunk: Any, chunk_id: int, metadata_map: dict) -> str | None: """Build a streaming event from a chunk response. This function processes chunks from the LLama Stack streaming response and formats @@ -88,6 +138,7 @@ def stream_build_event(chunk: Any, chunk_id: int) -> str | None: str | None: A formatted SSE data string with event information, or None if the chunk doesn't contain processable event data """ + # pylint: disable=R1702 if hasattr(chunk.event, "payload"): if chunk.event.payload.event_type == "step_progress": if hasattr(chunk.event.payload.delta, "text"): @@ -102,22 +153,33 @@ def stream_build_event(chunk: Any, chunk_id: int) -> str | None: }, } ) - if chunk.event.payload.event_type == "step_complete": - if chunk.event.payload.step_details.step_type == "tool_execution": - if chunk.event.payload.step_details.tool_calls: - tool_name = str( - chunk.event.payload.step_details.tool_calls[0].tool_name - ) - return format_stream_data( - { - "event": "token", - "data": { - "id": chunk_id, - "role": chunk.event.payload.step_type, - "token": tool_name, - }, - } - ) + if ( + chunk.event.payload.event_type == "step_complete" + and chunk.event.payload.step_details.step_type == "tool_execution" + ): + for r in chunk.event.payload.step_details.tool_responses: + if r.tool_name == "knowledge_search" and r.content: + for text_content_item in r.content: + if isinstance(text_content_item, TextContentItem): + for match in METADATA_PATTERN.findall( + text_content_item.text + ): + meta = json.loads(match.replace("'", '"')) + metadata_map[meta["document_id"]] = meta + if chunk.event.payload.step_details.tool_calls: + tool_name = str( + chunk.event.payload.step_details.tool_calls[0].tool_name + ) + return format_stream_data( + { + "event": "token", + "data": { + "id": chunk_id, + "role": chunk.event.payload.step_type, + "token": tool_name, + }, + } + ) return None @@ -126,54 +188,83 @@ async def streaming_query_endpoint_handler( _request: Request, query_request: QueryRequest, auth: Any = Depends(auth_dependency), + mcp_headers: dict[str, dict[str, str]] = Depends(mcp_headers_dependency), ) -> StreamingResponse: """Handle request to the /streaming_query endpoint.""" + check_configuration_loaded(configuration) + llama_stack_config = configuration.llama_stack_configuration logger.info("LLama stack config: %s", llama_stack_config) - client = await get_async_llama_stack_client(llama_stack_config) - model_id = select_model_id(await client.models.list(), query_request) - conversation_id = retrieve_conversation_id(query_request) - response = await retrieve_response(client, model_id, query_request) - - async def response_generator(turn_response: Any) -> AsyncIterator[str]: - """Generate SSE formatted streaming response.""" - chunk_id = 0 - complete_response = "" - - # Send start event - yield stream_start_event(conversation_id) - - async for chunk in turn_response: - if event := stream_build_event(chunk, chunk_id): - complete_response += json.loads(event.replace("data: ", ""))["data"][ - "token" - ] - chunk_id += 1 - yield event - - yield stream_end_event() - - if not is_transcripts_enabled(): - logger.debug("Transcript collection is disabled in the configuration") - else: - store_transcript( - user_id=retrieve_user_id(auth), - conversation_id=conversation_id, - query_is_valid=True, # TODO(lucasagomes): implement as part of query validation - query=query_request.query, - query_request=query_request, - response=complete_response, - rag_chunks=[], # TODO(lucasagomes): implement rag_chunks - truncated=False, # TODO(lucasagomes): implement truncation as part of quota work - attachments=query_request.attachments or [], - ) - - return StreamingResponse(response_generator(response)) + + _user_id, _user_name, token = auth + + try: + # try to get Llama Stack client + client = AsyncLlamaStackClientHolder().get_client() + model_id = select_model_id(await client.models.list(), query_request) + response, conversation_id = await retrieve_response( + client, + model_id, + query_request, + token, + mcp_headers=mcp_headers, + ) + metadata_map: dict[str, dict[str, Any]] = {} + + async def response_generator(turn_response: Any) -> AsyncIterator[str]: + """Generate SSE formatted streaming response.""" + chunk_id = 0 + complete_response = "" + + # Send start event + yield stream_start_event(conversation_id) + + async for chunk in turn_response: + if event := stream_build_event(chunk, chunk_id, metadata_map): + complete_response += json.loads(event.replace("data: ", ""))[ + "data" + ]["token"] + chunk_id += 1 + yield event + + yield stream_end_event(metadata_map) + + if not is_transcripts_enabled(): + logger.debug("Transcript collection is disabled in the configuration") + else: + store_transcript( + user_id=retrieve_user_id(auth), + conversation_id=conversation_id, + query_is_valid=True, # TODO(lucasagomes): implement as part of query validation + query=query_request.query, + query_request=query_request, + response=complete_response, + rag_chunks=[], # TODO(lucasagomes): implement rag_chunks + truncated=False, # TODO(lucasagomes): implement truncation as part + # of quota work + attachments=query_request.attachments or [], + ) + + return StreamingResponse(response_generator(response)) + # connection to Llama Stack server + except APIConnectionError as e: + logger.error("Unable to connect to Llama Stack: %s", e) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={ + "response": "Unable to connect to Llama Stack", + "cause": str(e), + }, + ) from e async def retrieve_response( - client: AsyncLlamaStackClient, model_id: str, query_request: QueryRequest -) -> Any: + client: AsyncLlamaStackClient, + model_id: str, + query_request: QueryRequest, + token: str, + mcp_headers: dict[str, dict[str, str]] | None = None, +) -> tuple[Any, str]: """Retrieve response from LLMs and agents.""" available_shields = [shield.identifier for shield in await client.shields.list()] if not available_shields: @@ -182,11 +273,7 @@ async def retrieve_response( logger.info("Available shields found: %s", available_shields) # use system prompt from request or default one - system_prompt = ( - query_request.system_prompt - if query_request.system_prompt - else constants.DEFAULT_SYSTEM_PROMPT - ) + system_prompt = get_system_prompt(query_request, configuration) logger.debug("Using system prompt: %s", system_prompt) # TODO(lucasagomes): redact attachments content before sending to LLM @@ -194,24 +281,47 @@ async def retrieve_response( if query_request.attachments: validate_attachments_metadata(query_request.attachments) - agent = AsyncAgent( - client, # type: ignore[arg-type] - model=model_id, - instructions=system_prompt, - input_shields=available_shields if available_shields else [], - tools=[], + agent, conversation_id = await get_agent( + client, + model_id, + system_prompt, + available_shields, + query_request.conversation_id, ) - session_id = await agent.create_session("chat_session") - logger.debug("Session ID: %s", session_id) + + # preserve compatibility when mcp_headers is not provided + if mcp_headers is None: + mcp_headers = {} + + mcp_headers = handle_mcp_headers_with_toolgroups(mcp_headers, configuration) + + if not mcp_headers and token: + for mcp_server in configuration.mcp_servers: + mcp_headers[mcp_server.url] = { + "Authorization": f"Bearer {token}", + } + + agent.extra_headers = { + "X-LlamaStack-Provider-Data": json.dumps( + { + "mcp_headers": mcp_headers, + } + ), + } + + logger.debug("Session ID: %s", conversation_id) vector_db_ids = [ vector_db.identifier for vector_db in await client.vector_dbs.list() ] + toolgroups = (get_rag_toolgroups(vector_db_ids) or []) + [ + mcp_server.name for mcp_server in configuration.mcp_servers + ] response = await agent.create_turn( messages=[UserMessage(role="user", content=query_request.query)], - session_id=session_id, + session_id=conversation_id, documents=query_request.get_documents(), stream=True, - toolgroups=get_rag_toolgroups(vector_db_ids), + toolgroups=toolgroups or None, ) - return response + return response, conversation_id diff --git a/src/app/main.py b/src/app/main.py index c315136fb..90c094468 100644 --- a/src/app/main.py +++ b/src/app/main.py @@ -1,6 +1,7 @@ """Definition of FastAPI based web service.""" from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware from app import routers import version @@ -25,6 +26,14 @@ }, ) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + logger.info("Including routers") routers.include_routers(app) diff --git a/src/app/routers.py b/src/app/routers.py index bedc5952e..fd17e26c1 100644 --- a/src/app/routers.py +++ b/src/app/routers.py @@ -11,6 +11,7 @@ config, feedback, streaming_query, + authorized, ) @@ -24,7 +25,10 @@ def include_routers(app: FastAPI) -> None: app.include_router(info.router, prefix="/v1") app.include_router(models.router, prefix="/v1") app.include_router(query.router, prefix="/v1") - app.include_router(health.router, prefix="/v1") + app.include_router(streaming_query.router, prefix="/v1") app.include_router(config.router, prefix="/v1") app.include_router(feedback.router, prefix="/v1") - app.include_router(streaming_query.router, prefix="/v1") + + # road-core does not version these endpoints + app.include_router(health.router) + app.include_router(authorized.router) diff --git a/src/auth/__init__.py b/src/auth/__init__.py new file mode 100644 index 000000000..22176b769 --- /dev/null +++ b/src/auth/__init__.py @@ -0,0 +1,38 @@ +"""This package contains authentication code and modules.""" + +import logging + +from auth.interface import AuthInterface +from auth import noop, noop_with_token, k8s +from configuration import configuration +import constants + + +logger = logging.getLogger(__name__) + + +def get_auth_dependency( + virtual_path: str = constants.DEFAULT_VIRTUAL_PATH, +) -> AuthInterface: + """Select the configured authentication dependency interface.""" + module = configuration.authentication_configuration.module # pyright: ignore + + logger.debug( + "Initializing authentication dependency: module='%s', virtual_path='%s'", + module, + virtual_path, + ) + + match module: + case constants.AUTH_MOD_NOOP: + return noop.NoopAuthDependency(virtual_path=virtual_path) + case constants.AUTH_MOD_NOOP_WITH_TOKEN: + return noop_with_token.NoopWithTokenAuthDependency( + virtual_path=virtual_path + ) + case constants.AUTH_MOD_K8S: + return k8s.K8SAuthDependency(virtual_path=virtual_path) + case _: + err_msg = f"Unsupported authentication module '{module}'" + logger.error(err_msg) + raise ValueError(err_msg) diff --git a/src/auth/interface.py b/src/auth/interface.py new file mode 100644 index 000000000..876ac0091 --- /dev/null +++ b/src/auth/interface.py @@ -0,0 +1,13 @@ +"""Abstract base class for authentication methods.""" + +from abc import ABC, abstractmethod + +from fastapi import Request + + +class AuthInterface(ABC): # pylint: disable=too-few-public-methods + """Base class for all authentication method implementations.""" + + @abstractmethod + async def __call__(self, request: Request) -> tuple[str, str, str]: + """Validate FastAPI Requests for authentication and authorization.""" diff --git a/src/auth/k8s.py b/src/auth/k8s.py new file mode 100644 index 000000000..5ff43dded --- /dev/null +++ b/src/auth/k8s.py @@ -0,0 +1,270 @@ +"""Manage authentication flow for FastAPI endpoints with K8S/OCP.""" + +import logging +import os +from pathlib import Path +from typing import Optional, Self +from fastapi import Request, HTTPException + +import kubernetes.client +from kubernetes.client.rest import ApiException +from kubernetes.config import ConfigException + +from configuration import configuration +from auth.utils import extract_user_token +from auth.interface import AuthInterface +from constants import DEFAULT_VIRTUAL_PATH + +logger = logging.getLogger(__name__) + + +CLUSTER_ID_LOCAL = "local" +RUNNING_IN_CLUSTER = ( + "KUBERNETES_SERVICE_HOST" in os.environ and "KUBERNETES_SERVICE_PORT" in os.environ +) + + +class ClusterIDUnavailableError(Exception): + """Cluster ID is not available.""" + + +class K8sClientSingleton: + """Return the Kubernetes client instances. + + Ensures we initialize the k8s client only once per application life cycle. + manage the initialization and config loading. + """ + + _instance = None + _api_client = None + _authn_api: kubernetes.client.AuthenticationV1Api + _authz_api: kubernetes.client.AuthorizationV1Api + _cluster_id = None + + def __new__(cls: type[Self]) -> Self: + """Create a new instance of the singleton, or returns the existing instance. + + This method initializes the Kubernetes API clients the first time it is called. + and ensures that subsequent calls return the same instance. + """ + if cls._instance is None: + cls._instance = super().__new__(cls) + k8s_config = kubernetes.client.Configuration() + + try: + try: + logger.info("loading in-cluster config") + kubernetes.config.load_incluster_config( + client_configuration=k8s_config + ) + except ConfigException as e: + logger.debug("unable to load in-cluster config: %s", e) + try: + logger.info("loading config from kube-config file") + kubernetes.config.load_kube_config( + client_configuration=k8s_config + ) + except ConfigException as ce: + logger.error( + "failed to load kubeconfig, in-cluster config\ + and no override token was provided: %s", + ce, + ) + + k8s_config.host = ( + configuration.authentication_configuration.k8s_cluster_api + or k8s_config.host + ) + k8s_config.verify_ssl = ( + not configuration.authentication_configuration.skip_tls_verification + ) + k8s_config.ssl_ca_cert = ( + configuration.authentication_configuration.k8s_ca_cert_path + if configuration.authentication_configuration.k8s_ca_cert_path + not in {None, Path()} + else k8s_config.ssl_ca_cert + ) + api_client = kubernetes.client.ApiClient(k8s_config) + cls._api_client = api_client + cls._custom_objects_api = kubernetes.client.CustomObjectsApi(api_client) + cls._authn_api = kubernetes.client.AuthenticationV1Api(api_client) + cls._authz_api = kubernetes.client.AuthorizationV1Api(api_client) + except Exception as e: + logger.info("Failed to initialize Kubernetes client: %s", e) + raise + return cls._instance + + @classmethod + def get_authn_api(cls) -> kubernetes.client.AuthenticationV1Api: + """Return the Authentication API client instance. + + Ensures the singleton is initialized before returning the Authentication API client. + """ + if cls._instance is None or cls._authn_api is None: + cls() + return cls._authn_api + + @classmethod + def get_authz_api(cls) -> kubernetes.client.AuthorizationV1Api: + """Return the Authorization API client instance. + + Ensures the singleton is initialized before returning the Authorization API client. + """ + if cls._instance is None or cls._authz_api is None: + cls() + return cls._authz_api + + @classmethod + def get_custom_objects_api(cls) -> kubernetes.client.CustomObjectsApi: + """Return the custom objects API instance. + + Ensures the singleton is initialized before returning the Authorization API client. + """ + if cls._instance is None or cls._custom_objects_api is None: + cls() + return cls._custom_objects_api + + @classmethod + def _get_cluster_id(cls) -> str: + try: + custom_objects_api = cls.get_custom_objects_api() + version_data = custom_objects_api.get_cluster_custom_object( + "config.openshift.io", "v1", "clusterversions", "version" + ) + cluster_id = version_data["spec"]["clusterID"] + cls._cluster_id = cluster_id + return cluster_id + except KeyError as e: + logger.error( + "Failed to get cluster_id from cluster, missing keys in version object" + ) + raise ClusterIDUnavailableError("Failed to get cluster ID") from e + except TypeError as e: + logger.error( + "Failed to get cluster_id, version object is: %s", version_data + ) + raise ClusterIDUnavailableError("Failed to get cluster ID") from e + except ApiException as e: + logger.error("API exception during ClusterInfo: %s", e) + raise ClusterIDUnavailableError("Failed to get cluster ID") from e + except Exception as e: + logger.error("Unexpected error during getting cluster ID: %s", e) + raise ClusterIDUnavailableError("Failed to get cluster ID") from e + + @classmethod + def get_cluster_id(cls) -> str: + """Return the cluster ID.""" + if cls._instance is None: + cls() + if cls._cluster_id is None: + if RUNNING_IN_CLUSTER: + cls._cluster_id = cls._get_cluster_id() + else: + logger.debug("Not running in cluster, setting cluster_id to 'local'") + cls._cluster_id = CLUSTER_ID_LOCAL + return cls._cluster_id + + +def get_user_info(token: str) -> Optional[kubernetes.client.V1TokenReview]: + """Perform a Kubernetes TokenReview to validate a given token. + + Args: + token: The bearer token to be validated. + + Returns: + The user information if the token is valid, None otherwise. + """ + auth_api = K8sClientSingleton.get_authn_api() + token_review = kubernetes.client.V1TokenReview( + spec=kubernetes.client.V1TokenReviewSpec(token=token) + ) + try: + response = auth_api.create_token_review(token_review) + if response.status.authenticated: + return response.status + return None + except ApiException as e: + logger.error("API exception during TokenReview: %s", e) + return None + except Exception as e: + logger.error("Unexpected error during TokenReview - Unauthorized: %s", e) + raise HTTPException( + status_code=500, + detail={"response": "Forbidden: Unable to Review Token", "cause": str(e)}, + ) from e + + +def _extract_bearer_token(header: str) -> str: + """Extract the bearer token from an HTTP authorization header. + + Args: + header: The authorization header containing the token. + + Returns: + The extracted token if present, else an empty string. + """ + try: + scheme, token = header.split(" ", 1) + return token if scheme.lower() == "bearer" else "" + except ValueError: + return "" + + +class K8SAuthDependency(AuthInterface): # pylint: disable=too-few-public-methods + """FastAPI dependency for Kubernetes (k8s) authentication and authorization. + + K8SAuthDependency is an authentication and authorization dependency for FastAPI endpoints, + integrating with Kubernetes RBAC via SubjectAccessReview (SAR). + + This class extracts the user token from the request headers, retrieves user information, + and performs a Kubernetes SAR to determine if the user is authorized. + + Raises: + HTTPException: HTTP 403 if the token is invalid, expired, or the user is not authorized. + + """ + + def __init__(self, virtual_path: str = DEFAULT_VIRTUAL_PATH) -> None: + """Initialize the required allowed paths for authorization checks.""" + self.virtual_path = virtual_path + + async def __call__(self, request: Request) -> tuple[str, str, str]: + """Validate FastAPI Requests for authentication and authorization. + + Args: + request: The FastAPI request object. + + Returns: + The user's UID and username if authentication and authorization succeed + user_id check is skipped with noop auth to allow consumers provide user_id + """ + token = extract_user_token(request.headers) + user_info = get_user_info(token) + if user_info is None: + raise HTTPException( + status_code=403, detail="Forbidden: Invalid or expired token" + ) + if user_info.user.username == "kube:admin": + user_info.user.uid = K8sClientSingleton.get_cluster_id() + authorization_api = K8sClientSingleton.get_authz_api() + + sar = kubernetes.client.V1SubjectAccessReview( + spec=kubernetes.client.V1SubjectAccessReviewSpec( + user=user_info.user.username, + groups=user_info.user.groups, + non_resource_attributes=kubernetes.client.V1NonResourceAttributes( + path=self.virtual_path, verb="get" + ), + ) + ) + try: + response = authorization_api.create_subject_access_review(sar) + if not response.status.allowed: + raise HTTPException( + status_code=403, detail="Forbidden: User does not have access" + ) + except ApiException as e: + logger.error("API exception during SubjectAccessReview: %s", e) + raise HTTPException(status_code=403, detail="Internal server error") from e + + return user_info.user.uid, user_info.user.username, token diff --git a/src/auth/noop.py b/src/auth/noop.py new file mode 100644 index 000000000..6e55fe72c --- /dev/null +++ b/src/auth/noop.py @@ -0,0 +1,42 @@ +"""Manage authentication flow for FastAPI endpoints with no-op auth.""" + +import logging + +from fastapi import Request + +from constants import ( + DEFAULT_USER_NAME, + DEFAULT_USER_UID, + NO_USER_TOKEN, + DEFAULT_VIRTUAL_PATH, +) +from auth.interface import AuthInterface + +logger = logging.getLogger(__name__) + + +class NoopAuthDependency(AuthInterface): # pylint: disable=too-few-public-methods + """No-op AuthDependency class that bypasses authentication and authorization checks.""" + + def __init__(self, virtual_path: str = DEFAULT_VIRTUAL_PATH) -> None: + """Initialize the required allowed paths for authorization checks.""" + self.virtual_path = virtual_path + + async def __call__(self, request: Request) -> tuple[str, str, str]: + """Validate FastAPI Requests for authentication and authorization. + + Args: + request: The FastAPI request object. + + Returns: + The user's UID and username if authentication and authorization succeed + user_id check is skipped with noop auth to allow consumers provide user_id + """ + logger.warning( + "No-op authentication dependency is being used. " + "The service is running in insecure mode intended solely for development purposes" + ) + # try to extract user ID from request + user_id = request.query_params.get("user_id", DEFAULT_USER_UID) + logger.debug("Retrieved user ID: %s", user_id) + return user_id, DEFAULT_USER_NAME, NO_USER_TOKEN diff --git a/src/auth/noop_with_token.py b/src/auth/noop_with_token.py new file mode 100644 index 000000000..c3c7d2906 --- /dev/null +++ b/src/auth/noop_with_token.py @@ -0,0 +1,46 @@ +"""Manage authentication flow for FastAPI endpoints with no-op auth.""" + +import logging + +from fastapi import Request + +from constants import ( + DEFAULT_USER_NAME, + DEFAULT_USER_UID, + DEFAULT_VIRTUAL_PATH, +) +from auth.interface import AuthInterface +from auth.utils import extract_user_token + +logger = logging.getLogger(__name__) + + +class NoopWithTokenAuthDependency( + AuthInterface +): # pylint: disable=too-few-public-methods + """No-op AuthDependency class that bypasses authentication and authorization checks.""" + + def __init__(self, virtual_path: str = DEFAULT_VIRTUAL_PATH) -> None: + """Initialize the required allowed paths for authorization checks.""" + self.virtual_path = virtual_path + + async def __call__(self, request: Request) -> tuple[str, str, str]: + """Validate FastAPI Requests for authentication and authorization. + + Args: + request: The FastAPI request object. + + Returns: + The user's UID and username if authentication and authorization succeed + user_id check is skipped with noop auth to allow consumers provide user_id + """ + logger.warning( + "No-op with token authentication dependency is being used. " + "The service is running in insecure mode intended solely for development purposes" + ) + # try to extract user token from request + user_token = extract_user_token(request.headers) + # try to extract user ID from request + user_id = request.query_params.get("user_id", DEFAULT_USER_UID) + logger.debug("Retrieved user ID: %s", user_id) + return user_id, DEFAULT_USER_NAME, user_token diff --git a/src/auth/utils.py b/src/auth/utils.py new file mode 100644 index 000000000..c92898ac2 --- /dev/null +++ b/src/auth/utils.py @@ -0,0 +1,26 @@ +"""Authentication utility functions.""" + +from fastapi import HTTPException +from starlette.datastructures import Headers + + +def extract_user_token(headers: Headers) -> str: + """Extract the bearer token from an HTTP authorization header. + + Args: + header: The authorization header containing the token. + + Returns: + The extracted token if present, else an empty string. + """ + authorization_header = headers.get("Authorization") + if not authorization_header: + raise HTTPException(status_code=400, detail="No Authorization header found") + + scheme_and_token = authorization_header.strip().split() + if len(scheme_and_token) != 2 or scheme_and_token[0].lower() != "bearer": + raise HTTPException( + status_code=400, detail="No token found in Authorization header" + ) + + return scheme_and_token[1] diff --git a/src/client.py b/src/client.py index 19f05551b..d01b0411e 100644 --- a/src/client.py +++ b/src/client.py @@ -2,55 +2,86 @@ import logging +from typing import Optional + from llama_stack.distribution.library_client import ( AsyncLlamaStackAsLibraryClient, # type: ignore LlamaStackAsLibraryClient, # type: ignore ) from llama_stack_client import AsyncLlamaStackClient, LlamaStackClient # type: ignore from models.config import LLamaStackConfiguration +from utils.types import Singleton + logger = logging.getLogger(__name__) -def get_llama_stack_client( - llama_stack_config: LLamaStackConfiguration, -) -> LlamaStackClient: - """Retrieve Llama stack client according to configuration.""" - if llama_stack_config.use_as_library_client is True: - if llama_stack_config.library_client_config_path is not None: - logger.info("Using Llama stack as library client") - client = LlamaStackAsLibraryClient( - llama_stack_config.library_client_config_path +class LlamaStackClientHolder(metaclass=Singleton): + """Container for an initialised LlamaStackClient.""" + + _lsc: Optional[LlamaStackClient] = None + + def load(self, llama_stack_config: LLamaStackConfiguration) -> None: + """Retrieve Llama stack client according to configuration.""" + if llama_stack_config.use_as_library_client is True: + if llama_stack_config.library_client_config_path is not None: + logger.info("Using Llama stack as library client") + client = LlamaStackAsLibraryClient( + llama_stack_config.library_client_config_path + ) + client.initialize() + self._lsc = client + else: + msg = "Configuration problem: library_client_config_path option is not set" + logger.error(msg) + # tisnik: use custom exception there - with cause etc. + raise ValueError(msg) + + else: + logger.info("Using Llama stack running as a service") + self._lsc = LlamaStackClient( + base_url=llama_stack_config.url, api_key=llama_stack_config.api_key + ) + + def get_client(self) -> LlamaStackClient: + """Return an initialised LlamaStackClient.""" + if not self._lsc: + raise RuntimeError( + "LlamaStackClient has not been initialised. Ensure 'load(..)' has been called." ) - client.initialize() - return client - msg = "Configuration problem: library_client_config_path option is not set" - logger.error(msg) - # tisnik: use custom exception there - with cause etc. - raise Exception(msg) # pylint: disable=broad-exception-raised - logger.info("Using Llama stack running as a service") - return LlamaStackClient( - base_url=llama_stack_config.url, api_key=llama_stack_config.api_key - ) - - -async def get_async_llama_stack_client( - llama_stack_config: LLamaStackConfiguration, -) -> AsyncLlamaStackClient: - """Retrieve Async Llama stack client according to configuration.""" - if llama_stack_config.use_as_library_client is True: - if llama_stack_config.library_client_config_path is not None: - logger.info("Using Llama stack as library client") - client = AsyncLlamaStackAsLibraryClient( - llama_stack_config.library_client_config_path + return self._lsc + + +class AsyncLlamaStackClientHolder(metaclass=Singleton): + """Container for an initialised AsyncLlamaStackClient.""" + + _lsc: Optional[AsyncLlamaStackClient] = None + + async def load(self, llama_stack_config: LLamaStackConfiguration) -> None: + """Retrieve Async Llama stack client according to configuration.""" + if llama_stack_config.use_as_library_client is True: + if llama_stack_config.library_client_config_path is not None: + logger.info("Using Llama stack as library client") + client = AsyncLlamaStackAsLibraryClient( + llama_stack_config.library_client_config_path + ) + await client.initialize() + self._lsc = client + else: + msg = "Configuration problem: library_client_config_path option is not set" + logger.error(msg) + # tisnik: use custom exception there - with cause etc. + raise ValueError(msg) + else: + logger.info("Using Llama stack running as a service") + self._lsc = AsyncLlamaStackClient( + base_url=llama_stack_config.url, api_key=llama_stack_config.api_key + ) + + def get_client(self) -> AsyncLlamaStackClient: + """Return an initialised AsyncLlamaStackClient.""" + if not self._lsc: + raise RuntimeError( + "AsyncLlamaStackClient has not been initialised. Ensure 'load(..)' has been called." ) - await client.initialize() - return client - msg = "Configuration problem: library_client_config_path option is not set" - logger.error(msg) - # tisnik: use custom exception there - with cause etc. - raise Exception(msg) # pylint: disable=broad-exception-raised - logger.info("Using Llama stack running as a service") - return AsyncLlamaStackClient( - base_url=llama_stack_config.url, api_key=llama_stack_config.api_key - ) + return self._lsc diff --git a/src/configuration.py b/src/configuration.py index 4bf3579d5..137d304a9 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -6,10 +6,12 @@ import yaml from models.config import ( Configuration, + Customization, LLamaStackConfiguration, UserDataCollection, ServiceConfiguration, ModelContextProtocolServer, + AuthenticationConfiguration, ) logger = logging.getLogger(__name__) @@ -81,5 +83,21 @@ def mcp_servers(self) -> list[ModelContextProtocolServer]: ), "logic error: configuration is not loaded" return self._configuration.mcp_servers + @property + def authentication_configuration(self) -> Optional[AuthenticationConfiguration]: + """Return authentication configuration.""" + assert ( + self._configuration is not None + ), "logic error: configuration is not loaded" + return self._configuration.authentication + + @property + def customization(self) -> Optional[Customization]: + """Return customization configuration.""" + assert ( + self._configuration is not None + ), "logic error: configuration is not loaded" + return self._configuration.customization + configuration: AppConfig = AppConfig() diff --git a/src/constants.py b/src/constants.py index d070b2765..d4c5b02a4 100644 --- a/src/constants.py +++ b/src/constants.py @@ -23,3 +23,27 @@ # Default system prompt used only when no other system prompt is specified in # configuration file nor in the query request DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant" + +# Authentication constants +DEFAULT_VIRTUAL_PATH = "/ls-access" +DEFAULT_USER_NAME = "lightspeed-user" +DEFAULT_USER_UID = "00000000-0000-0000-0000-000" +# default value for token when no token is provided +NO_USER_TOKEN = "" +AUTH_MOD_K8S = "k8s" +AUTH_MOD_NOOP = "noop" +AUTH_MOD_NOOP_WITH_TOKEN = "noop-with-token" +# Supported authentication modules +SUPPORTED_AUTHENTICATION_MODULES = frozenset( + { + AUTH_MOD_K8S, + AUTH_MOD_NOOP, + AUTH_MOD_NOOP_WITH_TOKEN, + } +) +DEFAULT_AUTHENTICATION_MODULE = AUTH_MOD_NOOP + +# Data collector constants +DATA_COLLECTOR_COLLECTION_INTERVAL = 7200 # 2 hours in seconds +DATA_COLLECTOR_CONNECTION_TIMEOUT = 30 +DATA_COLLECTOR_RETRY_INTERVAL = 300 # 5 minutes in seconds diff --git a/src/lightspeed_stack.py b/src/lightspeed_stack.py index a605aa77a..4479e308c 100644 --- a/src/lightspeed_stack.py +++ b/src/lightspeed_stack.py @@ -5,13 +5,14 @@ """ from argparse import ArgumentParser +import asyncio import logging - from rich.logging import RichHandler from runners.uvicorn import start_uvicorn +from runners.data_collector import start_data_collector from configuration import configuration - +from client import LlamaStackClientHolder, AsyncLlamaStackClientHolder FORMAT = "%(message)s" logging.basicConfig( @@ -47,6 +48,13 @@ def create_argument_parser() -> ArgumentParser: help="path to configuration file (default: lightspeed-stack.yaml)", default="lightspeed-stack.yaml", ) + parser.add_argument( + "--data-collector", + dest="start_data_collector", + help="start data collector service instead of web service", + action="store_true", + default=False, + ) return parser @@ -61,9 +69,19 @@ def main() -> None: logger.info( "Llama stack configuration: %s", configuration.llama_stack_configuration ) + logger.info("Creating LlamaStackClient") + LlamaStackClientHolder().load(configuration.configuration.llama_stack) + logger.info("Creating AsyncLlamaStackClient") + asyncio.run( + AsyncLlamaStackClientHolder().load(configuration.configuration.llama_stack) + ) if args.dump_configuration: configuration.configuration.dump() + elif args.start_data_collector: + start_data_collector( + configuration.user_data_collection_configuration.data_collector + ) else: start_uvicorn(configuration.service_configuration) logger.info("Lightspeed stack finished") diff --git a/src/models/config.py b/src/models/config.py index 943a6f46c..8e2d36e36 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -2,10 +2,13 @@ from typing import Optional -from pydantic import BaseModel, model_validator, FilePath - +from pydantic import BaseModel, model_validator, FilePath, AnyHttpUrl, PositiveInt from typing_extensions import Self +import constants + +from utils import checks + class TLSConfiguration(BaseModel): """TLS configuration.""" @@ -82,6 +85,31 @@ def check_llama_stack_model(self) -> Self: return self +class DataCollectorConfiguration(BaseModel): + """Data collector configuration for sending data to ingress server.""" + + enabled: bool = False + ingress_server_url: Optional[str] = None + ingress_server_auth_token: Optional[str] = None + ingress_content_service_name: Optional[str] = None + collection_interval: PositiveInt = constants.DATA_COLLECTOR_COLLECTION_INTERVAL + cleanup_after_send: bool = True # Remove local files after successful send + connection_timeout: PositiveInt = constants.DATA_COLLECTOR_CONNECTION_TIMEOUT + + @model_validator(mode="after") + def check_data_collector_configuration(self) -> Self: + """Check data collector configuration.""" + if self.enabled and self.ingress_server_url is None: + raise ValueError( + "ingress_server_url is required when data collector is enabled" + ) + if self.enabled and self.ingress_content_service_name is None: + raise ValueError( + "ingress_content_service_name is required when data collector is enabled" + ) + return self + + class UserDataCollection(BaseModel): """User data collection configuration.""" @@ -89,6 +117,7 @@ class UserDataCollection(BaseModel): feedback_storage: Optional[str] = None transcripts_disabled: bool = True transcripts_storage: Optional[str] = None + data_collector: DataCollectorConfiguration = DataCollectorConfiguration() @model_validator(mode="after") def check_storage_location_is_set_when_needed(self) -> Self: @@ -102,6 +131,44 @@ def check_storage_location_is_set_when_needed(self) -> Self: return self +class AuthenticationConfiguration(BaseModel): + """Authentication configuration.""" + + module: str = constants.DEFAULT_AUTHENTICATION_MODULE + skip_tls_verification: bool = False + k8s_cluster_api: Optional[AnyHttpUrl] = None + k8s_ca_cert_path: Optional[FilePath] = None + + @model_validator(mode="after") + def check_authentication_model(self) -> Self: + """Validate YAML containing authentication configuration section.""" + if self.module not in constants.SUPPORTED_AUTHENTICATION_MODULES: + supported_modules = ", ".join(constants.SUPPORTED_AUTHENTICATION_MODULES) + raise ValueError( + f"Unsupported authentication module '{self.module}'. " + f"Supported modules: {supported_modules}" + ) + return self + + +class Customization(BaseModel): + """Service customization.""" + + disable_query_system_prompt: bool = False + system_prompt_path: Optional[FilePath] = None + system_prompt: Optional[str] = None + + @model_validator(mode="after") + def check_customization_model(self) -> Self: + """Load system prompt from file.""" + if self.system_prompt_path is not None: + checks.file_check(self.system_prompt_path, "system prompt") + self.system_prompt = checks.get_attribute_from_file( + dict(self), "system_prompt_path" + ) + return self + + class Configuration(BaseModel): """Global service configuration.""" @@ -110,6 +177,10 @@ class Configuration(BaseModel): llama_stack: LLamaStackConfiguration user_data_collection: UserDataCollection mcp_servers: list[ModelContextProtocolServer] = [] + authentication: Optional[AuthenticationConfiguration] = ( + AuthenticationConfiguration() + ) + customization: Optional[Customization] = None def dump(self, filename: str = "configuration.json") -> None: """Dump actual configuration into JSON file.""" diff --git a/src/models/responses.py b/src/models/responses.py index 92c366c9b..a9778343f 100644 --- a/src/models/responses.py +++ b/src/models/responses.py @@ -242,3 +242,59 @@ class StatusResponse(BaseModel): ] } } + + +class AuthorizedResponse(BaseModel): + """Model representing a response to an authorization request. + + Attributes: + user_id: The ID of the logged in user. + username: The name of the logged in user. + """ + + user_id: str + username: str + + # provides examples for /docs endpoint + model_config = { + "json_schema_extra": { + "examples": [ + { + "user_id": "123e4567-e89b-12d3-a456-426614174000", + "username": "user1", + } + ] + } + } + + +class UnauthorizedResponse(BaseModel): + """Model representing response for missing or invalid credentials.""" + + detail: str + + # provides examples for /docs endpoint + model_config = { + "json_schema_extra": { + "examples": [ + { + "detail": "Unauthorized: No auth header found", + }, + ] + } + } + + +class ForbiddenResponse(UnauthorizedResponse): + """Model representing response for forbidden access.""" + + # provides examples for /docs endpoint + model_config = { + "json_schema_extra": { + "examples": [ + { + "detail": "Forbidden: User is not authorized to access this resource", + }, + ] + } + } diff --git a/src/runners/data_collector.py b/src/runners/data_collector.py new file mode 100644 index 000000000..7bf05e8f6 --- /dev/null +++ b/src/runners/data_collector.py @@ -0,0 +1,26 @@ +"""Data collector runner.""" + +import logging + +from models.config import DataCollectorConfiguration +from services.data_collector import DataCollectorService + +logger: logging.Logger = logging.getLogger(__name__) + + +def start_data_collector(configuration: DataCollectorConfiguration) -> None: + """Start the data collector service as a standalone process.""" + logger.info("Starting data collector runner") + + if not configuration.enabled: + logger.info("Data collection is disabled") + return + + try: + service = DataCollectorService() + service.run() + except Exception as e: + logger.error( + "Data collector service encountered an exception: %s", e, exc_info=True + ) + raise diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 000000000..c7775ec9a --- /dev/null +++ b/src/services/__init__.py @@ -0,0 +1 @@ +"""Services package.""" diff --git a/src/services/data_collector.py b/src/services/data_collector.py new file mode 100644 index 000000000..75ca74a70 --- /dev/null +++ b/src/services/data_collector.py @@ -0,0 +1,258 @@ +"""Data archival service for packaging and sending feedback and transcripts.""" + +import tarfile +import tempfile +import time +from datetime import datetime, UTC +from pathlib import Path +from typing import List + +import requests +import constants +from configuration import configuration +from log import get_logger + +logger = get_logger(__name__) + + +class DataCollectorService: # pylint: disable=too-few-public-methods + """Service for collecting and sending user data to ingress server. + + This service handles the periodic collection and transmission of user data + including feedback and transcripts to the configured ingress server. + """ + + def run(self) -> None: + """Run the periodic data collection loop.""" + collector_config = ( + configuration.user_data_collection_configuration.data_collector + ) + + logger.info("Starting data collection service") + + while True: + try: + self._perform_collection() + logger.info( + "Next collection scheduled in %s seconds", + collector_config.collection_interval, + ) + if collector_config.collection_interval is not None: + time.sleep(collector_config.collection_interval) + except KeyboardInterrupt: + logger.info("Data collection service stopped by user") + break + except (OSError, requests.RequestException) as e: + logger.error("Error during collection process: %s", e, exc_info=True) + time.sleep( + constants.DATA_COLLECTOR_RETRY_INTERVAL + ) # Wait 5 minutes before retrying on error + + def _perform_collection(self) -> None: + """Perform a single collection operation.""" + logger.info("Starting data collection process") + + # Collect files to archive + feedback_files = self._collect_feedback_files() + transcript_files = self._collect_transcript_files() + + if not feedback_files and not transcript_files: + logger.info("No files to collect") + return + + logger.info( + "Found %s feedback files and %s transcript files to collect", + len(feedback_files), + len(transcript_files), + ) + + # Create and send archives + collections_sent = 0 + try: + if feedback_files: + udc_config = configuration.user_data_collection_configuration + if udc_config.feedback_storage: + feedback_base = Path(udc_config.feedback_storage) + collections_sent += self._create_and_send_tarball( + feedback_files, "feedback", feedback_base + ) + if transcript_files: + udc_config = configuration.user_data_collection_configuration + if udc_config.transcripts_storage: + transcript_base = Path(udc_config.transcripts_storage) + collections_sent += self._create_and_send_tarball( + transcript_files, "transcripts", transcript_base + ) + + logger.info( + "Successfully sent %s collections to ingress server", collections_sent + ) + except (OSError, requests.RequestException, tarfile.TarError) as e: + logger.error("Failed to create or send collections: %s", e, exc_info=True) + raise + + def _collect_feedback_files(self) -> List[Path]: + """Collect all feedback files that need to be collected.""" + udc_config = configuration.user_data_collection_configuration + + if udc_config.feedback_disabled or not udc_config.feedback_storage: + return [] + + feedback_dir = Path(udc_config.feedback_storage) + if not feedback_dir.exists(): + return [] + + return list(feedback_dir.glob("*.json")) + + def _collect_transcript_files(self) -> List[Path]: + """Collect all transcript files that need to be collected.""" + udc_config = configuration.user_data_collection_configuration + + if udc_config.transcripts_disabled or not udc_config.transcripts_storage: + return [] + + transcripts_dir = Path(udc_config.transcripts_storage) + if not transcripts_dir.exists(): + return [] + + # Recursively find all JSON files in the transcript directory structure + return list(transcripts_dir.rglob("*.json")) + + def _create_and_send_tarball( + self, files: List[Path], data_type: str, base_directory: Path + ) -> int: + """Create a single tarball from all files and send to ingress server.""" + if not files: + return 0 + + collector_config = ( + configuration.user_data_collection_configuration.data_collector + ) + + # Create one tarball with all files + tarball_path = self._create_tarball(files, data_type, base_directory) + try: + self._send_tarball(tarball_path) + if collector_config.cleanup_after_send: + self._cleanup_files(files) + self._cleanup_empty_directories() + return 1 + finally: + self._cleanup_tarball(tarball_path) + + def _create_tarball( + self, files: List[Path], data_type: str, base_directory: Path + ) -> Path: + """Create a tarball containing the specified files.""" + timestamp = datetime.now(UTC).strftime("%Y%m%d_%H%M%S") + tarball_name = f"{data_type}_{timestamp}.tar.gz" + + # Create tarball in a temporary directory + temp_dir = Path(tempfile.gettempdir()) + tarball_path = temp_dir / tarball_name + + logger.info("Creating tarball %s with %s files", tarball_path, len(files)) + + with tarfile.open(tarball_path, "w:gz") as tar: + for file_path in files: + try: + # Add file with relative path to maintain directory structure + arcname = str(file_path.relative_to(base_directory)) + tar.add(file_path, arcname=arcname) + except (OSError, ValueError) as e: + logger.warning("Failed to add %s to tarball: %s", file_path, e) + + logger.info( + "Created tarball %s (%s bytes)", tarball_path, tarball_path.stat().st_size + ) + return tarball_path + + def _send_tarball(self, tarball_path: Path) -> None: + """Send the tarball to the ingress server.""" + collector_config = ( + configuration.user_data_collection_configuration.data_collector + ) + + if collector_config.ingress_server_url is None: + raise ValueError("Ingress server URL is not configured") + + # pylint: disable=line-too-long + headers = { + "Content-Type": f"application/vnd.redhat.{collector_config.ingress_content_service_name}.periodic+tar", + } + + if collector_config.ingress_server_auth_token: + headers["Authorization"] = ( + f"Bearer {collector_config.ingress_server_auth_token}" + ) + + with open(tarball_path, "rb") as f: + data = f.read() + + logger.info( + "Sending tarball %s to %s", + tarball_path.name, + collector_config.ingress_server_url, + ) + + response = requests.post( + collector_config.ingress_server_url, + data=data, + headers=headers, + timeout=collector_config.connection_timeout, + ) + + if response.status_code >= 400: + raise requests.HTTPError( + f"Failed to send tarball to ingress server. " + f"Status: {response.status_code}, Response: {response.text}" + ) + + logger.info("Successfully sent tarball %s to ingress server", tarball_path.name) + + def _cleanup_files(self, files: List[Path]) -> None: + """Remove files after successful transmission.""" + for file_path in files: + try: + file_path.unlink() + logger.debug("Removed file %s", file_path) + except OSError as e: + logger.warning("Failed to remove file %s: %s", file_path, e) + + def _cleanup_empty_directories(self) -> None: + """Remove empty directories from transcript storage.""" + udc_config = configuration.user_data_collection_configuration + + if udc_config.transcripts_disabled or not udc_config.transcripts_storage: + return + + transcripts_dir = Path(udc_config.transcripts_storage) + if not transcripts_dir.exists(): + return + + # Remove empty directories (conversation and user directories) + for user_dir in transcripts_dir.iterdir(): + if user_dir.is_dir(): + for conv_dir in user_dir.iterdir(): + if conv_dir.is_dir() and not any(conv_dir.iterdir()): + try: + conv_dir.rmdir() + logger.debug("Removed empty directory %s", conv_dir) + except OSError: + pass + + # Remove user directory if empty + if not any(user_dir.iterdir()): + try: + user_dir.rmdir() + logger.debug("Removed empty directory %s", user_dir) + except OSError: + pass + + def _cleanup_tarball(self, tarball_path: Path) -> None: + """Remove the temporary tarball file.""" + try: + tarball_path.unlink() + logger.debug("Removed temporary tarball %s", tarball_path) + except OSError as e: + logger.warning("Failed to remove temporary tarball %s: %s", tarball_path, e) diff --git a/src/utils/auth.py b/src/utils/auth.py deleted file mode 100644 index 10f69b53c..000000000 --- a/src/utils/auth.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Authentication handling.""" - -from fastapi import Request - - -# TODO(lucasagomes): implement this function to handle authentication -async def auth_dependency(_request: Request) -> str: - """Authenticate dependency to ensure the user is authenticated. - - Args: - request (Request): The FastAPI request object. - - Raises: - HTTPException: If the user is not authenticated. - """ - return extract_access_token(_request) - - -def extract_access_token(request: Request) -> str: - """Extract access token from the Authorization header. - - Args: - request: The FastAPI request object - - Returns: - The access token string, or empty string if not found - """ - authorization_header = request.headers.get("Authorization", "") - if authorization_header.startswith("Bearer "): - return authorization_header[7:] # Remove "Bearer " prefix - return "" diff --git a/src/utils/checks.py b/src/utils/checks.py new file mode 100644 index 000000000..8d7017503 --- /dev/null +++ b/src/utils/checks.py @@ -0,0 +1,27 @@ +"""Checks that are performed to configuration options.""" + +import os +from typing import Optional + +from pydantic import FilePath + + +class InvalidConfigurationError(Exception): + """Lightspeed configuration is invalid.""" + + +def get_attribute_from_file(data: dict, file_name_key: str) -> Optional[str]: + """Retrieve value of an attribute from a file.""" + file_path = data.get(file_name_key) + if file_path is not None: + with open(file_path, encoding="utf-8") as f: + return f.read().rstrip() + return None + + +def file_check(path: FilePath, desc: str) -> None: + """Check that path is a readable regular file.""" + if not os.path.isfile(path): + raise InvalidConfigurationError(f"{desc} '{path}' is not a file") + if not os.access(path, os.R_OK): + raise InvalidConfigurationError(f"{desc} '{path}' is not readable") diff --git a/src/utils/common.py b/src/utils/common.py index 4f8044349..001d2c3f8 100644 --- a/src/utils/common.py +++ b/src/utils/common.py @@ -3,14 +3,13 @@ from typing import Any, List, cast from logging import Logger -from llama_stack_client import LlamaStackClient +from llama_stack_client import LlamaStackClient, AsyncLlamaStackClient from llama_stack.distribution.library_client import ( - LlamaStackAsLibraryClient, AsyncLlamaStackAsLibraryClient, ) -from client import get_llama_stack_client +from client import LlamaStackClientHolder, AsyncLlamaStackClientHolder from models.config import Configuration, ModelContextProtocolServer @@ -39,36 +38,33 @@ async def register_mcp_servers_async( if configuration.llama_stack.use_as_library_client: # Library client - use async interface - # config.py validation ensures library_client_config_path is not None - # when use_as_library_client is True - config_path = cast(str, configuration.llama_stack.library_client_config_path) - client = LlamaStackAsLibraryClient(config_path) - await client.async_client.initialize() - - await _register_mcp_toolgroups_async( - client.async_client, configuration.mcp_servers, logger + client = cast( + AsyncLlamaStackAsLibraryClient, AsyncLlamaStackClientHolder().get_client() ) + await client.initialize() + await _register_mcp_toolgroups_async(client, configuration.mcp_servers, logger) else: # Service client - use sync interface - client = get_llama_stack_client(configuration.llama_stack) - + client = LlamaStackClientHolder().get_client() _register_mcp_toolgroups_sync(client, configuration.mcp_servers, logger) async def _register_mcp_toolgroups_async( - client: AsyncLlamaStackAsLibraryClient, + client: AsyncLlamaStackClient, mcp_servers: List[ModelContextProtocolServer], logger: Logger, ) -> None: """Async logic for registering MCP toolgroups.""" # Get registered tools - registered_tools = await client.tools.list() - registered_toolgroups = [tool.toolgroup_id for tool in registered_tools] - logger.debug("Registered toolgroups: %s", set(registered_toolgroups)) + registered_toolgroups = await client.toolgroups.list() + registered_toolgroups_ids = [ + tool_group.provider_resource_id for tool_group in registered_toolgroups + ] + logger.debug("Registered toolgroups: %s", registered_toolgroups_ids) # Register toolgroups for MCP servers if not already registered for mcp in mcp_servers: - if mcp.name not in registered_toolgroups: + if mcp.name not in registered_toolgroups_ids: logger.debug("Registering MCP server: %s, %s", mcp.name, mcp.url) registration_params = { @@ -87,14 +83,16 @@ def _register_mcp_toolgroups_sync( logger: Logger, ) -> None: """Sync logic for registering MCP toolgroups.""" - # Get registered tools - registered_tools = client.tools.list() - registered_toolgroups = [tool.toolgroup_id for tool in registered_tools] - logger.debug("Registered toolgroups: %s", set(registered_toolgroups)) + # Get registered tool groups + registered_toolgroups = client.toolgroups.list() + registered_toolgroups_ids = [ + tool_group.provider_resource_id for tool_group in registered_toolgroups + ] + logger.debug("Registered toolgroups: %s", registered_toolgroups_ids) # Register toolgroups for MCP servers if not already registered for mcp in mcp_servers: - if mcp.name not in registered_toolgroups: + if mcp.name not in registered_toolgroups_ids: logger.debug("Registering MCP server: %s, %s", mcp.name, mcp.url) registration_params = { diff --git a/src/utils/endpoints.py b/src/utils/endpoints.py new file mode 100644 index 000000000..c9a8f91c9 --- /dev/null +++ b/src/utils/endpoints.py @@ -0,0 +1,50 @@ +"""Utility functions for endpoint handlers.""" + +from fastapi import HTTPException, status + +import constants +from models.requests import QueryRequest +from configuration import AppConfig + + +def check_configuration_loaded(configuration: AppConfig) -> None: + """Check that configuration is loaded and raise exception when it is not.""" + if configuration is None: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail={"response": "Configuration is not loaded"}, + ) + + +def get_system_prompt(query_request: QueryRequest, configuration: AppConfig) -> str: + """Get the system prompt: the provided one, configured one, or default one.""" + system_prompt_disabled = ( + configuration.customization is not None + and configuration.customization.disable_query_system_prompt + ) + if system_prompt_disabled and query_request.system_prompt: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail={ + "response": ( + "This instance does not support customizing the system prompt in the " + "query request (disable_query_system_prompt is set). Please remove the " + "system_prompt field from your request." + ) + }, + ) + + if query_request.system_prompt: + # Query taking precedence over configuration is the only behavior that + # makes sense here - if the configuration wants precedence, it can + # disable query system prompt altogether with disable_system_prompt. + return query_request.system_prompt + + if ( + configuration.customization is not None + and configuration.customization.system_prompt is not None + ): + return configuration.customization.system_prompt + + # default system prompt has the lowest precedence + return constants.DEFAULT_SYSTEM_PROMPT diff --git a/src/utils/mcp_headers.py b/src/utils/mcp_headers.py new file mode 100644 index 000000000..0a2eb5708 --- /dev/null +++ b/src/utils/mcp_headers.py @@ -0,0 +1,90 @@ +"""MCP headers handling.""" + +import json +import logging +from urllib.parse import urlparse + +from fastapi import Request + +from configuration import AppConfig + + +logger = logging.getLogger("app.endpoints.dependencies") + + +async def mcp_headers_dependency(_request: Request) -> dict[str, dict[str, str]]: + """Get the mcp headers dependency to passed to mcp servers. + + mcp headers is a json dictionary or mcp url paths and their respective headers + + Args: + request (Request): The FastAPI request object. + + Returns: + The mcp headers dictionary, or empty dictionary if not found or on json decoding error + """ + return extract_mcp_headers(_request) + + +def extract_mcp_headers(request: Request) -> dict[str, dict[str, str]]: + """Extract mcp headers from MCP-HEADERS header. + + Args: + request: The FastAPI request object + + Returns: + The mcp headers dictionary, or empty dictionary if not found or on json decoding error + """ + mcp_headers_string = request.headers.get("MCP-HEADERS", "") + mcp_headers = {} + if mcp_headers_string: + try: + mcp_headers = json.loads(mcp_headers_string) + except json.decoder.JSONDecodeError as e: + logger.error("MCP headers decode error: %s", e) + + if not isinstance(mcp_headers, dict): + logger.error( + "MCP headers wrong type supplied (mcp headers must be a dictionary), " + "but type %s was supplied", + type(mcp_headers), + ) + mcp_headers = {} + return mcp_headers + + +def handle_mcp_headers_with_toolgroups( + mcp_headers: dict[str, dict[str, str]], config: AppConfig +) -> dict[str, dict[str, str]]: + """Process MCP headers by converting toolgroup names to URLs. + + This function takes MCP headers where keys can be either valid URLs or + toolgroup names. For valid URLs (HTTP/HTTPS), it keeps them as-is. For + toolgroup names, it looks up the corresponding MCP server URL in the + configuration and replaces the key with the URL. Unknown toolgroup names + are filtered out. + + Args: + mcp_headers: Dictionary with keys as URLs or toolgroup names + config: Application configuration containing MCP server definitions + + Returns: + Dictionary with URLs as keys and their corresponding headers as values + """ + converted_mcp_headers = {} + + for key, item in mcp_headers.items(): + key_url_parsed = urlparse(key) + if key_url_parsed.scheme in ("http", "https") and key_url_parsed.netloc: + # a valid url is supplied, deliver it as is + converted_mcp_headers[key] = item + else: + # assume the key is a toolgroup name + # look for toolgroups name in mcp_servers configuration + # if the mcp server is not found, the mcp header gets ignored + for mcp_server in config.mcp_servers: + if mcp_server.name == key and mcp_server.url: + converted_mcp_headers[mcp_server.url] = item + break + + return converted_mcp_headers diff --git a/src/utils/types.py b/src/utils/types.py new file mode 100644 index 000000000..52bbb8fe6 --- /dev/null +++ b/src/utils/types.py @@ -0,0 +1,37 @@ +"""Common types for the project.""" + +from typing import Optional + +from llama_stack_client.lib.agents.tool_parser import ToolParser +from llama_stack_client.types.shared.completion_message import CompletionMessage +from llama_stack_client.types.shared.tool_call import ToolCall + + +class Singleton(type): + """Metaclass for Singleton support.""" + + _instances = {} # type: ignore + + def __call__(cls, *args, **kwargs): # type: ignore + """Ensure a single instance is created.""" + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +# See https://github.com/meta-llama/llama-stack-client-python/issues/206 +class GraniteToolParser(ToolParser): + """Workaround for 'tool_calls' with granite models.""" + + def get_tool_calls(self, output_message: CompletionMessage) -> list[ToolCall]: + """Use 'tool_calls' associated with the CompletionMessage, if available.""" + if output_message and output_message.tool_calls: + return output_message.tool_calls + return [] + + @staticmethod + def get_parser(model_id: str) -> Optional[ToolParser]: + """Get the applicable ToolParser for the model.""" + if model_id and model_id.lower().startswith("granite"): + return GraniteToolParser() + return None diff --git a/src/version.py b/src/version.py index dff08d537..539dbcd31 100644 --- a/src/version.py +++ b/src/version.py @@ -9,4 +9,4 @@ # [tool.pdm.version] # source = "file" # path = "src/version.py" -__version__ = "0.0.1" +__version__ = "0.1.0" diff --git a/tests/e2e/features/steps/common.py b/tests/e2e/features/steps/common.py index 0e88fc741..d5168e9e0 100644 --- a/tests/e2e/features/steps/common.py +++ b/tests/e2e/features/steps/common.py @@ -1,16 +1,16 @@ """Implementation of common test steps.""" -from behave import given +from behave import given # pyright: ignore[reportAttributeAccessIssue] from behave.runner import Context @given("the service is started locally") def service_is_started_locally(context: Context) -> None: """Check the service status.""" - pass + assert context is not None @given("the system is in default state") def system_in_default_state(context: Context) -> None: """Check the default system state.""" - pass + assert context is not None diff --git a/tests/e2e/features/steps/common_http.py b/tests/e2e/features/steps/common_http.py index b462f7dfa..c2c3b3766 100644 --- a/tests/e2e/features/steps/common_http.py +++ b/tests/e2e/features/steps/common_http.py @@ -3,7 +3,7 @@ import json import requests -from behave import given, then, when +from behave import given, then, when # pyright: ignore[reportAttributeAccessIssue] from behave.runner import Context from tests.e2e.utils.utils import normalize_endpoint, validate_json @@ -33,10 +33,17 @@ def request_endpoint_with_body( def request_endpoint_with_json( context: Context, endpoint: str, hostname: str, port: int ) -> None: - """Perform a request to the local server with a given JSON in the request.""" + """Perform a request to the local server with a given JSON in the request. + + The JSON payload is parsed from `context.text`, which must not + be None in order for this step to succeed. The response is + saved to `context.response` attribute. + """ # initial value context.response = None + assert context.text is not None, "Payload needs to be specified" + # perform REST API call context.response = requests.get( f"http://{hostname}:{port}/{endpoint}", @@ -51,9 +58,16 @@ def request_endpoint_with_json( def request_endpoint_with_url_params( context: Context, endpoint: str, hostname: str, port: int ) -> None: - """Perform a request to the server defined by URL to a given endpoint.""" + """Perform a request to the server defined by URL to a given endpoint. + + The function asserts that `context.table` is provided and uses + its rows to build the query parameters for the request. The + HTTP response is stored in `context.response` attribute. + """ params = {} + assert context.table is not None, "Request parameters needs to be specified" + for row in context.table: name = row["param"] value = row["value"] @@ -118,8 +132,14 @@ def check_content_type(context: Context, content_type: str) -> None: @then("The body of the response has the following schema") def check_response_body_schema(context: Context) -> None: - """Check that response body is compliant with a given schema.""" + """Check that response body is compliant with a given schema. + + Asserts that a response has been received and that a schema is + present in `context.text` attribute. Loads the schema from + `context.text` attribute and validates the response body. + """ assert context.response is not None, "Request needs to be performed first" + assert context.text is not None, "Response does not contain any payload" schema = json.loads(context.text) body = context.response.json() @@ -137,8 +157,14 @@ def check_response_body_contains(context: Context, substring: str) -> None: @then("The body of the response is the following") def check_prediction_result(context: Context) -> None: - """Check the content of the response to be exactly the same.""" + """Check the content of the response to be exactly the same. + + Raises an assertion error if the response is missing, the + expected payload is not provided, or if the actual and expected + JSON objects differ. + """ assert context.response is not None, "Request needs to be performed first" + assert context.text is not None, "Response does not contain any payload" expected_body = json.loads(context.text) result = context.response.json() @@ -148,8 +174,16 @@ def check_prediction_result(context: Context) -> None: @then('The body of the response, ignoring the "{field}" field, is the following') def check_prediction_result_ignoring_field(context: Context, field: str) -> None: - """Check the content of the response to be exactly the same.""" + """Check the content of the response to be exactly the same. + + Asserts that the JSON response body matches the expected JSON + payload, ignoring a specified field. + + Parameters: + field (str): The name of the field to exclude from both the actual and expected JSON objects during comparison. + """ assert context.response is not None, "Request needs to be performed first" + assert context.text is not None, "Response does not contain any payload" expected_body = json.loads(context.text).copy() result = context.response.json().copy() @@ -212,11 +246,17 @@ def access_rest_api_endpoint_get(context: Context, endpoint: str) -> None: @when("I access endpoint {endpoint:w} using HTTP POST method") def access_rest_api_endpoint_post(context: Context, endpoint: str) -> None: - """Send GET HTTP request to tested service.""" + """Send POST HTTP request with JSON payload to tested service. + + The JSON payload is retrieved from `context.text` attribute, + which must not be None. The response is stored in + `context.response` attribute. + """ base = f"http://{context.hostname}:{context.port}" path = f"{context.api_prefix}/{endpoint}".replace("//", "/") url = base + path + assert context.text is not None, "Payload needs to be specified" data = json.loads(context.text) # initial value context.response = None diff --git a/tests/e2e/features/steps/llm_query_response.py b/tests/e2e/features/steps/llm_query_response.py index 10f0cf848..776dcf3d1 100644 --- a/tests/e2e/features/steps/llm_query_response.py +++ b/tests/e2e/features/steps/llm_query_response.py @@ -1,7 +1,7 @@ """LLM query and response steps.""" import requests -from behave import then, when +from behave import then, when # pyright: ignore[reportAttributeAccessIssue] from behave.runner import Context DEFAULT_LLM_TIMEOUT = 60 @@ -36,11 +36,21 @@ def check_llm_response_not_truncated(context: Context) -> None: @then("The response should contain following fragments") def check_fragments_in_response(context: Context) -> None: - """Check if the LLM response contain list of fragments.""" + """Check that all specified fragments are present in the LLM response. + + First checks that the HTTP response exists and contains a + "response" field. For each fragment listed in the scenario's + table under "Fragments in LLM response", asserts that it + appears as a substring in the LLM's response. Raises an + assertion error if any fragment is missing or if the fragments + table is not provided. + """ assert context.response is not None response_json = context.response.json() response = response_json["response"] + assert context.table is not None, "Fragments are not specified in table" + for fragment in context.table: expected = fragment["Fragments in LLM response"] assert ( diff --git a/tests/e2e/gen_scenario_list.py b/tests/e2e/gen_scenario_list.py new file mode 100644 index 000000000..94cf1744b --- /dev/null +++ b/tests/e2e/gen_scenario_list.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 + +# Copyright © 2022, 2023, 2025 Pavel Tisnovsky +# +# 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. + +"""Scenario list generator.""" + +# Usage +# python gen_scenario_list.py > docs/scenarios_list.md + +import os + +# repository URL +REPO_URL = "https://github.com/lightspeed-core/lightspeed-stack/" + +# URL prefix to create links to feature files +FEATURES_URL_PREFIX = f"{REPO_URL}blob/main/tests/e2e/features" + +# list of prefixes for scenarios or scenario outlines +PREFIXES = ("Scenario: ", "Scenario Outline: ") + +# sub-directory where feature files are stored +FEATURE_DIRECTORY = "features" + +# generate page header +print("---") +print("layout: page") +print("nav_order: 3") +print("---") +print() +print("# List of scenarios") +print() + +# generage list of scenarios + +# files within one subdirectory needs to be sorted so the +# resulting scenario list will have stable structure across versions +files = sorted(os.listdir(FEATURE_DIRECTORY)) +for filename in files: + # grep all .feature files + 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: + for line in fin.readlines(): + line = line.strip() + # process all scenarios and scenario outlines + for prefix in PREFIXES: + if line.startswith(prefix): + line = line[len(prefix) :] + print(f"* {line}") + # vertical space between subsections in generated file + print() diff --git a/tests/integration/test_configuration.py b/tests/integration/test_configuration.py index 72855b7c6..1f27211c8 100644 --- a/tests/integration/test_configuration.py +++ b/tests/integration/test_configuration.py @@ -6,8 +6,8 @@ from models.config import ModelContextProtocolServer -@pytest.fixture -def configuration_filename() -> str: +@pytest.fixture(name="configuration_filename") +def configuration_filename_fixture() -> str: """Retrieve configuration file name to be used by integration tests.""" return "tests/configuration/lightspeed-stack.yaml" @@ -17,7 +17,7 @@ def test_default_configuration() -> None: cfg = configuration assert cfg is not None with pytest.raises(Exception, match="logic error: configuration is not loaded"): - configuration.configuration + configuration.configuration # pylint: disable=pointless-statement def test_loading_proper_configuration(configuration_filename: str) -> None: @@ -60,7 +60,7 @@ def test_loading_proper_configuration(configuration_filename: str) -> None: # check MCP servers section mcp_servers = cfg.mcp_servers - assert mcp_servers != [] + assert mcp_servers != [] # pylint: disable=use-implicit-booleaness-not-comparison assert len(mcp_servers) == 3 assert mcp_servers[0] == ModelContextProtocolServer( name="server1", provider_id="provider1", url="http://url.com:1" diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index e0310a01e..92a33d78f 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1 +1,39 @@ """Unit tests.""" + +from configuration import configuration # noqa: F401 + +config_dict = { + "name": "test", + "service": { + "host": "localhost", + "port": 8080, + "auth_enabled": False, + "workers": 1, + "color_log": True, + "access_log": True, + }, + "llama_stack": { + "api_key": "test-key", + "url": "http://test.com:1234", + "use_as_library_client": False, + }, + "user_data_collection": { + "transcripts_disabled": True, + }, + "mcp_servers": [], + "authentication": { + "module": "noop", + "skip_tls_verification": False, + "k8s_ca_cert_path": None, + "k8s_cluster_api": None, + }, + "customization": { + "disable_query_system_prompt": False, + "system_prompt_path": None, + "system_prompt": None, + }, +} + +# NOTE(lucasagomes): Configuration must be initialized before importing +# endpoints, since get_auth_dependency() uses it during import time +configuration.init_from_dict(config_dict) diff --git a/tests/unit/app/endpoints/test_authorized.py b/tests/unit/app/endpoints/test_authorized.py new file mode 100644 index 000000000..441b354af --- /dev/null +++ b/tests/unit/app/endpoints/test_authorized.py @@ -0,0 +1,54 @@ +from unittest.mock import AsyncMock + +import pytest +from fastapi import Request, HTTPException + +from app.endpoints.authorized import authorized_endpoint_handler + + +def test_authorized_endpoint(mocker): + """Test the authorized endpoint handler.""" + # Mock the auth dependency to return a user ID and username + auth_dependency_mock = AsyncMock() + auth_dependency_mock.return_value = ("test-id", "test-user", None) + mocker.patch( + "app.endpoints.authorized.auth_dependency", side_effect=auth_dependency_mock + ) + + request = Request( + scope={ + "type": "http", + "query_string": b"", + } + ) + + response = authorized_endpoint_handler(request) + + assert response.model_dump() == { + "user_id": "test-id", + "username": "test-user", + } + + +def test_authorized_unauthorized(mocker): + """Test the authorized endpoint handler with a custom user ID.""" + auth_dependency_mock = AsyncMock() + auth_dependency_mock.side_effect = HTTPException( + status_code=403, detail="User is not authorized" + ) + mocker.patch( + "app.endpoints.authorized.auth_dependency", side_effect=auth_dependency_mock + ) + + request = Request( + scope={ + "type": "http", + "query_string": b"", + } + ) + + with pytest.raises(HTTPException) as exc_info: + authorized_endpoint_handler(request) + + assert exc_info.value.status_code == 403 + assert exc_info.value.detail == "User is not authorized" diff --git a/tests/unit/app/endpoints/test_config.py b/tests/unit/app/endpoints/test_config.py index 38296ce13..4a2cd3c1c 100644 --- a/tests/unit/app/endpoints/test_config.py +++ b/tests/unit/app/endpoints/test_config.py @@ -1,5 +1,6 @@ import pytest +from fastapi import HTTPException, status from app.endpoints.config import config_endpoint_handler from configuration import AppConfig @@ -7,13 +8,16 @@ def test_config_endpoint_handler_configuration_not_loaded(mocker): """Test the config endpoint handler.""" mocker.patch( - "app.endpoints.query.configuration", - return_value=mocker.Mock(), + "app.endpoints.config.configuration._configuration", + new=None, ) + mocker.patch("app.endpoints.config.configuration", None) request = None - with pytest.raises(Exception, match="logic error: configuration is not loaded"): + with pytest.raises(HTTPException) as e: config_endpoint_handler(request) + assert e.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert e.detail["response"] == "Configuration is not loaded" def test_config_endpoint_handler_configuration_loaded(mocker): @@ -36,6 +40,10 @@ def test_config_endpoint_handler_configuration_loaded(mocker): "user_data_collection": { "feedback_disabled": True, }, + "authentication": { + "module": "noop", + }, + "customization": None, } cfg = AppConfig() cfg.init_from_dict(config_dict) diff --git a/tests/unit/app/endpoints/test_health.py b/tests/unit/app/endpoints/test_health.py index df90bee07..0fca0b55f 100644 --- a/tests/unit/app/endpoints/test_health.py +++ b/tests/unit/app/endpoints/test_health.py @@ -98,14 +98,11 @@ class TestGetProvidersHealthStatuses: def test_get_providers_health_statuses(self, mocker): """Test get_providers_health_statuses with healthy providers.""" # Mock the imports - mock_get_llama_stack_client = mocker.patch( - "app.endpoints.health.get_llama_stack_client" - ) - mock_configuration = mocker.patch("app.endpoints.health.configuration") + mock_lsc = mocker.patch("client.LlamaStackClientHolder.get_client") # Mock the client and its methods mock_client = mocker.Mock() - mock_get_llama_stack_client.return_value = mock_client + mock_lsc.return_value = mock_client # Mock providers.list() to return providers with health mock_provider_1 = mocker.Mock() @@ -136,9 +133,6 @@ def test_get_providers_health_statuses(self, mocker): ] # Mock configuration - mock_llama_stack_config = mocker.Mock() - mock_configuration.llama_stack_configuration = mock_llama_stack_config - result = get_providers_health_statuses() assert len(result) == 3 @@ -155,17 +149,10 @@ def test_get_providers_health_statuses(self, mocker): def test_get_providers_health_statuses_connection_error(self, mocker): """Test get_providers_health_statuses when connection fails.""" # Mock the imports - mock_get_llama_stack_client = mocker.patch( - "app.endpoints.health.get_llama_stack_client" - ) - mock_configuration = mocker.patch("app.endpoints.health.configuration") - - # Mock configuration - mock_llama_stack_config = mocker.Mock() - mock_configuration.llama_stack_configuration = mock_llama_stack_config + mock_lsc = mocker.patch("client.LlamaStackClientHolder.get_client") # Mock get_llama_stack_client to raise an exception - mock_get_llama_stack_client.side_effect = Exception("Connection error") + mock_lsc.side_effect = Exception("Connection error") result = get_providers_health_statuses() diff --git a/tests/unit/app/endpoints/test_info.py b/tests/unit/app/endpoints/test_info.py index 9b36cbfa3..7dc1d46bc 100644 --- a/tests/unit/app/endpoints/test_info.py +++ b/tests/unit/app/endpoints/test_info.py @@ -22,6 +22,7 @@ def test_info_endpoint(mocker): "user_data_collection": { "feedback_disabled": True, }, + "customization": None, } cfg = AppConfig() cfg.init_from_dict(config_dict) diff --git a/tests/unit/app/endpoints/test_models.py b/tests/unit/app/endpoints/test_models.py new file mode 100644 index 000000000..f9bcc51ba --- /dev/null +++ b/tests/unit/app/endpoints/test_models.py @@ -0,0 +1,134 @@ +import pytest + +from unittest.mock import Mock +from fastapi import HTTPException, status + +from app.endpoints.models import models_endpoint_handler +from configuration import AppConfig + + +def test_models_endpoint_handler_configuration_not_loaded(mocker): + """Test the models endpoint handler if configuration is not loaded.""" + # simulate state when no configuration is loaded + mocker.patch( + "app.endpoints.models.configuration", + return_value=mocker.Mock(), + ) + mocker.patch("app.endpoints.models.configuration", None) + + request = None + with pytest.raises(HTTPException) as e: + models_endpoint_handler(request) + assert e.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert e.detail["response"] == "Configuration is not loaded" + + +def test_models_endpoint_handler_improper_llama_stack_configuration(mocker): + """Test the models endpoint handler if Llama Stack configuration is not proper.""" + # configuration for tests + config_dict = { + "name": "test", + "service": { + "host": "localhost", + "port": 8080, + "auth_enabled": False, + "workers": 1, + "color_log": True, + "access_log": True, + }, + "llama_stack": { + "api_key": "test-key", + "url": "http://test.com:1234", + "use_as_library_client": False, + }, + "user_data_collection": { + "transcripts_disabled": True, + }, + "mcp_servers": [], + "customization": None, + } + cfg = AppConfig() + cfg.init_from_dict(config_dict) + + mocker.patch( + "app.endpoints.models.configuration", + return_value=None, + ) + + request = None + with pytest.raises(HTTPException) as e: + models_endpoint_handler(request) + assert e.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert e.detail["response"] == "LLama stack is not configured" + + +def test_models_endpoint_handler_configuration_loaded(mocker): + """Test the models endpoint handler if configuration is loaded.""" + # configuration for tests + config_dict = { + "name": "foo", + "service": { + "host": "localhost", + "port": 8080, + "auth_enabled": False, + "workers": 1, + "color_log": True, + "access_log": True, + }, + "llama_stack": { + "api_key": "xyzzy", + "url": "http://x.y.com:1234", + "use_as_library_client": False, + }, + "user_data_collection": { + "feedback_disabled": True, + }, + "customization": None, + } + cfg = AppConfig() + cfg.init_from_dict(config_dict) + + with pytest.raises(HTTPException) as e: + request = None + models_endpoint_handler(request) + assert e.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert e.detail["response"] == "Unable to connect to Llama Stack" + + +def test_models_endpoint_handler_unable_to_retrieve_models_list(mocker): + """Test the models endpoint handler if configuration is loaded.""" + # configuration for tests + config_dict = { + "name": "foo", + "service": { + "host": "localhost", + "port": 8080, + "auth_enabled": False, + "workers": 1, + "color_log": True, + "access_log": True, + }, + "llama_stack": { + "api_key": "xyzzy", + "url": "http://x.y.com:1234", + "use_as_library_client": False, + }, + "user_data_collection": { + "feedback_disabled": True, + }, + "customization": None, + } + cfg = AppConfig() + cfg.init_from_dict(config_dict) + + # Mock the LlamaStack client + mock_client = Mock() + mock_client.models.list.return_value = [] + mock_lsc = mocker.patch("client.LlamaStackClientHolder.get_client") + mock_lsc.return_value = mock_client + mock_config = mocker.Mock() + mocker.patch("app.endpoints.models.configuration", mock_config) + + request = None + response = models_endpoint_handler(request) + assert response is not None diff --git a/tests/unit/app/endpoints/test_query.py b/tests/unit/app/endpoints/test_query.py index f0ddd98d1..4729c7a6b 100644 --- a/tests/unit/app/endpoints/test_query.py +++ b/tests/unit/app/endpoints/test_query.py @@ -7,19 +7,23 @@ query_endpoint_handler, select_model_id, retrieve_response, - retrieve_conversation_id, validate_attachments_metadata, is_transcripts_enabled, construct_transcripts_path, store_transcript, get_rag_toolgroups, + get_agent, + _agent_cache, ) +from llama_stack_client import APIConnectionError from models.requests import QueryRequest, Attachment from models.config import ModelContextProtocolServer from llama_stack_client.types import UserMessage # type: ignore +MOCK_AUTH = ("mock_user_id", "mock_username", "mock_token") -@pytest.fixture + +@pytest.fixture(autouse=True) def setup_configuration(): """Set up configuration for tests.""" config_dict = { @@ -41,12 +45,38 @@ def setup_configuration(): "transcripts_disabled": True, }, "mcp_servers": [], + "customization": None, } cfg = AppConfig() cfg.init_from_dict(config_dict) return cfg +@pytest.fixture(autouse=True) +def prepare_agent_mocks(mocker): + mock_client = mocker.Mock() + mock_agent = mocker.Mock() + """Cleanup agent cache after tests.""" + yield mock_client, mock_agent + _agent_cache.clear() + + +def test_query_endpoint_handler_configuration_not_loaded(mocker): + """Test the query endpoint handler if configuration is not loaded.""" + # simulate state when no configuration is loaded + mocker.patch( + "app.endpoints.query.configuration", + return_value=mocker.Mock(), + ) + mocker.patch("app.endpoints.query.configuration", None) + + request = None + with pytest.raises(HTTPException) as e: + query_endpoint_handler(request) + assert e.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert e.detail["response"] == "Configuration is not loaded" + + def test_is_transcripts_enabled(setup_configuration, mocker): """Test that is_transcripts_enabled returns True when transcripts is not disabled.""" # Override the transcripts_disabled setting @@ -68,45 +98,30 @@ def test_is_transcripts_disabled(setup_configuration, mocker): assert is_transcripts_enabled() is False, "Transcripts should be disabled" -def test_retrieve_conversation_id(): - """Test the retrieve_conversation_id function.""" - query_request = QueryRequest(query="What is OpenStack?", conversation_id=None) - conversation_id = retrieve_conversation_id(query_request) - - assert conversation_id is not None, "Conversation ID should be generated" - assert len(conversation_id) > 0, "Conversation ID should not be empty" - - -def test_retrieve_conversation_id_existing(): - # Test with an existing conversation ID - existing_conversation_id = "123e4567-e89b-12d3-a456-426614174000" - query_request = QueryRequest( - query="What is OpenStack?", conversation_id=existing_conversation_id - ) - - conversation_id = retrieve_conversation_id(query_request) - - assert ( - conversation_id == existing_conversation_id - ), "Should return the existing conversation ID" - - def _test_query_endpoint_handler(mocker, store_transcript=False): """Test the query endpoint handler.""" mock_client = mocker.Mock() + mock_lsc = mocker.patch("client.LlamaStackClientHolder.get_client") + mock_lsc.return_value = mock_client mock_client.models.list.return_value = [ mocker.Mock(identifier="model1", model_type="llm", provider_id="provider1"), mocker.Mock(identifier="model2", model_type="llm", provider_id="provider2"), ] - mocker.patch( - "app.endpoints.query.configuration", - return_value=mocker.Mock(), + mock_config = mocker.Mock() + mock_config.user_data_collection_configuration.transcripts_disabled = ( + not store_transcript ) + mocker.patch("app.endpoints.query.configuration", mock_config) + llm_response = "LLM answer" + conversation_id = "fake_conversation_id" query = "What is OpenStack?" - mocker.patch("app.endpoints.query.get_llama_stack_client", return_value=mock_client) - mocker.patch("app.endpoints.query.retrieve_response", return_value=llm_response) + + mocker.patch( + "app.endpoints.query.retrieve_response", + return_value=(llm_response, conversation_id), + ) mocker.patch("app.endpoints.query.select_model_id", return_value="fake_model_id") mocker.patch( "app.endpoints.query.is_transcripts_enabled", return_value=store_transcript @@ -115,16 +130,17 @@ def _test_query_endpoint_handler(mocker, store_transcript=False): query_request = QueryRequest(query=query) - response = query_endpoint_handler(query_request) + response = query_endpoint_handler(query_request, auth=MOCK_AUTH) # Assert the response is as expected - assert response.response == "LLM answer" + assert response.response == llm_response + assert response.conversation_id == conversation_id # Assert the store_transcript function is called if transcripts are enabled if store_transcript: mock_transcript.assert_called_once_with( user_id="user_id_placeholder", - conversation_id=mocker.ANY, + conversation_id=conversation_id, query_is_valid=True, query=query, query_request=query_request, @@ -137,7 +153,7 @@ def _test_query_endpoint_handler(mocker, store_transcript=False): mock_transcript.assert_not_called() -def test_query_endpoint_handler(mocker): +def test_query_endpoint_handler_transcript_storage_disabled(mocker): """Test the query endpoint handler with transcript storage disabled.""" _test_query_endpoint_handler(mocker, store_transcript=False) @@ -278,11 +294,10 @@ def test_validate_attachments_metadata_invalid_content_type(): ) -def test_retrieve_response_vector_db_available(mocker): +def test_retrieve_response_vector_db_available(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" - mock_agent = mocker.Mock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.Mock() mock_client.shields.list.return_value = [] mock_vector_db = mocker.Mock() mock_vector_db.identifier = "VectorDB-1" @@ -292,29 +307,33 @@ def test_retrieve_response_vector_db_available(mocker): mock_config = mocker.Mock() mock_config.mcp_servers = [] mocker.patch("app.endpoints.query.configuration", mock_config) - mocker.patch("app.endpoints.query.Agent", return_value=mock_agent) + mocker.patch( + "app.endpoints.query.get_agent", return_value=(mock_agent, "fake_session_id") + ) query_request = QueryRequest(query="What is OpenStack?") model_id = "fake_model_id" access_token = "test_token" - response = retrieve_response(mock_client, model_id, query_request, access_token) + response, conversation_id = retrieve_response( + mock_client, model_id, query_request, access_token + ) assert response == "LLM answer" + assert conversation_id == "fake_session_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(content="What is OpenStack?", role="user")], + session_id="fake_session_id", documents=[], stream=False, toolgroups=get_rag_toolgroups(["VectorDB-1"]), ) -def test_retrieve_response_no_available_shields(mocker): +def test_retrieve_response_no_available_shields(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" - mock_agent = mocker.Mock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.Mock() mock_client.shields.list.return_value = [] mock_client.vector_dbs.list.return_value = [] @@ -322,37 +341,38 @@ def test_retrieve_response_no_available_shields(mocker): mock_config = mocker.Mock() mock_config.mcp_servers = [] mocker.patch("app.endpoints.query.configuration", mock_config) - mocker.patch("app.endpoints.query.Agent", return_value=mock_agent) + mocker.patch( + "app.endpoints.query.get_agent", return_value=(mock_agent, "fake_session_id") + ) query_request = QueryRequest(query="What is OpenStack?") model_id = "fake_model_id" access_token = "test_token" - response = retrieve_response(mock_client, model_id, query_request, access_token) + response, conversation_id = retrieve_response( + mock_client, model_id, query_request, access_token + ) assert response == "LLM answer" + assert conversation_id == "fake_session_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(content="What is OpenStack?", role="user")], + session_id="fake_session_id", documents=[], stream=False, toolgroups=None, ) -def test_retrieve_response_one_available_shield(mocker): +def test_retrieve_response_one_available_shield(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" class MockShield: def __init__(self, identifier): self.identifier = identifier - def identifier(self): - return self.identifier - - mock_agent = mocker.Mock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.Mock() mock_client.shields.list.return_value = [MockShield("shield1")] mock_client.vector_dbs.list.return_value = [] @@ -360,37 +380,38 @@ def identifier(self): mock_config = mocker.Mock() mock_config.mcp_servers = [] mocker.patch("app.endpoints.query.configuration", mock_config) - mocker.patch("app.endpoints.query.Agent", return_value=mock_agent) + mocker.patch( + "app.endpoints.query.get_agent", return_value=(mock_agent, "fake_session_id") + ) query_request = QueryRequest(query="What is OpenStack?") model_id = "fake_model_id" access_token = "test_token" - response = retrieve_response(mock_client, model_id, query_request, access_token) + response, conversation_id = retrieve_response( + mock_client, model_id, query_request, access_token + ) assert response == "LLM answer" + assert conversation_id == "fake_session_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(content="What is OpenStack?", role="user")], + session_id="fake_session_id", documents=[], stream=False, toolgroups=None, ) -def test_retrieve_response_two_available_shields(mocker): +def test_retrieve_response_two_available_shields(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" class MockShield: def __init__(self, identifier): self.identifier = identifier - def identifier(self): - return self.identifier - - mock_agent = mocker.Mock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.Mock() mock_client.shields.list.return_value = [ MockShield("shield1"), MockShield("shield2"), @@ -401,29 +422,33 @@ def identifier(self): mock_config = mocker.Mock() mock_config.mcp_servers = [] mocker.patch("app.endpoints.query.configuration", mock_config) - mocker.patch("app.endpoints.query.Agent", return_value=mock_agent) + mocker.patch( + "app.endpoints.query.get_agent", return_value=(mock_agent, "fake_session_id") + ) query_request = QueryRequest(query="What is OpenStack?") model_id = "fake_model_id" access_token = "test_token" - response = retrieve_response(mock_client, model_id, query_request, access_token) + response, conversation_id = retrieve_response( + mock_client, model_id, query_request, access_token + ) assert response == "LLM answer" + assert conversation_id == "fake_session_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(content="What is OpenStack?", role="user")], + session_id="fake_session_id", documents=[], stream=False, toolgroups=None, ) -def test_retrieve_response_with_one_attachment(mocker): +def test_retrieve_response_with_one_attachment(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" - mock_agent = mocker.Mock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.Mock() mock_client.shields.list.return_value = [] mock_client.vector_dbs.list.return_value = [] @@ -439,18 +464,23 @@ def test_retrieve_response_with_one_attachment(mocker): content="this is attachment", ), ] - mocker.patch("app.endpoints.query.Agent", return_value=mock_agent) + mocker.patch( + "app.endpoints.query.get_agent", return_value=(mock_agent, "fake_session_id") + ) query_request = QueryRequest(query="What is OpenStack?", attachments=attachments) model_id = "fake_model_id" access_token = "test_token" - response = retrieve_response(mock_client, model_id, query_request, access_token) + response, conversation_id = retrieve_response( + mock_client, model_id, query_request, access_token + ) assert response == "LLM answer" + assert conversation_id == "fake_session_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(content="What is OpenStack?", role="user")], + session_id="fake_session_id", stream=False, documents=[ { @@ -462,11 +492,10 @@ def test_retrieve_response_with_one_attachment(mocker): ) -def test_retrieve_response_with_two_attachments(mocker): +def test_retrieve_response_with_two_attachments(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" - mock_agent = mocker.Mock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.Mock() mock_client.shields.list.return_value = [] mock_client.vector_dbs.list.return_value = [] @@ -487,18 +516,23 @@ def test_retrieve_response_with_two_attachments(mocker): content="kind: Pod\n metadata:\n name: private-reg", ), ] - mocker.patch("app.endpoints.query.Agent", return_value=mock_agent) + mocker.patch( + "app.endpoints.query.get_agent", return_value=(mock_agent, "fake_session_id") + ) query_request = QueryRequest(query="What is OpenStack?", attachments=attachments) model_id = "fake_model_id" access_token = "test_token" - response = retrieve_response(mock_client, model_id, query_request, access_token) + response, conversation_id = retrieve_response( + mock_client, model_id, query_request, access_token + ) assert response == "LLM answer" + assert conversation_id == "fake_session_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(content="What is OpenStack?", role="user")], + session_id="fake_session_id", stream=False, documents=[ { @@ -514,11 +548,10 @@ def test_retrieve_response_with_two_attachments(mocker): ) -def test_retrieve_response_with_mcp_servers(mocker): +def test_retrieve_response_with_mcp_servers(prepare_agent_mocks, mocker): """Test the retrieve_response function with MCP servers configured.""" - mock_agent = mocker.Mock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.Mock() mock_client.shields.list.return_value = [] mock_client.vector_dbs.list.return_value = [] @@ -536,46 +569,105 @@ def test_retrieve_response_with_mcp_servers(mocker): mock_config = mocker.Mock() mock_config.mcp_servers = mcp_servers mocker.patch("app.endpoints.query.configuration", mock_config) - mock_agent_class = mocker.patch( - "app.endpoints.query.Agent", return_value=mock_agent + mock_get_agent = mocker.patch( + "app.endpoints.query.get_agent", return_value=(mock_agent, "fake_session_id") ) query_request = QueryRequest(query="What is OpenStack?") model_id = "fake_model_id" access_token = "test_token_123" - response = retrieve_response(mock_client, model_id, query_request, access_token) + response, conversation_id = retrieve_response( + mock_client, model_id, query_request, access_token + ) assert response == "LLM answer" + assert conversation_id == "fake_session_id" + + # Verify get_agent was called with the correct parameters + mock_get_agent.assert_called_once_with( + mock_client, + model_id, + mocker.ANY, # system_prompt + [], # available_shields + None, # conversation_id + ) - # Verify Agent was created with MCP server tools and headers - mock_agent_class.assert_called_once() - agent_kwargs = mock_agent_class.call_args[1] + # Check that the agent's extra_headers property was set correctly + expected_extra_headers = { + "X-LlamaStack-Provider-Data": json.dumps( + { + "mcp_headers": { + "http://localhost:3000": {"Authorization": "Bearer test_token_123"}, + "https://git.example.com/mcp": { + "Authorization": "Bearer test_token_123" + }, + } + } + ) + } + assert mock_agent.extra_headers == expected_extra_headers + + # Check that create_turn was called with the correct parameters + mock_agent.create_turn.assert_called_once_with( + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="fake_session_id", + documents=[], + stream=False, + toolgroups=[mcp_server.name for mcp_server in mcp_servers], + ) - # Check that tools include MCP server names - assert "filesystem-server" in agent_kwargs["tools"] - assert "git-server" in agent_kwargs["tools"] - # Check that extra_headers contains MCP headers with authorization +def test_retrieve_response_with_mcp_servers_empty_token(prepare_agent_mocks, mocker): + """Test the retrieve_response function with MCP servers and empty access token.""" + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_turn.return_value.output_message.content = "LLM answer" + mock_client.shields.list.return_value = [] + mock_client.vector_dbs.list.return_value = [] - extra_headers_data = json.loads( - agent_kwargs["extra_headers"]["X-LlamaStack-Provider-Data"] + # Mock configuration with MCP servers + mcp_servers = [ + ModelContextProtocolServer(name="test-server", url="http://localhost:8080"), + ] + mock_config = mocker.Mock() + mock_config.mcp_servers = mcp_servers + mocker.patch("app.endpoints.query.configuration", mock_config) + mock_get_agent = mocker.patch( + "app.endpoints.query.get_agent", return_value=(mock_agent, "fake_session_id") ) - mcp_headers = extra_headers_data["mcp_headers"] - assert "http://localhost:3000" in mcp_headers - assert ( - mcp_headers["http://localhost:3000"]["Authorization"] == "Bearer test_token_123" + query_request = QueryRequest(query="What is OpenStack?") + model_id = "fake_model_id" + access_token = "" # Empty token + + response, conversation_id = retrieve_response( + mock_client, model_id, query_request, access_token ) - assert "https://git.example.com/mcp" in mcp_headers - assert ( - mcp_headers["https://git.example.com/mcp"]["Authorization"] - == "Bearer test_token_123" + + assert response == "LLM answer" + assert conversation_id == "fake_session_id" + + # Verify get_agent was called with the correct parameters + mock_get_agent.assert_called_once_with( + mock_client, + model_id, + mocker.ANY, # system_prompt + [], # available_shields + None, # conversation_id ) + # Check that create_turn was called with the correct parameters + mock_agent.create_turn.assert_called_once_with( + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="fake_session_id", + documents=[], + stream=False, + toolgroups=[mcp_server.name for mcp_server in mcp_servers], + ) -def test_retrieve_response_with_mcp_servers_empty_token(mocker): - """Test the retrieve_response function with MCP servers and empty access token.""" + +def test_retrieve_response_with_mcp_servers_and_mcp_headers(mocker): + """Test the retrieve_response function with MCP servers configured.""" mock_agent = mocker.Mock() mock_agent.create_turn.return_value.output_message.content = "LLM answer" mock_client = mocker.Mock() @@ -584,37 +676,84 @@ def test_retrieve_response_with_mcp_servers_empty_token(mocker): # Mock configuration with MCP servers mcp_servers = [ - ModelContextProtocolServer(name="test-server", url="http://localhost:8080"), + ModelContextProtocolServer( + name="filesystem-server", url="http://localhost:3000" + ), + ModelContextProtocolServer( + name="git-server", + provider_id="custom-git", + url="https://git.example.com/mcp", + ), ] mock_config = mocker.Mock() mock_config.mcp_servers = mcp_servers mocker.patch("app.endpoints.query.configuration", mock_config) - mock_agent_class = mocker.patch( - "app.endpoints.query.Agent", return_value=mock_agent + mock_get_agent = mocker.patch( + "app.endpoints.query.get_agent", return_value=(mock_agent, "fake_session_id") ) query_request = QueryRequest(query="What is OpenStack?") model_id = "fake_model_id" - access_token = "" # Empty token + access_token = "" + mcp_headers = { + "filesystem-server": {"Authorization": "Bearer test_token_123"}, + "git-server": {"Authorization": "Bearer test_token_456"}, + "http://another-server-mcp-server:3000": { + "Authorization": "Bearer test_token_789" + }, + "unknown-mcp-server": { + "Authorization": "Bearer test_token_for_unknown-mcp-server" + }, + } - response = retrieve_response(mock_client, model_id, query_request, access_token) + response, conversation_id = retrieve_response( + mock_client, + model_id, + query_request, + access_token, + mcp_headers=mcp_headers, + ) assert response == "LLM answer" + assert conversation_id == "fake_session_id" + + # Verify get_agent was called with the correct parameters + mock_get_agent.assert_called_once_with( + mock_client, + model_id, + mocker.ANY, # system_prompt + [], # available_shields + None, # conversation_id + ) - # Verify Agent was created with MCP server tools and empty bearer header - mock_agent_class.assert_called_once() - agent_kwargs = mock_agent_class.call_args[1] + expected_mcp_headers = { + "http://localhost:3000": {"Authorization": "Bearer test_token_123"}, + "https://git.example.com/mcp": {"Authorization": "Bearer test_token_456"}, + "http://another-server-mcp-server:3000": { + "Authorization": "Bearer test_token_789" + }, + # we do not put "unknown-mcp-server" url as it's unknown to lightspeed-stack + } - # Check that tools include MCP server names - assert "test-server" in agent_kwargs["tools"] + # Check that the agent's extra_headers property was set correctly + expected_extra_headers = { + "X-LlamaStack-Provider-Data": json.dumps( + { + "mcp_headers": expected_mcp_headers, + } + ) + } - # Check that extra_headers contains MCP headers with empty authorization + assert mock_agent.extra_headers == expected_extra_headers - extra_headers_data = json.loads( - agent_kwargs["extra_headers"]["X-LlamaStack-Provider-Data"] + # Check that create_turn was called with the correct parameters + mock_agent.create_turn.assert_called_once_with( + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="fake_session_id", + documents=[], + stream=False, + toolgroups=[mcp_server.name for mcp_server in mcp_servers], ) - mcp_headers = extra_headers_data["mcp_headers"] - assert len(mcp_headers) == 0 def test_construct_transcripts_path(setup_configuration, mocker): @@ -704,3 +843,323 @@ def test_get_rag_toolgroups(mocker): assert len(result) == 1 assert result[0]["name"] == "builtin::rag/knowledge_search" assert result[0]["args"]["vector_db_ids"] == vector_db_ids + + +def test_query_endpoint_handler_on_connection_error(mocker): + """Test the query endpoint handler.""" + mocker.patch( + "app.endpoints.query.configuration", + return_value=mocker.Mock(), + ) + + # construct mocked query + query = "What is OpenStack?" + query_request = QueryRequest(query=query) + + # simulate situation when it is not possible to connect to Llama Stack + mock_lsc = mocker.Mock() + mock_lsc.get_client.side_effect = APIConnectionError(request=query_request) + + with pytest.raises(Exception): + query_endpoint_handler(query_request) + + +def test_get_agent_cache_hit(prepare_agent_mocks, mocker): + """Test get_agent function when agent exists in cache.""" + mock_client, mock_agent = prepare_agent_mocks + + # Set up cache with existing agent + conversation_id = "test_conversation_id" + _agent_cache[conversation_id] = mock_agent + + result_agent, result_conversation_id = get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=["shield1"], + conversation_id=conversation_id, + ) + + # Assert cached agent is returned + assert result_agent == mock_agent + assert result_conversation_id == conversation_id + + +def test_get_agent_cache_miss_with_conversation_id( + setup_configuration, prepare_agent_mocks, mocker +): + """Test get_agent function when conversation_id is provided but agent not in cache.""" + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_session.return_value = "new_session_id" + + # Mock Agent class + mock_agent_class = mocker.patch( + "app.endpoints.query.Agent", return_value=mock_agent + ) + + # Mock get_suid + mocker.patch("app.endpoints.query.get_suid", return_value="new_session_id") + + # Mock configuration + mock_mcp_server = mocker.Mock() + mock_mcp_server.name = "mcp_server_1" + mocker.patch.object( + type(setup_configuration), + "mcp_servers", + new_callable=mocker.PropertyMock, + return_value=[mock_mcp_server], + ) + mocker.patch("app.endpoints.query.configuration", setup_configuration) + + # Call function with conversation_id but no cached agent + result_agent, result_conversation_id = get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=["shield1"], + conversation_id="non_existent_conversation_id", + ) + + # Assert new agent is created + assert result_agent == mock_agent + assert result_conversation_id == "new_session_id" + + # Verify Agent was created with correct parameters + mock_agent_class.assert_called_once_with( + mock_client, + model="test_model", + instructions="test_prompt", + input_shields=["shield1"], + tool_parser=None, + enable_session_persistence=True, + ) + + # Verify agent was stored in cache + assert _agent_cache["new_session_id"] == mock_agent + + +def test_get_agent_no_conversation_id(setup_configuration, prepare_agent_mocks, mocker): + """Test get_agent function when conversation_id is None.""" + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_session.return_value = "new_session_id" + + # Mock Agent class + mock_agent_class = mocker.patch( + "app.endpoints.query.Agent", return_value=mock_agent + ) + + # Mock get_suid + mocker.patch("app.endpoints.query.get_suid", return_value="new_session_id") + + # Mock configuration + mock_mcp_server = mocker.Mock() + mock_mcp_server.name = "mcp_server_1" + mocker.patch.object( + type(setup_configuration), + "mcp_servers", + new_callable=mocker.PropertyMock, + return_value=[mock_mcp_server], + ) + mocker.patch("app.endpoints.query.configuration", setup_configuration) + + # Call function with None conversation_id + result_agent, result_conversation_id = get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=["shield1"], + conversation_id=None, + ) + + # Assert new agent is created + assert result_agent == mock_agent + assert result_conversation_id == "new_session_id" + + # Verify Agent was created with correct parameters + mock_agent_class.assert_called_once_with( + mock_client, + model="test_model", + instructions="test_prompt", + input_shields=["shield1"], + tool_parser=None, + enable_session_persistence=True, + ) + + # Verify agent was stored in cache + assert _agent_cache["new_session_id"] == mock_agent + + +def test_get_agent_empty_shields(setup_configuration, prepare_agent_mocks, mocker): + """Test get_agent function with empty shields list.""" + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_session.return_value = "new_session_id" + + # Mock Agent class + mock_agent_class = mocker.patch( + "app.endpoints.query.Agent", return_value=mock_agent + ) + + # Mock get_suid + mocker.patch("app.endpoints.query.get_suid", return_value="new_session_id") + + # Mock configuration + mock_mcp_server = mocker.Mock() + mock_mcp_server.name = "mcp_server_1" + mocker.patch.object( + type(setup_configuration), + "mcp_servers", + new_callable=mocker.PropertyMock, + return_value=[mock_mcp_server], + ) + mocker.patch("app.endpoints.query.configuration", setup_configuration) + + # Call function with empty shields list + result_agent, result_conversation_id = get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=[], + conversation_id=None, + ) + + # Assert new agent is created + assert result_agent == mock_agent + assert result_conversation_id == "new_session_id" + + # Verify Agent was created with empty shields + mock_agent_class.assert_called_once_with( + mock_client, + model="test_model", + instructions="test_prompt", + input_shields=[], + tool_parser=None, + enable_session_persistence=True, + ) + + +def test_get_agent_multiple_mcp_servers( + setup_configuration, prepare_agent_mocks, mocker +): + """Test get_agent function with multiple MCP servers.""" + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_session.return_value = "new_session_id" + + # Mock Agent class + mock_agent_class = mocker.patch( + "app.endpoints.query.Agent", return_value=mock_agent + ) + + # Mock get_suid + mocker.patch("app.endpoints.query.get_suid", return_value="new_session_id") + + # Mock configuration with multiple MCP servers + mock_mcp_server1 = mocker.Mock() + mock_mcp_server1.name = "mcp_server_1" + mock_mcp_server2 = mocker.Mock() + mock_mcp_server2.name = "mcp_server_2" + mocker.patch.object( + type(setup_configuration), + "mcp_servers", + new_callable=mocker.PropertyMock, + return_value=[mock_mcp_server1, mock_mcp_server2], + ) + mocker.patch("app.endpoints.query.configuration", setup_configuration) + + # Call function + result_agent, result_conversation_id = get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=["shield1", "shield2"], + conversation_id=None, + ) + + # Assert new agent is created + assert result_agent == mock_agent + assert result_conversation_id == "new_session_id" + + # Verify Agent was created with tools from both MCP servers + mock_agent_class.assert_called_once_with( + mock_client, + model="test_model", + instructions="test_prompt", + input_shields=["shield1", "shield2"], + tool_parser=None, + enable_session_persistence=True, + ) + + +def test_get_agent_session_persistence_enabled( + setup_configuration, prepare_agent_mocks, mocker +): + """Test get_agent function ensures session persistence is enabled.""" + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_session.return_value = "new_session_id" + + # Mock Agent class + mock_agent_class = mocker.patch( + "app.endpoints.query.Agent", return_value=mock_agent + ) + + # Mock get_suid + mocker.patch("app.endpoints.query.get_suid", return_value="new_session_id") + + # Mock configuration + mock_mcp_server = mocker.Mock() + mock_mcp_server.name = "mcp_server_1" + mocker.patch.object( + type(setup_configuration), + "mcp_servers", + new_callable=mocker.PropertyMock, + return_value=[mock_mcp_server], + ) + mocker.patch("app.endpoints.query.configuration", setup_configuration) + + # Call function + get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=["shield1"], + conversation_id=None, + ) + + # Verify Agent was created with session persistence enabled + mock_agent_class.assert_called_once_with( + mock_client, + model="test_model", + instructions="test_prompt", + input_shields=["shield1"], + tool_parser=None, + enable_session_persistence=True, + ) + + +def test_auth_tuple_unpacking_in_query_endpoint_handler(mocker): + """Test that auth tuple is correctly unpacked in query endpoint handler.""" + # Mock dependencies + mock_config = mocker.Mock() + mock_config.llama_stack_configuration = mocker.Mock() + mocker.patch("app.endpoints.query.configuration", mock_config) + + mock_client = mocker.Mock() + mock_client.models.list.return_value = [ + mocker.Mock(identifier="model1", model_type="llm", provider_id="provider1") + ] + mocker.patch("client.LlamaStackClientHolder.get_client", return_value=mock_client) + + mock_retrieve_response = mocker.patch( + "app.endpoints.query.retrieve_response", + return_value=("test response", "test_conversation_id"), + ) + + mocker.patch("app.endpoints.query.select_model_id", return_value="test_model") + mocker.patch("app.endpoints.query.is_transcripts_enabled", return_value=False) + + _ = query_endpoint_handler( + QueryRequest(query="test query"), + auth=("user123", "username", "auth_token_123"), + mcp_headers=None, + ) + + assert mock_retrieve_response.call_args[0][3] == "auth_token_123" diff --git a/tests/unit/app/endpoints/test_streaming_query.py b/tests/unit/app/endpoints/test_streaming_query.py index a0c15e96a..8b680c430 100644 --- a/tests/unit/app/endpoints/test_streaming_query.py +++ b/tests/unit/app/endpoints/test_streaming_query.py @@ -1,18 +1,141 @@ import pytest +import json +from fastapi import HTTPException, status +from llama_stack_client.types.shared.interleaved_content_item import TextContentItem + +from configuration import AppConfig from app.endpoints.query import get_rag_toolgroups from app.endpoints.streaming_query import ( streaming_query_endpoint_handler, retrieve_response, stream_build_event, + get_agent, + _agent_cache, ) +from llama_stack_client import APIConnectionError from models.requests import QueryRequest, Attachment +from models.config import ModelContextProtocolServer from llama_stack_client.types import UserMessage # type: ignore +MOCK_AUTH = ("mock_user_id", "mock_username", "mock_token") + + +SAMPLE_KNOWLEDGE_SEARCH_RESULTS = [ + """knowledge_search tool found 2 chunks: +BEGIN of knowledge_search tool results. +""", + """Result 1 +Content: ABC +Metadata: {'docs_url': 'https://example.com/doc1', 'title': 'Doc1', 'document_id': 'doc-1'} +""", + """Result 2 +Content: ABC +Metadata: {'docs_url': 'https://example.com/doc2', 'title': 'Doc2', 'document_id': 'doc-2'} +""", + """END of knowledge_search tool results. +""", + # Following metadata contains an intentionally incorrect keyword "Title" (instead of "title") + # and it is not picked as a referenced document. + """Result 3 +Content: ABC +Metadata: {'docs_url': 'https://example.com/doc3', 'Title': 'Doc3', 'document_id': 'doc-3'} +""", + """The above results were retrieved to help answer the user\'s query: "Sample Query". +Use them as supporting information only in answering this query. +""", +] + + +@pytest.fixture(autouse=True) +def setup_configuration(): + """Set up configuration for tests.""" + config_dict = { + "name": "test", + "service": { + "host": "localhost", + "port": 8080, + "auth_enabled": False, + "workers": 1, + "color_log": True, + "access_log": True, + }, + "llama_stack": { + "api_key": "test-key", + "url": "http://test.com:1234", + "use_as_library_client": False, + }, + "user_data_collection": { + "transcripts_disabled": True, + }, + "mcp_servers": [], + } + cfg = AppConfig() + cfg.init_from_dict(config_dict) + return cfg + + +@pytest.fixture(autouse=True) +def prepare_agent_mocks(mocker): + mock_client = mocker.AsyncMock() + mock_agent = mocker.AsyncMock() + """Cleanup agent cache after tests.""" + yield mock_client, mock_agent + _agent_cache.clear() + + +@pytest.mark.asyncio +async def test_streaming_query_endpoint_handler_configuration_not_loaded(mocker): + """Test the streaming query endpoint handler if configuration is not loaded.""" + # simulate state when no configuration is loaded + mocker.patch( + "app.endpoints.streaming_query.configuration", + return_value=mocker.Mock(), + ) + mocker.patch("app.endpoints.streaming_query.configuration", None) + + query = "What is OpenStack?" + query_request = QueryRequest(query=query) + + # await the async function + with pytest.raises(HTTPException) as e: + await streaming_query_endpoint_handler(None, query_request, auth=MOCK_AUTH) + assert e.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert e.detail["response"] == "Configuration is not loaded" + + +@pytest.mark.asyncio +async def test_streaming_query_endpoint_on_connection_error(mocker): + """Test the streaming query endpoint handler if connection can not be established.""" + # simulate state when no configuration is loaded + mocker.patch( + "app.endpoints.streaming_query.configuration", + return_value=mocker.Mock(), + ) + + query = "What is OpenStack?" + query_request = QueryRequest(query=query) + + # simulate situation when it is not possible to connect to Llama Stack + mock_client = mocker.AsyncMock() + mock_client.models.side_effect = APIConnectionError(request=query_request) + mock_lsc = mocker.patch("client.LlamaStackClientHolder.get_client") + mock_lsc.return_value = mock_client + mock_async_lsc = mocker.patch("client.AsyncLlamaStackClientHolder.get_client") + mock_async_lsc.return_value = mock_client + + # await the async function + with pytest.raises(HTTPException) as e: + await streaming_query_endpoint_handler(None, query_request, auth=MOCK_AUTH) + assert e.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR + assert e.detail["response"] == "Configuration is not loaded" + async def _test_streaming_query_endpoint_handler(mocker, store_transcript=False): """Test the streaming query endpoint handler.""" mock_client = mocker.AsyncMock() + mock_async_lsc = mocker.patch("client.AsyncLlamaStackClientHolder.get_client") + mock_async_lsc.return_value = mock_client mock_client.models.list.return_value = [ mocker.Mock(identifier="model1", model_type="llm", provider_id="provider1"), mocker.Mock(identifier="model2", model_type="llm", provider_id="provider2"), @@ -30,20 +153,37 @@ async def _test_streaming_query_endpoint_handler(mocker, store_transcript=False) ) ) ), + mocker.Mock( + event=mocker.Mock( + payload=mocker.Mock( + event_type="step_complete", + step_type="tool_execution", + step_details=mocker.Mock( + step_type="tool_execution", + tool_responses=[ + mocker.Mock( + tool_name="knowledge_search", + content=[ + TextContentItem(text=s, type="text") + for s in SAMPLE_KNOWLEDGE_SEARCH_RESULTS + ], + ) + ], + tool_calls=[ + mocker.Mock( + tool_name="knowledge_search", + ) + ], + ), + ) + ) + ), ] - mocker.patch( - "app.endpoints.streaming_query.configuration", - return_value=mocker.Mock(), - ) query = "What is OpenStack?" - mocker.patch( - "app.endpoints.streaming_query.get_async_llama_stack_client", - return_value=mock_client, - ) mocker.patch( "app.endpoints.streaming_query.retrieve_response", - return_value=mock_streaming_response, + return_value=(mock_streaming_response, "test_conversation_id"), ) mocker.patch( "app.endpoints.streaming_query.select_model_id", return_value="fake_model_id" @@ -62,7 +202,7 @@ async def _test_streaming_query_endpoint_handler(mocker, store_transcript=False) # Await the async function response = await streaming_query_endpoint_handler( - None, query_request, auth="mock_auth" + None, query_request, auth=MOCK_AUTH ) # Assert the response is a StreamingResponse @@ -86,15 +226,22 @@ async def _test_streaming_query_endpoint_handler(mocker, store_transcript=False) assert '"event": "end"' in full_content assert "LLM answer" in full_content + # Assert referenced documents + assert len(streaming_content) == 4 + d = json.loads(streaming_content[3][5:]) + referenced_documents = d["data"]["referenced_documents"] + assert len(referenced_documents) == 2 + assert referenced_documents[1]["doc_title"] == "Doc2" + # Assert the store_transcript function is called if transcripts are enabled if store_transcript: mock_transcript.assert_called_once_with( user_id="user_id_placeholder", - conversation_id=mocker.ANY, + conversation_id="test_conversation_id", query_is_valid=True, query=query, query_request=query_request, - response="LLM answer", + response="LLM answerknowledge_search", attachments=[], rag_chunks=[], truncated=False, @@ -115,61 +262,81 @@ async def test_streaming_query_endpoint_handler_store_transcript(mocker): await _test_streaming_query_endpoint_handler(mocker, store_transcript=True) -async def test_retrieve_response_vector_db_available(mocker): +async def test_retrieve_response_vector_db_available(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" - mock_agent = mocker.AsyncMock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.AsyncMock() mock_client.shields.list.return_value = [] mock_vector_db = mocker.Mock() mock_vector_db.identifier = "VectorDB-1" mock_client.vector_dbs.list.return_value = [mock_vector_db] - mocker.patch("app.endpoints.streaming_query.AsyncAgent", return_value=mock_agent) + # Mock configuration with empty MCP servers + mock_config = mocker.Mock() + mock_config.mcp_servers = [] + mocker.patch("app.endpoints.streaming_query.configuration", mock_config) + mocker.patch( + "app.endpoints.streaming_query.get_agent", + return_value=(mock_agent, "test_conversation_id"), + ) query_request = QueryRequest(query="What is OpenStack?") model_id = "fake_model_id" + token = "test_token" - response = await retrieve_response(mock_client, model_id, query_request) + response, conversation_id = await retrieve_response( + mock_client, model_id, query_request, token + ) - # For streaming, the response should be the streaming object + # For streaming, the response should be the streaming object and conversation_id should be returned assert response is not None + assert conversation_id == "test_conversation_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="test_conversation_id", documents=[], stream=True, # Should be True for streaming endpoint toolgroups=get_rag_toolgroups(["VectorDB-1"]), ) -async def test_retrieve_response_no_available_shields(mocker): +async def test_retrieve_response_no_available_shields(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" - mock_agent = mocker.AsyncMock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.AsyncMock() mock_client.shields.list.return_value = [] mock_client.vector_dbs.list.return_value = [] - mocker.patch("app.endpoints.streaming_query.AsyncAgent", return_value=mock_agent) + # Mock configuration with empty MCP servers + mock_config = mocker.Mock() + mock_config.mcp_servers = [] + mocker.patch("app.endpoints.streaming_query.configuration", mock_config) + mocker.patch( + "app.endpoints.streaming_query.get_agent", + return_value=(mock_agent, "test_conversation_id"), + ) query_request = QueryRequest(query="What is OpenStack?") model_id = "fake_model_id" + token = "test_token" - response = await retrieve_response(mock_client, model_id, query_request) + response, conversation_id = await retrieve_response( + mock_client, model_id, query_request, token + ) - # For streaming, the response should be the streaming object + # For streaming, the response should be the streaming object and conversation_id should be returned assert response is not None + assert conversation_id == "test_conversation_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="test_conversation_id", documents=[], stream=True, # Should be True for streaming endpoint toolgroups=None, ) -async def test_retrieve_response_one_available_shield(mocker): +async def test_retrieve_response_one_available_shield(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" class MockShield: @@ -179,29 +346,40 @@ def __init__(self, identifier): def identifier(self): return self.identifier - mock_agent = mocker.AsyncMock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.AsyncMock() mock_client.shields.list.return_value = [MockShield("shield1")] + mock_client.vector_dbs.list.return_value = [] - mocker.patch("app.endpoints.streaming_query.AsyncAgent", return_value=mock_agent) + # Mock configuration with empty MCP servers + mock_config = mocker.Mock() + mock_config.mcp_servers = [] + mocker.patch("app.endpoints.streaming_query.configuration", mock_config) + mocker.patch( + "app.endpoints.streaming_query.get_agent", + return_value=(mock_agent, "test_conversation_id"), + ) query_request = QueryRequest(query="What is OpenStack?") model_id = "fake_model_id" + token = "test_token" - response = await retrieve_response(mock_client, model_id, query_request) + response, conversation_id = await retrieve_response( + mock_client, model_id, query_request, token + ) assert response is not None + assert conversation_id == "test_conversation_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="test_conversation_id", documents=[], stream=True, # Should be True for streaming endpoint toolgroups=None, ) -async def test_retrieve_response_two_available_shields(mocker): +async def test_retrieve_response_two_available_shields(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" class MockShield: @@ -211,40 +389,54 @@ def __init__(self, identifier): def identifier(self): return self.identifier - mock_agent = mocker.AsyncMock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.AsyncMock() mock_client.shields.list.return_value = [ MockShield("shield1"), MockShield("shield2"), ] mock_client.vector_dbs.list.return_value = [] - mocker.patch("app.endpoints.streaming_query.AsyncAgent", return_value=mock_agent) + # Mock configuration with empty MCP servers + mock_config = mocker.Mock() + mock_config.mcp_servers = [] + mocker.patch("app.endpoints.streaming_query.configuration", mock_config) + mocker.patch( + "app.endpoints.streaming_query.get_agent", + return_value=(mock_agent, "test_conversation_id"), + ) query_request = QueryRequest(query="What is OpenStack?") model_id = "fake_model_id" + token = "test_token" - response = await retrieve_response(mock_client, model_id, query_request) + response, conversation_id = await retrieve_response( + mock_client, model_id, query_request, token + ) assert response is not None + assert conversation_id == "test_conversation_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="test_conversation_id", documents=[], stream=True, # Should be True for streaming endpoint toolgroups=None, ) -async def test_retrieve_response_with_one_attachment(mocker): +async def test_retrieve_response_with_one_attachment(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" - mock_agent = mocker.AsyncMock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.AsyncMock() mock_client.shields.list.return_value = [] mock_client.vector_dbs.list.return_value = [] + # Mock configuration with empty MCP servers + mock_config = mocker.Mock() + mock_config.mcp_servers = [] + mocker.patch("app.endpoints.streaming_query.configuration", mock_config) + attachments = [ Attachment( attachment_type="log", @@ -252,17 +444,24 @@ async def test_retrieve_response_with_one_attachment(mocker): content="this is attachment", ), ] - mocker.patch("app.endpoints.streaming_query.AsyncAgent", return_value=mock_agent) + mocker.patch( + "app.endpoints.streaming_query.get_agent", + return_value=(mock_agent, "test_conversation_id"), + ) query_request = QueryRequest(query="What is OpenStack?", attachments=attachments) model_id = "fake_model_id" + token = "test_token" - response = await retrieve_response(mock_client, model_id, query_request) + response, conversation_id = await retrieve_response( + mock_client, model_id, query_request, token + ) assert response is not None + assert conversation_id == "test_conversation_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="test_conversation_id", stream=True, # Should be True for streaming endpoint documents=[ { @@ -274,14 +473,18 @@ async def test_retrieve_response_with_one_attachment(mocker): ) -async def test_retrieve_response_with_two_attachments(mocker): +async def test_retrieve_response_with_two_attachments(prepare_agent_mocks, mocker): """Test the retrieve_response function.""" - mock_agent = mocker.AsyncMock() + mock_client, mock_agent = prepare_agent_mocks mock_agent.create_turn.return_value.output_message.content = "LLM answer" - mock_client = mocker.AsyncMock() mock_client.shields.list.return_value = [] mock_client.vector_dbs.list.return_value = [] + # Mock configuration with empty MCP servers + mock_config = mocker.Mock() + mock_config.mcp_servers = [] + mocker.patch("app.endpoints.streaming_query.configuration", mock_config) + attachments = [ Attachment( attachment_type="log", @@ -294,17 +497,24 @@ async def test_retrieve_response_with_two_attachments(mocker): content="kind: Pod\n metadata:\n name: private-reg", ), ] - mocker.patch("app.endpoints.streaming_query.AsyncAgent", return_value=mock_agent) + mocker.patch( + "app.endpoints.streaming_query.get_agent", + return_value=(mock_agent, "test_conversation_id"), + ) query_request = QueryRequest(query="What is OpenStack?", attachments=attachments) model_id = "fake_model_id" + token = "test_token" - response = await retrieve_response(mock_client, model_id, query_request) + response, conversation_id = await retrieve_response( + mock_client, model_id, query_request, token + ) assert response is not None + assert conversation_id == "test_conversation_id" mock_agent.create_turn.assert_called_once_with( - messages=[UserMessage(content="What is OpenStack?", role="user", context=None)], - session_id=mocker.ANY, + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="test_conversation_id", stream=True, # Should be True for streaming endpoint documents=[ { @@ -332,7 +542,7 @@ def test_stream_build_event_step_progress(mocker): mock_chunk.event.payload.delta.text = "This is a test response" chunk_id = 0 - result = stream_build_event(mock_chunk, chunk_id) + result = stream_build_event(mock_chunk, chunk_id, {}) assert result is not None assert "data: " in result @@ -345,24 +555,39 @@ def test_stream_build_event_step_progress(mocker): def test_stream_build_event_step_complete(mocker): """Test stream_build_event function with step_complete event type.""" # Create a properly nested mock chunk structure - mock_chunk = mocker.Mock() - mock_chunk.event = mocker.Mock() - mock_chunk.event.payload = mocker.Mock() - mock_chunk.event.payload.event_type = "step_complete" - mock_chunk.event.payload.step_type = "tool_execution" - mock_chunk.event.payload.step_details = mocker.Mock() - mock_chunk.event.payload.step_details.step_type = "tool_execution" - mock_chunk.event.payload.step_details.tool_calls = [ - mocker.Mock(tool_name="search_tool") - ] + mock_chunk = mocker.Mock( + event=mocker.Mock( + payload=mocker.Mock( + event_type="step_complete", + step_type="tool_execution", + step_details=mocker.Mock( + step_type="tool_execution", + tool_responses=[ + mocker.Mock( + tool_name="knowledge_search", + content=[ + TextContentItem(text=s, type="text") + for s in SAMPLE_KNOWLEDGE_SEARCH_RESULTS + ], + ) + ], + tool_calls=[ + mocker.Mock( + tool_name="knowledge_search", + ) + ], + ), + ) + ) + ) chunk_id = 0 - result = stream_build_event(mock_chunk, chunk_id) + result = stream_build_event(mock_chunk, chunk_id, {}) assert result is not None assert "data: " in result assert '"event": "token"' in result - assert '"token": "search_tool"' in result + assert '"token": "knowledge_search"' in result assert '"role": "tool_execution"' in result assert '"id": 0' in result @@ -375,6 +600,560 @@ def test_stream_build_event_returns_none(mocker): # Deliberately not setting payload attribute chunk_id = 0 - result = stream_build_event(mock_chunk, chunk_id) + result = stream_build_event(mock_chunk, chunk_id, {}) assert result is None + + +async def test_retrieve_response_with_mcp_servers(prepare_agent_mocks, mocker): + """Test the retrieve_response function with MCP servers configured.""" + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_turn.return_value.output_message.content = "LLM answer" + mock_client.shields.list.return_value = [] + mock_client.vector_dbs.list.return_value = [] + + # Mock configuration with MCP servers + mcp_servers = [ + ModelContextProtocolServer( + name="filesystem-server", url="http://localhost:3000" + ), + ModelContextProtocolServer( + name="git-server", + provider_id="custom-git", + url="https://git.example.com/mcp", + ), + ] + mock_config = mocker.Mock() + mock_config.mcp_servers = mcp_servers + mocker.patch("app.endpoints.streaming_query.configuration", mock_config) + mock_get_agent = mocker.patch( + "app.endpoints.streaming_query.get_agent", + return_value=(mock_agent, "test_conversation_id"), + ) + + query_request = QueryRequest(query="What is OpenStack?") + model_id = "fake_model_id" + access_token = "test_token_123" + + response, conversation_id = await retrieve_response( + mock_client, model_id, query_request, access_token + ) + + assert response is not None + assert conversation_id == "test_conversation_id" + + # Verify get_agent was called with the correct parameters + mock_get_agent.assert_called_once_with( + mock_client, + model_id, + mocker.ANY, # system_prompt + [], # available_shields + None, # conversation_id + ) + + # Check that the agent's extra_headers property was set correctly + expected_extra_headers = { + "X-LlamaStack-Provider-Data": json.dumps( + { + "mcp_headers": { + "http://localhost:3000": {"Authorization": "Bearer test_token_123"}, + "https://git.example.com/mcp": { + "Authorization": "Bearer test_token_123" + }, + } + } + ) + } + assert mock_agent.extra_headers == expected_extra_headers + + # Check that create_turn was called with the correct parameters + mock_agent.create_turn.assert_called_once_with( + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="test_conversation_id", + documents=[], + stream=True, + toolgroups=[mcp_server.name for mcp_server in mcp_servers], + ) + + +async def test_retrieve_response_with_mcp_servers_empty_token( + prepare_agent_mocks, mocker +): + """Test the retrieve_response function with MCP servers and empty access token.""" + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_turn.return_value.output_message.content = "LLM answer" + mock_client.shields.list.return_value = [] + mock_client.vector_dbs.list.return_value = [] + + # Mock configuration with MCP servers + mcp_servers = [ + ModelContextProtocolServer(name="test-server", url="http://localhost:8080"), + ] + mock_config = mocker.Mock() + mock_config.mcp_servers = mcp_servers + mocker.patch("app.endpoints.streaming_query.configuration", mock_config) + mock_get_agent = mocker.patch( + "app.endpoints.streaming_query.get_agent", + return_value=(mock_agent, "test_conversation_id"), + ) + + query_request = QueryRequest(query="What is OpenStack?") + model_id = "fake_model_id" + access_token = "" # Empty token + + response, conversation_id = await retrieve_response( + mock_client, model_id, query_request, access_token + ) + + assert response is not None + assert conversation_id == "test_conversation_id" + + # Verify get_agent was called with the correct parameters + mock_get_agent.assert_called_once_with( + mock_client, + model_id, + mocker.ANY, # system_prompt + [], # available_shields + None, # conversation_id + ) + + # Check that the agent's extra_headers property was set correctly (empty mcp_headers) + expected_extra_headers = { + "X-LlamaStack-Provider-Data": json.dumps({"mcp_headers": {}}) + } + assert mock_agent.extra_headers == expected_extra_headers + + # Check that create_turn was called with the correct parameters + mock_agent.create_turn.assert_called_once_with( + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="test_conversation_id", + documents=[], + stream=True, + toolgroups=[mcp_server.name for mcp_server in mcp_servers], + ) + + +async def test_retrieve_response_with_mcp_servers_and_mcp_headers(mocker): + """Test the retrieve_response function with MCP servers configured.""" + mock_agent = mocker.AsyncMock() + mock_agent.create_turn.return_value.output_message.content = "LLM answer" + mock_client = mocker.AsyncMock() + mock_client.shields.list.return_value = [] + mock_client.vector_dbs.list.return_value = [] + + # Mock configuration with MCP servers + mcp_servers = [ + ModelContextProtocolServer( + name="filesystem-server", url="http://localhost:3000" + ), + ModelContextProtocolServer( + name="git-server", + provider_id="custom-git", + url="https://git.example.com/mcp", + ), + ] + mock_config = mocker.Mock() + mock_config.mcp_servers = mcp_servers + mocker.patch("app.endpoints.streaming_query.configuration", mock_config) + mock_get_agent = mocker.patch( + "app.endpoints.streaming_query.get_agent", + return_value=(mock_agent, "test_conversation_id"), + ) + + query_request = QueryRequest(query="What is OpenStack?") + model_id = "fake_model_id" + access_token = "" + mcp_headers = { + "filesystem-server": {"Authorization": "Bearer test_token_123"}, + "git-server": {"Authorization": "Bearer test_token_456"}, + "http://another-server-mcp-server:3000": { + "Authorization": "Bearer test_token_789" + }, + "unknown-mcp-server": { + "Authorization": "Bearer test_token_for_unknown-mcp-server" + }, + } + + response, conversation_id = await retrieve_response( + mock_client, + model_id, + query_request, + access_token, + mcp_headers=mcp_headers, + ) + + assert response is not None + assert conversation_id == "test_conversation_id" + + # Verify get_agent was called with the correct parameters + mock_get_agent.assert_called_once_with( + mock_client, + model_id, + mocker.ANY, # system_prompt + [], # available_shields + None, # conversation_id + ) + + expected_mcp_headers = { + "http://localhost:3000": {"Authorization": "Bearer test_token_123"}, + "https://git.example.com/mcp": {"Authorization": "Bearer test_token_456"}, + "http://another-server-mcp-server:3000": { + "Authorization": "Bearer test_token_789" + }, + # we do not put "unknown-mcp-server" url as it's unknown to lightspeed-stack + } + # Check that the agent's extra_headers property was set correctly + expected_extra_headers = { + "X-LlamaStack-Provider-Data": json.dumps({"mcp_headers": expected_mcp_headers}) + } + assert mock_agent.extra_headers == expected_extra_headers + + # Check that create_turn was called with the correct parameters + mock_agent.create_turn.assert_called_once_with( + messages=[UserMessage(role="user", content="What is OpenStack?")], + session_id="test_conversation_id", + documents=[], + stream=True, + toolgroups=[mcp_server.name for mcp_server in mcp_servers], + ) + + +@pytest.mark.asyncio +async def test_get_agent_cache_hit(prepare_agent_mocks): + """Test get_agent function when agent exists in cache.""" + + mock_client, mock_agent = prepare_agent_mocks + + # Set up cache with existing agent + conversation_id = "test_conversation_id" + _agent_cache[conversation_id] = mock_agent + + result_agent, result_conversation_id = await get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=["shield1"], + conversation_id=conversation_id, + ) + + # Assert cached agent is returned + assert result_agent == mock_agent + assert result_conversation_id == conversation_id + + +@pytest.mark.asyncio +async def test_get_agent_cache_miss_with_conversation_id( + setup_configuration, prepare_agent_mocks, mocker +): + """Test get_agent function when conversation_id is provided but agent not in cache.""" + + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_session.return_value = "new_session_id" + + # Mock Agent class + mock_agent_class = mocker.patch( + "app.endpoints.streaming_query.AsyncAgent", return_value=mock_agent + ) + + # Mock get_suid + mocker.patch( + "app.endpoints.streaming_query.get_suid", return_value="new_session_id" + ) + + # Mock configuration + mock_mcp_server = mocker.Mock() + mock_mcp_server.name = "mcp_server_1" + mocker.patch.object( + type(setup_configuration), + "mcp_servers", + new_callable=mocker.PropertyMock, + return_value=[mock_mcp_server], + ) + mocker.patch("app.endpoints.streaming_query.configuration", setup_configuration) + + # Call function with conversation_id but no cached agent + result_agent, result_conversation_id = await get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=["shield1"], + conversation_id="non_existent_conversation_id", + ) + + # Assert new agent is created + assert result_agent == mock_agent + assert result_conversation_id == "new_session_id" + + # Verify Agent was created with correct parameters + mock_agent_class.assert_called_once_with( + mock_client, + model="test_model", + instructions="test_prompt", + input_shields=["shield1"], + tool_parser=None, + enable_session_persistence=True, + ) + + # Verify agent was stored in cache + assert _agent_cache["new_session_id"] == mock_agent + + +@pytest.mark.asyncio +async def test_get_agent_no_conversation_id( + setup_configuration, prepare_agent_mocks, mocker +): + """Test get_agent function when conversation_id is None.""" + + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_session.return_value = "new_session_id" + + # Mock Agent class + mock_agent_class = mocker.patch( + "app.endpoints.streaming_query.AsyncAgent", return_value=mock_agent + ) + + # Mock get_suid + mocker.patch( + "app.endpoints.streaming_query.get_suid", return_value="new_session_id" + ) + + # Mock configuration + mock_mcp_server = mocker.Mock() + mock_mcp_server.name = "mcp_server_1" + mocker.patch.object( + type(setup_configuration), + "mcp_servers", + new_callable=mocker.PropertyMock, + return_value=[mock_mcp_server], + ) + mocker.patch("app.endpoints.streaming_query.configuration", setup_configuration) + + # Call function with None conversation_id + result_agent, result_conversation_id = await get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=["shield1"], + conversation_id=None, + ) + + # Assert new agent is created + assert result_agent == mock_agent + assert result_conversation_id == "new_session_id" + + # Verify Agent was created with correct parameters + mock_agent_class.assert_called_once_with( + mock_client, + model="test_model", + instructions="test_prompt", + input_shields=["shield1"], + tool_parser=None, + enable_session_persistence=True, + ) + + # Verify agent was stored in cache + assert _agent_cache["new_session_id"] == mock_agent + + +@pytest.mark.asyncio +async def test_get_agent_empty_shields( + setup_configuration, prepare_agent_mocks, mocker +): + """Test get_agent function with empty shields list.""" + + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_session.return_value = "new_session_id" + + # Mock Agent class + mock_agent_class = mocker.patch( + "app.endpoints.streaming_query.AsyncAgent", return_value=mock_agent + ) + + # Mock get_suid + mocker.patch( + "app.endpoints.streaming_query.get_suid", return_value="new_session_id" + ) + + # Mock configuration + mock_mcp_server = mocker.Mock() + mock_mcp_server.name = "mcp_server_1" + mocker.patch.object( + type(setup_configuration), + "mcp_servers", + new_callable=mocker.PropertyMock, + return_value=[mock_mcp_server], + ) + mocker.patch("app.endpoints.streaming_query.configuration", setup_configuration) + + # Call function with empty shields list + result_agent, result_conversation_id = await get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=[], + conversation_id=None, + ) + + # Assert new agent is created + assert result_agent == mock_agent + assert result_conversation_id == "new_session_id" + + # Verify Agent was created with empty shields + mock_agent_class.assert_called_once_with( + mock_client, + model="test_model", + instructions="test_prompt", + input_shields=[], + tool_parser=None, + enable_session_persistence=True, + ) + + +@pytest.mark.asyncio +async def test_get_agent_multiple_mcp_servers( + setup_configuration, prepare_agent_mocks, mocker +): + """Test get_agent function with multiple MCP servers.""" + + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_session.return_value = "new_session_id" + + # Mock Agent class + mock_agent_class = mocker.patch( + "app.endpoints.streaming_query.AsyncAgent", return_value=mock_agent + ) + + # Mock get_suid + mocker.patch( + "app.endpoints.streaming_query.get_suid", return_value="new_session_id" + ) + + # Mock configuration with multiple MCP servers + mock_mcp_server1 = mocker.Mock() + mock_mcp_server1.name = "mcp_server_1" + mock_mcp_server2 = mocker.Mock() + mock_mcp_server2.name = "mcp_server_2" + mocker.patch.object( + type(setup_configuration), + "mcp_servers", + new_callable=mocker.PropertyMock, + return_value=[mock_mcp_server1, mock_mcp_server2], + ) + mocker.patch("app.endpoints.streaming_query.configuration", setup_configuration) + + # Call function + result_agent, result_conversation_id = await get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=["shield1", "shield2"], + conversation_id=None, + ) + + # Assert new agent is created + assert result_agent == mock_agent + assert result_conversation_id == "new_session_id" + + # Verify Agent was created with tools from both MCP servers + mock_agent_class.assert_called_once_with( + mock_client, + model="test_model", + instructions="test_prompt", + input_shields=["shield1", "shield2"], + tool_parser=None, + enable_session_persistence=True, + ) + + +@pytest.mark.asyncio +async def test_get_agent_session_persistence_enabled( + setup_configuration, prepare_agent_mocks, mocker +): + """Test get_agent function ensures session persistence is enabled.""" + + mock_client, mock_agent = prepare_agent_mocks + mock_agent.create_session.return_value = "new_session_id" + + # Mock Agent class + mock_agent_class = mocker.patch( + "app.endpoints.streaming_query.AsyncAgent", return_value=mock_agent + ) + + # Mock get_suid + mocker.patch( + "app.endpoints.streaming_query.get_suid", return_value="new_session_id" + ) + + # Mock configuration + mock_mcp_server = mocker.Mock() + mock_mcp_server.name = "mcp_server_1" + mocker.patch.object( + type(setup_configuration), + "mcp_servers", + new_callable=mocker.PropertyMock, + return_value=[mock_mcp_server], + ) + mocker.patch("app.endpoints.streaming_query.configuration", setup_configuration) + + # Call function + await get_agent( + client=mock_client, + model_id="test_model", + system_prompt="test_prompt", + available_shields=["shield1"], + conversation_id=None, + ) + + # Verify Agent was created with session persistence enabled + mock_agent_class.assert_called_once_with( + mock_client, + model="test_model", + instructions="test_prompt", + input_shields=["shield1"], + tool_parser=None, + enable_session_persistence=True, + ) + + +@pytest.mark.asyncio +async def test_auth_tuple_unpacking_in_streaming_query_endpoint_handler(mocker): + """Test that auth tuple is correctly unpacked in streaming query endpoint handler.""" + # Mock dependencies + mock_config = mocker.Mock() + mock_config.llama_stack_configuration = mocker.Mock() + mocker.patch("app.endpoints.streaming_query.configuration", mock_config) + + mock_client = mocker.AsyncMock() + mock_client.models.list.return_value = [ + mocker.Mock(identifier="model1", model_type="llm", provider_id="provider1") + ] + mocker.patch( + "client.AsyncLlamaStackClientHolder.get_client", return_value=mock_client + ) + + # Mock retrieve_response to verify token is passed correctly + mock_streaming_response = mocker.AsyncMock() + mock_streaming_response.__aiter__.return_value = iter([]) + mock_retrieve_response = mocker.patch( + "app.endpoints.streaming_query.retrieve_response", + return_value=(mock_streaming_response, "test_conversation_id"), + ) + + mocker.patch( + "app.endpoints.streaming_query.select_model_id", return_value="test_model" + ) + mocker.patch( + "app.endpoints.streaming_query.is_transcripts_enabled", return_value=False + ) + mocker.patch( + "app.endpoints.streaming_query.retrieve_user_id", return_value="user123" + ) + + _ = await streaming_query_endpoint_handler( + None, + QueryRequest(query="test query"), + auth=("user123", "username", "auth_token_123"), + mcp_headers=None, + ) + + assert mock_retrieve_response.call_args[0][3] == "auth_token_123" diff --git a/tests/unit/app/test_routers.py b/tests/unit/app/test_routers.py index 458f94a06..9b1a94c9b 100644 --- a/tests/unit/app/test_routers.py +++ b/tests/unit/app/test_routers.py @@ -13,6 +13,7 @@ config, feedback, streaming_query, + authorized, ) # noqa:E402 @@ -21,11 +22,17 @@ class MockFastAPI: def __init__(self) -> None: """Initialize mock class.""" - self.routers: list[Any] = [] + self.routers: list[tuple[Any, Optional[str]]] = [] def include_router(self, router: Any, prefix: Optional[str] = None) -> None: """Register new router.""" - self.routers.append(router) + self.routers.append((router, prefix)) + + def get_routers(self) -> list[Any]: + return [r[0] for r in self.routers] + + def get_router_prefix(self, router: Any) -> Optional[str]: + return list(filter(lambda r: r[0] == router, self.routers))[0][1] def test_include_routers() -> None: @@ -34,12 +41,31 @@ def test_include_routers() -> None: include_routers(app) # are all routers added? - assert len(app.routers) == 8 - assert root.router in app.routers - assert info.router in app.routers - assert models.router in app.routers - assert query.router in app.routers - assert health.router in app.routers - assert config.router in app.routers - assert feedback.router in app.routers - assert streaming_query.router in app.routers + assert len(app.routers) == 9 + assert root.router in app.get_routers() + assert info.router in app.get_routers() + assert models.router in app.get_routers() + assert query.router in app.get_routers() + assert streaming_query.router in app.get_routers() + assert config.router in app.get_routers() + assert feedback.router in app.get_routers() + assert health.router in app.get_routers() + assert authorized.router in app.get_routers() + + +def test_check_prefixes() -> None: + """Test the router prefixes.""" + app = MockFastAPI() + include_routers(app) + + # are all routers added? + assert len(app.routers) == 9 + assert app.get_router_prefix(root.router) is None + assert app.get_router_prefix(info.router) == "/v1" + assert app.get_router_prefix(models.router) == "/v1" + assert app.get_router_prefix(query.router) == "/v1" + assert app.get_router_prefix(streaming_query.router) == "/v1" + assert app.get_router_prefix(config.router) == "/v1" + assert app.get_router_prefix(feedback.router) == "/v1" + assert app.get_router_prefix(health.router) is None + assert app.get_router_prefix(authorized.router) is None diff --git a/tests/unit/auth/__init__.py b/tests/unit/auth/__init__.py new file mode 100644 index 000000000..85611888d --- /dev/null +++ b/tests/unit/auth/__init__.py @@ -0,0 +1 @@ +"""Authentication unit tests package.""" diff --git a/tests/unit/auth/test_auth.py b/tests/unit/auth/test_auth.py new file mode 100644 index 000000000..b61df57f6 --- /dev/null +++ b/tests/unit/auth/test_auth.py @@ -0,0 +1,27 @@ +"""Unit tests for functions defined in auth/__init__.py""" + +from auth import get_auth_dependency +from auth import noop, noop_with_token, k8s +from constants import AUTH_MOD_NOOP, AUTH_MOD_NOOP_WITH_TOKEN, AUTH_MOD_K8S +from configuration import configuration + + +def test_get_auth_dependency_noop(): + """Test getting Noop authentication dependency.""" + configuration.authentication_configuration.module = AUTH_MOD_NOOP + auth_dependency = get_auth_dependency() + assert isinstance(auth_dependency, noop.NoopAuthDependency) + + +def test_get_auth_dependency_noop_with_token(): + """Test getting Noop with token authentication dependency.""" + configuration.authentication_configuration.module = AUTH_MOD_NOOP_WITH_TOKEN + auth_dependency = get_auth_dependency() + assert isinstance(auth_dependency, noop_with_token.NoopWithTokenAuthDependency) + + +def test_get_auth_dependency_k8s(): + """Test getting K8s authentication dependency.""" + configuration.authentication_configuration.module = AUTH_MOD_K8S + auth_dependency = get_auth_dependency() + assert isinstance(auth_dependency, k8s.K8SAuthDependency) diff --git a/tests/unit/auth/test_k8s.py b/tests/unit/auth/test_k8s.py new file mode 100644 index 000000000..4b121fa66 --- /dev/null +++ b/tests/unit/auth/test_k8s.py @@ -0,0 +1,244 @@ +"""Unit tests for auth/k8s module.""" + +import os + +import pytest +from fastapi import HTTPException, Request +from kubernetes.client import AuthenticationV1Api, AuthorizationV1Api +from kubernetes.client.rest import ApiException + +from auth.k8s import ( + K8sClientSingleton, + K8SAuthDependency, + ClusterIDUnavailableError, + CLUSTER_ID_LOCAL, +) + + +class MockK8sResponseStatus: + """Mock Kubernetes Response Status. + + Holds the status of a mocked Kubernetes API response, + including authentication and authorization details, + and user information if authenticated. + """ + + def __init__(self, authenticated, allowed, username=None, uid=None, groups=None): + """Init function.""" + self.authenticated = authenticated + self.allowed = allowed + if authenticated: + self.user = MockK8sUser(username, uid, groups) + else: + self.user = None + + +class MockK8sUser: + """Mock Kubernetes User. + + Represents a user in the mocked Kubernetes environment. + """ + + def __init__(self, username=None, uid=None, groups=None): + """Init function.""" + self.username = username + self.uid = uid + self.groups = groups + + +class MockK8sResponse: + """Mock Kubernetes API Response. + + This class is designed to mock Kubernetes API responses for testing purposes. + """ + + def __init__( + self, authenticated=None, allowed=None, username=None, uid=None, groups=None + ): + """Init function.""" + self.status = MockK8sResponseStatus( + authenticated, allowed, username, uid, groups + ) + + +def test_singleton_pattern(): + """Test if K8sClientSingleton is really a singleton.""" + k1 = K8sClientSingleton() + k2 = K8sClientSingleton() + assert k1 is k2 + + +async def test_auth_dependency_valid_token(mocker): + """Tests the auth dependency with a mocked valid-token.""" + dependency = K8SAuthDependency() + + # Mock the Kubernetes API calls + mock_authn_api = mocker.patch("auth.k8s.K8sClientSingleton.get_authn_api") + mock_authz_api = mocker.patch("auth.k8s.K8sClientSingleton.get_authz_api") + + # Mock a successful token review response + mock_authn_api.return_value.create_token_review.return_value = MockK8sResponse( + authenticated=True, username="valid-user", uid="valid-uid", groups=["ols-group"] + ) + mock_authz_api.return_value.create_subject_access_review.return_value = ( + MockK8sResponse(allowed=True) + ) + + # Simulate a request with a valid token + request = Request( + scope={ + "type": "http", + "headers": [(b"authorization", b"Bearer valid-token")], + } + ) + + user_uid, username, token = await dependency(request) + + # Check if the correct user info has been returned + assert user_uid == "valid-uid" + assert username == "valid-user" + assert token == "valid-token" + + +async def test_auth_dependency_invalid_token(mocker): + """Test the auth dependency with a mocked invalid-token.""" + dependency = K8SAuthDependency() + + # Mock the Kubernetes API calls + mock_authn_api = mocker.patch("auth.k8s.K8sClientSingleton.get_authn_api") + mock_authz_api = mocker.patch("auth.k8s.K8sClientSingleton.get_authz_api") + + # Setup mock responses for invalid token + mock_authn_api.return_value.create_token_review.return_value = MockK8sResponse( + authenticated=False + ) + mock_authz_api.return_value.create_subject_access_review.return_value = ( + MockK8sResponse(allowed=False) + ) + + # Simulate a request with an invalid token + request = Request( + scope={ + "type": "http", + "headers": [(b"authorization", b"Bearer invalid-token")], + } + ) + + # Expect an HTTPException for invalid tokens + with pytest.raises(HTTPException) as exc_info: + await dependency(request) + + # Check if the correct status code is returned for unauthorized access + assert exc_info.value.status_code == 403 + + +async def test_cluster_id_is_used_for_kube_admin(mocker): + """Test the cluster id is used as user_id when user is kube:admin.""" + dependency = K8SAuthDependency() + mock_authz_api = mocker.patch("auth.k8s.K8sClientSingleton.get_authz_api") + mock_authz_api.return_value.create_subject_access_review.return_value = ( + MockK8sResponse(allowed=True) + ) + + # simulate a request with a valid token + request = Request( + scope={ + "type": "http", + "headers": [(b"authorization", b"Bearer valid-token")], + } + ) + + mocker.patch( + "auth.k8s.get_user_info", + return_value=MockK8sResponseStatus( + authenticated=True, + allowed=True, + username="kube:admin", + uid="some-uuid", + groups=["ols-group"], + ), + ) + mocker.patch( + "auth.k8s.K8sClientSingleton.get_cluster_id", + return_value="some-cluster-id", + ) + + user_uid, username, token = await dependency(request) + + # check if the correct user info has been returned + assert user_uid == "some-cluster-id" + assert username == "kube:admin" + assert token == "valid-token" + + +def test_auth_dependency_config(mocker): + """Test the auth dependency can load kubeconfig file.""" + mocker.patch.dict(os.environ, {"MY_ENV_VAR": "mocked"}) + + authn_client = K8sClientSingleton.get_authn_api() + authz_client = K8sClientSingleton.get_authz_api() + assert isinstance( + authn_client, AuthenticationV1Api + ), "authn_client is not an instance of AuthenticationV1Api" + assert isinstance( + authz_client, AuthorizationV1Api + ), "authz_client is not an instance of AuthorizationV1Api" + + +def test_get_cluster_id(mocker): + """Test get_cluster_id function.""" + mock_get_custom_objects_api = mocker.patch( + "auth.k8s.K8sClientSingleton.get_custom_objects_api" + ) + + cluster_id = {"spec": {"clusterID": "some-cluster-id"}} + mocked_call = mocker.MagicMock() + mocked_call.get_cluster_custom_object.return_value = cluster_id + mock_get_custom_objects_api.return_value = mocked_call + assert K8sClientSingleton._get_cluster_id() == "some-cluster-id" + + # keyerror + cluster_id = {"spec": {}} + mocked_call = mocker.MagicMock() + mocked_call.get_cluster_custom_object.return_value = cluster_id + mock_get_custom_objects_api.return_value = mocked_call + with pytest.raises(ClusterIDUnavailableError, match="Failed to get cluster ID"): + K8sClientSingleton._get_cluster_id() + + # typeerror + cluster_id = None + mocked_call = mocker.MagicMock() + mocked_call.get_cluster_custom_object.return_value = cluster_id + mock_get_custom_objects_api.return_value = mocked_call + with pytest.raises(ClusterIDUnavailableError, match="Failed to get cluster ID"): + K8sClientSingleton._get_cluster_id() + + # typeerror + mock_get_custom_objects_api.side_effect = ApiException() + with pytest.raises(ClusterIDUnavailableError, match="Failed to get cluster ID"): + K8sClientSingleton._get_cluster_id() + + # exception + mock_get_custom_objects_api.side_effect = Exception() + with pytest.raises(ClusterIDUnavailableError, match="Failed to get cluster ID"): + K8sClientSingleton._get_cluster_id() + + +def test_get_cluster_id_in_cluster(mocker): + """Test get_cluster_id function when running inside of cluster.""" + mocker.patch("auth.k8s.RUNNING_IN_CLUSTER", True) + mocker.patch("auth.k8s.K8sClientSingleton.__new__") + mock_get_cluster_id = mocker.patch("auth.k8s.K8sClientSingleton._get_cluster_id") + + mock_get_cluster_id.return_value = "some-cluster-id" + assert K8sClientSingleton.get_cluster_id() == "some-cluster-id" + + +def test_get_cluster_id_outside_of_cluster(mocker): + """Test get_cluster_id function when running outside of cluster.""" + mocker.patch("auth.k8s.RUNNING_IN_CLUSTER", False) + mocker.patch("auth.k8s.K8sClientSingleton.__new__") + + # ensure cluster_id is None to trigger the condition + K8sClientSingleton._cluster_id = None + assert K8sClientSingleton.get_cluster_id() == CLUSTER_ID_LOCAL diff --git a/tests/unit/auth/test_noop.py b/tests/unit/auth/test_noop.py new file mode 100644 index 000000000..77a30f01e --- /dev/null +++ b/tests/unit/auth/test_noop.py @@ -0,0 +1,37 @@ +"""Unit tests for functions defined in auth/noop.py""" + +from fastapi import Request +from auth.noop import NoopAuthDependency +from constants import DEFAULT_USER_NAME, DEFAULT_USER_UID, NO_USER_TOKEN + + +async def test_noop_auth_dependency(): + """Test the NoopAuthDependency class with default user ID.""" + dependency = NoopAuthDependency() + + # Create a mock request without user_id + request = Request(scope={"type": "http", "query_string": b""}) + + # Call the dependency + user_id, username, user_token = await dependency(request) + + # Assert the expected values + assert user_id == DEFAULT_USER_UID + assert username == DEFAULT_USER_NAME + assert user_token == NO_USER_TOKEN + + +async def test_noop_auth_dependency_custom_user_id(): + """Test the NoopAuthDependency class.""" + dependency = NoopAuthDependency() + + # Create a mock request + request = Request(scope={"type": "http", "query_string": b"user_id=test-user"}) + + # Call the dependency + user_id, username, user_token = await dependency(request) + + # Assert the expected values + assert user_id == "test-user" + assert username == DEFAULT_USER_NAME + assert user_token == NO_USER_TOKEN diff --git a/tests/unit/auth/test_noop_with_token.py b/tests/unit/auth/test_noop_with_token.py new file mode 100644 index 000000000..0ed460b2d --- /dev/null +++ b/tests/unit/auth/test_noop_with_token.py @@ -0,0 +1,96 @@ +"""Unit tests for functions defined in auth/noop_with_token.py""" + +from fastapi import Request, HTTPException +import pytest + +from auth.noop_with_token import NoopWithTokenAuthDependency +from constants import DEFAULT_USER_NAME, DEFAULT_USER_UID + + +async def test_noop_with_token_auth_dependency(): + """Test the NoopWithTokenAuthDependency class with default user ID.""" + dependency = NoopWithTokenAuthDependency() + + request = Request( + scope={ + "type": "http", + "query_string": b"", + "headers": [ + (b"authorization", b"Bearer spongebob-token"), + ], + }, + ) + + # Call the dependency + user_id, username, user_token = await dependency(request) + + # Assert the expected values + assert user_id == DEFAULT_USER_UID + assert username == DEFAULT_USER_NAME + assert user_token == "spongebob-token" + + +async def test_noop_with_token_auth_dependency_custom_user_id(): + """Test the NoopWithTokenAuthDependency class with custom user ID.""" + dependency = NoopWithTokenAuthDependency() + + # Create a mock request + request = Request( + scope={ + "type": "http", + "query_string": b"user_id=test-user", + "headers": [ + (b"authorization", b"Bearer spongebob-token"), + ], + }, + ) + + # Call the dependency + user_id, username, user_token = await dependency(request) + + # Assert the expected values + assert user_id == "test-user" + assert username == DEFAULT_USER_NAME + assert user_token == "spongebob-token" + + +async def test_noop_with_token_auth_dependency_no_token(): + """Test the NoopWithTokenAuthDependency class with no token.""" + dependency = NoopWithTokenAuthDependency() + + # Create a mock request without token + request = Request( + scope={ + "type": "http", + "query_string": b"", + "headers": [], + }, + ) + + # Assert that an HTTPException is raised when no Authorization header is found + with pytest.raises(HTTPException) as exc_info: + await dependency(request) + + assert exc_info.value.status_code == 400 + assert exc_info.value.detail == "No Authorization header found" + + +async def test_noop_with_token_auth_dependency_no_bearer(): + """Test the NoopWithTokenAuthDependency class with no token.""" + dependency = NoopWithTokenAuthDependency() + + # Create a mock request without token + request = Request( + scope={ + "type": "http", + "query_string": b"", + "headers": [(b"authorization", b"NotBearer anything")], + }, + ) + + # Assert that an HTTPException is raised when no Authorization header is found + with pytest.raises(HTTPException) as exc_info: + await dependency(request) + + assert exc_info.value.status_code == 400 + assert exc_info.value.detail == "No token found in Authorization header" diff --git a/tests/unit/auth/test_utils.py b/tests/unit/auth/test_utils.py new file mode 100644 index 000000000..68fa847dc --- /dev/null +++ b/tests/unit/auth/test_utils.py @@ -0,0 +1,31 @@ +"""Unit tests for functions defined in auth/utils.py""" + +from fastapi import HTTPException +from auth.utils import extract_user_token + + +def test_extract_user_token(): + """Test extracting user token from headers.""" + headers = {"Authorization": "Bearer abcdef123"} + token = extract_user_token(headers) + assert token == "abcdef123" + + +def test_extract_user_token_no_header(): + """Test extracting user token when no Authorization header is present.""" + headers = {} + try: + extract_user_token(headers) + except HTTPException as exc: + assert exc.status_code == 400 + assert exc.detail == "No Authorization header found" + + +def test_extract_user_token_invalid_format(): + """Test extracting user token with invalid Authorization header format.""" + headers = {"Authorization": "InvalidFormat"} + try: + extract_user_token(headers) + except HTTPException as exc: + assert exc.status_code == 400 + assert exc.detail == "No token found in Authorization header" diff --git a/tests/unit/models/test_config.py b/tests/unit/models/test_config.py index d9040e88e..950dbc382 100644 --- a/tests/unit/models/test_config.py +++ b/tests/unit/models/test_config.py @@ -5,6 +5,13 @@ from pathlib import Path +from constants import ( + AUTH_MOD_NOOP, + AUTH_MOD_K8S, + DATA_COLLECTOR_COLLECTION_INTERVAL, + DATA_COLLECTOR_CONNECTION_TIMEOUT, +) + from models.config import ( Configuration, LLamaStackConfiguration, @@ -12,6 +19,7 @@ UserDataCollection, TLSConfiguration, ModelContextProtocolServer, + DataCollectorConfiguration, ) @@ -128,6 +136,53 @@ def test_user_data_collection_transcripts_disabled() -> None: UserDataCollection(transcripts_disabled=False, transcripts_storage=None) +def test_user_data_collection_data_collector_enabled() -> None: + """Test the UserDataCollection constructor for data collector.""" + # correct configuration + cfg = UserDataCollection( + data_collector=DataCollectorConfiguration( + enabled=True, + ingress_server_url="http://localhost:8080", + ingress_server_auth_token="xyzzy", + ingress_content_service_name="lightspeed-core", + collection_interval=60, + ) + ) + assert cfg is not None + assert cfg.data_collector.enabled is True + + +def test_user_data_collection_data_collector_wrong_configuration() -> None: + """Test the UserDataCollection constructor for data collector.""" + # incorrect configuration + with pytest.raises( + ValueError, + match="ingress_server_url is required when data collector is enabled", + ): + UserDataCollection( + data_collector=DataCollectorConfiguration( + enabled=True, + ingress_server_url=None, + ingress_server_auth_token="xyzzy", + ingress_content_service_name="lightspeed-core", + collection_interval=60, + ) + ) + with pytest.raises( + ValueError, + match="ingress_content_service_name is required when data collector is enabled", + ): + UserDataCollection( + data_collector=DataCollectorConfiguration( + enabled=True, + ingress_server_url="http://localhost:8080", + ingress_server_auth_token="xyzzy", + ingress_content_service_name=None, + collection_interval=60, + ) + ) + + def test_tls_configuration() -> None: """Test the TLS configuration.""" cfg = TLSConfiguration( @@ -249,6 +304,7 @@ def test_configuration_empty_mcp_servers() -> None: feedback_disabled=True, feedback_storage=None ), mcp_servers=[], + customization=None, ) assert cfg is not None assert cfg.mcp_servers == [] @@ -269,6 +325,7 @@ def test_configuration_single_mcp_server() -> None: feedback_disabled=True, feedback_storage=None ), mcp_servers=[mcp_server], + customization=None, ) assert cfg is not None assert len(cfg.mcp_servers) == 1 @@ -295,6 +352,7 @@ def test_configuration_multiple_mcp_servers() -> None: feedback_disabled=True, feedback_storage=None ), mcp_servers=mcp_servers, + customization=None, ) assert cfg is not None assert len(cfg.mcp_servers) == 3 @@ -316,6 +374,7 @@ def test_dump_configuration(tmp_path) -> None: feedback_disabled=True, feedback_storage=None ), mcp_servers=[], + customization=None, ) assert cfg is not None dump_file = tmp_path / "test.json" @@ -332,6 +391,7 @@ def test_dump_configuration(tmp_path) -> None: assert "llama_stack" in content assert "user_data_collection" in content assert "mcp_servers" in content + assert "authentication" in content # check the whole deserialized JSON file content assert content == { @@ -360,8 +420,24 @@ def test_dump_configuration(tmp_path) -> None: "feedback_storage": None, "transcripts_disabled": True, "transcripts_storage": None, + "data_collector": { + "enabled": False, + "ingress_server_url": None, + "ingress_server_auth_token": None, + "ingress_content_service_name": None, + "collection_interval": DATA_COLLECTOR_COLLECTION_INTERVAL, + "cleanup_after_send": True, + "connection_timeout": DATA_COLLECTOR_CONNECTION_TIMEOUT, + }, }, "mcp_servers": [], + "authentication": { + "module": "noop", + "skip_tls_verification": False, + "k8s_ca_cert_path": None, + "k8s_cluster_api": None, + }, + "customization": None, } @@ -380,6 +456,7 @@ def test_dump_configuration_with_one_mcp_server(tmp_path) -> None: feedback_disabled=True, feedback_storage=None ), mcp_servers=mcp_servers, + customization=None, ) dump_file = tmp_path / "test.json" cfg.dump(dump_file) @@ -420,6 +497,15 @@ def test_dump_configuration_with_one_mcp_server(tmp_path) -> None: "feedback_storage": None, "transcripts_disabled": True, "transcripts_storage": None, + "data_collector": { + "enabled": False, + "ingress_server_url": None, + "ingress_server_auth_token": None, + "ingress_content_service_name": None, + "collection_interval": DATA_COLLECTOR_COLLECTION_INTERVAL, + "cleanup_after_send": True, + "connection_timeout": DATA_COLLECTOR_CONNECTION_TIMEOUT, + }, }, "mcp_servers": [ { @@ -428,6 +514,13 @@ def test_dump_configuration_with_one_mcp_server(tmp_path) -> None: "url": "http://localhost:8080", }, ], + "authentication": { + "module": "noop", + "skip_tls_verification": False, + "k8s_ca_cert_path": None, + "k8s_cluster_api": None, + }, + "customization": None, } @@ -448,6 +541,7 @@ def test_dump_configuration_with_more_mcp_servers(tmp_path) -> None: feedback_disabled=True, feedback_storage=None ), mcp_servers=mcp_servers, + customization=None, ) dump_file = tmp_path / "test.json" cfg.dump(dump_file) @@ -494,6 +588,15 @@ def test_dump_configuration_with_more_mcp_servers(tmp_path) -> None: "feedback_storage": None, "transcripts_disabled": True, "transcripts_storage": None, + "data_collector": { + "enabled": False, + "ingress_server_url": None, + "ingress_server_auth_token": None, + "ingress_content_service_name": None, + "collection_interval": DATA_COLLECTOR_COLLECTION_INTERVAL, + "cleanup_after_send": True, + "connection_timeout": DATA_COLLECTOR_CONNECTION_TIMEOUT, + }, }, "mcp_servers": [ { @@ -512,4 +615,59 @@ def test_dump_configuration_with_more_mcp_servers(tmp_path) -> None: "url": "http://localhost:8083", }, ], + "authentication": { + "module": "noop", + "skip_tls_verification": False, + "k8s_ca_cert_path": None, + "k8s_cluster_api": None, + }, + "customization": None, } + + +def test_authentication_configuration() -> None: + """Test the AuthenticationConfiguration constructor.""" + from models.config import AuthenticationConfiguration + + auth_config = AuthenticationConfiguration( + module=AUTH_MOD_NOOP, + skip_tls_verification=False, + k8s_ca_cert_path=None, + k8s_cluster_api=None, + ) + assert auth_config is not None + assert auth_config.module == AUTH_MOD_NOOP + assert auth_config.skip_tls_verification is False + assert auth_config.k8s_ca_cert_path is None + assert auth_config.k8s_cluster_api is None + + +def test_authentication_configuration_supported() -> None: + """Test the AuthenticationConfiguration constructor.""" + from models.config import AuthenticationConfiguration + + auth_config = AuthenticationConfiguration( + module=AUTH_MOD_K8S, + skip_tls_verification=False, + k8s_ca_cert_path=None, + k8s_cluster_api=None, + ) + assert auth_config is not None + assert auth_config.module == AUTH_MOD_K8S + assert auth_config.skip_tls_verification is False + assert auth_config.k8s_ca_cert_path is None + assert auth_config.k8s_cluster_api is None + + +def test_authentication_configuration_module_unsupported() -> None: + """Test the AuthenticationConfiguration constructor with module as None.""" + from models.config import AuthenticationConfiguration + from pydantic import ValidationError + + with pytest.raises(ValidationError, match="Unsupported authentication module"): + AuthenticationConfiguration( + module="non-existing-module", + skip_tls_verification=False, + k8s_ca_cert_path=None, + k8s_cluster_api=None, + ) diff --git a/tests/unit/models/test_responses.py b/tests/unit/models/test_responses.py index 21bdcb06a..9ee236d78 100644 --- a/tests/unit/models/test_responses.py +++ b/tests/unit/models/test_responses.py @@ -1,4 +1,9 @@ -from models.responses import QueryResponse, StatusResponse +from models.responses import ( + QueryResponse, + StatusResponse, + AuthorizedResponse, + UnauthorizedResponse, +) class TestQueryResponse: @@ -28,3 +33,27 @@ def test_constructor(self) -> None: sr = StatusResponse(functionality="feedback", status={"enabled": True}) assert sr.functionality == "feedback" assert sr.status == {"enabled": True} + + +class TestAuthorizedResponse: + """Test cases for the AuthorizedResponse model.""" + + def test_constructor(self) -> None: + """Test the AuthorizedResponse constructor.""" + ar = AuthorizedResponse( + user_id="123e4567-e89b-12d3-a456-426614174000", + username="testuser", + ) + assert ar.user_id == "123e4567-e89b-12d3-a456-426614174000" + assert ar.username == "testuser" + + +class TestUnauthorizedResponse: + """Test cases for the UnauthorizedResponse model.""" + + def test_constructor(self) -> None: + """Test the UnauthorizedResponse constructor.""" + ur = UnauthorizedResponse( + detail="Missing or invalid credentials provided by client" + ) + assert ur.detail == "Missing or invalid credentials provided by client" diff --git a/tests/unit/runners/test_data_collector_runner.py b/tests/unit/runners/test_data_collector_runner.py new file mode 100644 index 000000000..5fb623a59 --- /dev/null +++ b/tests/unit/runners/test_data_collector_runner.py @@ -0,0 +1,60 @@ +"""Unit tests for runners.""" + +from unittest.mock import patch + +from models.config import DataCollectorConfiguration +from runners.data_collector import start_data_collector + + +def test_start_data_collector() -> None: + """Test the function to start data collector service.""" + configuration = DataCollectorConfiguration( + enabled=True, + ingress_server_url="http://localhost:8080", + ingress_server_auth_token="xyzzy", + ingress_content_service_name="lightspeed-core", + collection_interval=60, + ) + + # don't start real data collector service + with patch("services.data_collector.DataCollectorService.run") as mocked_run: + start_data_collector(configuration) + mocked_run.assert_called_once() + + +def test_start_data_collector_disabled() -> None: + """Test the function to start data collector service.""" + configuration = DataCollectorConfiguration( + enabled=False, + ingress_server_url="http://localhost:8080", + ingress_server_auth_token="xyzzy", + ingress_content_service_name="lightspeed-core", + collection_interval=60, + ) + + # don't start real data collector service + with patch("services.data_collector.DataCollectorService.run") as mocked_run: + start_data_collector(configuration) + mocked_run.assert_not_called() + + +def test_start_data_collector_exception() -> None: + """Test the function to start data collector service when an exception occurs.""" + configuration = DataCollectorConfiguration( + enabled=True, + ingress_server_url="http://localhost:8080", + ingress_server_auth_token="xyzzy", + ingress_content_service_name="lightspeed-core", + collection_interval=60, + ) + + # Mock the DataCollectorService to raise an exception + with patch("services.data_collector.DataCollectorService.run") as mocked_run: + mocked_run.side_effect = Exception("Test exception") + + try: + start_data_collector(configuration) + assert False, "Expected exception to be raised" + except Exception as e: + assert str(e) == "Test exception" + mocked_run.assert_called_once() diff --git a/tests/unit/services/test_data_collector.py b/tests/unit/services/test_data_collector.py new file mode 100644 index 000000000..03448c4ae --- /dev/null +++ b/tests/unit/services/test_data_collector.py @@ -0,0 +1,587 @@ +"""Unit tests for data collector service.""" + +from pathlib import Path +from unittest.mock import patch, MagicMock +import requests +import tarfile + +from services.data_collector import DataCollectorService + + +def test_data_collector_service_creation() -> None: + """Test that DataCollectorService can be created.""" + service = DataCollectorService() + assert service is not None + + +@patch("services.data_collector.time.sleep") +@patch("services.data_collector.configuration") +def test_run_normal_operation(mock_config, mock_sleep) -> None: + """Test normal operation of the run method.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.data_collector.collection_interval = ( + 60 + ) + + with patch.object(service, "_perform_collection") as mock_perform: + mock_perform.side_effect = [None, KeyboardInterrupt()] + + service.run() + + assert mock_perform.call_count == 2 + mock_sleep.assert_called_once_with(60) + + +@patch("services.data_collector.time.sleep") +@patch("services.data_collector.configuration") +def test_run_with_exception(mock_config, mock_sleep) -> None: + """Test run method with exception handling.""" + service = DataCollectorService() + + with patch.object(service, "_perform_collection") as mock_perform: + mock_perform.side_effect = [OSError("Test error"), KeyboardInterrupt()] + + service.run() + + assert mock_perform.call_count == 2 + mock_sleep.assert_called_once_with(300) + + +@patch("services.data_collector.configuration") +def test_collect_feedback_files_disabled(mock_config) -> None: + """Test collecting feedback files when disabled.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.feedback_disabled = True + + result = service._collect_feedback_files() + assert result == [] + + +@patch("services.data_collector.configuration") +def test_collect_feedback_files_no_storage(mock_config) -> None: + """Test collecting feedback files when no storage configured.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.feedback_disabled = False + mock_config.user_data_collection_configuration.feedback_storage = None + + result = service._collect_feedback_files() + assert result == [] + + +@patch("services.data_collector.configuration") +def test_collect_feedback_files_directory_not_exists(mock_config) -> None: + """Test collecting feedback files when directory doesn't exist.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.feedback_disabled = False + mock_config.user_data_collection_configuration.feedback_storage = "/tmp/feedback" + + with patch("services.data_collector.Path") as mock_path: + mock_path.return_value.exists.return_value = False + + result = service._collect_feedback_files() + assert result == [] + + +@patch("services.data_collector.configuration") +def test_collect_feedback_files_success(mock_config) -> None: + """Test collecting feedback files successfully.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.feedback_disabled = False + mock_config.user_data_collection_configuration.feedback_storage = "/tmp/feedback" + + mock_files = [Path("/tmp/feedback/file1.json")] + + with patch("services.data_collector.Path") as mock_path: + mock_path.return_value.exists.return_value = True + mock_path.return_value.glob.return_value = mock_files + + result = service._collect_feedback_files() + assert result == mock_files + + +@patch("services.data_collector.configuration") +def test_collect_transcript_files_disabled(mock_config) -> None: + """Test collecting transcript files when disabled.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.transcripts_disabled = True + + result = service._collect_transcript_files() + assert result == [] + + +@patch("services.data_collector.configuration") +def test_collect_transcript_files_directory_not_exists(mock_config) -> None: + """Test collecting transcript files when directory doesn't exist.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.transcripts_disabled = False + mock_config.user_data_collection_configuration.transcripts_storage = ( + "/tmp/transcripts" + ) + + with patch("services.data_collector.Path") as mock_path: + mock_path.return_value.exists.return_value = False + + result = service._collect_transcript_files() + assert result == [] + + +@patch("services.data_collector.configuration") +def test_collect_transcript_files_success(mock_config) -> None: + """Test collecting transcript files successfully.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.transcripts_disabled = False + mock_config.user_data_collection_configuration.transcripts_storage = ( + "/tmp/transcripts" + ) + + mock_files = [Path("/tmp/transcripts/user1/conv1/file1.json")] + + with patch("services.data_collector.Path") as mock_path: + mock_path.return_value.exists.return_value = True + mock_path.return_value.rglob.return_value = mock_files + + result = service._collect_transcript_files() + assert result == mock_files + + +@patch("services.data_collector.configuration") +def test_perform_collection_no_files(mock_config) -> None: + """Test _perform_collection when no files are found.""" + service = DataCollectorService() + + with ( + patch.object(service, "_collect_feedback_files", return_value=[]), + patch.object(service, "_collect_transcript_files", return_value=[]), + ): + service._perform_collection() + + +@patch("services.data_collector.configuration") +def test_perform_collection_with_files(mock_config) -> None: + """Test _perform_collection when files are found.""" + service = DataCollectorService() + + feedback_files = [Path("/tmp/feedback/file1.json")] + + with ( + patch.object(service, "_collect_feedback_files", return_value=feedback_files), + patch.object(service, "_collect_transcript_files", return_value=[]), + patch.object(service, "_create_and_send_tarball", return_value=1), + ): + service._perform_collection() + + +@patch("services.data_collector.configuration") +def test_perform_collection_with_exception(mock_config) -> None: + """Test _perform_collection when an exception occurs.""" + service = DataCollectorService() + + with ( + patch.object( + service, "_collect_feedback_files", return_value=[Path("/tmp/test.json")] + ), + patch.object(service, "_collect_transcript_files", return_value=[]), + patch.object( + service, "_create_and_send_tarball", side_effect=Exception("Test error") + ), + ): + try: + service._perform_collection() + assert False, "Expected exception" + except Exception as e: + assert str(e) == "Test error" + + +@patch("services.data_collector.configuration") +def test_create_and_send_tarball_no_files(mock_config) -> None: + """Test creating tarball with no files.""" + service = DataCollectorService() + + result = service._create_and_send_tarball([], "test", Path("/tmp")) + assert result == 0 + + +@patch("services.data_collector.configuration") +def test_create_and_send_tarball_success(mock_config) -> None: + """Test creating and sending tarball successfully.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.data_collector.cleanup_after_send = ( + True + ) + + files = [Path("/tmp/test/file1.json")] + tarball_path = Path("/tmp/test_tarball.tar.gz") + + with ( + patch.object(service, "_create_tarball", return_value=tarball_path), + patch.object(service, "_send_tarball"), + patch.object(service, "_cleanup_files"), + patch.object(service, "_cleanup_empty_directories"), + patch.object(service, "_cleanup_tarball"), + ): + result = service._create_and_send_tarball(files, "test", Path("/tmp")) + assert result == 1 + + +@patch("services.data_collector.configuration") +def test_create_and_send_tarball_no_cleanup(mock_config) -> None: + """Test creating and sending tarball without cleanup.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.data_collector.cleanup_after_send = ( + False + ) + + files = [Path("/tmp/test/file1.json")] + + with ( + patch.object(service, "_create_tarball", return_value=Path("/tmp/test.tar.gz")), + patch.object(service, "_send_tarball"), + patch.object(service, "_cleanup_tarball"), + ): + result = service._create_and_send_tarball(files, "test", Path("/tmp")) + assert result == 1 + + +@patch("services.data_collector.datetime") +@patch("services.data_collector.tempfile.gettempdir") +@patch("services.data_collector.tarfile.open") +def test_create_tarball_success(mock_tarfile, mock_gettempdir, mock_datetime) -> None: + """Test creating tarball successfully.""" + service = DataCollectorService() + mock_datetime.now.return_value.strftime.return_value = "20230101_120000" + mock_gettempdir.return_value = "/tmp" + + mock_tar = MagicMock() + mock_tarfile.return_value.__enter__.return_value = mock_tar + + files = [Path("/data/test/file1.json")] + + with patch.object(Path, "stat") as mock_stat: + mock_stat.return_value.st_size = 1024 + + result = service._create_tarball(files, "test", Path("/data")) + + expected_path = Path("/tmp/test_20230101_120000.tar.gz") + assert result == expected_path + mock_tar.add.assert_called_once() + + +@patch("services.data_collector.datetime") +@patch("services.data_collector.tempfile.gettempdir") +@patch("services.data_collector.tarfile.open") +def test_create_tarball_file_add_error( + mock_tarfile, mock_gettempdir, mock_datetime +) -> None: + """Test creating tarball with file add error.""" + service = DataCollectorService() + mock_datetime.now.return_value.strftime.return_value = "20230101_120000" + mock_gettempdir.return_value = "/tmp" + + mock_tar = MagicMock() + mock_tar.add.side_effect = OSError("File error") + mock_tarfile.return_value.__enter__.return_value = mock_tar + + files = [Path("/data/test/file1.json")] + + with patch.object(Path, "stat") as mock_stat: + mock_stat.return_value.st_size = 1024 + + result = service._create_tarball(files, "test", Path("/data")) + + expected_path = Path("/tmp/test_20230101_120000.tar.gz") + assert result == expected_path + + +@patch("services.data_collector.configuration") +@patch("services.data_collector.requests.post") +def test_send_tarball_success(mock_post, mock_config) -> None: + """Test successful tarball sending.""" + service = DataCollectorService() + + mock_config.user_data_collection_configuration.data_collector.ingress_server_url = ( + "http://test.com" + ) + mock_config.user_data_collection_configuration.data_collector.ingress_server_auth_token = ( + "token" + ) + mock_config.user_data_collection_configuration.data_collector.connection_timeout = ( + 30 + ) + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + with patch("builtins.open", create=True) as mock_open: + mock_open.return_value.__enter__.return_value.read.return_value = b"test data" + service._send_tarball(Path("/tmp/test.tar.gz")) + + mock_post.assert_called_once() + + +@patch("services.data_collector.configuration") +@patch("services.data_collector.requests.post") +def test_send_tarball_no_auth_token(mock_post, mock_config) -> None: + """Test sending tarball without auth token.""" + service = DataCollectorService() + + mock_config.user_data_collection_configuration.data_collector.ingress_server_url = ( + "http://test.com" + ) + mock_config.user_data_collection_configuration.data_collector.ingress_server_auth_token = ( + None + ) + mock_config.user_data_collection_configuration.data_collector.connection_timeout = ( + 30 + ) + + mock_response = MagicMock() + mock_response.status_code = 200 + mock_post.return_value = mock_response + + with patch("builtins.open", create=True): + service._send_tarball(Path("/tmp/test.tar.gz")) + mock_post.assert_called_once() + + +@patch("services.data_collector.configuration") +@patch("services.data_collector.requests.post") +def test_send_tarball_http_error(mock_post, mock_config) -> None: + """Test tarball sending with HTTP error.""" + service = DataCollectorService() + + mock_config.user_data_collection_configuration.data_collector.ingress_server_url = ( + "http://test.com" + ) + mock_config.user_data_collection_configuration.data_collector.connection_timeout = ( + 30 + ) + + mock_response = MagicMock() + mock_response.status_code = 500 + mock_response.text = "Server Error" + mock_post.return_value = mock_response + + with patch("builtins.open", create=True): + try: + service._send_tarball(Path("/tmp/test.tar.gz")) + assert False, "Expected exception" + except Exception as e: + assert "Failed to send tarball" in str(e) + + +@patch("services.data_collector.configuration") +def test_send_tarball_missing_url(mock_config) -> None: + """Test tarball sending when ingress server URL is None.""" + service = DataCollectorService() + + mock_config.user_data_collection_configuration.data_collector.ingress_server_url = ( + None + ) + + try: + service._send_tarball(Path("/tmp/test.tar.gz")) + assert False, "Expected ValueError" + except ValueError as e: + assert "Ingress server URL is not configured" in str(e) + + +@patch("services.data_collector.configuration") +def test_perform_collection_with_specific_exceptions(mock_config) -> None: + """Test _perform_collection with specific exception types that should be caught.""" + service = DataCollectorService() + + # Test with OSError + with ( + patch.object( + service, "_collect_feedback_files", return_value=[Path("/tmp/test.json")] + ), + patch.object(service, "_collect_transcript_files", return_value=[]), + patch.object( + service, "_create_and_send_tarball", side_effect=OSError("OS Error") + ), + ): + try: + service._perform_collection() + assert False, "Expected OSError" + except OSError as e: + assert str(e) == "OS Error" + + # Test with requests.RequestException + with ( + patch.object( + service, "_collect_feedback_files", return_value=[Path("/tmp/test.json")] + ), + patch.object(service, "_collect_transcript_files", return_value=[]), + patch.object( + service, + "_create_and_send_tarball", + side_effect=requests.RequestException("Request Error"), + ), + ): + try: + service._perform_collection() + assert False, "Expected RequestException" + except requests.RequestException as e: + assert str(e) == "Request Error" + + # Test with tarfile.TarError + with ( + patch.object( + service, "_collect_feedback_files", return_value=[Path("/tmp/test.json")] + ), + patch.object(service, "_collect_transcript_files", return_value=[]), + patch.object( + service, + "_create_and_send_tarball", + side_effect=tarfile.TarError("Tar Error"), + ), + ): + try: + service._perform_collection() + assert False, "Expected TarError" + except tarfile.TarError as e: + assert str(e) == "Tar Error" + + +def test_cleanup_files_success() -> None: + """Test successful file cleanup.""" + service = DataCollectorService() + files = [Path("/tmp/test1.json"), Path("/tmp/test2.json")] + + with patch.object(Path, "unlink") as mock_unlink: + service._cleanup_files(files) + assert mock_unlink.call_count == 2 + + +def test_cleanup_files_with_error() -> None: + """Test file cleanup with error.""" + service = DataCollectorService() + files = [Path("/tmp/test1.json")] + + with patch.object(Path, "unlink") as mock_unlink: + mock_unlink.side_effect = OSError("Permission denied") + service._cleanup_files(files) + mock_unlink.assert_called_once() + + +def test_cleanup_tarball_success() -> None: + """Test successful tarball cleanup.""" + service = DataCollectorService() + + with patch.object(Path, "unlink") as mock_unlink: + service._cleanup_tarball(Path("/tmp/test.tar.gz")) + mock_unlink.assert_called_once() + + +def test_cleanup_tarball_with_error() -> None: + """Test tarball cleanup with error.""" + service = DataCollectorService() + + with patch.object(Path, "unlink") as mock_unlink: + mock_unlink.side_effect = OSError("Permission denied") + service._cleanup_tarball(Path("/tmp/test.tar.gz")) + mock_unlink.assert_called_once() + + +@patch("services.data_collector.configuration") +def test_cleanup_empty_directories_disabled(mock_config) -> None: + """Test directory cleanup when transcripts disabled.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.transcripts_disabled = True + + service._cleanup_empty_directories() + + +@patch("services.data_collector.configuration") +def test_cleanup_empty_directories_success(mock_config) -> None: + """Test successful directory cleanup.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.transcripts_disabled = False + mock_config.user_data_collection_configuration.transcripts_storage = ( + "/tmp/transcripts" + ) + + transcripts_dir = MagicMock() + user_dir = MagicMock() + conv_dir = MagicMock() + + transcripts_dir.exists.return_value = True + transcripts_dir.iterdir.return_value = [user_dir] + user_dir.is_dir.return_value = True + user_dir.iterdir.side_effect = [ + [conv_dir], + [], + ] # First call returns conv_dir, second call empty + conv_dir.is_dir.return_value = True + conv_dir.iterdir.return_value = [] # Empty directory + + with patch("services.data_collector.Path", return_value=transcripts_dir): + service._cleanup_empty_directories() + + conv_dir.rmdir.assert_called_once() + user_dir.rmdir.assert_called_once() + + +@patch("services.data_collector.configuration") +def test_cleanup_empty_directories_with_errors(mock_config) -> None: + """Test directory cleanup when rmdir operations fail.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.transcripts_disabled = False + mock_config.user_data_collection_configuration.transcripts_storage = ( + "/tmp/transcripts" + ) + + transcripts_dir = MagicMock() + user_dir = MagicMock() + conv_dir = MagicMock() + + transcripts_dir.exists.return_value = True + transcripts_dir.iterdir.return_value = [user_dir] + user_dir.is_dir.return_value = True + user_dir.iterdir.side_effect = [[conv_dir], []] + conv_dir.is_dir.return_value = True + conv_dir.iterdir.return_value = [] + + # Both rmdir operations fail + conv_dir.rmdir.side_effect = OSError("Permission denied") + user_dir.rmdir.side_effect = OSError("Permission denied") + + with patch("services.data_collector.Path", return_value=transcripts_dir): + # Should not raise exception + service._cleanup_empty_directories() + + conv_dir.rmdir.assert_called_once() + user_dir.rmdir.assert_called_once() + + +@patch("services.data_collector.configuration") +def test_cleanup_empty_directories_directory_not_exists(mock_config) -> None: + """Test directory cleanup when transcripts directory doesn't exist.""" + service = DataCollectorService() + mock_config.user_data_collection_configuration.transcripts_disabled = False + mock_config.user_data_collection_configuration.transcripts_storage = ( + "/tmp/transcripts" + ) + + with patch("services.data_collector.Path") as mock_path: + mock_path.return_value.exists.return_value = False + + service._cleanup_empty_directories() + + +@patch("services.data_collector.configuration") +def test_perform_collection_with_transcript_files(mock_config) -> None: + """Test _perform_collection with transcript files only.""" + service = DataCollectorService() + + transcript_files = [Path("/tmp/transcripts/file1.json")] + + with ( + patch.object(service, "_collect_feedback_files", return_value=[]), + patch.object( + service, "_collect_transcript_files", return_value=transcript_files + ), + patch.object(service, "_create_and_send_tarball", return_value=1), + ): + service._perform_collection() diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index a492251dd..97b1e555a 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -2,35 +2,38 @@ import pytest -from client import get_llama_stack_client, get_async_llama_stack_client +from client import LlamaStackClientHolder, AsyncLlamaStackClientHolder from models.config import LLamaStackConfiguration -# [tisnik] Need to resolve dependencies on CI to be able to run this tests def test_get_llama_stack_library_client() -> None: + """Test if Llama Stack can be initialized in library client mode.""" cfg = LLamaStackConfiguration( url=None, api_key=None, use_as_library_client=True, library_client_config_path="./tests/configuration/minimal-stack.yaml", ) - - client = get_llama_stack_client(cfg) + client = LlamaStackClientHolder() + client.load(cfg) assert client is not None def test_get_llama_stack_remote_client() -> None: + """Test if Llama Stack can be initialized in remove client (server) mode.""" cfg = LLamaStackConfiguration( url="http://localhost:8321", api_key=None, use_as_library_client=False, library_client_config_path="./tests/configuration/minimal-stack.yaml", ) - client = get_llama_stack_client(cfg) + client = LlamaStackClientHolder() + client.load(cfg) assert client is not None def test_get_llama_stack_wrong_configuration() -> None: + """Test if configuration is checked before Llama Stack is initialized.""" cfg = LLamaStackConfiguration( url=None, api_key=None, @@ -42,33 +45,38 @@ def test_get_llama_stack_wrong_configuration() -> None: Exception, match="Configuration problem: library_client_config_path option is not set", ): - get_llama_stack_client(cfg) + client = LlamaStackClientHolder() + client.load(cfg) async def test_get_async_llama_stack_library_client() -> None: + """Test the initialization of asynchronous Llama Stack client in library mode.""" cfg = LLamaStackConfiguration( url=None, api_key=None, use_as_library_client=True, library_client_config_path="./tests/configuration/minimal-stack.yaml", ) - - client = await get_async_llama_stack_client(cfg) + client = AsyncLlamaStackClientHolder() + await client.load(cfg) assert client is not None async def test_get_async_llama_stack_remote_client() -> None: + """Test the initialization of asynchronous Llama Stack client in server mode.""" cfg = LLamaStackConfiguration( url="http://localhost:8321", api_key=None, use_as_library_client=False, library_client_config_path="./tests/configuration/minimal-stack.yaml", ) - client = await get_async_llama_stack_client(cfg) + client = AsyncLlamaStackClientHolder() + await client.load(cfg) assert client is not None async def test_get_async_llama_stack_wrong_configuration() -> None: + """Test if configuration is checked before Llama Stack is initialized.""" cfg = LLamaStackConfiguration( url=None, api_key=None, @@ -80,4 +88,5 @@ async def test_get_async_llama_stack_wrong_configuration() -> None: Exception, match="Configuration problem: library_client_config_path option is not set", ): - await get_async_llama_stack_client(cfg) + client = AsyncLlamaStackClientHolder() + await client.load(cfg) diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index 61eba2331..cd097e94a 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -6,30 +6,49 @@ def test_default_configuration() -> None: + """Test that configuration attributes are not accessible for uninitialized app.""" cfg = AppConfig() assert cfg is not None # configuration is not loaded with pytest.raises(Exception, match="logic error: configuration is not loaded"): # try to read property - cfg.configuration + cfg.configuration # pylint: disable=pointless-statement with pytest.raises(Exception, match="logic error: configuration is not loaded"): # try to read property - cfg.llama_stack_configuration + cfg.service_configuration # pylint: disable=pointless-statement with pytest.raises(Exception, match="logic error: configuration is not loaded"): # try to read property - cfg.user_data_collection_configuration + cfg.llama_stack_configuration # pylint: disable=pointless-statement + + with pytest.raises(Exception, match="logic error: configuration is not loaded"): + # try to read property + cfg.user_data_collection_configuration # pylint: disable=pointless-statement + + with pytest.raises(Exception, match="logic error: configuration is not loaded"): + # try to read property + cfg.mcp_servers # pylint: disable=pointless-statement + + with pytest.raises(Exception, match="logic error: configuration is not loaded"): + # try to read property + cfg.authentication_configuration # pylint: disable=pointless-statement + + with pytest.raises(Exception, match="logic error: configuration is not loaded"): + # try to read property + cfg.customization # pylint: disable=pointless-statement def test_configuration_is_singleton() -> None: + """Test that configuration is singleton.""" cfg1 = AppConfig() cfg2 = AppConfig() assert cfg1 == cfg2 def test_init_from_dict() -> None: + """Test the configuration initialization from dictionary with config values.""" config_dict = { "name": "foo", "service": { @@ -49,6 +68,7 @@ def test_init_from_dict() -> None: "feedback_disabled": True, }, "mcp_servers": [], + "customization": None, } cfg = AppConfig() cfg.init_from_dict(config_dict) @@ -110,6 +130,7 @@ def test_init_from_dict_with_mcp_servers() -> None: "url": "https://api.example.com", }, ], + "customization": None, } cfg = AppConfig() cfg.init_from_dict(config_dict) @@ -124,8 +145,9 @@ def test_init_from_dict_with_mcp_servers() -> None: def test_load_proper_configuration(tmpdir) -> None: + """Test loading proper configuration from YAML file.""" cfg_filename = tmpdir / "config.yaml" - with open(cfg_filename, "w") as fout: + with open(cfg_filename, "w", encoding="utf-8") as fout: fout.write( """ name: foo bar baz @@ -157,7 +179,7 @@ def test_load_proper_configuration(tmpdir) -> None: def test_load_configuration_with_mcp_servers(tmpdir) -> None: """Test loading configuration from YAML file with MCP servers.""" cfg_filename = tmpdir / "config.yaml" - with open(cfg_filename, "w") as fout: + with open(cfg_filename, "w", encoding="utf-8") as fout: fout.write( """ name: test service @@ -216,6 +238,7 @@ def test_mcp_servers_property_empty() -> None: "feedback_disabled": True, }, "mcp_servers": [], + "customization": None, } cfg = AppConfig() cfg.init_from_dict(config_dict) @@ -251,6 +274,7 @@ def test_mcp_servers_property_with_servers() -> None: "url": "http://localhost:8080", }, ], + "customization": None, } cfg = AppConfig() cfg.init_from_dict(config_dict) @@ -269,7 +293,7 @@ def test_configuration_not_loaded(): with pytest.raises( AssertionError, match="logic error: configuration is not loaded" ): - cfg.configuration + cfg.configuration # pylint: disable=pointless-statement def test_service_configuration_not_loaded(): @@ -278,7 +302,7 @@ def test_service_configuration_not_loaded(): with pytest.raises( AssertionError, match="logic error: configuration is not loaded" ): - cfg.service_configuration + cfg.service_configuration # pylint: disable=pointless-statement def test_llama_stack_configuration_not_loaded(): @@ -287,7 +311,7 @@ def test_llama_stack_configuration_not_loaded(): with pytest.raises( AssertionError, match="logic error: configuration is not loaded" ): - cfg.llama_stack_configuration + cfg.llama_stack_configuration # pylint: disable=pointless-statement def test_user_data_collection_configuration_not_loaded(): @@ -296,7 +320,7 @@ def test_user_data_collection_configuration_not_loaded(): with pytest.raises( AssertionError, match="logic error: configuration is not loaded" ): - cfg.user_data_collection_configuration + cfg.user_data_collection_configuration # pylint: disable=pointless-statement def test_mcp_servers_not_loaded(): @@ -305,4 +329,109 @@ def test_mcp_servers_not_loaded(): with pytest.raises( AssertionError, match="logic error: configuration is not loaded" ): - cfg.mcp_servers + cfg.mcp_servers # pylint: disable=pointless-statement + + +def test_authentication_configuration_not_loaded(): + """Test that accessing authentication_configuration before loading raises an error.""" + cfg = AppConfig() + with pytest.raises( + AssertionError, match="logic error: configuration is not loaded" + ): + cfg.authentication_configuration # pylint: disable=pointless-statement + + +def test_customization_not_loaded(): + """Test that accessing customization before loading raises an error.""" + cfg = AppConfig() + with pytest.raises( + AssertionError, match="logic error: configuration is not loaded" + ): + cfg.customization # pylint: disable=pointless-statement + + +def test_load_configuration_with_customization_system_prompt_path(tmpdir) -> None: + """Test loading configuration from YAML file with customization.""" + system_prompt_filename = tmpdir / "system_prompt.txt" + with open(system_prompt_filename, "w", encoding="utf-8") as fout: + fout.write("this is system prompt") + + cfg_filename = tmpdir / "config.yaml" + with open(cfg_filename, "w", encoding="utf-8") as fout: + fout.write( + f""" +name: test service +service: + host: localhost + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + use_as_library_client: false + url: http://localhost:8321 + api_key: test-key +user_data_collection: + feedback_disabled: true +mcp_servers: + - name: filesystem-server + url: http://localhost:3000 + - name: git-server + provider_id: custom-git-provider + url: https://git.example.com/mcp +customization: + disable_query_system_prompt: true + system_prompt_path: {system_prompt_filename} + """ + ) + + cfg = AppConfig() + cfg.load_configuration(cfg_filename) + + assert cfg.customization is not None + assert cfg.customization.system_prompt is not None + assert cfg.customization.system_prompt == "this is system prompt" + + +def test_load_configuration_with_customization_system_prompt(tmpdir) -> None: + """Test loading configuration from YAML file with system_prompt in the customization.""" + cfg_filename = tmpdir / "config.yaml" + with open(cfg_filename, "w", encoding="utf-8") as fout: + fout.write( + """ +name: test service +service: + host: localhost + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + use_as_library_client: false + url: http://localhost:8321 + api_key: test-key +user_data_collection: + feedback_disabled: true +mcp_servers: + - name: filesystem-server + url: http://localhost:3000 + - name: git-server + provider_id: custom-git-provider + url: https://git.example.com/mcp +customization: + system_prompt: |- + this is system prompt in the customization section + """ + ) + + cfg = AppConfig() + cfg.load_configuration(cfg_filename) + + assert cfg.customization is not None + assert cfg.customization.system_prompt is not None + assert ( + cfg.customization.system_prompt.strip() + == "this is system prompt in the customization section" + ) diff --git a/tests/unit/utils/test_auth.py b/tests/unit/utils/test_auth.py deleted file mode 100644 index d534bf3e6..000000000 --- a/tests/unit/utils/test_auth.py +++ /dev/null @@ -1,32 +0,0 @@ -from utils.auth import auth_dependency - - -# TODO(lucasagomes): Implement this test when the auth_dependency function is implemented -async def test_auth_dependency(mocker): - """Test that auth_dependency does not raise an exception.""" - # Create a mock request with proper headers - mock_request = mocker.Mock() - mock_request.headers.get.return_value = "Bearer test_token" - - result = await auth_dependency(mock_request) - assert result == "test_token" - - -async def test_auth_dependency_no_auth_header(mocker): - """Test that auth_dependency returns empty string when no Authorization header.""" - # Create a mock request with no Authorization header - mock_request = mocker.Mock() - mock_request.headers.get.return_value = "" - - result = await auth_dependency(mock_request) - assert result == "" - - -async def test_auth_dependency_invalid_auth_header(mocker): - """Test that auth_dependency returns empty string for invalid Authorization header.""" - # Create a mock request with invalid Authorization header - mock_request = mocker.Mock() - mock_request.headers.get.return_value = "Invalid header" - - result = await auth_dependency(mock_request) - assert result == "" diff --git a/tests/unit/utils/test_checks.py b/tests/unit/utils/test_checks.py new file mode 100644 index 000000000..e01b46fc4 --- /dev/null +++ b/tests/unit/utils/test_checks.py @@ -0,0 +1,77 @@ +import os +import pytest + +from unittest.mock import patch + +from utils import checks + + +@pytest.fixture +def input_file(tmp_path): + """Create file manually using the tmp_path fixture.""" + filename = os.path.join(tmp_path, "mydoc.csv") + with open(filename, "wt") as fout: + fout.write("some content!") + return filename + + +def test_get_attribute_from_file_no_record(): + """Test the get_attribute_from_file function when record is not in dictionary.""" + # no data + d = {} + + # non-existing key + key = "" + value = checks.get_attribute_from_file(d, key) + assert value is None + + # non-existing key + key = "this-does-not-exists" + value = checks.get_attribute_from_file(d, "this-does-not-exists") + assert value is None + + +def test_get_attribute_from_file_proper_record(input_file): + """Test the get_attribute_from_file function when record is present in dictionary.""" + # existing key + key = "my_file" + + # attribute with proper and existing filename + d = {key: input_file} + + # file content should be read properly + value = checks.get_attribute_from_file(d, key) + assert value is not None + assert value == "some content!" + + +def test_get_attribute_from_file_improper_filename(): + """Test the get_attribute_from_file when the file does not exist.""" + # existing key + key = "my_file" + + # filename for file that does not exist + input_file = "this-does-not-exists" + d = {key: input_file} + + with pytest.raises(FileNotFoundError, match="this-does-not-exists"): + checks.get_attribute_from_file(d, "my_file") + + +def test_file_check_existing_file(input_file): + """Test the function file_check for existing file.""" + # just call the function, it should not raise an exception + checks.file_check(input_file, "description") + + +def test_file_check_non_existing_file(): + """Test the function file_check for non existing file.""" + with pytest.raises(checks.InvalidConfigurationError): + checks.file_check("does-not-exists", "description") + + +def test_file_check_not_readable_file(input_file): + """Test the function file_check for not readable file.""" + with patch("os.access", return_value=False): + with pytest.raises(checks.InvalidConfigurationError): + checks.file_check(input_file, "description") diff --git a/tests/unit/utils/test_common.py b/tests/unit/utils/test_common.py index df969dc9d..0ca387f2f 100644 --- a/tests/unit/utils/test_common.py +++ b/tests/unit/utils/test_common.py @@ -31,7 +31,7 @@ async def test_register_mcp_servers_empty_list(mocker): mock_logger = Mock(spec=Logger) # Mock the LlamaStack client (shouldn't be called since no MCP servers) - mock_get_client = mocker.patch("utils.common.get_llama_stack_client") + mock_lsc = mocker.patch("client.LlamaStackClientHolder.get_client") # Create configuration with empty MCP servers config = Configuration( @@ -42,12 +42,13 @@ async def test_register_mcp_servers_empty_list(mocker): ), user_data_collection=UserDataCollection(feedback_disabled=True), mcp_servers=[], + customization=None, ) # Call the function await register_mcp_servers_async(mock_logger, config) # Verify get_llama_stack_client was NOT called since no MCP servers - mock_get_client.assert_not_called() + mock_lsc.assert_not_called() # Verify debug message was logged mock_logger.debug.assert_called_with( "No MCP servers configured, skipping registration" @@ -62,11 +63,12 @@ async def test_register_mcp_servers_single_server_not_registered(mocker): # Mock the LlamaStack client mock_client = Mock() + mock_lsc = mocker.patch("client.LlamaStackClientHolder.get_client") + mock_lsc.return_value = mock_client mock_tool = Mock() - mock_tool.toolgroup_id = "existing-server" - mock_client.tools.list.return_value = [mock_tool] + mock_tool.provider_resource_id = "existing-server" + mock_client.toolgroups.list.return_value = [mock_tool] mock_client.toolgroups.register.return_value = None - mocker.patch("utils.common.get_llama_stack_client", return_value=mock_client) # Create configuration with one MCP server mcp_server = ModelContextProtocolServer( @@ -80,13 +82,14 @@ async def test_register_mcp_servers_single_server_not_registered(mocker): ), user_data_collection=UserDataCollection(feedback_disabled=True), mcp_servers=[mcp_server], + customization=None, ) # Call the function await register_mcp_servers_async(mock_logger, config) - # Verify client.tools.list was called - mock_client.tools.list.assert_called_once() + # Verify client.toolgroups.list was called + mock_client.toolgroups.list.assert_called_once() # Verify client.toolgroups.register was called with correct parameters mock_client.toolgroups.register.assert_called_once_with( toolgroup_id="new-server", @@ -106,9 +109,10 @@ async def test_register_mcp_servers_single_server_already_registered(mocker): # Mock the LlamaStack client mock_client = Mock() mock_tool = Mock() - mock_tool.toolgroup_id = "existing-server" - mock_client.tools.list.return_value = [mock_tool] - mocker.patch("utils.common.get_llama_stack_client", return_value=mock_client) + mock_tool.provider_resource_id = "existing-server" + mock_client.toolgroups.list.return_value = [mock_tool] + mock_lsc = mocker.patch("client.LlamaStackClientHolder.get_client") + mock_lsc.return_value = mock_client # Create configuration with MCP server that matches existing toolgroup mcp_server = ModelContextProtocolServer( @@ -122,13 +126,14 @@ async def test_register_mcp_servers_single_server_already_registered(mocker): ), user_data_collection=UserDataCollection(feedback_disabled=True), mcp_servers=[mcp_server], + customization=None, ) # Call the function await register_mcp_servers_async(mock_logger, config) # Verify client.tools.list was called - mock_client.tools.list.assert_called_once() + mock_client.toolgroups.list.assert_called_once() # Verify client.toolgroups.register was NOT called since server already registered assert not mock_client.toolgroups.register.called @@ -141,13 +146,14 @@ async def test_register_mcp_servers_multiple_servers_mixed_registration(mocker): # Mock the LlamaStack client mock_client = Mock() + mock_lsc = mocker.patch("client.LlamaStackClientHolder.get_client") + mock_lsc.return_value = mock_client mock_tool1 = Mock() - mock_tool1.toolgroup_id = "existing-server" + mock_tool1.provider_resource_id = "existing-server" mock_tool2 = Mock() - mock_tool2.toolgroup_id = "another-existing" - mock_client.tools.list.return_value = [mock_tool1, mock_tool2] + mock_tool2.provider_resource_id = "another-existing" + mock_client.toolgroups.list.return_value = [mock_tool1, mock_tool2] mock_client.toolgroups.register.return_value = None - mocker.patch("utils.common.get_llama_stack_client", return_value=mock_client) # Create configuration with multiple MCP servers mcp_servers = [ @@ -167,13 +173,14 @@ async def test_register_mcp_servers_multiple_servers_mixed_registration(mocker): ), user_data_collection=UserDataCollection(feedback_disabled=True), mcp_servers=mcp_servers, + customization=None, ) # Call the function await register_mcp_servers_async(mock_logger, config) # Verify client.tools.list was called - mock_client.tools.list.assert_called_once() + mock_client.toolgroups.list.assert_called_once() # Verify client.toolgroups.register was called twice (for the two new servers) assert mock_client.toolgroups.register.call_count == 2 @@ -201,9 +208,10 @@ async def test_register_mcp_servers_with_custom_provider(mocker): # Mock the LlamaStack client mock_client = Mock() - mock_client.tools.list.return_value = [] + mock_client.toolgroups.list.return_value = [] mock_client.toolgroups.register.return_value = None - mocker.patch("utils.common.get_llama_stack_client", return_value=mock_client) + mock_lsc = mocker.patch("client.LlamaStackClientHolder.get_client") + mock_lsc.return_value = mock_client # Create configuration with MCP server using custom provider mcp_server = ModelContextProtocolServer( @@ -219,6 +227,7 @@ async def test_register_mcp_servers_with_custom_provider(mocker): ), user_data_collection=UserDataCollection(feedback_disabled=True), mcp_servers=[mcp_server], + customization=None, ) # Call the function @@ -239,21 +248,17 @@ async def test_register_mcp_servers_async_with_library_client(mocker): mock_logger = Mock(spec=Logger) # Mock the LlamaStackAsLibraryClient - mock_library_client = Mock() mock_async_client = AsyncMock() mock_async_client.initialize = AsyncMock() - mock_library_client.async_client = mock_async_client + mock_lsc = mocker.patch("client.AsyncLlamaStackClientHolder.get_client") + mock_lsc.return_value = mock_async_client # Mock tools.list to return empty list mock_tool = Mock() - mock_tool.toolgroup_id = "existing-tool" - mock_async_client.tools.list = AsyncMock(return_value=[mock_tool]) + mock_tool.provider_resource_id = "existing-tool" + mock_async_client.toolgroups.list = AsyncMock(return_value=[mock_tool]) mock_async_client.toolgroups.register = AsyncMock() - mocker.patch( - "utils.common.LlamaStackAsLibraryClient", return_value=mock_library_client - ) - # Create configuration with library client enabled mcp_server = ModelContextProtocolServer( name="test-server", url="http://localhost:8080" @@ -267,6 +272,7 @@ async def test_register_mcp_servers_async_with_library_client(mocker): ), user_data_collection=UserDataCollection(feedback_disabled=True), mcp_servers=[mcp_server], + customization=None, ) # Call the async function @@ -275,7 +281,7 @@ async def test_register_mcp_servers_async_with_library_client(mocker): # Verify initialization was called mock_async_client.initialize.assert_called_once() # Verify tools.list was called - mock_async_client.tools.list.assert_called_once() + mock_async_client.toolgroups.list.assert_called_once() # Verify toolgroups.register was called for the new server mock_async_client.toolgroups.register.assert_called_once_with( toolgroup_id="test-server", diff --git a/tests/unit/utils/test_endpoints.py b/tests/unit/utils/test_endpoints.py new file mode 100644 index 000000000..9970afef7 --- /dev/null +++ b/tests/unit/utils/test_endpoints.py @@ -0,0 +1,142 @@ +"""Unit tests for endpoints utility functions.""" + +import os +import pytest +from fastapi import HTTPException + +import constants +from configuration import AppConfig +from tests.unit import config_dict + +from models.requests import QueryRequest +from utils import endpoints + +CONFIGURED_SYSTEM_PROMPT = "This is a configured system prompt" + + +@pytest.fixture +def input_file(tmp_path): + """Create file manually using the tmp_path fixture.""" + filename = os.path.join(tmp_path, "prompt.txt") + with open(filename, "wt") as fout: + fout.write("this is prompt!") + return filename + + +@pytest.fixture +def config_without_system_prompt(): + test_config = config_dict.copy() + + # no customization provided + test_config["customization"] = None + + cfg = AppConfig() + cfg.init_from_dict(test_config) + + return cfg + + +@pytest.fixture +def config_with_custom_system_prompt(): + test_config = config_dict.copy() + + # system prompt is customized + test_config["customization"] = { + "system_prompt": CONFIGURED_SYSTEM_PROMPT, + } + cfg = AppConfig() + cfg.init_from_dict(test_config) + + return cfg + + +@pytest.fixture +def config_with_custom_system_prompt_and_disable_query_system_prompt(): + test_config = config_dict.copy() + + # system prompt is customized and query system prompt is disabled + test_config["customization"] = { + "system_prompt": CONFIGURED_SYSTEM_PROMPT, + "disable_query_system_prompt": True, + } + cfg = AppConfig() + cfg.init_from_dict(test_config) + + return cfg + + +@pytest.fixture +def query_request_without_system_prompt(): + """Fixture for query request without system prompt.""" + return QueryRequest(query="query", system_prompt=None) + + +@pytest.fixture +def query_request_with_system_prompt(): + """Fixture for query request with system prompt.""" + return QueryRequest(query="query", system_prompt="System prompt defined in query") + + +def test_get_default_system_prompt( + config_without_system_prompt, query_request_without_system_prompt +): + """Test that default system prompt is returned when other prompts are not provided.""" + system_prompt = endpoints.get_system_prompt( + query_request_without_system_prompt, config_without_system_prompt + ) + assert system_prompt == constants.DEFAULT_SYSTEM_PROMPT + + +def test_get_customized_system_prompt( + config_with_custom_system_prompt, query_request_without_system_prompt +): + """Test that customized system prompt is used when system prompt is not provided in query.""" + system_prompt = endpoints.get_system_prompt( + query_request_without_system_prompt, config_with_custom_system_prompt + ) + assert system_prompt == CONFIGURED_SYSTEM_PROMPT + + +def test_get_query_system_prompt( + config_without_system_prompt, query_request_with_system_prompt +): + """Test that system prompt from query is returned.""" + system_prompt = endpoints.get_system_prompt( + query_request_with_system_prompt, config_without_system_prompt + ) + assert system_prompt == query_request_with_system_prompt.system_prompt + + +def test_get_query_system_prompt_not_customized_one( + config_with_custom_system_prompt, query_request_with_system_prompt +): + """Test that system prompt from query is returned even when customized one is specified.""" + system_prompt = endpoints.get_system_prompt( + query_request_with_system_prompt, config_with_custom_system_prompt + ) + assert system_prompt == query_request_with_system_prompt.system_prompt + + +def test_get_system_prompt_with_disable_query_system_prompt( + config_with_custom_system_prompt_and_disable_query_system_prompt, + query_request_with_system_prompt, +): + """Test that query system prompt is disallowed when disable_query_system_prompt is True.""" + with pytest.raises(HTTPException) as exc_info: + endpoints.get_system_prompt( + query_request_with_system_prompt, + config_with_custom_system_prompt_and_disable_query_system_prompt, + ) + assert exc_info.value.status_code == 422 + + +def test_get_system_prompt_with_disable_query_system_prompt_and_non_system_prompt_query( + config_with_custom_system_prompt_and_disable_query_system_prompt, + query_request_without_system_prompt, +): + """Test that query without system prompt is allowed when disable_query_system_prompt is True.""" + system_prompt = endpoints.get_system_prompt( + query_request_without_system_prompt, + config_with_custom_system_prompt_and_disable_query_system_prompt, + ) + assert system_prompt == CONFIGURED_SYSTEM_PROMPT diff --git a/tests/unit/utils/test_types.py b/tests/unit/utils/test_types.py new file mode 100644 index 000000000..70f284dea --- /dev/null +++ b/tests/unit/utils/test_types.py @@ -0,0 +1,48 @@ +"""Test module for utils/types.py.""" + +from unittest.mock import Mock + +from utils.types import GraniteToolParser + + +class TestGraniteToolParser: + def test_get_tool_parser_when_model_is_is_not_granite(self): + """Test that the tool_parser is None when model_id is not a granite model.""" + assert ( + GraniteToolParser.get_parser("ollama3.3") is None + ), "tool_parser should be None" + + def test_get_tool_parser_when_model_id_does_not_start_with_granite(self): + """Test that the tool_parser is None when model_id does not start with granite.""" + assert ( + GraniteToolParser.get_parser("a-fine-trained-granite-model") is None + ), "tool_parser should be None" + + def test_get_tool_parser_when_model_id_starts_with_granite(self): + """Test that the tool_parser is not None when model_id starts with granite.""" + tool_parser = GraniteToolParser.get_parser("granite-3.3-8b-instruct") + assert tool_parser is not None, "tool_parser should not be None" + + def test_get_tool_calls_from_completion_message_when_none(self): + """Test that get_tool_calls returns an empty array when CompletionMessage is None.""" + tool_parser = GraniteToolParser.get_parser("granite-3.3-8b-instruct") + assert tool_parser.get_tool_calls(None) == [], "get_tool_calls should return []" + + def test_get_tool_calls_from_completion_message_when_not_none(self): + """Test that get_tool_calls returns an empty array when CompletionMessage has no tool_calls.""" + tool_parser = GraniteToolParser.get_parser("granite-3.3-8b-instruct") + completion_message = Mock() + completion_message.tool_calls = [] + assert ( + tool_parser.get_tool_calls(completion_message) == [] + ), "get_tool_calls should return []" + + def test_get_tool_calls_from_completion_message_when_message_has_tool_calls(self): + """Test that get_tool_calls returns the tool_calls when CompletionMessage has tool_calls.""" + tool_parser = GraniteToolParser.get_parser("granite-3.3-8b-instruct") + completion_message = Mock() + tool_calls = [Mock(tool_name="tool-1"), Mock(tool_name="tool-2")] + completion_message.tool_calls = tool_calls + assert ( + tool_parser.get_tool_calls(completion_message) == tool_calls + ), f"get_tool_calls should return {tool_calls}" diff --git a/uv.lock b/uv.lock index 5ec959453..c57c7f4ef 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.12.13" +version = "3.12.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -24,54 +24,55 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload-time = "2025-06-14T15:15:41.354Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491, upload-time = "2025-06-14T15:14:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104, upload-time = "2025-06-14T15:14:01.691Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948, upload-time = "2025-06-14T15:14:03.561Z" }, - { url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742, upload-time = "2025-06-14T15:14:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393, upload-time = "2025-06-14T15:14:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486, upload-time = "2025-06-14T15:14:08.808Z" }, - { url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643, upload-time = "2025-06-14T15:14:10.767Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082, upload-time = "2025-06-14T15:14:12.38Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884, upload-time = "2025-06-14T15:14:14.415Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943, upload-time = "2025-06-14T15:14:16.48Z" }, - { url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398, upload-time = "2025-06-14T15:14:18.589Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051, upload-time = "2025-06-14T15:14:20.223Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611, upload-time = "2025-06-14T15:14:21.988Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586, upload-time = "2025-06-14T15:14:23.979Z" }, - { url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197, upload-time = "2025-06-14T15:14:25.692Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771, upload-time = "2025-06-14T15:14:27.364Z" }, - { url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869, upload-time = "2025-06-14T15:14:29.05Z" }, - { url = "https://files.pythonhosted.org/packages/11/0f/db19abdf2d86aa1deec3c1e0e5ea46a587b97c07a16516b6438428b3a3f8/aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938", size = 694910, upload-time = "2025-06-14T15:14:30.604Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/0ab551e1b5d7f1339e2d6eb482456ccbe9025605b28eed2b1c0203aaaade/aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace", size = 472566, upload-time = "2025-06-14T15:14:32.275Z" }, - { url = "https://files.pythonhosted.org/packages/34/3f/6b7d336663337672d29b1f82d1f252ec1a040fe2d548f709d3f90fa2218a/aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb", size = 464856, upload-time = "2025-06-14T15:14:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/26/7f/32ca0f170496aa2ab9b812630fac0c2372c531b797e1deb3deb4cea904bd/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7", size = 1703683, upload-time = "2025-06-14T15:14:36.034Z" }, - { url = "https://files.pythonhosted.org/packages/ec/53/d5513624b33a811c0abea8461e30a732294112318276ce3dbf047dbd9d8b/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b", size = 1684946, upload-time = "2025-06-14T15:14:38Z" }, - { url = "https://files.pythonhosted.org/packages/37/72/4c237dd127827b0247dc138d3ebd49c2ded6114c6991bbe969058575f25f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177", size = 1737017, upload-time = "2025-06-14T15:14:39.951Z" }, - { url = "https://files.pythonhosted.org/packages/0d/67/8a7eb3afa01e9d0acc26e1ef847c1a9111f8b42b82955fcd9faeb84edeb4/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef", size = 1786390, upload-time = "2025-06-14T15:14:42.151Z" }, - { url = "https://files.pythonhosted.org/packages/48/19/0377df97dd0176ad23cd8cad4fd4232cfeadcec6c1b7f036315305c98e3f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103", size = 1708719, upload-time = "2025-06-14T15:14:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/61/97/ade1982a5c642b45f3622255173e40c3eed289c169f89d00eeac29a89906/aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da", size = 1622424, upload-time = "2025-06-14T15:14:45.945Z" }, - { url = "https://files.pythonhosted.org/packages/99/ab/00ad3eea004e1d07ccc406e44cfe2b8da5acb72f8c66aeeb11a096798868/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d", size = 1675447, upload-time = "2025-06-14T15:14:47.911Z" }, - { url = "https://files.pythonhosted.org/packages/3f/fe/74e5ce8b2ccaba445fe0087abc201bfd7259431d92ae608f684fcac5d143/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041", size = 1707110, upload-time = "2025-06-14T15:14:50.334Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c4/39af17807f694f7a267bd8ab1fbacf16ad66740862192a6c8abac2bff813/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1", size = 1649706, upload-time = "2025-06-14T15:14:52.378Z" }, - { url = "https://files.pythonhosted.org/packages/38/e8/f5a0a5f44f19f171d8477059aa5f28a158d7d57fe1a46c553e231f698435/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1", size = 1725839, upload-time = "2025-06-14T15:14:54.617Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ac/81acc594c7f529ef4419d3866913f628cd4fa9cab17f7bf410a5c3c04c53/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911", size = 1759311, upload-time = "2025-06-14T15:14:56.597Z" }, - { url = "https://files.pythonhosted.org/packages/38/0d/aabe636bd25c6ab7b18825e5a97d40024da75152bec39aa6ac8b7a677630/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3", size = 1708202, upload-time = "2025-06-14T15:14:58.598Z" }, - { url = "https://files.pythonhosted.org/packages/1f/ab/561ef2d8a223261683fb95a6283ad0d36cb66c87503f3a7dde7afe208bb2/aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd", size = 420794, upload-time = "2025-06-14T15:15:00.939Z" }, - { url = "https://files.pythonhosted.org/packages/9d/47/b11d0089875a23bff0abd3edb5516bcd454db3fefab8604f5e4b07bd6210/aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706", size = 446735, upload-time = "2025-06-14T15:15:02.858Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e6/0b/e39ad954107ebf213a2325038a3e7a506be3d98e1435e1f82086eec4cde2/aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2", size = 7822921, upload-time = "2025-07-10T13:05:33.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/0d/29026524e9336e33d9767a1e593ae2b24c2b8b09af7c2bd8193762f76b3e/aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22", size = 701055, upload-time = "2025-07-10T13:03:45.59Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b8/a5e8e583e6c8c1056f4b012b50a03c77a669c2e9bf012b7cf33d6bc4b141/aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a", size = 475670, upload-time = "2025-07-10T13:03:47.249Z" }, + { url = "https://files.pythonhosted.org/packages/29/e8/5202890c9e81a4ec2c2808dd90ffe024952e72c061729e1d49917677952f/aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff", size = 468513, upload-time = "2025-07-10T13:03:49.377Z" }, + { url = "https://files.pythonhosted.org/packages/23/e5/d11db8c23d8923d3484a27468a40737d50f05b05eebbb6288bafcb467356/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d", size = 1715309, upload-time = "2025-07-10T13:03:51.556Z" }, + { url = "https://files.pythonhosted.org/packages/53/44/af6879ca0eff7a16b1b650b7ea4a827301737a350a464239e58aa7c387ef/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869", size = 1697961, upload-time = "2025-07-10T13:03:53.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/94/18457f043399e1ec0e59ad8674c0372f925363059c276a45a1459e17f423/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c", size = 1753055, upload-time = "2025-07-10T13:03:55.368Z" }, + { url = "https://files.pythonhosted.org/packages/26/d9/1d3744dc588fafb50ff8a6226d58f484a2242b5dd93d8038882f55474d41/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7", size = 1799211, upload-time = "2025-07-10T13:03:57.216Z" }, + { url = "https://files.pythonhosted.org/packages/73/12/2530fb2b08773f717ab2d249ca7a982ac66e32187c62d49e2c86c9bba9b4/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660", size = 1718649, upload-time = "2025-07-10T13:03:59.469Z" }, + { url = "https://files.pythonhosted.org/packages/b9/34/8d6015a729f6571341a311061b578e8b8072ea3656b3d72329fa0faa2c7c/aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088", size = 1634452, upload-time = "2025-07-10T13:04:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4b/08b83ea02595a582447aeb0c1986792d0de35fe7a22fb2125d65091cbaf3/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7", size = 1695511, upload-time = "2025-07-10T13:04:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/9c7c31037a063eec13ecf1976185c65d1394ded4a5120dd5965e3473cb21/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9", size = 1716967, upload-time = "2025-07-10T13:04:06.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/02/84406e0ad1acb0fb61fd617651ab6de760b2d6a31700904bc0b33bd0894d/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3", size = 1657620, upload-time = "2025-07-10T13:04:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/07/53/da018f4013a7a179017b9a274b46b9a12cbeb387570f116964f498a6f211/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb", size = 1737179, upload-time = "2025-07-10T13:04:10.182Z" }, + { url = "https://files.pythonhosted.org/packages/49/e8/ca01c5ccfeaafb026d85fa4f43ceb23eb80ea9c1385688db0ef322c751e9/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425", size = 1765156, upload-time = "2025-07-10T13:04:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/22/32/5501ab525a47ba23c20613e568174d6c63aa09e2caa22cded5c6ea8e3ada/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0", size = 1724766, upload-time = "2025-07-10T13:04:13.961Z" }, + { url = "https://files.pythonhosted.org/packages/06/af/28e24574801fcf1657945347ee10df3892311c2829b41232be6089e461e7/aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729", size = 422641, upload-time = "2025-07-10T13:04:16.018Z" }, + { url = "https://files.pythonhosted.org/packages/98/d5/7ac2464aebd2eecac38dbe96148c9eb487679c512449ba5215d233755582/aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338", size = 449316, upload-time = "2025-07-10T13:04:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/06/48/e0d2fa8ac778008071e7b79b93ab31ef14ab88804d7ba71b5c964a7c844e/aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767", size = 695471, upload-time = "2025-07-10T13:04:20.124Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e7/f73206afa33100804f790b71092888f47df65fd9a4cd0e6800d7c6826441/aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e", size = 473128, upload-time = "2025-07-10T13:04:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/df/e2/4dd00180be551a6e7ee979c20fc7c32727f4889ee3fd5b0586e0d47f30e1/aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63", size = 465426, upload-time = "2025-07-10T13:04:24.071Z" }, + { url = "https://files.pythonhosted.org/packages/de/dd/525ed198a0bb674a323e93e4d928443a680860802c44fa7922d39436b48b/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d", size = 1704252, upload-time = "2025-07-10T13:04:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b1/01e542aed560a968f692ab4fc4323286e8bc4daae83348cd63588e4f33e3/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab", size = 1685514, upload-time = "2025-07-10T13:04:28.186Z" }, + { url = "https://files.pythonhosted.org/packages/b3/06/93669694dc5fdabdc01338791e70452d60ce21ea0946a878715688d5a191/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4", size = 1737586, upload-time = "2025-07-10T13:04:30.195Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3a/18991048ffc1407ca51efb49ba8bcc1645961f97f563a6c480cdf0286310/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026", size = 1786958, upload-time = "2025-07-10T13:04:32.482Z" }, + { url = "https://files.pythonhosted.org/packages/30/a8/81e237f89a32029f9b4a805af6dffc378f8459c7b9942712c809ff9e76e5/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd", size = 1709287, upload-time = "2025-07-10T13:04:34.493Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e3/bd67a11b0fe7fc12c6030473afd9e44223d456f500f7cf526dbaa259ae46/aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88", size = 1622990, upload-time = "2025-07-10T13:04:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/83/ba/e0cc8e0f0d9ce0904e3cf2d6fa41904e379e718a013c721b781d53dcbcca/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086", size = 1676015, upload-time = "2025-07-10T13:04:38.958Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b3/1e6c960520bda094c48b56de29a3d978254637ace7168dd97ddc273d0d6c/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933", size = 1707678, upload-time = "2025-07-10T13:04:41.275Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/929a3eb8c35b7f9f076a462eaa9830b32c7f27d3395397665caa5e975614/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151", size = 1650274, upload-time = "2025-07-10T13:04:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/81682a6f20dd1b18ce3d747de8eba11cbef9b270f567426ff7880b096b48/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8", size = 1726408, upload-time = "2025-07-10T13:04:45.577Z" }, + { url = "https://files.pythonhosted.org/packages/8c/17/884938dffaa4048302985483f77dfce5ac18339aad9b04ad4aaa5e32b028/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3", size = 1759879, upload-time = "2025-07-10T13:04:47.663Z" }, + { url = "https://files.pythonhosted.org/packages/95/78/53b081980f50b5cf874359bde707a6eacd6c4be3f5f5c93937e48c9d0025/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758", size = 1708770, upload-time = "2025-07-10T13:04:49.944Z" }, + { url = "https://files.pythonhosted.org/packages/ed/91/228eeddb008ecbe3ffa6c77b440597fdf640307162f0c6488e72c5a2d112/aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5", size = 421688, upload-time = "2025-07-10T13:04:51.993Z" }, + { url = "https://files.pythonhosted.org/packages/66/5f/8427618903343402fdafe2850738f735fd1d9409d2a8f9bcaae5e630d3ba/aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa", size = 448098, upload-time = "2025-07-10T13:04:53.999Z" }, ] [[package]] name = "aiosignal" -version = "1.3.2" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424, upload-time = "2024-12-13T17:10:40.86Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] @@ -111,11 +112,11 @@ wheels = [ [[package]] name = "astroid" -version = "3.3.10" +version = "3.3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/c2/9b2de9ed027f9fe5734a6c0c0a601289d796b3caaf1e372e23fa88a73047/astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce", size = 398941, upload-time = "2025-05-10T13:33:10.405Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/74/dfb75f9ccd592bbedb175d4a32fc643cf569d7c218508bfbd6ea7ef9c091/astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce", size = 400439, upload-time = "2025-07-13T18:04:23.177Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/58/5260205b9968c20b6457ed82f48f9e3d6edf2f1f95103161798b73aeccf0/astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb", size = 275388, upload-time = "2025-05-10T13:33:08.391Z" }, + { url = "https://files.pythonhosted.org/packages/af/0f/3b8fdc946b4d9cc8cc1e8af42c4e409468c84441b933d037e101b3d72d86/astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec", size = 275612, upload-time = "2025-07-13T18:04:21.07Z" }, ] [[package]] @@ -189,13 +190,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, ] +[[package]] +name = "build" +version = "1.2.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, +] + +[[package]] +name = "cachetools" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/89/817ad5d0411f136c484d535952aef74af9b25e0d99e90cdffbe121e6d628/cachetools-6.1.0.tar.gz", hash = "sha256:b4c4f404392848db3ce7aac34950d17be4d864da4b8b66911008e430bc544587", size = 30714, upload-time = "2025-06-16T18:51:03.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/f0/2ef431fe4141f5e334759d73e81120492b23b2824336883a91ac04ba710b/cachetools-6.1.0-py3-none-any.whl", hash = "sha256:1c7bb3cf9193deaf3508b7c5f2a79986c13ea38965c5adcff1f84519cf39163e", size = 11189, upload-time = "2025-06-16T18:51:01.514Z" }, +] + [[package]] name = "certifi" -version = "2025.6.15" +version = "2025.7.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, + { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, ] [[package]] @@ -256,44 +305,73 @@ wheels = [ [[package]] name = "coverage" -version = "7.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" }, - { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" }, - { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" }, - { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" }, - { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" }, - { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" }, - { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a7/a027970c991ca90f24e968999f7d509332daf6b8c3533d68633930aaebac/coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631", size = 212358, upload-time = "2025-06-13T13:01:30.909Z" }, - { url = "https://files.pythonhosted.org/packages/f2/48/6aaed3651ae83b231556750280682528fea8ac7f1232834573472d83e459/coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f", size = 212620, upload-time = "2025-06-13T13:01:32.256Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/f4b613f3b44d8b9f144847c89151992b2b6b79cbc506dee89ad0c35f209d/coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd", size = 245788, upload-time = "2025-06-13T13:01:33.948Z" }, - { url = "https://files.pythonhosted.org/packages/04/d2/de4fdc03af5e4e035ef420ed26a703c6ad3d7a07aff2e959eb84e3b19ca8/coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86", size = 243001, upload-time = "2025-06-13T13:01:35.285Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e8/eed18aa5583b0423ab7f04e34659e51101135c41cd1dcb33ac1d7013a6d6/coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43", size = 244985, upload-time = "2025-06-13T13:01:36.712Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/ae9e5cce8885728c934eaa58ebfa8281d488ef2afa81c3dbc8ee9e6d80db/coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1", size = 245152, upload-time = "2025-06-13T13:01:39.303Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c8/272c01ae792bb3af9b30fac14d71d63371db227980682836ec388e2c57c0/coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751", size = 243123, upload-time = "2025-06-13T13:01:40.727Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/2819a1e3086143c094ab446e3bdf07138527a7b88cb235c488e78150ba7a/coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67", size = 244506, upload-time = "2025-06-13T13:01:42.184Z" }, - { url = "https://files.pythonhosted.org/packages/8b/4e/9f6117b89152df7b6112f65c7a4ed1f2f5ec8e60c4be8f351d91e7acc848/coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643", size = 214766, upload-time = "2025-06-13T13:01:44.482Z" }, - { url = "https://files.pythonhosted.org/packages/27/0f/4b59f7c93b52c2c4ce7387c5a4e135e49891bb3b7408dcc98fe44033bbe0/coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a", size = 215568, upload-time = "2025-06-13T13:01:45.772Z" }, - { url = "https://files.pythonhosted.org/packages/09/1e/9679826336f8c67b9c39a359352882b24a8a7aee48d4c9cad08d38d7510f/coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d", size = 213939, upload-time = "2025-06-13T13:01:47.087Z" }, - { url = "https://files.pythonhosted.org/packages/bb/5b/5c6b4e7a407359a2e3b27bf9c8a7b658127975def62077d441b93a30dbe8/coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0", size = 213079, upload-time = "2025-06-13T13:01:48.554Z" }, - { url = "https://files.pythonhosted.org/packages/a2/22/1e2e07279fd2fd97ae26c01cc2186e2258850e9ec125ae87184225662e89/coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d", size = 213299, upload-time = "2025-06-13T13:01:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/14/c0/4c5125a4b69d66b8c85986d3321520f628756cf524af810baab0790c7647/coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f", size = 256535, upload-time = "2025-06-13T13:01:51.314Z" }, - { url = "https://files.pythonhosted.org/packages/81/8b/e36a04889dda9960be4263e95e777e7b46f1bb4fc32202612c130a20c4da/coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029", size = 252756, upload-time = "2025-06-13T13:01:54.403Z" }, - { url = "https://files.pythonhosted.org/packages/98/82/be04eff8083a09a4622ecd0e1f31a2c563dbea3ed848069e7b0445043a70/coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece", size = 254912, upload-time = "2025-06-13T13:01:56.769Z" }, - { url = "https://files.pythonhosted.org/packages/0f/25/c26610a2c7f018508a5ab958e5b3202d900422cf7cdca7670b6b8ca4e8df/coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683", size = 256144, upload-time = "2025-06-13T13:01:58.19Z" }, - { url = "https://files.pythonhosted.org/packages/c5/8b/fb9425c4684066c79e863f1e6e7ecebb49e3a64d9f7f7860ef1688c56f4a/coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f", size = 254257, upload-time = "2025-06-13T13:01:59.645Z" }, - { url = "https://files.pythonhosted.org/packages/93/df/27b882f54157fc1131e0e215b0da3b8d608d9b8ef79a045280118a8f98fe/coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10", size = 255094, upload-time = "2025-06-13T13:02:01.37Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/cad1c3dbed8b3ee9e16fa832afe365b4e3eeab1fb6edb65ebbf745eabc92/coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363", size = 215437, upload-time = "2025-06-13T13:02:02.905Z" }, - { url = "https://files.pythonhosted.org/packages/99/4d/fad293bf081c0e43331ca745ff63673badc20afea2104b431cdd8c278b4c/coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7", size = 216605, upload-time = "2025-06-13T13:02:05.638Z" }, - { url = "https://files.pythonhosted.org/packages/1f/56/4ee027d5965fc7fc126d7ec1187529cc30cc7d740846e1ecb5e92d31b224/coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c", size = 214392, upload-time = "2025-06-13T13:02:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, +version = "7.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/b7/c0465ca253df10a9e8dae0692a4ae6e9726d245390aaef92360e1d6d3832/coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b", size = 813556, upload-time = "2025-07-03T10:54:15.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/d7/7deefc6fd4f0f1d4c58051f4004e366afc9e7ab60217ac393f247a1de70a/coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0", size = 212344, upload-time = "2025-07-03T10:53:09.3Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/ee03c95d32be4d519e6a02e601267769ce2e9a91fc8faa1b540e3626c680/coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3", size = 212580, upload-time = "2025-07-03T10:53:11.52Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9f/826fa4b544b27620086211b87a52ca67592622e1f3af9e0a62c87aea153a/coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1", size = 246383, upload-time = "2025-07-03T10:53:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b3/4477aafe2a546427b58b9c540665feff874f4db651f4d3cb21b308b3a6d2/coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615", size = 243400, upload-time = "2025-07-03T10:53:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c2/efffa43778490c226d9d434827702f2dfbc8041d79101a795f11cbb2cf1e/coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b", size = 245591, upload-time = "2025-07-03T10:53:15.872Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e7/a59888e882c9a5f0192d8627a30ae57910d5d449c80229b55e7643c078c4/coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9", size = 245402, upload-time = "2025-07-03T10:53:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/92/a5/72fcd653ae3d214927edc100ce67440ed8a0a1e3576b8d5e6d066ed239db/coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f", size = 243583, upload-time = "2025-07-03T10:53:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f5/84e70e4df28f4a131d580d7d510aa1ffd95037293da66fd20d446090a13b/coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d", size = 244815, upload-time = "2025-07-03T10:53:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/39/e7/d73d7cbdbd09fdcf4642655ae843ad403d9cbda55d725721965f3580a314/coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355", size = 214719, upload-time = "2025-07-03T10:53:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d6/7486dcc3474e2e6ad26a2af2db7e7c162ccd889c4c68fa14ea8ec189c9e9/coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0", size = 215509, upload-time = "2025-07-03T10:53:22.853Z" }, + { url = "https://files.pythonhosted.org/packages/b7/34/0439f1ae2593b0346164d907cdf96a529b40b7721a45fdcf8b03c95fcd90/coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b", size = 213910, upload-time = "2025-07-03T10:53:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/94/9d/7a8edf7acbcaa5e5c489a646226bed9591ee1c5e6a84733c0140e9ce1ae1/coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038", size = 212367, upload-time = "2025-07-03T10:53:25.811Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9e/5cd6f130150712301f7e40fb5865c1bc27b97689ec57297e568d972eec3c/coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d", size = 212632, upload-time = "2025-07-03T10:53:27.075Z" }, + { url = "https://files.pythonhosted.org/packages/a8/de/6287a2c2036f9fd991c61cefa8c64e57390e30c894ad3aa52fac4c1e14a8/coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3", size = 245793, upload-time = "2025-07-03T10:53:28.408Z" }, + { url = "https://files.pythonhosted.org/packages/06/cc/9b5a9961d8160e3cb0b558c71f8051fe08aa2dd4b502ee937225da564ed1/coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14", size = 243006, upload-time = "2025-07-03T10:53:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/49/d9/4616b787d9f597d6443f5588619c1c9f659e1f5fc9eebf63699eb6d34b78/coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6", size = 244990, upload-time = "2025-07-03T10:53:31.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/83/801cdc10f137b2d02b005a761661649ffa60eb173dcdaeb77f571e4dc192/coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b", size = 245157, upload-time = "2025-07-03T10:53:32.717Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/41911ed7e9d3ceb0ffb019e7635468df7499f5cc3edca5f7dfc078e9c5ec/coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d", size = 243128, upload-time = "2025-07-03T10:53:34.009Z" }, + { url = "https://files.pythonhosted.org/packages/10/41/344543b71d31ac9cb00a664d5d0c9ef134a0fe87cb7d8430003b20fa0b7d/coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868", size = 244511, upload-time = "2025-07-03T10:53:35.434Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/3b68c77e4812105e2a060f6946ba9e6f898ddcdc0d2bfc8b4b152a9ae522/coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a", size = 214765, upload-time = "2025-07-03T10:53:36.787Z" }, + { url = "https://files.pythonhosted.org/packages/06/a2/7fac400f6a346bb1a4004eb2a76fbff0e242cd48926a2ce37a22a6a1d917/coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b", size = 215536, upload-time = "2025-07-03T10:53:38.188Z" }, + { url = "https://files.pythonhosted.org/packages/08/47/2c6c215452b4f90d87017e61ea0fd9e0486bb734cb515e3de56e2c32075f/coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694", size = 213943, upload-time = "2025-07-03T10:53:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/e211e942b22d6af5e0f323faa8a9bc7c447a1cf1923b64c47523f36ed488/coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5", size = 213088, upload-time = "2025-07-03T10:53:40.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2f/762551f97e124442eccd907bf8b0de54348635b8866a73567eb4e6417acf/coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b", size = 213298, upload-time = "2025-07-03T10:53:42.218Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b7/76d2d132b7baf7360ed69be0bcab968f151fa31abe6d067f0384439d9edb/coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3", size = 256541, upload-time = "2025-07-03T10:53:43.823Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/392b219837d7ad47d8e5974ce5f8dc3deb9f99a53b3bd4d123602f960c81/coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8", size = 252761, upload-time = "2025-07-03T10:53:45.19Z" }, + { url = "https://files.pythonhosted.org/packages/d5/77/4256d3577fe1b0daa8d3836a1ebe68eaa07dd2cbaf20cf5ab1115d6949d4/coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46", size = 254917, upload-time = "2025-07-03T10:53:46.931Z" }, + { url = "https://files.pythonhosted.org/packages/53/99/fc1a008eef1805e1ddb123cf17af864743354479ea5129a8f838c433cc2c/coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584", size = 256147, upload-time = "2025-07-03T10:53:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/92/c0/f63bf667e18b7f88c2bdb3160870e277c4874ced87e21426128d70aa741f/coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e", size = 254261, upload-time = "2025-07-03T10:53:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/8c/32/37dd1c42ce3016ff8ec9e4b607650d2e34845c0585d3518b2a93b4830c1a/coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac", size = 255099, upload-time = "2025-07-03T10:53:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/da/2e/af6b86f7c95441ce82f035b3affe1cd147f727bbd92f563be35e2d585683/coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926", size = 215440, upload-time = "2025-07-03T10:53:52.808Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bb/8a785d91b308867f6b2e36e41c569b367c00b70c17f54b13ac29bcd2d8c8/coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd", size = 216537, upload-time = "2025-07-03T10:53:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a0/a6bffb5e0f41a47279fd45a8f3155bf193f77990ae1c30f9c224b61cacb0/coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb", size = 214398, upload-time = "2025-07-03T10:53:56.715Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/bbe2e63902847cf79036ecc75550d0698af31c91c7575352eb25190d0fb3/coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4", size = 204005, upload-time = "2025-07-03T10:54:13.491Z" }, +] + +[[package]] +name = "cryptography" +version = "45.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, + { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, + { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" }, + { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, + { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, ] [[package]] @@ -314,6 +392,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "durationpy" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, +] + [[package]] name = "ecdsa" version = "0.19.1" @@ -328,16 +424,16 @@ wheels = [ [[package]] name = "fastapi" -version = "0.115.14" +version = "0.116.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" }, + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, ] [[package]] @@ -427,6 +523,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, ] +[[package]] +name = "google-auth" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/77/eb1d3288dbe2ba6f4fe50b9bb41770bac514cd2eb91466b56d44a99e2f8d/google-auth-1.6.3.tar.gz", hash = "sha256:0f7c6a64927d34c1a474da92cfc59e552a5d3b940d3266606c6a28b72888b9e4", size = 80899, upload-time = "2019-02-19T21:14:58.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/9b/ed0516cc1f7609fb0217e3057ff4f0f9f3e3ce79a369c6af4a6c5ca25664/google_auth-1.6.3-py2.py3-none-any.whl", hash = "sha256:20705f6803fd2c4d1cc2dcb0df09d4dfcb9a7d51fd59e94a3a28231fd93119ed", size = 73441, upload-time = "2019-02-19T21:14:56.623Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.70.0" @@ -493,7 +604,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.33.1" +version = "0.33.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -505,9 +616,21 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/01/bfe0534a63ce7a2285e90dbb33e8a5b815ff096d8f7743b135c256916589/huggingface_hub-0.33.1.tar.gz", hash = "sha256:589b634f979da3ea4b8bdb3d79f97f547840dc83715918daf0b64209c0844c7b", size = 426728, upload-time = "2025-06-25T12:02:57.605Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/9e/9366b7349fc125dd68b9d384a0fea84d67b7497753fe92c71b67e13f47c4/huggingface_hub-0.33.4.tar.gz", hash = "sha256:6af13478deae120e765bfd92adad0ae1aec1ad8c439b46f23058ad5956cbca0a", size = 426674, upload-time = "2025-07-11T12:32:48.694Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/7b/98daa50a2db034cab6cd23a3de04fa2358cb691593d28e9130203eb7a805/huggingface_hub-0.33.4-py3-none-any.whl", hash = "sha256:09f9f4e7ca62547c70f8b82767eefadd2667f4e116acba2e3e62a5a81815a7bb", size = 515339, upload-time = "2025-07-11T12:32:46.346Z" }, +] + +[[package]] +name = "id" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/fb/5307bd3612eb0f0e62c3a916ae531d3a31e58fb5c82b58e3ebf7fd6f47a1/huggingface_hub-0.33.1-py3-none-any.whl", hash = "sha256:ec8d7444628210c0ba27e968e3c4c973032d44dcea59ca0d78ef3f612196f095", size = 515377, upload-time = "2025-06-25T12:02:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, ] [[package]] @@ -549,6 +672,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/1c/831faaaa0f090b711c355c6d8b2abf277c72133aab472b6932b03322294c/jaraco_functools-4.2.1.tar.gz", hash = "sha256:be634abfccabce56fa3053f8c7ebe37b682683a4ee7793670ced17bab0087353", size = 19661, upload-time = "2025-06-21T19:22:03.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/fd/179a20f832824514df39a90bb0e5372b314fea99f217f5ab942b10a8a4e8/jaraco_functools-4.2.1-py3-none-any.whl", hash = "sha256:590486285803805f4b1f99c60ca9e94ed348d4added84b74c7a12885561e524e", size = 10349, upload-time = "2025-06-21T19:22:02.039Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -623,17 +788,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, ] +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, +] + +[[package]] +name = "kubernetes" +version = "33.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "durationpy" }, + { name = "google-auth" }, + { name = "oauthlib" }, + { name = "python-dateutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "six" }, + { name = "urllib3" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/52/19ebe8004c243fdfa78268a96727c71e08f00ff6fe69a301d0b7fcbce3c2/kubernetes-33.1.0.tar.gz", hash = "sha256:f64d829843a54c251061a8e7a14523b521f2dc5c896cf6d65ccf348648a88993", size = 1036779, upload-time = "2025-06-09T21:57:58.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/43/d9bebfc3db7dea6ec80df5cb2aad8d274dd18ec2edd6c4f21f32c237cbbb/kubernetes-33.1.0-py2.py3-none-any.whl", hash = "sha256:544de42b24b64287f7e0aa9513c93cb503f7f40eea39b20f66810011a86eabc5", size = 1941335, upload-time = "2025-06-09T21:57:56.327Z" }, +] + [[package]] name = "lightspeed-stack" source = { editable = "." } dependencies = [ + { name = "cachetools" }, { name = "fastapi" }, + { name = "kubernetes" }, { name = "llama-stack" }, { name = "rich" }, { name = "uvicorn" }, ] [package.dev-dependencies] +build = [ + { name = "build" }, + { name = "twine" }, +] dev = [ { name = "aiosqlite" }, { name = "behave" }, @@ -648,17 +858,24 @@ dev = [ { name = "pytest-mock" }, { name = "ruff" }, { name = "types-pyyaml" }, + { name = "types-requests" }, ] [package.metadata] requires-dist = [ + { name = "cachetools", specifier = ">=6.1.0" }, { name = "fastapi", specifier = ">=0.115.6" }, + { name = "kubernetes", specifier = ">=30.1.0" }, { name = "llama-stack", specifier = ">=0.2.13" }, { name = "rich", specifier = ">=14.0.0" }, { name = "uvicorn", specifier = ">=0.34.3" }, ] [package.metadata.requires-dev] +build = [ + { name = "build", specifier = ">=1.2.2.post1" }, + { name = "twine", specifier = ">=5.1.1" }, +] dev = [ { name = "aiosqlite" }, { name = "behave", specifier = ">=1.2.6" }, @@ -673,11 +890,12 @@ dev = [ { name = "pytest-mock", specifier = ">=3.14.0" }, { name = "ruff", specifier = ">=0.11.13" }, { name = "types-pyyaml", specifier = ">=6.0.2" }, + { name = "types-requests", specifier = ">=2.28.0" }, ] [[package]] name = "llama-stack" -version = "0.2.13" +version = "0.2.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -706,14 +924,14 @@ dependencies = [ { name = "tiktoken" }, { name = "uvicorn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/8f/71efe6408ac0412e5d6f9cc2ceb984d4e4723f393dd05197ae1b6673ae94/llama_stack-0.2.13.tar.gz", hash = "sha256:be59570fde5e39224ea8358e2b04e6d92dc37a36a24f5fdf20c853f02793aaa2", size = 3289810, upload-time = "2025-06-27T23:55:56.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d2/03/8ca9c1aa98e901ee97238ff7779aca9bd5d6edb1cc00b15135f9e031063e/llama_stack-0.2.14.tar.gz", hash = "sha256:d6ca562607029466e97a0dcf822895f3a89c480506f4903dd95928f5a88f3f79", size = 3298570, upload-time = "2025-07-04T06:04:48.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/f0/8b7cb34a1cea38ce32a0c997e5d71e04fbe6d410d1621f9467fbefef8d03/llama_stack-0.2.13-py3-none-any.whl", hash = "sha256:426c0cab38d561b25ce3f2eb757f26ea4bce9783ef80eb6d1dad1bb9bcc4e168", size = 3660630, upload-time = "2025-06-27T23:55:54.76Z" }, + { url = "https://files.pythonhosted.org/packages/6a/17/b8ba17fb6f37fc6351851b5b9f0d359da7321090e98a2cda73a5926e29fe/llama_stack-0.2.14-py3-none-any.whl", hash = "sha256:a1323260aa64bbcaba8ee6913819d2cf724012cdb8502b54f57c9bc6d1e2cebf", size = 3669221, upload-time = "2025-07-04T06:04:47.532Z" }, ] [[package]] name = "llama-stack-client" -version = "0.2.13" +version = "0.2.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -732,9 +950,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/a6/272b9a522df3580df763627c4bf74447aec02d44b9218fe192efc8721a46/llama_stack_client-0.2.13.tar.gz", hash = "sha256:af4a6cff681126e9a42d4c5c9522bc5946d5ad6e2d620e8e6727dc0c8cc82989", size = 252548, upload-time = "2025-06-27T23:55:48.395Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/a5/342290f9a028b2d1b507a2a88408541cc2ac90aece38be7a4bf9fbc19067/llama_stack_client-0.2.14.tar.gz", hash = "sha256:c97c4d4cf6f97e5e9b8409ce8da9e2e7637e1d3c1c6e12696af7009b8b59da7e", size = 258614, upload-time = "2025-07-04T06:04:41.595Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/c2/74bd3f28a4537fc3e5edd4cb00fd50941479f5b6d5c5cb278a24857551f2/llama_stack_client-0.2.13-py3-none-any.whl", hash = "sha256:cec627ce58a6a42ccfcd29f6329f6cd891170ae012dac676bfc25ae1440d6769", size = 343112, upload-time = "2025-06-27T23:55:46.927Z" }, + { url = "https://files.pythonhosted.org/packages/75/f9/90bb372d2b63f0c82a02827c4007ad842918f2a8886268b7ff718ec86bf5/llama_stack_client-0.2.14-py3-none-any.whl", hash = "sha256:45c1aa5a6be97377151cc63aa8e638b97806f9b915fbe2c9ec3892136fa0c4b4", size = 353443, upload-time = "2025-07-04T06:04:40.377Z" }, ] [[package]] @@ -805,67 +1023,76 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "more-itertools" +version = "10.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" }, +] + [[package]] name = "multidict" -version = "6.6.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/5d/d72502cd6dd64b0c5a5117b1701f05c38e94ffb4a1b4ab65ff0cd9b974e8/multidict-6.6.2.tar.gz", hash = "sha256:c1e8b8b0523c0361a78ce9b99d9850c51cf25e1fa3c5686030ce75df6fdf2918", size = 100939, upload-time = "2025-06-28T14:38:20.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/67/244bc9038eb05bae87a07d494ff48e43a4be7417c3fd538e0ea65c1beebf/multidict-6.6.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6e5e426aff6b5b32167b1185909ea390e51e59c7a6adfe65de16746e5739d8c1", size = 76357, upload-time = "2025-06-28T14:36:37.071Z" }, - { url = "https://files.pythonhosted.org/packages/61/3c/03a4d33683ffa9851a14e14cafa76130be99101b2a1b446d47967f47f68e/multidict-6.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c4617af352d3e03b5febd040100d1bba67ac844e0f7780c8a124358883119dd", size = 45313, upload-time = "2025-06-28T14:36:38.121Z" }, - { url = "https://files.pythonhosted.org/packages/50/3c/5eca9c3be9ccb31c26ad144b5fb5160c29d853cd8bc52c1ce53ffd838a0a/multidict-6.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:65854da6c2f065f7e52c4385727494d72b25eaf4e901b15fb3f61e21bb0b52eb", size = 43528, upload-time = "2025-06-28T14:36:39.169Z" }, - { url = "https://files.pythonhosted.org/packages/3a/d8/6707b7ac3fd336b034b89e9ac5fdcca045e8f6b84ee4163c1857795366b4/multidict-6.6.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c477d3bc9a1aa0214f5639a8c1b4a6b3cd9faea5a861b4001a6df62294dcc952", size = 238181, upload-time = "2025-06-28T14:36:40.703Z" }, - { url = "https://files.pythonhosted.org/packages/a8/24/b822b9f9bceed4f22008172717d601d6209bbe7daca2d35828be60208ba9/multidict-6.6.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19a4bc7c5c1a25424812a26e8dccb18fff65a5f24515d2f3b85302ca3f3914f", size = 257172, upload-time = "2025-06-28T14:36:42.402Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/7995824cc95a15daebb15da87fc9509cc3c35027885d534d80718c55d10e/multidict-6.6.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:54ffaf44483b082602c1e1831472e3369c005f092271dbbcad2f7b12d1e84019", size = 242147, upload-time = "2025-06-28T14:36:43.702Z" }, - { url = "https://files.pythonhosted.org/packages/f8/44/23c9b50461423766d9f32b013a49ce07b358a1188d43cfa977385a872d03/multidict-6.6.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a526df4fe21d2dc68265c046685042fc89187dc40754489f32f7efc05e264b0f", size = 267431, upload-time = "2025-06-28T14:36:44.964Z" }, - { url = "https://files.pythonhosted.org/packages/28/e4/72cc549230e7d93f9eca0206fac402af239058d8a9f0fb95f348762e8fdd/multidict-6.6.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:73e8763061f0a38cec6d084b696964ee7b7e50c10c89a64b20be7044dca36a74", size = 269480, upload-time = "2025-06-28T14:36:46.569Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e3/a809cf2e624cb37f29f4569e756bd708cd96a93d3d940143464d9079a2f5/multidict-6.6.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81a13031b6831e4de5a89fe391d7f8b60a929f2d22dad403c69d60853e5ba1ca", size = 256759, upload-time = "2025-06-28T14:36:47.859Z" }, - { url = "https://files.pythonhosted.org/packages/03/85/ad1127e662ed20d8ba2751bf67d874380a817577cd486a7309dd50d116a1/multidict-6.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6421f4d5acd632af56ae90906755b242e518d59f5313a7b41cd55fb59adfcd74", size = 252393, upload-time = "2025-06-28T14:36:49.169Z" }, - { url = "https://files.pythonhosted.org/packages/36/b3/67c331269372e38c435dff4c4b3b5ca8aba958dd58936153c5e64d07a515/multidict-6.6.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3fed2cfff9d8e3316fc4c5aca40f33d7cd29b5a9a4cbf4aa17dfcae592ccb17c", size = 249848, upload-time = "2025-06-28T14:36:50.88Z" }, - { url = "https://files.pythonhosted.org/packages/06/56/54d51eb89cdcb5518828081cb396219699468f70266ef0fcacf57a339319/multidict-6.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eb97a4eed516fb3d63028fc0a8a8661e1acdf7925eace9c85153ff967926331c", size = 249993, upload-time = "2025-06-28T14:36:52.562Z" }, - { url = "https://files.pythonhosted.org/packages/78/8e/afc23d4d59ac2969743fdabb7fbd722c0b8bf333c31b02e8594e21661755/multidict-6.6.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9bbef50bfefe84213b791c9a326d3221fa31181ba39576049a55c1eef9768109", size = 262437, upload-time = "2025-06-28T14:36:54.251Z" }, - { url = "https://files.pythonhosted.org/packages/3c/ec/74586ce0ebb48a7394719d5d2fda019ec7cc41e3fc01cb50ecd82cf80f6e/multidict-6.6.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d7d15b9285d439c3ca80b97b0ed6cc98a2df22c481de1848b77117563ddba14", size = 259363, upload-time = "2025-06-28T14:36:55.514Z" }, - { url = "https://files.pythonhosted.org/packages/ae/82/1fa2fbdc85d98b6c764b4a49e22e118b8d987f1fb5936cadfbdf091f06ef/multidict-6.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5244c5c12889d84b9b7bf22f875e52c5ba4daa89c8ab92719863a14cd76dd04d", size = 252014, upload-time = "2025-06-28T14:36:56.734Z" }, - { url = "https://files.pythonhosted.org/packages/59/bc/21e7c4bb6e7911cac9fb41d4b295abb2d98c2123196d7c692e9e6e9f1ac4/multidict-6.6.2-cp312-cp312-win32.whl", hash = "sha256:a2ec0e52d7b298d53983cc4987fe76a25e033305f58d597fbcc1ff139b5e417e", size = 41826, upload-time = "2025-06-28T14:36:57.924Z" }, - { url = "https://files.pythonhosted.org/packages/61/57/5bd2019d7b2a5846c75372b1a5d994358739649d8863c73c37f2f7a418a6/multidict-6.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:96d2d55c01ce4ec000a1b6eadbaa22971c91ec512819abee8b5b13f4af3fd566", size = 45920, upload-time = "2025-06-28T14:36:59.352Z" }, - { url = "https://files.pythonhosted.org/packages/91/c1/b1038c82ccc2e2ae3c40c912b8ee6a45ed0c9349dffdd1c3fc073f733ee9/multidict-6.6.2-cp312-cp312-win_arm64.whl", hash = "sha256:a0af3b15eab84e0d4f62a365927070d7f200db7efb8bb1e17de7c14fab5183bb", size = 43207, upload-time = "2025-06-28T14:37:00.872Z" }, - { url = "https://files.pythonhosted.org/packages/10/8d/3334cceab0ca6eaa6da56ae1031c86c908ea3569a963c87d6c1142c966db/multidict-6.6.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c7f285ab85059a75b053027231626aeeabb4432191420d5c83cd91e2e462d25e", size = 75703, upload-time = "2025-06-28T14:37:02.224Z" }, - { url = "https://files.pythonhosted.org/packages/6f/47/950c13434ecb30551c4a0afefa654deb2b08953dc47fc00e529ca8e58abd/multidict-6.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6f6fae153d1c78c37c2203b46c3062e942297eede21ebabea15fbfcaa7fa51be", size = 44980, upload-time = "2025-06-28T14:37:03.717Z" }, - { url = "https://files.pythonhosted.org/packages/07/83/39a809575daf4dae47493eb0c15cc365b045bc52b955efea1f7d3182045c/multidict-6.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2c051b7191d185fc7add9cdc52326acf93791884e51062605da0ff4371f679a1", size = 43215, upload-time = "2025-06-28T14:37:05.236Z" }, - { url = "https://files.pythonhosted.org/packages/13/99/447c1c69dc603a16b7bf7d1c2ed0c5068a89248b91983f325bc643fca8bb/multidict-6.6.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dc609fb7fadeb0e2d3a752dcfbb62fc23a2d5cc88316fe199366f73aa74a3215", size = 236699, upload-time = "2025-06-28T14:37:06.405Z" }, - { url = "https://files.pythonhosted.org/packages/68/99/be2925fd170c3216dbd5321766b477cb411e57ed5d68ee302a114535730e/multidict-6.6.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5245f0cba904051470c1b9c281533301457bc952992ac0d71a8668c2d10a4134", size = 254988, upload-time = "2025-06-28T14:37:07.768Z" }, - { url = "https://files.pythonhosted.org/packages/49/09/4881007f3b82b6e2c110bc381ec52b2a5a97603505b54068549b59fa2cf4/multidict-6.6.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4409bbc4595a182908b539b156f882a5d72688a91343e0d3b0782c5cf1e85322", size = 240550, upload-time = "2025-06-28T14:37:09.118Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/0e44d04eb2b87848545b04b88feba8d256e0e39eef61031b5634c30c4ab1/multidict-6.6.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ebd518dcc942616a539310e5deb0c29fbf4d0efa80de60186b53a2905192997", size = 266128, upload-time = "2025-06-28T14:37:10.432Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f4/d999b4b52cab130a0a30485511e3612cfca79f440bd5f80736f6cdf6416e/multidict-6.6.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a50596d0b2f950015f80d277b42922cf30e0e1fb2348af3da5f4a07808f2c385", size = 266930, upload-time = "2025-06-28T14:37:11.886Z" }, - { url = "https://files.pythonhosted.org/packages/69/cb/b84afdb961dcf09b8e8c0238f068122d85480bfaac2c5c0b03120e497318/multidict-6.6.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6c2d7686d2f9ecb18192455aa04345a05646f45a286d67b31b438eaf749a91e", size = 255081, upload-time = "2025-06-28T14:37:13.302Z" }, - { url = "https://files.pythonhosted.org/packages/23/a0/9c2f05cf91a8f645565e06529149542badebdc19b1fda24f220f1de4022f/multidict-6.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140fe62aaac9c401918a6cc5919afb99c1c676c6b449f58a6702db8ed333f394", size = 250410, upload-time = "2025-06-28T14:37:15.79Z" }, - { url = "https://files.pythonhosted.org/packages/84/6e/0754123af79ef30760cbb09b65fd389b014b5d608eba308e23af93a4af09/multidict-6.6.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2a7ea17e5d3cab04047b8880da1224a124c1ee7a8703dddce2cb66e6931c70f3", size = 249469, upload-time = "2025-06-28T14:37:17.151Z" }, - { url = "https://files.pythonhosted.org/packages/dd/73/06876ff14d142f9a88e782998b85efb9062b0dbd5006fa38f3ffb563e13f/multidict-6.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a88f0fab41095ff860987a4f4f2c12bf96193c0bce8b59f574c20c46542a4e5a", size = 249482, upload-time = "2025-06-28T14:37:18.418Z" }, - { url = "https://files.pythonhosted.org/packages/41/d2/499276d2afb6d854897f4dd1e1e92b4db034bbfcbcee532bbd42628fb386/multidict-6.6.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:35c3c207c03c3d81d25afaa99df9f04158011d069a0716bbfc1c37e1006bab7c", size = 261314, upload-time = "2025-06-28T14:37:19.697Z" }, - { url = "https://files.pythonhosted.org/packages/60/85/3a1d095e153cfbb6fe0bcde75b245d2b53c4686bb7574a100f311abbcd5a/multidict-6.6.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:845b5d2f1a8f4a9c4115ef84ab4f6cd4f35dbc2cebd5ab0a3d84d79510b3a27c", size = 257589, upload-time = "2025-06-28T14:37:21.068Z" }, - { url = "https://files.pythonhosted.org/packages/ec/28/131c945fb113310325d56df838e32547fd40b65906bb82196d6605e09397/multidict-6.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ac9cf00f77d7bb3c796a08812a9a4fcad052a313373a5e294b3fb1c9efe042fd", size = 250255, upload-time = "2025-06-28T14:37:22.347Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2a/78462ad3539d8dd1ecbed5a4ea4f4a341b3625dd5d479b5c93841217bcba/multidict-6.6.2-cp313-cp313-win32.whl", hash = "sha256:21a6477462132909c072671b51e74786eb6b9ce267257e305a7c924df79838a6", size = 41643, upload-time = "2025-06-28T14:37:23.684Z" }, - { url = "https://files.pythonhosted.org/packages/08/5e/9c90011f219572369cb76140a8438516f401a965cf83f3ab226a6b567f1d/multidict-6.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:09dc7f1bfb1571bfed0c32f8f66e2065a48488ed0da5b58de7a9be58079c95e6", size = 45733, upload-time = "2025-06-28T14:37:25.14Z" }, - { url = "https://files.pythonhosted.org/packages/0d/86/e6f96122fe6c7d1d221ac96384db403ee2af0edd2693b235d986caed69d4/multidict-6.6.2-cp313-cp313-win_arm64.whl", hash = "sha256:7a7748bffbfd398bd3e82cbb1c78dcf91f1dd67d1a23388f508adfb606cd4d77", size = 43053, upload-time = "2025-06-28T14:37:26.409Z" }, - { url = "https://files.pythonhosted.org/packages/35/a0/31723594e1a7a4432611a1bc2fc31a1b15042e077f3cd03ad03b9b3fc7c8/multidict-6.6.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0d2471a261c977b71b9bf1f3bb2aab1dc8a807a43f018a9d5fb420723fa9c77e", size = 82669, upload-time = "2025-06-28T14:37:27.582Z" }, - { url = "https://files.pythonhosted.org/packages/f5/d7/b6d56e5790b91ad91693159bc10379fe96972c1f72c63bb94aa5c6c25837/multidict-6.6.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:03f20dfe1e118ce85467b88a2cab5e93fd750489731b3dd7a550d1da27238d80", size = 48214, upload-time = "2025-06-28T14:37:28.747Z" }, - { url = "https://files.pythonhosted.org/packages/5f/83/ff37ebd9b8c213eab3685bf5e68eceb133315835033b0a676102175a7c8b/multidict-6.6.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7f40cff60aa06eec355988f35670057fa843893652648b658e6fa3402a725d72", size = 46708, upload-time = "2025-06-28T14:37:29.912Z" }, - { url = "https://files.pythonhosted.org/packages/7a/bd/3861ca6d5bafb14191c2b1fd24dd454a7b3ab54ea835ca63c286c6baf832/multidict-6.6.2-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:aaecf4537cce47944b7eb142ba047e031650cb2150d5692d49133f58d7d8fcbf", size = 229570, upload-time = "2025-06-28T14:37:31.088Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/f077d2d28ff3a9c466c8eecc6ce2e69c5fb74d20dc660880df4719b693cf/multidict-6.6.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fbadc2c5c7bc633212e65df2908485679fa791b309c6636eafbd111c353af3d", size = 249766, upload-time = "2025-06-28T14:37:32.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/fd/0ce8c59b099396c2f765fcc71a0a4eaac50c11befcdd15d880025cac091b/multidict-6.6.2-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a914d3b7d4f4f22d34588b5af75ddb496a9947f67b2a8a4ea515d23118d338b0", size = 228452, upload-time = "2025-06-28T14:37:33.809Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cb/252c1185fcff431fd727f11c39f64adc63b1f7d8c3ce826dd178b2f7c7e6/multidict-6.6.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57dc09924085acf2bba3923d6f043a90fd527dac5f05874e3f740f45f1ca1c3c", size = 256795, upload-time = "2025-06-28T14:37:35.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d4/b78ade0ab9bd90a70490ac9f421f2d6a1f655c307d2815c82bb0bca364d7/multidict-6.6.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2a00b5f20e601edc82759734c6b64cd68795c1a9691735b599cba51c5d406914", size = 257372, upload-time = "2025-06-28T14:37:36.572Z" }, - { url = "https://files.pythonhosted.org/packages/52/10/b20998c7063e8db0bfb250a359cbbb8b38ba5f211b2e9db4d0939e0657b3/multidict-6.6.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcfa8cafb01c892d3dae719083127af0cf2e3eb0ce4c73b33c9f88af60f2a313", size = 246618, upload-time = "2025-06-28T14:37:37.909Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/5d7caecad5376dea7f7d4e3ac0d996ee1587345e13178438df9b561ff60e/multidict-6.6.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bd7a2bf9148f0dcab31207bb6c92522043b1781b43f5053c08f29a755cb5c91b", size = 244442, upload-time = "2025-06-28T14:37:39.285Z" }, - { url = "https://files.pythonhosted.org/packages/2f/29/4699a19c43abd2dc09375a2927511af61266b5d96692e761e8b05b9cb04a/multidict-6.6.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:db34ee9ebe45126fc7f90206531704ac0d2da268101180886380fe601bffe124", size = 235214, upload-time = "2025-06-28T14:37:41.079Z" }, - { url = "https://files.pythonhosted.org/packages/13/9e/94a776796154e8481fc5d175c788a20efa6552c2fd7c879bc85be537c5bd/multidict-6.6.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b7c25f8e195d4fe34270208a568c843cfc85b2906ae20600ea8bbb2248ea9774", size = 243583, upload-time = "2025-06-28T14:37:42.803Z" }, - { url = "https://files.pythonhosted.org/packages/0a/d8/d9d683c8517bae2fb076dd0c728df432af08a41d9aaf6b0c901b56b18630/multidict-6.6.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b8fb447ff4ebe073c2f4e250d9f253712f1b6eb8f2830d4f09942f50359d85ff", size = 251522, upload-time = "2025-06-28T14:37:44.156Z" }, - { url = "https://files.pythonhosted.org/packages/da/fb/d546f5d59cc897b715ca619cd2fbba9379e99a64b044a6aadd41a5abda7f/multidict-6.6.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f4d5732f6bb3bf875fffbc9b155ab2c3b65924405d76fde6ea6c21253eab58c7", size = 247023, upload-time = "2025-06-28T14:37:45.847Z" }, - { url = "https://files.pythonhosted.org/packages/41/1e/e660f11b028b35cdc5e87cf948b73128385d275b2fced205f165a0cc6d95/multidict-6.6.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b33458d93a8c06e028ffe0ca155852b0cf2078b90081e791a7aafe9380a3ee2b", size = 241903, upload-time = "2025-06-28T14:37:47.35Z" }, - { url = "https://files.pythonhosted.org/packages/ea/1f/95c5d8f164e7366fa5c2c3dc6b1955cd2cb8372cfe08c614018ba0940cf3/multidict-6.6.2-cp313-cp313t-win32.whl", hash = "sha256:43a7ddcf8f1e7ccae2197745152d4f97bb22b1b21afec05e271751dae56a576e", size = 47769, upload-time = "2025-06-28T14:37:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a1/a56a786d04f21625d14b68b8cbf9fc7cee58f837a789ae97da5d1c39a29f/multidict-6.6.2-cp313-cp313t-win_amd64.whl", hash = "sha256:6d0d1dbbe970870e23a198d2b62f81cc0b145cca3eea1ba60670125a3184561c", size = 52947, upload-time = "2025-06-28T14:37:49.95Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/f5a42680277af3f0bde14038c8caf8830279a8e788f1f95f7ed6f0e98414/multidict-6.6.2-cp313-cp313t-win_arm64.whl", hash = "sha256:3ead8284906e416fd990d44e964286393096fe0c0eedd4102fbc3a935250172a", size = 45248, upload-time = "2025-06-28T14:37:51.147Z" }, - { url = "https://files.pythonhosted.org/packages/0c/30/7b7d121f76ea3ea7561814531e5cc19e75e9b6646818491179c2c875b591/multidict-6.6.2-py3-none-any.whl", hash = "sha256:a7d14275ff2f85a8ff3c2a32e30f94b9fc8a2125b59a4ecc32271a347fad6e78", size = 12312, upload-time = "2025-06-28T14:38:19.677Z" }, +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514, upload-time = "2025-06-30T15:51:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394, upload-time = "2025-06-30T15:51:49.986Z" }, + { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590, upload-time = "2025-06-30T15:51:51.331Z" }, + { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292, upload-time = "2025-06-30T15:51:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385, upload-time = "2025-06-30T15:51:53.913Z" }, + { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328, upload-time = "2025-06-30T15:51:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057, upload-time = "2025-06-30T15:51:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341, upload-time = "2025-06-30T15:51:59.111Z" }, + { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081, upload-time = "2025-06-30T15:52:00.533Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581, upload-time = "2025-06-30T15:52:02.43Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750, upload-time = "2025-06-30T15:52:04.26Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548, upload-time = "2025-06-30T15:52:06.002Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718, upload-time = "2025-06-30T15:52:07.707Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603, upload-time = "2025-06-30T15:52:09.58Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351, upload-time = "2025-06-30T15:52:10.947Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860, upload-time = "2025-06-30T15:52:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982, upload-time = "2025-06-30T15:52:13.6Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210, upload-time = "2025-06-30T15:52:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843, upload-time = "2025-06-30T15:52:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053, upload-time = "2025-06-30T15:52:17.429Z" }, + { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273, upload-time = "2025-06-30T15:52:19.346Z" }, + { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124, upload-time = "2025-06-30T15:52:20.773Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892, upload-time = "2025-06-30T15:52:22.242Z" }, + { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547, upload-time = "2025-06-30T15:52:23.736Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223, upload-time = "2025-06-30T15:52:25.185Z" }, + { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262, upload-time = "2025-06-30T15:52:26.969Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345, upload-time = "2025-06-30T15:52:28.467Z" }, + { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248, upload-time = "2025-06-30T15:52:29.938Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115, upload-time = "2025-06-30T15:52:31.416Z" }, + { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649, upload-time = "2025-06-30T15:52:32.996Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203, upload-time = "2025-06-30T15:52:34.521Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051, upload-time = "2025-06-30T15:52:35.999Z" }, + { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601, upload-time = "2025-06-30T15:52:37.473Z" }, + { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683, upload-time = "2025-06-30T15:52:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811, upload-time = "2025-06-30T15:52:40.207Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056, upload-time = "2025-06-30T15:52:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811, upload-time = "2025-06-30T15:52:43.281Z" }, + { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304, upload-time = "2025-06-30T15:52:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775, upload-time = "2025-06-30T15:52:46.459Z" }, + { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773, upload-time = "2025-06-30T15:52:47.88Z" }, + { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083, upload-time = "2025-06-30T15:52:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980, upload-time = "2025-06-30T15:52:50.903Z" }, + { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776, upload-time = "2025-06-30T15:52:52.764Z" }, + { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882, upload-time = "2025-06-30T15:52:54.596Z" }, + { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816, upload-time = "2025-06-30T15:52:56.175Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341, upload-time = "2025-06-30T15:52:57.752Z" }, + { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854, upload-time = "2025-06-30T15:52:59.74Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432, upload-time = "2025-06-30T15:53:01.602Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731, upload-time = "2025-06-30T15:53:03.517Z" }, + { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086, upload-time = "2025-06-30T15:53:05.48Z" }, + { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338, upload-time = "2025-06-30T15:53:07.522Z" }, + { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812, upload-time = "2025-06-30T15:53:09.263Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011, upload-time = "2025-06-30T15:53:11.038Z" }, + { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254, upload-time = "2025-06-30T15:53:12.421Z" }, + { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, ] [[package]] @@ -903,6 +1130,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "nh3" +version = "0.2.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581, upload-time = "2025-02-25T13:38:44.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/81/b83775687fcf00e08ade6d4605f0be9c4584cb44c4973d9f27b7456a31c9/nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286", size = 1297678, upload-time = "2025-02-25T13:37:56.063Z" }, + { url = "https://files.pythonhosted.org/packages/22/ee/d0ad8fb4b5769f073b2df6807f69a5e57ca9cea504b78809921aef460d20/nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", size = 733774, upload-time = "2025-02-25T13:37:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/b450141e2d384ede43fe53953552f1c6741a499a8c20955ad049555cabc8/nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", size = 760012, upload-time = "2025-02-25T13:38:01.017Z" }, + { url = "https://files.pythonhosted.org/packages/97/90/1182275db76cd8fbb1f6bf84c770107fafee0cb7da3e66e416bcb9633da2/nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", size = 923619, upload-time = "2025-02-25T13:38:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/269a7cfbec9693fad8d767c34a755c25ccb8d048fc1dfc7a7d86bc99375c/nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", size = 1000384, upload-time = "2025-02-25T13:38:04.402Z" }, + { url = "https://files.pythonhosted.org/packages/68/a9/48479dbf5f49ad93f0badd73fbb48b3d769189f04c6c69b0df261978b009/nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", size = 918908, upload-time = "2025-02-25T13:38:06.693Z" }, + { url = "https://files.pythonhosted.org/packages/d7/da/0279c118f8be2dc306e56819880b19a1cf2379472e3b79fc8eab44e267e3/nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", size = 909180, upload-time = "2025-02-25T13:38:10.941Z" }, + { url = "https://files.pythonhosted.org/packages/26/16/93309693f8abcb1088ae143a9c8dbcece9c8f7fb297d492d3918340c41f1/nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", size = 532747, upload-time = "2025-02-25T13:38:12.548Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3a/96eb26c56cbb733c0b4a6a907fab8408ddf3ead5d1b065830a8f6a9c3557/nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", size = 528908, upload-time = "2025-02-25T13:38:14.059Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133, upload-time = "2025-02-25T13:38:16.601Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328, upload-time = "2025-02-25T13:38:18.972Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020, upload-time = "2025-02-25T13:38:20.571Z" }, + { url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878, upload-time = "2025-02-25T13:38:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460, upload-time = "2025-02-25T13:38:25.951Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369, upload-time = "2025-02-25T13:38:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036, upload-time = "2025-02-25T13:38:30.539Z" }, + { url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712, upload-time = "2025-02-25T13:38:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559, upload-time = "2025-02-25T13:38:35.204Z" }, + { url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591, upload-time = "2025-02-25T13:38:37.099Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670, upload-time = "2025-02-25T13:38:38.696Z" }, + { url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093, upload-time = "2025-02-25T13:38:40.249Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623, upload-time = "2025-02-25T13:38:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283, upload-time = "2025-02-25T13:38:43.355Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -953,9 +1211,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/ca/af82bf0fad4c3e573c6930ed743b5308492ff19917c7caaf2f9b6f9e2e98/numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244", size = 10260376, upload-time = "2025-06-21T12:24:56.884Z" }, ] +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + [[package]] name = "openai" -version = "1.93.0" +version = "1.95.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -967,39 +1234,39 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/d7/e91c6a9cf71726420cddf539852ee4c29176ebb716a702d9118d0409fd8e/openai-1.93.0.tar.gz", hash = "sha256:988f31ade95e1ff0585af11cc5a64510225e4f5cd392698c675d0a9265b8e337", size = 486573, upload-time = "2025-06-27T21:21:39.421Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/a3/70cd57c7d71086c532ce90de5fdef4165dc6ae9dbf346da6737ff9ebafaa/openai-1.95.1.tar.gz", hash = "sha256:f089b605282e2a2b6776090b4b46563ac1da77f56402a222597d591e2dcc1086", size = 488271, upload-time = "2025-07-11T20:47:24.437Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/46/a10d9df4673df56f71201d129ba1cb19eaff3366d08c8664d61a7df52e65/openai-1.93.0-py3-none-any.whl", hash = "sha256:3d746fe5498f0dd72e0d9ab706f26c91c0f646bf7459e5629af8ba7c9dbdf090", size = 755038, upload-time = "2025-06-27T21:21:37.532Z" }, + { url = "https://files.pythonhosted.org/packages/02/1d/0432ea635097f4dbb34641a3650803d8a4aa29d06bafc66583bf1adcceb4/openai-1.95.1-py3-none-any.whl", hash = "sha256:8bbdfeceef231b1ddfabbc232b179d79f8b849aab5a7da131178f8d10e0f162f", size = 755613, upload-time = "2025-07-11T20:47:22.629Z" }, ] [[package]] name = "opentelemetry-api" -version = "1.34.1" +version = "1.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/5e/94a8cb759e4e409022229418294e098ca7feca00eb3c467bb20cbd329bda/opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3", size = 64987, upload-time = "2025-06-10T08:55:19.818Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/c9/4509bfca6bb43220ce7f863c9f791e0d5001c2ec2b5867d48586008b3d96/opentelemetry_api-1.35.0.tar.gz", hash = "sha256:a111b959bcfa5b4d7dffc2fbd6a241aa72dd78dd8e79b5b1662bda896c5d2ffe", size = 64778, upload-time = "2025-07-11T12:23:28.804Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/3a/2ba85557e8dc024c0842ad22c570418dc02c36cbd1ab4b832a93edf071b8/opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c", size = 65767, upload-time = "2025-06-10T08:54:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5a/3f8d078dbf55d18442f6a2ecedf6786d81d7245844b2b20ce2b8ad6f0307/opentelemetry_api-1.35.0-py3-none-any.whl", hash = "sha256:c4ea7e258a244858daf18474625e9cc0149b8ee354f37843415771a40c25ee06", size = 65566, upload-time = "2025-07-11T12:23:07.944Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.34.1" +version = "1.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/f0/ff235936ee40db93360233b62da932d4fd9e8d103cd090c6bcb9afaf5f01/opentelemetry_exporter_otlp_proto_common-1.34.1.tar.gz", hash = "sha256:b59a20a927facd5eac06edaf87a07e49f9e4a13db487b7d8a52b37cb87710f8b", size = 20817, upload-time = "2025-06-10T08:55:22.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/d1/887f860529cba7fc3aba2f6a3597fefec010a17bd1b126810724707d9b51/opentelemetry_exporter_otlp_proto_common-1.35.0.tar.gz", hash = "sha256:6f6d8c39f629b9fa5c79ce19a2829dbd93034f8ac51243cdf40ed2196f00d7eb", size = 20299, upload-time = "2025-07-11T12:23:31.046Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/e8/8b292a11cc8d8d87ec0c4089ae21b6a58af49ca2e51fa916435bc922fdc7/opentelemetry_exporter_otlp_proto_common-1.34.1-py3-none-any.whl", hash = "sha256:8e2019284bf24d3deebbb6c59c71e6eef3307cd88eff8c633e061abba33f7e87", size = 18834, upload-time = "2025-06-10T08:55:00.806Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2c/e31dd3c719bff87fa77391eb7f38b1430d22868c52312cba8aad60f280e5/opentelemetry_exporter_otlp_proto_common-1.35.0-py3-none-any.whl", hash = "sha256:863465de697ae81279ede660f3918680b4480ef5f69dcdac04f30722ed7b74cc", size = 18349, upload-time = "2025-07-11T12:23:11.713Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.34.1" +version = "1.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -1010,48 +1277,48 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/8f/954bc725961cbe425a749d55c0ba1df46832a5999eae764d1a7349ac1c29/opentelemetry_exporter_otlp_proto_http-1.34.1.tar.gz", hash = "sha256:aaac36fdce46a8191e604dcf632e1f9380c7d5b356b27b3e0edb5610d9be28ad", size = 15351, upload-time = "2025-06-10T08:55:24.657Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/7f/7bdc06e84266a5b4b0fefd9790b3859804bf7682ce2daabcba2e22fdb3b2/opentelemetry_exporter_otlp_proto_http-1.35.0.tar.gz", hash = "sha256:cf940147f91b450ef5f66e9980d40eb187582eed399fa851f4a7a45bb880de79", size = 15908, upload-time = "2025-07-11T12:23:32.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/54/b05251c04e30c1ac70cf4a7c5653c085dfcf2c8b98af71661d6a252adc39/opentelemetry_exporter_otlp_proto_http-1.34.1-py3-none-any.whl", hash = "sha256:5251f00ca85872ce50d871f6d3cc89fe203b94c3c14c964bbdc3883366c705d8", size = 17744, upload-time = "2025-06-10T08:55:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/d4/71/f118cd90dc26797077931dd598bde5e0cc652519db166593f962f8fcd022/opentelemetry_exporter_otlp_proto_http-1.35.0-py3-none-any.whl", hash = "sha256:9a001e3df3c7f160fb31056a28ed7faa2de7df68877ae909516102ae36a54e1d", size = 18589, upload-time = "2025-07-11T12:23:13.906Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.34.1" +version = "1.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/b3/c3158dd012463bb7c0eb7304a85a6f63baeeb5b4c93a53845cf89f848c7e/opentelemetry_proto-1.34.1.tar.gz", hash = "sha256:16286214e405c211fc774187f3e4bbb1351290b8dfb88e8948af209ce85b719e", size = 34344, upload-time = "2025-06-10T08:55:32.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/a2/7366e32d9a2bccbb8614942dbea2cf93c209610385ea966cb050334f8df7/opentelemetry_proto-1.35.0.tar.gz", hash = "sha256:532497341bd3e1c074def7c5b00172601b28bb83b48afc41a4b779f26eb4ee05", size = 46151, upload-time = "2025-07-11T12:23:38.797Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/ab/4591bfa54e946350ce8b3f28e5c658fe9785e7cd11e9c11b1671a867822b/opentelemetry_proto-1.34.1-py3-none-any.whl", hash = "sha256:eb4bb5ac27f2562df2d6857fc557b3a481b5e298bc04f94cc68041f00cebcbd2", size = 55692, upload-time = "2025-06-10T08:55:14.904Z" }, + { url = "https://files.pythonhosted.org/packages/00/a7/3f05de580da7e8a8b8dff041d3d07a20bf3bb62d3bcc027f8fd669a73ff4/opentelemetry_proto-1.35.0-py3-none-any.whl", hash = "sha256:98fffa803164499f562718384e703be8d7dfbe680192279a0429cb150a2f8809", size = 72536, upload-time = "2025-07-11T12:23:23.247Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.34.1" +version = "1.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/41/fe20f9036433da8e0fcef568984da4c1d1c771fa072ecd1a4d98779dccdd/opentelemetry_sdk-1.34.1.tar.gz", hash = "sha256:8091db0d763fcd6098d4781bbc80ff0971f94e260739aa6afe6fd379cdf3aa4d", size = 159441, upload-time = "2025-06-10T08:55:33.028Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/cf/1eb2ed2ce55e0a9aa95b3007f26f55c7943aeef0a783bb006bdd92b3299e/opentelemetry_sdk-1.35.0.tar.gz", hash = "sha256:2a400b415ab68aaa6f04e8a6a9f6552908fb3090ae2ff78d6ae0c597ac581954", size = 160871, upload-time = "2025-07-11T12:23:39.566Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/1b/def4fe6aa73f483cabf4c748f4c25070d5f7604dcc8b52e962983491b29e/opentelemetry_sdk-1.34.1-py3-none-any.whl", hash = "sha256:308effad4059562f1d92163c61c8141df649da24ce361827812c40abb2a1e96e", size = 118477, upload-time = "2025-06-10T08:55:16.02Z" }, + { url = "https://files.pythonhosted.org/packages/01/4f/8e32b757ef3b660511b638ab52d1ed9259b666bdeeceba51a082ce3aea95/opentelemetry_sdk-1.35.0-py3-none-any.whl", hash = "sha256:223d9e5f5678518f4842311bb73966e0b6db5d1e0b74e35074c052cd2487f800", size = 119379, upload-time = "2025-07-11T12:23:24.521Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.55b1" +version = "0.56b0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/f0/f33458486da911f47c4aa6db9bda308bb80f3236c111bf848bd870c16b16/opentelemetry_semantic_conventions-0.55b1.tar.gz", hash = "sha256:ef95b1f009159c28d7a7849f5cbc71c4c34c845bb514d66adfdf1b3fff3598b3", size = 119829, upload-time = "2025-06-10T08:55:33.881Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/8e/214fa817f63b9f068519463d8ab46afd5d03b98930c39394a37ae3e741d0/opentelemetry_semantic_conventions-0.56b0.tar.gz", hash = "sha256:c114c2eacc8ff6d3908cb328c811eaf64e6d68623840be9224dc829c4fd6c2ea", size = 124221, upload-time = "2025-07-11T12:23:40.71Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/89/267b0af1b1d0ba828f0e60642b6a5116ac1fd917cde7fc02821627029bd1/opentelemetry_semantic_conventions-0.55b1-py3-none-any.whl", hash = "sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed", size = 196223, upload-time = "2025-06-10T08:55:17.638Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3f/e80c1b017066a9d999efffe88d1cce66116dcf5cb7f80c41040a83b6e03b/opentelemetry_semantic_conventions-0.56b0-py3-none-any.whl", hash = "sha256:df44492868fd6b482511cc43a942e7194be64e94945f572db24df2e279a001a2", size = 201625, upload-time = "2025-07-11T12:23:25.63Z" }, ] [[package]] @@ -1065,7 +1332,7 @@ wheels = [ [[package]] name = "pandas" -version = "2.3.0" +version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -1073,28 +1340,28 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/51/48f713c4c728d7c55ef7444ba5ea027c26998d96d1a40953b346438602fc/pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133", size = 4484490, upload-time = "2025-06-05T03:27:54.133Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/46/24192607058dd607dbfacdd060a2370f6afb19c2ccb617406469b9aeb8e7/pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf", size = 11573865, upload-time = "2025-06-05T03:26:46.774Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cc/ae8ea3b800757a70c9fdccc68b67dc0280a6e814efcf74e4211fd5dea1ca/pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027", size = 10702154, upload-time = "2025-06-05T16:50:14.439Z" }, - { url = "https://files.pythonhosted.org/packages/d8/ba/a7883d7aab3d24c6540a2768f679e7414582cc389876d469b40ec749d78b/pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09", size = 11262180, upload-time = "2025-06-05T16:50:17.453Z" }, - { url = "https://files.pythonhosted.org/packages/01/a5/931fc3ad333d9d87b10107d948d757d67ebcfc33b1988d5faccc39c6845c/pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d", size = 11991493, upload-time = "2025-06-05T03:26:51.813Z" }, - { url = "https://files.pythonhosted.org/packages/d7/bf/0213986830a92d44d55153c1d69b509431a972eb73f204242988c4e66e86/pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20", size = 12470733, upload-time = "2025-06-06T00:00:18.651Z" }, - { url = "https://files.pythonhosted.org/packages/a4/0e/21eb48a3a34a7d4bac982afc2c4eb5ab09f2d988bdf29d92ba9ae8e90a79/pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b", size = 13212406, upload-time = "2025-06-05T03:26:55.992Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d9/74017c4eec7a28892d8d6e31ae9de3baef71f5a5286e74e6b7aad7f8c837/pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be", size = 10976199, upload-time = "2025-06-05T03:26:59.594Z" }, - { url = "https://files.pythonhosted.org/packages/d3/57/5cb75a56a4842bbd0511c3d1c79186d8315b82dac802118322b2de1194fe/pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983", size = 11518913, upload-time = "2025-06-05T03:27:02.757Z" }, - { url = "https://files.pythonhosted.org/packages/05/01/0c8785610e465e4948a01a059562176e4c8088aa257e2e074db868f86d4e/pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd", size = 10655249, upload-time = "2025-06-05T16:50:20.17Z" }, - { url = "https://files.pythonhosted.org/packages/e8/6a/47fd7517cd8abe72a58706aab2b99e9438360d36dcdb052cf917b7bf3bdc/pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f", size = 11328359, upload-time = "2025-06-05T03:27:06.431Z" }, - { url = "https://files.pythonhosted.org/packages/2a/b3/463bfe819ed60fb7e7ddffb4ae2ee04b887b3444feee6c19437b8f834837/pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3", size = 12024789, upload-time = "2025-06-05T03:27:09.875Z" }, - { url = "https://files.pythonhosted.org/packages/04/0c/e0704ccdb0ac40aeb3434d1c641c43d05f75c92e67525df39575ace35468/pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8", size = 12480734, upload-time = "2025-06-06T00:00:22.246Z" }, - { url = "https://files.pythonhosted.org/packages/e9/df/815d6583967001153bb27f5cf075653d69d51ad887ebbf4cfe1173a1ac58/pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9", size = 13223381, upload-time = "2025-06-05T03:27:15.641Z" }, - { url = "https://files.pythonhosted.org/packages/79/88/ca5973ed07b7f484c493e941dbff990861ca55291ff7ac67c815ce347395/pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390", size = 10970135, upload-time = "2025-06-05T03:27:24.131Z" }, - { url = "https://files.pythonhosted.org/packages/24/fb/0994c14d1f7909ce83f0b1fb27958135513c4f3f2528bde216180aa73bfc/pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575", size = 12141356, upload-time = "2025-06-05T03:27:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/9d/a2/9b903e5962134497ac4f8a96f862ee3081cb2506f69f8e4778ce3d9c9d82/pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042", size = 11474674, upload-time = "2025-06-05T03:27:39.448Z" }, - { url = "https://files.pythonhosted.org/packages/81/3a/3806d041bce032f8de44380f866059437fb79e36d6b22c82c187e65f765b/pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c", size = 11439876, upload-time = "2025-06-05T03:27:43.652Z" }, - { url = "https://files.pythonhosted.org/packages/15/aa/3fc3181d12b95da71f5c2537c3e3b3af6ab3a8c392ab41ebb766e0929bc6/pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67", size = 11966182, upload-time = "2025-06-05T03:27:47.652Z" }, - { url = "https://files.pythonhosted.org/packages/37/e7/e12f2d9b0a2c4a2cc86e2aabff7ccfd24f03e597d770abfa2acd313ee46b/pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f", size = 12547686, upload-time = "2025-06-06T00:00:26.142Z" }, - { url = "https://files.pythonhosted.org/packages/39/c2/646d2e93e0af70f4e5359d870a63584dacbc324b54d73e6b3267920ff117/pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249", size = 13231847, upload-time = "2025-06-05T03:27:51.465Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d1/6f/75aa71f8a14267117adeeed5d21b204770189c0a0025acbdc03c337b28fc/pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2", size = 4487493, upload-time = "2025-07-07T19:20:04.079Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/de/b8445e0f5d217a99fe0eeb2f4988070908979bec3587c0633e5428ab596c/pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3", size = 11588172, upload-time = "2025-07-07T19:18:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e0/801cdb3564e65a5ac041ab99ea6f1d802a6c325bb6e58c79c06a3f1cd010/pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232", size = 10717365, upload-time = "2025-07-07T19:18:54.785Z" }, + { url = "https://files.pythonhosted.org/packages/51/a5/c76a8311833c24ae61a376dbf360eb1b1c9247a5d9c1e8b356563b31b80c/pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e", size = 11280411, upload-time = "2025-07-07T19:18:57.045Z" }, + { url = "https://files.pythonhosted.org/packages/da/01/e383018feba0a1ead6cf5fe8728e5d767fee02f06a3d800e82c489e5daaf/pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4", size = 11988013, upload-time = "2025-07-07T19:18:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/5b/14/cec7760d7c9507f11c97d64f29022e12a6cc4fc03ac694535e89f88ad2ec/pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8", size = 12767210, upload-time = "2025-07-07T19:19:02.944Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/6e2d2c6728ed29fb3d4d4d302504fb66f1a543e37eb2e43f352a86365cdf/pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679", size = 13440571, upload-time = "2025-07-07T19:19:06.82Z" }, + { url = "https://files.pythonhosted.org/packages/80/a5/3a92893e7399a691bad7664d977cb5e7c81cf666c81f89ea76ba2bff483d/pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8", size = 10987601, upload-time = "2025-07-07T19:19:09.589Z" }, + { url = "https://files.pythonhosted.org/packages/32/ed/ff0a67a2c5505e1854e6715586ac6693dd860fbf52ef9f81edee200266e7/pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22", size = 11531393, upload-time = "2025-07-07T19:19:12.245Z" }, + { url = "https://files.pythonhosted.org/packages/c7/db/d8f24a7cc9fb0972adab0cc80b6817e8bef888cfd0024eeb5a21c0bb5c4a/pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a", size = 10668750, upload-time = "2025-07-07T19:19:14.612Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b0/80f6ec783313f1e2356b28b4fd8d2148c378370045da918c73145e6aab50/pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928", size = 11342004, upload-time = "2025-07-07T19:19:16.857Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e2/20a317688435470872885e7fc8f95109ae9683dec7c50be29b56911515a5/pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9", size = 12050869, upload-time = "2025-07-07T19:19:19.265Z" }, + { url = "https://files.pythonhosted.org/packages/55/79/20d746b0a96c67203a5bee5fb4e00ac49c3e8009a39e1f78de264ecc5729/pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12", size = 12750218, upload-time = "2025-07-07T19:19:21.547Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/145c8b41e48dbf03dd18fdd7f24f8ba95b8254a97a3379048378f33e7838/pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb", size = 13416763, upload-time = "2025-07-07T19:19:23.939Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c0/54415af59db5cdd86a3d3bf79863e8cc3fa9ed265f0745254061ac09d5f2/pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956", size = 10987482, upload-time = "2025-07-07T19:19:42.699Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/2fd2e400073a1230e13b8cd604c9bc95d9e3b962e5d44088ead2e8f0cfec/pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a", size = 12029159, upload-time = "2025-07-07T19:19:26.362Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0a/d84fd79b0293b7ef88c760d7dca69828d867c89b6d9bc52d6a27e4d87316/pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9", size = 11393287, upload-time = "2025-07-07T19:19:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/50/ae/ff885d2b6e88f3c7520bb74ba319268b42f05d7e583b5dded9837da2723f/pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275", size = 11309381, upload-time = "2025-07-07T19:19:31.436Z" }, + { url = "https://files.pythonhosted.org/packages/85/86/1fa345fc17caf5d7780d2699985c03dbe186c68fee00b526813939062bb0/pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab", size = 11883998, upload-time = "2025-07-07T19:19:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/e58541a49b5e6310d89474333e994ee57fea97c8aaa8fc7f00b873059bbf/pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96", size = 12704705, upload-time = "2025-07-07T19:19:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f9/07086f5b0f2a19872554abeea7658200824f5835c58a106fa8f2ae96a46c/pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444", size = 13189044, upload-time = "2025-07-07T19:19:39.999Z" }, ] [[package]] @@ -1130,43 +1397,46 @@ wheels = [ [[package]] name = "pillow" -version = "11.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" }, - { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" }, - { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" }, - { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" }, - { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" }, - { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" }, - { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, - { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" }, - { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" }, - { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" }, - { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" }, - { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" }, - { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" }, - { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" }, - { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" }, - { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" }, - { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" }, - { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" }, - { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" }, - { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" }, - { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" }, - { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" }, - { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" }, - { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" }, - { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" }, +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, ] [[package]] @@ -1258,28 +1528,28 @@ wheels = [ [[package]] name = "protobuf" -version = "5.29.5" +version = "6.31.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226, upload-time = "2025-05-28T23:51:59.82Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963, upload-time = "2025-05-28T23:51:41.204Z" }, - { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818, upload-time = "2025-05-28T23:51:44.297Z" }, - { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091, upload-time = "2025-05-28T23:51:45.907Z" }, - { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824, upload-time = "2025-05-28T23:51:47.545Z" }, - { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942, upload-time = "2025-05-28T23:51:49.11Z" }, - { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823, upload-time = "2025-05-28T23:51:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" }, + { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" }, + { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" }, ] [[package]] name = "pyaml" -version = "25.5.0" +version = "25.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/40/94f10f32ab952c5cca713d9ac9d8b2fdc37392d90eea403823eeac674c24/pyaml-25.5.0.tar.gz", hash = "sha256:5799560c7b1c9daf35a7a4535f53e2c30323f74cbd7cb4f2e715b16dd681a58a", size = 29812, upload-time = "2025-05-29T05:34:05.292Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/01/41f63d66a801a561c9e335523516bd5f761bc43cc61f8b75918306bf2da8/pyaml-25.7.0.tar.gz", hash = "sha256:e113a64ec16881bf2b092e2beb84b7dcf1bd98096ad17f5f14e8fb782a75d99b", size = 29814, upload-time = "2025-07-10T18:44:51.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/7d/1b5061beff826f902285827261485a058b943332eba8a5532a0164735205/pyaml-25.5.0-py3-none-any.whl", hash = "sha256:b9e0c4e58a5e8003f8f18e802db49fd0563ada587209b13e429bdcbefa87d035", size = 26422, upload-time = "2025-05-29T05:34:03.594Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ee/a878f2ad010cbccb311f947f0f2f09d38f613938ee28c34e60fceecc75a1/pyaml-25.7.0-py3-none-any.whl", hash = "sha256:ce5d7867cc2b455efdb9b0448324ff7b9f74d99f64650f12ca570102db6b985f", size = 26418, upload-time = "2025-07-10T18:44:50.679Z" }, ] [[package]] @@ -1291,6 +1561,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, ] +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + [[package]] name = "pydantic" version = "2.11.7" @@ -1387,17 +1678,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/83/bff755d09e31b5d25cc7fdc4bf3915d1a404e181f1abf0359af376845c24/pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d", size = 522565, upload-time = "2025-05-04T17:07:48.714Z" }, ] +[[package]] +name = "pyproject-hooks" +version = "1.2.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" } +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" }, +] + [[package]] name = "pyright" -version = "1.1.402" +version = "1.1.403" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/04/ce0c132d00e20f2d2fb3b3e7c125264ca8b909e693841210534b1ea1752f/pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683", size = 3888207, upload-time = "2025-06-11T08:48:35.759Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/f6/35f885264ff08c960b23d1542038d8da86971c5d8c955cfab195a4f672d7/pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104", size = 3913526, upload-time = "2025-07-09T07:15:52.882Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/37/1a1c62d955e82adae588be8e374c7f77b165b6cb4203f7d581269959abbc/pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982", size = 5624004, upload-time = "2025-06-11T08:48:33.998Z" }, + { url = "https://files.pythonhosted.org/packages/49/b6/b04e5c2f41a5ccad74a1a4759da41adb20b4bc9d59a5e08d29ba60084d07/pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3", size = 5684504, upload-time = "2025-07-09T07:15:50.958Z" }, ] [[package]] @@ -1507,6 +1807,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1533,6 +1842,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, +] + [[package]] name = "referencing" version = "0.36.2" @@ -1600,6 +1923,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + [[package]] name = "rich" version = "14.0.0" @@ -1615,51 +1972,51 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.25.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/a6/60184b7fc00dd3ca80ac635dd5b8577d444c57e8e8742cecabfacb829921/rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3", size = 27304, upload-time = "2025-05-21T12:46:12.502Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/81/28ab0408391b1dc57393653b6a0cf2014cc282cc2909e4615e63e58262be/rpds_py-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5ffe453cde61f73fea9430223c81d29e2fbf412a6073951102146c84e19e34c", size = 364647, upload-time = "2025-05-21T12:43:28.559Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9a/7797f04cad0d5e56310e1238434f71fc6939d0bc517192a18bb99a72a95f/rpds_py-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:115874ae5e2fdcfc16b2aedc95b5eef4aebe91b28e7e21951eda8a5dc0d3461b", size = 350454, upload-time = "2025-05-21T12:43:30.615Z" }, - { url = "https://files.pythonhosted.org/packages/69/3c/93d2ef941b04898011e5d6eaa56a1acf46a3b4c9f4b3ad1bbcbafa0bee1f/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a714bf6e5e81b0e570d01f56e0c89c6375101b8463999ead3a93a5d2a4af91fa", size = 389665, upload-time = "2025-05-21T12:43:32.629Z" }, - { url = "https://files.pythonhosted.org/packages/c1/57/ad0e31e928751dde8903a11102559628d24173428a0f85e25e187defb2c1/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35634369325906bcd01577da4c19e3b9541a15e99f31e91a02d010816b49bfda", size = 403873, upload-time = "2025-05-21T12:43:34.576Z" }, - { url = "https://files.pythonhosted.org/packages/16/ad/c0c652fa9bba778b4f54980a02962748479dc09632e1fd34e5282cf2556c/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cb2b3ddc16710548801c6fcc0cfcdeeff9dafbc983f77265877793f2660309", size = 525866, upload-time = "2025-05-21T12:43:36.123Z" }, - { url = "https://files.pythonhosted.org/packages/2a/39/3e1839bc527e6fcf48d5fec4770070f872cdee6c6fbc9b259932f4e88a38/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ceca1cf097ed77e1a51f1dbc8d174d10cb5931c188a4505ff9f3e119dfe519b", size = 416886, upload-time = "2025-05-21T12:43:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/7a/95/dd6b91cd4560da41df9d7030a038298a67d24f8ca38e150562644c829c48/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2cd1a4b0c2b8c5e31ffff50d09f39906fe351389ba143c195566056c13a7ea", size = 390666, upload-time = "2025-05-21T12:43:40.065Z" }, - { url = "https://files.pythonhosted.org/packages/64/48/1be88a820e7494ce0a15c2d390ccb7c52212370badabf128e6a7bb4cb802/rpds_py-0.25.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de336a4b164c9188cb23f3703adb74a7623ab32d20090d0e9bf499a2203ad65", size = 425109, upload-time = "2025-05-21T12:43:42.263Z" }, - { url = "https://files.pythonhosted.org/packages/cf/07/3e2a17927ef6d7720b9949ec1b37d1e963b829ad0387f7af18d923d5cfa5/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9fca84a15333e925dd59ce01da0ffe2ffe0d6e5d29a9eeba2148916d1824948c", size = 567244, upload-time = "2025-05-21T12:43:43.846Z" }, - { url = "https://files.pythonhosted.org/packages/d2/e5/76cf010998deccc4f95305d827847e2eae9c568099c06b405cf96384762b/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88ec04afe0c59fa64e2f6ea0dd9657e04fc83e38de90f6de201954b4d4eb59bd", size = 596023, upload-time = "2025-05-21T12:43:45.932Z" }, - { url = "https://files.pythonhosted.org/packages/52/9a/df55efd84403736ba37a5a6377b70aad0fd1cb469a9109ee8a1e21299a1c/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8bd2f19e312ce3e1d2c635618e8a8d8132892bb746a7cf74780a489f0f6cdcb", size = 561634, upload-time = "2025-05-21T12:43:48.263Z" }, - { url = "https://files.pythonhosted.org/packages/ab/aa/dc3620dd8db84454aaf9374bd318f1aa02578bba5e567f5bf6b79492aca4/rpds_py-0.25.1-cp312-cp312-win32.whl", hash = "sha256:e5e2f7280d8d0d3ef06f3ec1b4fd598d386cc6f0721e54f09109a8132182fbfe", size = 222713, upload-time = "2025-05-21T12:43:49.897Z" }, - { url = "https://files.pythonhosted.org/packages/a3/7f/7cef485269a50ed5b4e9bae145f512d2a111ca638ae70cc101f661b4defd/rpds_py-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:db58483f71c5db67d643857404da360dce3573031586034b7d59f245144cc192", size = 235280, upload-time = "2025-05-21T12:43:51.893Z" }, - { url = "https://files.pythonhosted.org/packages/99/f2/c2d64f6564f32af913bf5f3f7ae41c7c263c5ae4c4e8f1a17af8af66cd46/rpds_py-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:6d50841c425d16faf3206ddbba44c21aa3310a0cebc3c1cdfc3e3f4f9f6f5728", size = 225399, upload-time = "2025-05-21T12:43:53.351Z" }, - { url = "https://files.pythonhosted.org/packages/2b/da/323848a2b62abe6a0fec16ebe199dc6889c5d0a332458da8985b2980dffe/rpds_py-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:659d87430a8c8c704d52d094f5ba6fa72ef13b4d385b7e542a08fc240cb4a559", size = 364498, upload-time = "2025-05-21T12:43:54.841Z" }, - { url = "https://files.pythonhosted.org/packages/1f/b4/4d3820f731c80fd0cd823b3e95b9963fec681ae45ba35b5281a42382c67d/rpds_py-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68f6f060f0bbdfb0245267da014d3a6da9be127fe3e8cc4a68c6f833f8a23bb1", size = 350083, upload-time = "2025-05-21T12:43:56.428Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b1/3a8ee1c9d480e8493619a437dec685d005f706b69253286f50f498cbdbcf/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083a9513a33e0b92cf6e7a6366036c6bb43ea595332c1ab5c8ae329e4bcc0a9c", size = 389023, upload-time = "2025-05-21T12:43:57.995Z" }, - { url = "https://files.pythonhosted.org/packages/3b/31/17293edcfc934dc62c3bf74a0cb449ecd549531f956b72287203e6880b87/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:816568614ecb22b18a010c7a12559c19f6fe993526af88e95a76d5a60b8b75fb", size = 403283, upload-time = "2025-05-21T12:43:59.546Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ca/e0f0bc1a75a8925024f343258c8ecbd8828f8997ea2ac71e02f67b6f5299/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c6564c0947a7f52e4792983f8e6cf9bac140438ebf81f527a21d944f2fd0a40", size = 524634, upload-time = "2025-05-21T12:44:01.087Z" }, - { url = "https://files.pythonhosted.org/packages/3e/03/5d0be919037178fff33a6672ffc0afa04ea1cfcb61afd4119d1b5280ff0f/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c4a128527fe415d73cf1f70a9a688d06130d5810be69f3b553bf7b45e8acf79", size = 416233, upload-time = "2025-05-21T12:44:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325", size = 390375, upload-time = "2025-05-21T12:44:04.162Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ac/a87f339f0e066b9535074a9f403b9313fd3892d4a164d5d5f5875ac9f29f/rpds_py-0.25.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d74ec9bc0e2feb81d3f16946b005748119c0f52a153f6db6a29e8cd68636f295", size = 424537, upload-time = "2025-05-21T12:44:06.175Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8f/8d5c1567eaf8c8afe98a838dd24de5013ce6e8f53a01bd47fe8bb06b5533/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3af5b4cc10fa41e5bc64e5c198a1b2d2864337f8fcbb9a67e747e34002ce812b", size = 566425, upload-time = "2025-05-21T12:44:08.242Z" }, - { url = "https://files.pythonhosted.org/packages/95/33/03016a6be5663b389c8ab0bbbcca68d9e96af14faeff0a04affcb587e776/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79dc317a5f1c51fd9c6a0c4f48209c6b8526d0524a6904fc1076476e79b00f98", size = 595197, upload-time = "2025-05-21T12:44:10.449Z" }, - { url = "https://files.pythonhosted.org/packages/33/8d/da9f4d3e208c82fda311bff0cf0a19579afceb77cf456e46c559a1c075ba/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1521031351865e0181bc585147624d66b3b00a84109b57fcb7a779c3ec3772cd", size = 561244, upload-time = "2025-05-21T12:44:12.387Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b3/39d5dcf7c5f742ecd6dbc88f6f84ae54184b92f5f387a4053be2107b17f1/rpds_py-0.25.1-cp313-cp313-win32.whl", hash = "sha256:5d473be2b13600b93a5675d78f59e63b51b1ba2d0476893415dfbb5477e65b31", size = 222254, upload-time = "2025-05-21T12:44:14.261Z" }, - { url = "https://files.pythonhosted.org/packages/5f/19/2d6772c8eeb8302c5f834e6d0dfd83935a884e7c5ce16340c7eaf89ce925/rpds_py-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7b74e92a3b212390bdce1d93da9f6488c3878c1d434c5e751cbc202c5e09500", size = 234741, upload-time = "2025-05-21T12:44:16.236Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/145ada26cfaf86018d0eb304fe55eafdd4f0b6b84530246bb4a7c4fb5c4b/rpds_py-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:dd326a81afe332ede08eb39ab75b301d5676802cdffd3a8f287a5f0b694dc3f5", size = 224830, upload-time = "2025-05-21T12:44:17.749Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ca/d435844829c384fd2c22754ff65889c5c556a675d2ed9eb0e148435c6690/rpds_py-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a58d1ed49a94d4183483a3ce0af22f20318d4a1434acee255d683ad90bf78129", size = 359668, upload-time = "2025-05-21T12:44:19.322Z" }, - { url = "https://files.pythonhosted.org/packages/1f/01/b056f21db3a09f89410d493d2f6614d87bb162499f98b649d1dbd2a81988/rpds_py-0.25.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f251bf23deb8332823aef1da169d5d89fa84c89f67bdfb566c49dea1fccfd50d", size = 345649, upload-time = "2025-05-21T12:44:20.962Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0f/e0d00dc991e3d40e03ca36383b44995126c36b3eafa0ccbbd19664709c88/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd586bfa270c1103ece2109314dd423df1fa3d9719928b5d09e4840cec0d72", size = 384776, upload-time = "2025-05-21T12:44:22.516Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a2/59374837f105f2ca79bde3c3cd1065b2f8c01678900924949f6392eab66d/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d273f136e912aa101a9274c3145dcbddbe4bac560e77e6d5b3c9f6e0ed06d34", size = 395131, upload-time = "2025-05-21T12:44:24.147Z" }, - { url = "https://files.pythonhosted.org/packages/9c/dc/48e8d84887627a0fe0bac53f0b4631e90976fd5d35fff8be66b8e4f3916b/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666fa7b1bd0a3810a7f18f6d3a25ccd8866291fbbc3c9b912b917a6715874bb9", size = 520942, upload-time = "2025-05-21T12:44:25.915Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f5/ee056966aeae401913d37befeeab57a4a43a4f00099e0a20297f17b8f00c/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:921954d7fbf3fccc7de8f717799304b14b6d9a45bbeec5a8d7408ccbf531faf5", size = 411330, upload-time = "2025-05-21T12:44:27.638Z" }, - { url = "https://files.pythonhosted.org/packages/ab/74/b2cffb46a097cefe5d17f94ede7a174184b9d158a0aeb195f39f2c0361e8/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d86373ff19ca0441ebeb696ef64cb58b8b5cbacffcda5a0ec2f3911732a194", size = 387339, upload-time = "2025-05-21T12:44:29.292Z" }, - { url = "https://files.pythonhosted.org/packages/7f/9a/0ff0b375dcb5161c2b7054e7d0b7575f1680127505945f5cabaac890bc07/rpds_py-0.25.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8980cde3bb8575e7c956a530f2c217c1d6aac453474bf3ea0f9c89868b531b6", size = 418077, upload-time = "2025-05-21T12:44:30.877Z" }, - { url = "https://files.pythonhosted.org/packages/0d/a1/fda629bf20d6b698ae84c7c840cfb0e9e4200f664fc96e1f456f00e4ad6e/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8eb8c84ecea987a2523e057c0d950bcb3f789696c0499290b8d7b3107a719d78", size = 562441, upload-time = "2025-05-21T12:44:32.541Z" }, - { url = "https://files.pythonhosted.org/packages/20/15/ce4b5257f654132f326f4acd87268e1006cc071e2c59794c5bdf4bebbb51/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e43a005671a9ed5a650f3bc39e4dbccd6d4326b24fb5ea8be5f3a43a6f576c72", size = 590750, upload-time = "2025-05-21T12:44:34.557Z" }, - { url = "https://files.pythonhosted.org/packages/fb/ab/e04bf58a8d375aeedb5268edcc835c6a660ebf79d4384d8e0889439448b0/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58f77c60956501a4a627749a6dcb78dac522f249dd96b5c9f1c6af29bfacfb66", size = 558891, upload-time = "2025-05-21T12:44:37.358Z" }, - { url = "https://files.pythonhosted.org/packages/90/82/cb8c6028a6ef6cd2b7991e2e4ced01c854b6236ecf51e81b64b569c43d73/rpds_py-0.25.1-cp313-cp313t-win32.whl", hash = "sha256:2cb9e5b5e26fc02c8a4345048cd9998c2aca7c2712bd1b36da0c72ee969a3523", size = 218718, upload-time = "2025-05-21T12:44:38.969Z" }, - { url = "https://files.pythonhosted.org/packages/b6/97/5a4b59697111c89477d20ba8a44df9ca16b41e737fa569d5ae8bff99e650/rpds_py-0.25.1-cp313-cp313t-win_amd64.whl", hash = "sha256:401ca1c4a20cc0510d3435d89c069fe0a9ae2ee6495135ac46bdd49ec0495763", size = 232218, upload-time = "2025-05-21T12:44:40.512Z" }, +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/aa/4456d84bbb54adc6a916fb10c9b374f78ac840337644e4a5eda229c81275/rpds_py-0.26.0.tar.gz", hash = "sha256:20dae58a859b0906f0685642e591056f1e787f3a8b39c8e8749a45dc7d26bdb0", size = 27385, upload-time = "2025-07-01T15:57:13.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/86/90eb87c6f87085868bd077c7a9938006eb1ce19ed4d06944a90d3560fce2/rpds_py-0.26.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:894514d47e012e794f1350f076c427d2347ebf82f9b958d554d12819849a369d", size = 363933, upload-time = "2025-07-01T15:54:15.734Z" }, + { url = "https://files.pythonhosted.org/packages/63/78/4469f24d34636242c924626082b9586f064ada0b5dbb1e9d096ee7a8e0c6/rpds_py-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc921b96fa95a097add244da36a1d9e4f3039160d1d30f1b35837bf108c21136", size = 350447, upload-time = "2025-07-01T15:54:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/ad/91/c448ed45efdfdade82348d5e7995e15612754826ea640afc20915119734f/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1157659470aa42a75448b6e943c895be8c70531c43cb78b9ba990778955582", size = 384711, upload-time = "2025-07-01T15:54:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/ec/43/e5c86fef4be7f49828bdd4ecc8931f0287b1152c0bb0163049b3218740e7/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:521ccf56f45bb3a791182dc6b88ae5f8fa079dd705ee42138c76deb1238e554e", size = 400865, upload-time = "2025-07-01T15:54:19.295Z" }, + { url = "https://files.pythonhosted.org/packages/55/34/e00f726a4d44f22d5c5fe2e5ddd3ac3d7fd3f74a175607781fbdd06fe375/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9def736773fd56b305c0eef698be5192c77bfa30d55a0e5885f80126c4831a15", size = 517763, upload-time = "2025-07-01T15:54:20.858Z" }, + { url = "https://files.pythonhosted.org/packages/52/1c/52dc20c31b147af724b16104500fba13e60123ea0334beba7b40e33354b4/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdad4ea3b4513b475e027be79e5a0ceac8ee1c113a1a11e5edc3c30c29f964d8", size = 406651, upload-time = "2025-07-01T15:54:22.508Z" }, + { url = "https://files.pythonhosted.org/packages/2e/77/87d7bfabfc4e821caa35481a2ff6ae0b73e6a391bb6b343db2c91c2b9844/rpds_py-0.26.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82b165b07f416bdccf5c84546a484cc8f15137ca38325403864bfdf2b5b72f6a", size = 386079, upload-time = "2025-07-01T15:54:23.987Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d4/7f2200c2d3ee145b65b3cddc4310d51f7da6a26634f3ac87125fd789152a/rpds_py-0.26.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d04cab0a54b9dba4d278fe955a1390da3cf71f57feb78ddc7cb67cbe0bd30323", size = 421379, upload-time = "2025-07-01T15:54:25.073Z" }, + { url = "https://files.pythonhosted.org/packages/ae/13/9fdd428b9c820869924ab62236b8688b122baa22d23efdd1c566938a39ba/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:79061ba1a11b6a12743a2b0f72a46aa2758613d454aa6ba4f5a265cc48850158", size = 562033, upload-time = "2025-07-01T15:54:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e1/b69686c3bcbe775abac3a4c1c30a164a2076d28df7926041f6c0eb5e8d28/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f405c93675d8d4c5ac87364bb38d06c988e11028a64b52a47158a355079661f3", size = 591639, upload-time = "2025-07-01T15:54:27.424Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c9/1e3d8c8863c84a90197ac577bbc3d796a92502124c27092413426f670990/rpds_py-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dafd4c44b74aa4bed4b250f1aed165b8ef5de743bcca3b88fc9619b6087093d2", size = 557105, upload-time = "2025-07-01T15:54:29.93Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c5/90c569649057622959f6dcc40f7b516539608a414dfd54b8d77e3b201ac0/rpds_py-0.26.0-cp312-cp312-win32.whl", hash = "sha256:3da5852aad63fa0c6f836f3359647870e21ea96cf433eb393ffa45263a170d44", size = 223272, upload-time = "2025-07-01T15:54:31.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/16/19f5d9f2a556cfed454eebe4d354c38d51c20f3db69e7b4ce6cff904905d/rpds_py-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf47cfdabc2194a669dcf7a8dbba62e37a04c5041d2125fae0233b720da6f05c", size = 234995, upload-time = "2025-07-01T15:54:32.195Z" }, + { url = "https://files.pythonhosted.org/packages/83/f0/7935e40b529c0e752dfaa7880224771b51175fce08b41ab4a92eb2fbdc7f/rpds_py-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:20ab1ae4fa534f73647aad289003f1104092890849e0266271351922ed5574f8", size = 223198, upload-time = "2025-07-01T15:54:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/6a/67/bb62d0109493b12b1c6ab00de7a5566aa84c0e44217c2d94bee1bd370da9/rpds_py-0.26.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:696764a5be111b036256c0b18cd29783fab22154690fc698062fc1b0084b511d", size = 363917, upload-time = "2025-07-01T15:54:34.755Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f3/34e6ae1925a5706c0f002a8d2d7f172373b855768149796af87bd65dcdb9/rpds_py-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6c15d2080a63aaed876e228efe4f814bc7889c63b1e112ad46fdc8b368b9e1", size = 350073, upload-time = "2025-07-01T15:54:36.292Z" }, + { url = "https://files.pythonhosted.org/packages/75/83/1953a9d4f4e4de7fd0533733e041c28135f3c21485faaef56a8aadbd96b5/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:390e3170babf42462739a93321e657444f0862c6d722a291accc46f9d21ed04e", size = 384214, upload-time = "2025-07-01T15:54:37.469Z" }, + { url = "https://files.pythonhosted.org/packages/48/0e/983ed1b792b3322ea1d065e67f4b230f3b96025f5ce3878cc40af09b7533/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7da84c2c74c0f5bc97d853d9e17bb83e2dcafcff0dc48286916001cc114379a1", size = 400113, upload-time = "2025-07-01T15:54:38.954Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/36c0925fff6f660a80be259c5b4f5e53a16851f946eb080351d057698528/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c5fe114a6dd480a510b6d3661d09d67d1622c4bf20660a474507aaee7eeeee9", size = 515189, upload-time = "2025-07-01T15:54:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/13/45/cbf07fc03ba7a9b54662c9badb58294ecfb24f828b9732970bd1a431ed5c/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3100b3090269f3a7ea727b06a6080d4eb7439dca4c0e91a07c5d133bb1727ea7", size = 406998, upload-time = "2025-07-01T15:54:43.025Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/8fa5e36e58657997873fd6a1cf621285ca822ca75b4b3434ead047daa307/rpds_py-0.26.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c03c9b0c64afd0320ae57de4c982801271c0c211aa2d37f3003ff5feb75bb04", size = 385903, upload-time = "2025-07-01T15:54:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f7/b25437772f9f57d7a9fbd73ed86d0dcd76b4c7c6998348c070d90f23e315/rpds_py-0.26.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5963b72ccd199ade6ee493723d18a3f21ba7d5b957017607f815788cef50eaf1", size = 419785, upload-time = "2025-07-01T15:54:46.043Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/63ffa55743dfcb4baf2e9e77a0b11f7f97ed96a54558fcb5717a4b2cd732/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9da4e873860ad5bab3291438525cae80169daecbfafe5657f7f5fb4d6b3f96b9", size = 561329, upload-time = "2025-07-01T15:54:47.64Z" }, + { url = "https://files.pythonhosted.org/packages/2f/07/1f4f5e2886c480a2346b1e6759c00278b8a69e697ae952d82ae2e6ee5db0/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5afaddaa8e8c7f1f7b4c5c725c0070b6eed0228f705b90a1732a48e84350f4e9", size = 590875, upload-time = "2025-07-01T15:54:48.9Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bc/e6639f1b91c3a55f8c41b47d73e6307051b6e246254a827ede730624c0f8/rpds_py-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4916dc96489616a6f9667e7526af8fa693c0fdb4f3acb0e5d9f4400eb06a47ba", size = 556636, upload-time = "2025-07-01T15:54:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/05/4c/b3917c45566f9f9a209d38d9b54a1833f2bb1032a3e04c66f75726f28876/rpds_py-0.26.0-cp313-cp313-win32.whl", hash = "sha256:2a343f91b17097c546b93f7999976fd6c9d5900617aa848c81d794e062ab302b", size = 222663, upload-time = "2025-07-01T15:54:52.023Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0b/0851bdd6025775aaa2365bb8de0697ee2558184c800bfef8d7aef5ccde58/rpds_py-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:0a0b60701f2300c81b2ac88a5fb893ccfa408e1c4a555a77f908a2596eb875a5", size = 234428, upload-time = "2025-07-01T15:54:53.692Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e8/a47c64ed53149c75fb581e14a237b7b7cd18217e969c30d474d335105622/rpds_py-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:257d011919f133a4746958257f2c75238e3ff54255acd5e3e11f3ff41fd14256", size = 222571, upload-time = "2025-07-01T15:54:54.822Z" }, + { url = "https://files.pythonhosted.org/packages/89/bf/3d970ba2e2bcd17d2912cb42874107390f72873e38e79267224110de5e61/rpds_py-0.26.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:529c8156d7506fba5740e05da8795688f87119cce330c244519cf706a4a3d618", size = 360475, upload-time = "2025-07-01T15:54:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/9f/283e7e2979fc4ec2d8ecee506d5a3675fce5ed9b4b7cb387ea5d37c2f18d/rpds_py-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f53ec51f9d24e9638a40cabb95078ade8c99251945dad8d57bf4aabe86ecee35", size = 346692, upload-time = "2025-07-01T15:54:58.561Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/7e50423c04d78daf391da3cc4330bdb97042fc192a58b186f2d5deb7befd/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab504c4d654e4a29558eaa5bb8cea5fdc1703ea60a8099ffd9c758472cf913f", size = 379415, upload-time = "2025-07-01T15:54:59.751Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/d11ee60d4d3b16808432417951c63df803afb0e0fc672b5e8d07e9edaaae/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0641abca296bc1a00183fe44f7fced8807ed49d501f188faa642d0e4975b83", size = 391783, upload-time = "2025-07-01T15:55:00.898Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/1069c394d9c0d6d23c5b522e1f6546b65793a22950f6e0210adcc6f97c3e/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b312fecc1d017b5327afa81d4da1480f51c68810963a7336d92203dbb3d4f1", size = 512844, upload-time = "2025-07-01T15:55:02.201Z" }, + { url = "https://files.pythonhosted.org/packages/08/3b/c4fbf0926800ed70b2c245ceca99c49f066456755f5d6eb8863c2c51e6d0/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c741107203954f6fc34d3066d213d0a0c40f7bb5aafd698fb39888af277c70d8", size = 402105, upload-time = "2025-07-01T15:55:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/db69b52ca07413e568dae9dc674627a22297abb144c4d6022c6d78f1e5cc/rpds_py-0.26.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3e55a7db08dc9a6ed5fb7103019d2c1a38a349ac41901f9f66d7f95750942f", size = 383440, upload-time = "2025-07-01T15:55:05.398Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e1/c65255ad5b63903e56b3bb3ff9dcc3f4f5c3badde5d08c741ee03903e951/rpds_py-0.26.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9e851920caab2dbcae311fd28f4313c6953993893eb5c1bb367ec69d9a39e7ed", size = 412759, upload-time = "2025-07-01T15:55:08.316Z" }, + { url = "https://files.pythonhosted.org/packages/e4/22/bb731077872377a93c6e93b8a9487d0406c70208985831034ccdeed39c8e/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dfbf280da5f876d0b00c81f26bedce274e72a678c28845453885a9b3c22ae632", size = 556032, upload-time = "2025-07-01T15:55:09.52Z" }, + { url = "https://files.pythonhosted.org/packages/e0/8b/393322ce7bac5c4530fb96fc79cc9ea2f83e968ff5f6e873f905c493e1c4/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1cc81d14ddfa53d7f3906694d35d54d9d3f850ef8e4e99ee68bc0d1e5fed9a9c", size = 585416, upload-time = "2025-07-01T15:55:11.216Z" }, + { url = "https://files.pythonhosted.org/packages/49/ae/769dc372211835bf759319a7aae70525c6eb523e3371842c65b7ef41c9c6/rpds_py-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dca83c498b4650a91efcf7b88d669b170256bf8017a5db6f3e06c2bf031f57e0", size = 554049, upload-time = "2025-07-01T15:55:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f9/4c43f9cc203d6ba44ce3146246cdc38619d92c7bd7bad4946a3491bd5b70/rpds_py-0.26.0-cp313-cp313t-win32.whl", hash = "sha256:4d11382bcaf12f80b51d790dee295c56a159633a8e81e6323b16e55d81ae37e9", size = 218428, upload-time = "2025-07-01T15:55:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8b/9286b7e822036a4a977f2f1e851c7345c20528dbd56b687bb67ed68a8ede/rpds_py-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff110acded3c22c033e637dd8896e411c7d3a11289b2edf041f86663dbc791e9", size = 231524, upload-time = "2025-07-01T15:55:15.745Z" }, ] [[package]] @@ -1676,27 +2033,40 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.1" +version = "0.12.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/2a/43955b530c49684d3c38fcda18c43caf91e99204c2a065552528e0552d4f/ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77", size = 4459341, upload-time = "2025-07-11T13:21:16.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/fd/b44c5115539de0d598d75232a1cc7201430b6891808df111b8b0506aae43/ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2", size = 10430499, upload-time = "2025-07-11T13:20:26.321Z" }, + { url = "https://files.pythonhosted.org/packages/43/c5/9eba4f337970d7f639a37077be067e4ec80a2ad359e4cc6c5b56805cbc66/ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041", size = 11213413, upload-time = "2025-07-11T13:20:30.017Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2c/fac3016236cf1fe0bdc8e5de4f24c76ce53c6dd9b5f350d902549b7719b2/ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882", size = 10586941, upload-time = "2025-07-11T13:20:33.046Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0f/41fec224e9dfa49a139f0b402ad6f5d53696ba1800e0f77b279d55210ca9/ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901", size = 10783001, upload-time = "2025-07-11T13:20:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/dd64a9ce56d9ed6cad109606ac014860b1c217c883e93bf61536400ba107/ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0", size = 10269641, upload-time = "2025-07-11T13:20:38.459Z" }, + { url = "https://files.pythonhosted.org/packages/63/5c/2be545034c6bd5ce5bb740ced3e7014d7916f4c445974be11d2a406d5088/ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6", size = 11875059, upload-time = "2025-07-11T13:20:41.517Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d4/a74ef1e801ceb5855e9527dae105eaff136afcb9cc4d2056d44feb0e4792/ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc", size = 12658890, upload-time = "2025-07-11T13:20:44.442Z" }, + { url = "https://files.pythonhosted.org/packages/13/c8/1057916416de02e6d7c9bcd550868a49b72df94e3cca0aeb77457dcd9644/ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687", size = 12232008, upload-time = "2025-07-11T13:20:47.374Z" }, + { url = "https://files.pythonhosted.org/packages/f5/59/4f7c130cc25220392051fadfe15f63ed70001487eca21d1796db46cbcc04/ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e", size = 11499096, upload-time = "2025-07-11T13:20:50.348Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/a0ad24a5d2ed6be03a312e30d32d4e3904bfdbc1cdbe63c47be9d0e82c79/ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311", size = 11688307, upload-time = "2025-07-11T13:20:52.945Z" }, + { url = "https://files.pythonhosted.org/packages/93/72/08f9e826085b1f57c9a0226e48acb27643ff19b61516a34c6cab9d6ff3fa/ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07", size = 10661020, upload-time = "2025-07-11T13:20:55.799Z" }, + { url = "https://files.pythonhosted.org/packages/80/a0/68da1250d12893466c78e54b4a0ff381370a33d848804bb51279367fc688/ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12", size = 10246300, upload-time = "2025-07-11T13:20:58.222Z" }, + { url = "https://files.pythonhosted.org/packages/6a/22/5f0093d556403e04b6fd0984fc0fb32fbb6f6ce116828fd54306a946f444/ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b", size = 11263119, upload-time = "2025-07-11T13:21:01.503Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/f4c0b69bdaffb9968ba40dd5fa7df354ae0c73d01f988601d8fac0c639b1/ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f", size = 11746990, upload-time = "2025-07-11T13:21:04.524Z" }, + { url = "https://files.pythonhosted.org/packages/fe/84/7cc7bd73924ee6be4724be0db5414a4a2ed82d06b30827342315a1be9e9c/ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d", size = 10589263, upload-time = "2025-07-11T13:21:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/07/87/c070f5f027bd81f3efee7d14cb4d84067ecf67a3a8efb43aadfc72aa79a6/ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7", size = 11695072, upload-time = "2025-07-11T13:21:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/e0/30/f3eaf6563c637b6e66238ed6535f6775480db973c836336e4122161986fc/ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1", size = 10805855, upload-time = "2025-07-11T13:21:13.547Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/97/38/796a101608a90494440856ccfb52b1edae90de0b817e76bfade66b12d320/ruff-0.12.1.tar.gz", hash = "sha256:806bbc17f1104fd57451a98a58df35388ee3ab422e029e8f5cf30aa4af2c138c", size = 4413426, upload-time = "2025-06-26T20:34:14.784Z" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/bf/3dba52c1d12ab5e78d75bd78ad52fb85a6a1f29cc447c2423037b82bed0d/ruff-0.12.1-py3-none-linux_armv6l.whl", hash = "sha256:6013a46d865111e2edb71ad692fbb8262e6c172587a57c0669332a449384a36b", size = 10305649, upload-time = "2025-06-26T20:33:39.242Z" }, - { url = "https://files.pythonhosted.org/packages/8c/65/dab1ba90269bc8c81ce1d499a6517e28fe6f87b2119ec449257d0983cceb/ruff-0.12.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b3f75a19e03a4b0757d1412edb7f27cffb0c700365e9d6b60bc1b68d35bc89e0", size = 11120201, upload-time = "2025-06-26T20:33:42.207Z" }, - { url = "https://files.pythonhosted.org/packages/3f/3e/2d819ffda01defe857fa2dd4cba4d19109713df4034cc36f06bbf582d62a/ruff-0.12.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a256522893cb7e92bb1e1153283927f842dea2e48619c803243dccc8437b8be", size = 10466769, upload-time = "2025-06-26T20:33:44.102Z" }, - { url = "https://files.pythonhosted.org/packages/63/37/bde4cf84dbd7821c8de56ec4ccc2816bce8125684f7b9e22fe4ad92364de/ruff-0.12.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:069052605fe74c765a5b4272eb89880e0ff7a31e6c0dbf8767203c1fbd31c7ff", size = 10660902, upload-time = "2025-06-26T20:33:45.98Z" }, - { url = "https://files.pythonhosted.org/packages/0e/3a/390782a9ed1358c95e78ccc745eed1a9d657a537e5c4c4812fce06c8d1a0/ruff-0.12.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a684f125a4fec2d5a6501a466be3841113ba6847827be4573fddf8308b83477d", size = 10167002, upload-time = "2025-06-26T20:33:47.81Z" }, - { url = "https://files.pythonhosted.org/packages/6d/05/f2d4c965009634830e97ffe733201ec59e4addc5b1c0efa035645baa9e5f/ruff-0.12.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdecdef753bf1e95797593007569d8e1697a54fca843d78f6862f7dc279e23bd", size = 11751522, upload-time = "2025-06-26T20:33:49.857Z" }, - { url = "https://files.pythonhosted.org/packages/35/4e/4bfc519b5fcd462233f82fc20ef8b1e5ecce476c283b355af92c0935d5d9/ruff-0.12.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:70d52a058c0e7b88b602f575d23596e89bd7d8196437a4148381a3f73fcd5010", size = 12520264, upload-time = "2025-06-26T20:33:52.199Z" }, - { url = "https://files.pythonhosted.org/packages/85/b2/7756a6925da236b3a31f234b4167397c3e5f91edb861028a631546bad719/ruff-0.12.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84d0a69d1e8d716dfeab22d8d5e7c786b73f2106429a933cee51d7b09f861d4e", size = 12133882, upload-time = "2025-06-26T20:33:54.231Z" }, - { url = "https://files.pythonhosted.org/packages/dd/00/40da9c66d4a4d51291e619be6757fa65c91b92456ff4f01101593f3a1170/ruff-0.12.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cc32e863adcf9e71690248607ccdf25252eeeab5193768e6873b901fd441fed", size = 11608941, upload-time = "2025-06-26T20:33:56.202Z" }, - { url = "https://files.pythonhosted.org/packages/91/e7/f898391cc026a77fbe68dfea5940f8213622474cb848eb30215538a2dadf/ruff-0.12.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fd49a4619f90d5afc65cf42e07b6ae98bb454fd5029d03b306bd9e2273d44cc", size = 11602887, upload-time = "2025-06-26T20:33:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/f6/02/0891872fc6aab8678084f4cf8826f85c5d2d24aa9114092139a38123f94b/ruff-0.12.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ed5af6aaaea20710e77698e2055b9ff9b3494891e1b24d26c07055459bb717e9", size = 10521742, upload-time = "2025-06-26T20:34:00.465Z" }, - { url = "https://files.pythonhosted.org/packages/2a/98/d6534322c74a7d47b0f33b036b2498ccac99d8d8c40edadb552c038cecf1/ruff-0.12.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:801d626de15e6bf988fbe7ce59b303a914ff9c616d5866f8c79eb5012720ae13", size = 10149909, upload-time = "2025-06-26T20:34:02.603Z" }, - { url = "https://files.pythonhosted.org/packages/34/5c/9b7ba8c19a31e2b6bd5e31aa1e65b533208a30512f118805371dbbbdf6a9/ruff-0.12.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2be9d32a147f98a1972c1e4df9a6956d612ca5f5578536814372113d09a27a6c", size = 11136005, upload-time = "2025-06-26T20:34:04.723Z" }, - { url = "https://files.pythonhosted.org/packages/dc/34/9bbefa4d0ff2c000e4e533f591499f6b834346025e11da97f4ded21cb23e/ruff-0.12.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:49b7ce354eed2a322fbaea80168c902de9504e6e174fd501e9447cad0232f9e6", size = 11648579, upload-time = "2025-06-26T20:34:06.766Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1c/20cdb593783f8f411839ce749ec9ae9e4298c2b2079b40295c3e6e2089e1/ruff-0.12.1-py3-none-win32.whl", hash = "sha256:d973fa626d4c8267848755bd0414211a456e99e125dcab147f24daa9e991a245", size = 10519495, upload-time = "2025-06-26T20:34:08.718Z" }, - { url = "https://files.pythonhosted.org/packages/cf/56/7158bd8d3cf16394928f47c637d39a7d532268cd45220bdb6cd622985760/ruff-0.12.1-py3-none-win_amd64.whl", hash = "sha256:9e1123b1c033f77bd2590e4c1fe7e8ea72ef990a85d2484351d408224d603013", size = 11547485, upload-time = "2025-06-26T20:34:11.008Z" }, - { url = "https://files.pythonhosted.org/packages/91/d0/6902c0d017259439d6fd2fd9393cea1cfe30169940118b007d5e0ea7e954/ruff-0.12.1-py3-none-win_arm64.whl", hash = "sha256:78ad09a022c64c13cc6077707f036bab0fac8cd7088772dcd1e5be21c5002efc", size = 10691209, upload-time = "2025-06-26T20:34:12.928Z" }, + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, ] [[package]] @@ -1728,14 +2098,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.46.2" +version = "0.47.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/69/662169fdb92fb96ec3eaee218cf540a629d629c86d7993d9651226a6789b/starlette-0.47.1.tar.gz", hash = "sha256:aef012dd2b6be325ffa16698f9dc533614fb1cebd593a906b90dc1025529a79b", size = 2583072, upload-time = "2025-06-21T04:03:17.337Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, + { url = "https://files.pythonhosted.org/packages/82/95/38ef0cd7fa11eaba6a99b3c4f5ac948d8bc6ff199aabd327a29cc000840c/starlette-0.47.1-py3-none-any.whl", hash = "sha256:5e11c9f5c7c3f24959edbf2dffdc01bba860228acf657129467d8a7468591527", size = 72747, upload-time = "2025-06-21T04:03:15.705Z" }, ] [[package]] @@ -1792,6 +2163,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, ] +[[package]] +name = "twine" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404, upload-time = "2025-01-21T18:45:26.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791, upload-time = "2025-01-21T18:45:24.584Z" }, +] + [[package]] name = "types-pyyaml" version = "6.0.12.20250516" @@ -1801,13 +2192,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312, upload-time = "2025-05-16T03:08:04.019Z" }, ] +[[package]] +name = "types-requests" +version = "2.32.4.20250611" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" }, +] + [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] @@ -1862,6 +2265,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, ] +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, +] + [[package]] name = "yarl" version = "1.20.1"