diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7281070e4..1bf55bb45 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,7 +9,10 @@ on: pull_request: branches: - master - - feat/3.0-release + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: simple-checks: @@ -127,3 +130,28 @@ jobs: run: | uv sync --all-extras --all-packages uvx tox -c ${TOXCFG} -e ${TOXENV} + + custom-backend-tests: + name: Custom Backend Tests + needs: unit-tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + - name: Setup Bats and bats libs + id: setup-bats + uses: bats-core/bats-action@3.0.1 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@v6 + + - name: Run tests + env: + BATS_LIB_PATH: ${{ steps.setup-bats.outputs.lib-path }} + working-directory: ./example/custom_backend + run: ./run_tests.sh diff --git a/MANIFEST.in b/MANIFEST.in index 05b2580c1..0f4d4bfe8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include tavern/_core/schema/tests.jsonschema.yaml include tavern/_plugins/mqtt/jsonschema.yaml +include tavern/_plugins/rest/jsonschema.yaml include tavern/_plugins/grpc/schema.yaml include LICENSE diff --git a/example/custom_backend/.gitignore b/example/custom_backend/.gitignore new file mode 100644 index 000000000..0030b0d2f --- /dev/null +++ b/example/custom_backend/.gitignore @@ -0,0 +1,2 @@ +hello.txt +some_other_file.txt \ No newline at end of file diff --git a/example/custom_backend/README.md b/example/custom_backend/README.md new file mode 100644 index 000000000..c968ac362 --- /dev/null +++ b/example/custom_backend/README.md @@ -0,0 +1,66 @@ +# Tavern Custom Backend Plugin + +This example demonstrates how to create a custom backend plugin for Tavern, a pytest plugin for API testing. The custom +backend allows you to extend Tavern's functionality with your own request/response handling logic. + +## Overview + +This example plugin implements a simple file touch/verification system: + +- `touch_file` stage: Creates or updates a file timestamp (similar to the Unix `touch` command) +- `file_exists` stage: Verifies that a specified file exists + +## Implementation Details + +This example includes: + +- `Request` class: Extends `tavern.request.BaseRequest` and implements the `request_vars` property and `run()` method +- `Response` class: Extends `tavern.response.BaseResponse` and implements the `verify()` method +- `Session` class: Context manager for maintaining any state +- `get_expected_from_request` function: Optional function to generate expected response from request +- `jsonschema.yaml`: Schema validation for request/response objects +- `schema_path`: Path to the schema file for validation + +## Entry Point Configuration + +In your project's `pyproject.toml`, configure the plugin entry point: + +```toml +[project.entry-points.tavern_your_backend_name] +my_implementation = 'your.package.path:your_backend_module' +``` + +Then when running tests, specify the extra backend: + +```bash +pytest --tavern-extra-backends=your_backend_name +# Or, to specify an implementation to override the project entrypoint: +pytest --tavern-extra-backends=your_backend_name=my_other_implementation +``` + +Or the equivalent in pyproject.toml or pytest.ini. Note: + +- The entry point name should start with `tavern_`. +- The key of the entrypoint is just a name of the implementation and can be anything. +- The `--tavern-extra-backends` flag should *not* be prefixed with `tavern_`. +- If Tavern detects multiple entrypoints for a backend, it will raise an error. In this case, you must use the second + form to specify which implementation of the backend to use. This is similar to the build-in `--tavern-http-backend` + flag. + +This is because Tavern by default only tries to load "grpc", "http" and "mqtt" backends. The flag registers the custom +backend with Tavern, which can then tell [stevedore](https://github.com/openstack/stevedore) to load the plugin from the +entrypoint. + +## Example Test + +```yaml +--- +test_name: Test file touched + +stages: + - name: Touch file and check it exists + touch_file: + filename: hello.txt + file_exists: + filename: hello.txt +``` diff --git a/example/custom_backend/my_tavern_plugin/__init__.py b/example/custom_backend/my_tavern_plugin/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/example/custom_backend/my_tavern_plugin/jsonschema.yaml b/example/custom_backend/my_tavern_plugin/jsonschema.yaml new file mode 100644 index 000000000..511c27e59 --- /dev/null +++ b/example/custom_backend/my_tavern_plugin/jsonschema.yaml @@ -0,0 +1,39 @@ +$schema: "http://json-schema.org/draft-07/schema#" + +title: file touch schema +description: Schema for touching files + +### + +definitions: + touch_file: + type: object + description: touch a file + additionalProperties: false + required: + - filename + + properties: + filename: + type: string + description: Name of file to touch + + file_exists: + type: object + description: name of file which should exist + additionalProperties: false + required: + - filename + + properties: + filename: + type: string + description: Name of file to check for + + stage: + properties: + touch_file: + $ref: "#/definitions/touch_file" + + file_exists: + $ref: "#/definitions/file_exists" diff --git a/example/custom_backend/my_tavern_plugin/plugin.py b/example/custom_backend/my_tavern_plugin/plugin.py new file mode 100644 index 000000000..5f7b8a72b --- /dev/null +++ b/example/custom_backend/my_tavern_plugin/plugin.py @@ -0,0 +1,83 @@ +import logging +import pathlib +from collections.abc import Iterable +from os.path import abspath, dirname, join +from typing import Any, Optional, Union + +import box +import yaml + +from tavern._core import exceptions +from tavern._core.pytest.config import TestConfig +from tavern.request import BaseRequest +from tavern.response import BaseResponse + + +class Session: + """No-op session, but must implement the context manager protocol""" + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + +class Request(BaseRequest): + """Touches a file when the 'request' is made""" + def __init__( + self, session: Any, rspec: dict, test_block_config: TestConfig + ) -> None: + self.session = session + + self._request_vars = rspec + + @property + def request_vars(self) -> box.Box: + return self._request_vars + + def run(self): + pathlib.Path(self._request_vars["filename"]).touch() + + +class Response(BaseResponse): + def verify(self, response): + if not pathlib.Path(self.expected["filename"]).exists(): + raise exceptions.BadSchemaError( + f"Expected file '{self.expected['filename']}' does not exist" + ) + + return {} + + def __init__( + self, + client, + name: str, + expected: TestConfig, + test_block_config: TestConfig, + ) -> None: + super().__init__(name, expected, test_block_config) + + +logger: logging.Logger = logging.getLogger(__name__) + +session_type = Session + +request_type = Request +request_block_name = "touch_file" + + +verifier_type = Response +response_block_name = "file_exists" + + +def get_expected_from_request( + response_block: Union[dict, Iterable[dict]], + test_block_config: TestConfig, + session: Session, +) -> Optional[dict]: + return response_block + + +schema_path: str = join(abspath(dirname(__file__)), "jsonschema.yaml") +with open(schema_path, encoding="utf-8") as schema_file: + schema = yaml.load(schema_file, Loader=yaml.SafeLoader) diff --git a/example/custom_backend/pyproject.toml b/example/custom_backend/pyproject.toml new file mode 100644 index 000000000..7d5be92b5 --- /dev/null +++ b/example/custom_backend/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "my_tavern_plugin" +version = "0.1.0" +description = "A custom 'generic' plugin for tavern that touches files and checks if they are created." +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[project.entry-points.tavern_file] +my_tavern_plugin = "my_tavern_plugin.plugin" \ No newline at end of file diff --git a/example/custom_backend/run_tests.sh b/example/custom_backend/run_tests.sh new file mode 100755 index 000000000..1b50da9c1 --- /dev/null +++ b/example/custom_backend/run_tests.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -ex + +if [ ! -d ".venv" ]; then + uv venv +fi +. .venv/bin/activate + +uv sync + +if ! command -v bats; then + exit 1 +fi + +# Run tests using bats +bats --timing --print-output-on-failure "$@" tests.bats diff --git a/example/custom_backend/tests.bats b/example/custom_backend/tests.bats new file mode 100644 index 000000000..333064550 --- /dev/null +++ b/example/custom_backend/tests.bats @@ -0,0 +1,36 @@ +#!/usr/bin/env bats + +setup() { + if [ ! -d ".venv" ]; then + uv venv + fi + . .venv/bin/activate + uv pip install -e . 'tavern @ ../..' +} + +@test "run tavern-ci with --tavern-extra-backends=file" { + PYTHONPATH=. run tavern-ci \ + --tavern-extra-backends=file \ + --debug \ + tests + + [ "$status" -eq 0 ] +} + +@test "run tavern-ci with --tavern-extra-backends=file=my_tavern_plugin" { + PYTHONPATH=. run tavern-ci \ + --tavern-extra-backends=file=my_tavern_plugin \ + --debug \ + tests + + [ "$status" -eq 0 ] +} + +@test "run tavern-ci with --tavern-extra-backends=file=i_dont_exist should fail" { + PYTHONPATH=. run tavern-ci \ + --tavern-extra-backends=file=i_dont_exist \ + --debug \ + tests + + [ "$status" -ne 0 ] +} \ No newline at end of file diff --git a/example/custom_backend/tests/test_file_touched.tavern.yaml b/example/custom_backend/tests/test_file_touched.tavern.yaml new file mode 100644 index 000000000..ed2311fef --- /dev/null +++ b/example/custom_backend/tests/test_file_touched.tavern.yaml @@ -0,0 +1,46 @@ +--- +test_name: Test file touched + +stages: + - name: Touch file and check it exists + touch_file: + filename: hello.txt + file_exists: + filename: hello.txt + +--- +test_name: Test file touched - should fail because file doesn't exist + +marks: + - xfail + +stages: + - name: Touch file that doesn't exist + touch_file: + filename: some_other_file.txt + file_exists: + filename: nonexistent_file.txt + +--- +test_name: Test with invalid schema - should fail + +_xfail: verify + +stages: + - name: Test invalid touch_file schema + touch_file: + nonexistent_field: some_value + file_exists: + filename: hello.txt + +--- +test_name: Test with invalid response schema - should fail + +_xfail: verify + +stages: + - name: Test invalid file_exists schema + touch_file: + filename: hello.txt + file_exists: + nonexistent_field: some_value \ No newline at end of file diff --git a/tavern/_core/plugins.py b/tavern/_core/plugins.py index c2ca409fa..403f5db87 100644 --- a/tavern/_core/plugins.py +++ b/tavern/_core/plugins.py @@ -11,6 +11,7 @@ from typing import Any, Optional, Protocol import stevedore +import stevedore.extension from tavern._core import exceptions from tavern._core.dict_util import format_keys @@ -125,20 +126,51 @@ def _load_plugins(self, test_block_config: TestConfig) -> list[_Plugin]: """ plugins = [] + discovered_plugins: dict[str, list[str]] = {} + + def is_plugin_backend_enabled( + current_backend: str, ext: stevedore.extension.Extension + ) -> bool: + """Checks if a plugin backend is enabled based on configuration. + + If no specific backend is configured, defaults to enabled. + Adds enabled plugins to discovered_plugins tracking dictionary. + + Args: + current_backend: The backend being checked (e.g. 'http', 'mqtt') + ext: The stevedore extension object representing the plugin + + Returns: + Whether the plugin backend is enabled + """ + if test_block_config.tavern_internal.backends[current_backend] is None: + # Use whatever default - will raise an error if >1 is discovered + is_enabled = True + logger.debug(f"Using default backend for {ext.name}") + else: + is_enabled = ( + ext.name + == test_block_config.tavern_internal.backends[current_backend] + ) + logger.debug( + f"Is {current_backend} for {ext.name} enabled? {is_enabled}" + ) - def enabled(current_backend, ext): - return ( - ext.name == test_block_config.tavern_internal.backends[current_backend] - ) + if is_enabled: + if current_backend not in discovered_plugins: + discovered_plugins[current_backend] = [] + discovered_plugins[current_backend].append(ext.name) - for backend in test_block_config.backends(): + return is_enabled + + for backend in test_block_config.tavern_internal.backends.keys(): logger.debug("loading backend for %s", backend) namespace = f"tavern_{backend}" manager = stevedore.EnabledExtensionManager( namespace=namespace, - check_func=partial(enabled, backend), + check_func=partial(is_plugin_backend_enabled, backend), verify_requirements=True, on_load_failure_callback=plugin_load_error, ) @@ -153,6 +185,12 @@ def enabled(current_backend, ext): plugins.extend(manager.extensions) + for plugin, enabled in discovered_plugins.items(): + if len(enabled) > 1: + raise exceptions.PluginLoadError( + f"Multiple plugins enabled for '{plugin}' backend: {enabled}" + ) + return plugins diff --git a/tavern/_core/pytest/config.py b/tavern/_core/pytest/config.py index 2f5eaca8e..86a6532fd 100644 --- a/tavern/_core/pytest/config.py +++ b/tavern/_core/pytest/config.py @@ -60,7 +60,7 @@ def backends() -> list[str]: if all( has_module(module) for module in ("grpc_status", "grpc_reflection", "grpc", "google.protobuf") - ): + ) and has_module("grpc_reflection"): available_backends.append("grpc") if has_module("gql"): available_backends.append("graphql") diff --git a/tavern/_core/pytest/util.py b/tavern/_core/pytest/util.py index 33e0208bf..098434c28 100644 --- a/tavern/_core/pytest/util.py +++ b/tavern/_core/pytest/util.py @@ -5,6 +5,7 @@ import pytest +from tavern._core import exceptions from tavern._core.dict_util import format_keys, get_tavern_box from tavern._core.general import load_global_config from tavern._core.pytest.config import TavernInternalConfig, TestConfig @@ -76,6 +77,13 @@ def add_parser_options(parser_addoption, with_defaults: bool = True) -> None: default=False, action="store_true", ) + parser_addoption( + "--tavern-extra-backends", + help="list of extra backends to register", + default="", + type=str, + action="store", + ) def add_ini_options(parser: pytest.Parser) -> None: @@ -90,13 +98,19 @@ def add_ini_options(parser: pytest.Parser) -> None: default=[], ) parser.addini( - "tavern-http-backend", help="Which http backend to use", default="requests" + "tavern-http-backend", + help="Which http backend to use", + default="requests", ) parser.addini( - "tavern-mqtt-backend", help="Which mqtt backend to use", default="paho-mqtt" + "tavern-mqtt-backend", + help="Which mqtt backend to use", + default="paho-mqtt", ) parser.addini( - "tavern-grpc-backend", help="Which grpc backend to use", default="grpc" + "tavern-grpc-backend", + help="Which grpc backend to use", + default="grpc", ) parser.addini( "tavern-graphql-backend", @@ -133,6 +147,12 @@ def add_ini_options(parser: pytest.Parser) -> None: type="bool", default=False, ) + parser.addini( + "tavern-extra-backends", + help="list of extra backends to register", + type="args", + default=[], + ) def load_global_cfg(pytest_config: pytest.Config) -> TestConfig: @@ -187,11 +207,28 @@ def _load_global_cfg(pytest_config: pytest.Config) -> TestConfig: def _load_global_backends(pytest_config: pytest.Config) -> dict[str, Any]: """Load which backend should be used""" - return { + backends: dict[str, str | None] = { b: get_option_generic(pytest_config, f"tavern-{b}-backend", None) for b in TestConfig.backends() } + extra_backends: list[str] = get_option_generic( + pytest_config, "tavern-extra-backends", [] + ) + for backend in extra_backends: + split = backend.split("=") + if len(split) == 1: + backends[split[0]] = None + elif len(split) == 2: + key, value = split + backends[key] = value + else: + raise exceptions.BadSchemaError( + f"extra backends must be in the form 'name' or 'name=value', got '{backend}'" + ) + + return backends + def _load_global_strictness(pytest_config: pytest.Config) -> StrictLevel: """Load the global 'strictness' setting""" @@ -224,11 +261,24 @@ def get_option_generic( use = default # Middle priority - if pytest_config.getini(ini_flag) is not None: - use = pytest_config.getini(ini_flag) + if ini := pytest_config.getini(ini_flag): + if isinstance(default, list): + if isinstance(ini, list): + use = default[:] # type:ignore + use.extend(ini) # type:ignore + else: + raise ValueError( + f"Expected list for {ini_flag} option, got {ini} of type {type(ini)}" + ) + else: + use = ini # Top priority - if pytest_config.getoption(cli_flag) is not None: - use = pytest_config.getoption(cli_flag) + if cli := pytest_config.getoption(cli_flag): + if isinstance(default, list): + cli = cli.split(",") + use.extend(cli) # type:ignore + else: + use = cli return use diff --git a/tavern/_core/schema/files.py b/tavern/_core/schema/files.py index 3d9429101..85eedad62 100644 --- a/tavern/_core/schema/files.py +++ b/tavern/_core/schema/files.py @@ -5,6 +5,7 @@ import tempfile from collections.abc import Mapping +import box import pykwalify import yaml from pykwalify import core @@ -53,7 +54,13 @@ def _load_schema_with_plugins(self, schema_filename: str) -> dict: # Don't require a schema logger.debug("No schema defined for %s", p.name) else: - base_schema["properties"].update(plugin_schema.get("properties", {})) + for key in ["properties", "definitions"]: + if key not in plugin_schema: + continue + + value = box.Box(plugin_schema[key]) + value.merge_update(base_schema[key]) + base_schema[key] = value self._loaded[mangled] = base_schema return self._loaded[mangled] diff --git a/tavern/_core/schema/tests.jsonschema.yaml b/tavern/_core/schema/tests.jsonschema.yaml index 5f935573c..63608d10e 100644 --- a/tavern/_core/schema/tests.jsonschema.yaml +++ b/tavern/_core/schema/tests.jsonschema.yaml @@ -64,363 +64,6 @@ definitions: items: $ref: "#/definitions/stage" - http_request: - type: object - additionalProperties: false - description: HTTP request to perform as part of stage - - required: - - url - - properties: - url: - description: URL to make request to - oneOf: - - type: string - - type: object - properties: - "$ext": - $ref: "#/definitions/verify_block" - - cert: - description: Certificate to use - either a path to a certificate and key in one file, or a two item list containing the certificate and key separately - oneOf: - - type: string - - type: array - minItems: 2 - maxItems: 2 - items: - type: string - - auth: - description: Authorisation to use for request - a list containing username and password - type: array - minItems: 2 - maxItems: 2 - items: - type: string - - verify: - description: Whether to verify the server's certificates - oneOf: - - type: boolean - default: false - - type: string - - method: - description: HTTP method to use for request - default: GET - type: string - - follow_redirects: - type: boolean - description: Whether to follow redirects from 3xx responses - default: false - - stream: - type: boolean - description: Whether to stream the download from the request - default: false - - cookies: - type: array - description: Which cookies to use in the request - - items: - oneOf: - - type: string - - type: object - - json: - description: JSON body to send in request body - $ref: "#/definitions/any_json" - - params: - description: Query parameters - type: object - - headers: - description: Headers for request - type: object - - data: - description: Form data to send in request - oneOf: - - type: object - - type: string - - timeout: - description: How long to wait for requests to time out - oneOf: - - type: number - - type: array - minItems: 2 - maxItems: 2 - items: - type: number - - file_body: - type: string - description: Path to a file to upload as the request body - - files: - oneOf: - - type: object - - type: array - description: Files to send as part of the request - - clear_session_cookies: - description: Whether to clear sesion cookies before running this request - type: boolean - - mqtt_publish: - type: object - description: Publish MQTT message - additionalProperties: false - - properties: - topic: - type: string - description: Topic to publish on - - payload: - type: string - description: Raw payload to post - - json: - description: JSON payload to post - $ref: "#/definitions/any_json" - - qos: - type: integer - description: QoS level to use for request - default: 0 - - retain: - type: boolean - description: Whether the message should be retained - default: false - - mqtt_response: - type: object - additionalProperties: false - description: Expected MQTT response - - properties: - unexpected: - type: boolean - description: Receiving this message fails the test - - topic: - type: string - description: Topic message should be received on - - payload: - description: Expected raw payload in response - oneOf: - - type: number - - type: integer - - type: string - - type: boolean - - json: - description: Expected JSON payload in response - $ref: "#/definitions/any_json" - - timeout: - type: number - description: How long to wait for response to arrive - - qos: - type: integer - description: QoS level that message should be received on - minimum: 0 - maximum: 2 - - verify_response_with: - oneOf: - - $ref: "#/definitions/verify_block" - - type: array - items: - $ref: "#/definitions/verify_block" - - save: - type: object - description: Which objects to save from the response - - grpc_request: - type: object - required: - - service - properties: - host: - type: string - - service: - type: string - - body: - type: object - - json: - type: object - - retain: - type: boolean - - grpc_response: - type: object - properties: - status: - oneOf: - - type: string - - type: integer - - details: - type: object - - proto_body: - type: object - - timeout: - type: number - - verify_response_with: - oneOf: - - $ref: "#/definitions/verify_block" - - type: array - items: - $ref: "#/definitions/verify_block" - - graphql_request: - type: object - description: GraphQL request to perform as part of stage - additionalProperties: false - required: - - url - - query - - properties: - url: - description: URL to make GraphQL request to - oneOf: - - type: string - - type: object - properties: - "$ext": - $ref: "#/definitions/verify_block" - - query: - type: string - description: GraphQL query string - - variables: - description: Variables for GraphQL query - oneOf: - - type: object - - type: array - - type: string - - type: number - - type: boolean - - files: - type: object - description: Files to send as part of the request. Mapping of graphql variable name to file path, or to 'long form' files - - headers: - description: Headers for GraphQL request - type: object - - operation_name: - description: Operation name for GraphQL request - type: string - - graphql_response: - type: object - description: Expected GraphQL response - additionalProperties: false - - properties: - data: - description: The data returned by the GraphQL query - type: object - - errors: - description: Errors returned by the GraphQL query - type: array - items: - type: object - - verify_response_with: - oneOf: - - $ref: "#/definitions/verify_block" - - type: array - items: - $ref: "#/definitions/verify_block" - - save: - type: object - description: Which objects to save from response - - subscription: - description: Name of subscription that the response is expected for - type: string - - timeout: - type: number - description: Request timeout in seconds - default: 5 - - http_response: - type: object - additionalProperties: false - description: Expected HTTP response - - properties: - strict: - $ref: "#/definitions/strict_block" - - status_code: - description: Status code(s) to match - oneOf: - - type: integer - - type: array - minItems: 1 - items: - type: integer - - cookies: - type: array - description: Cookies expected to be returned - uniqueItems: true - minItems: 1 - - items: - type: string - - json: - description: Expected JSON response - $ref: "#/definitions/any_json" - - redirect_query_params: - description: Query parameters parsed from the 'location' of a redirect - type: object - - verify_response_with: - oneOf: - - $ref: "#/definitions/verify_block" - - type: array - items: - $ref: "#/definitions/verify_block" - - headers: - description: Headers expected in response - type: object - - save: - type: object - description: Which objects to save from the response - stage_ref: type: object description: Reference to another stage from an included config file @@ -487,40 +130,6 @@ definitions: type: string description: Name of this stage - mqtt_publish: - $ref: "#/definitions/mqtt_publish" - - mqtt_response: - oneOf: - - $ref: "#/definitions/mqtt_response" - - type: array - minItems: 1 - items: - $ref: "#/definitions/mqtt_response" - - request: - $ref: "#/definitions/http_request" - - response: - $ref: "#/definitions/http_response" - - grpc_request: - $ref: "#/definitions/grpc_request" - - grpc_response: - $ref: "#/definitions/grpc_response" - - graphql_request: - $ref: "#/definitions/graphql_request" - - graphql_response: - oneOf: - - $ref: "#/definitions/graphql_response" - - type: array - minItems: 1 - items: - $ref: "#/definitions/graphql_response" - ### type: object diff --git a/tavern/_core/strict_util.py b/tavern/_core/strict_util.py index 88a48a9fa..3b87179d1 100644 --- a/tavern/_core/strict_util.py +++ b/tavern/_core/strict_util.py @@ -58,10 +58,28 @@ def is_on(self) -> bool: def validate_and_parse_option(key: str) -> StrictOption: + """Parse and validate a strict option configuration string. + + Args: + key: String in format "section[:setting]" where: + section: One of "json", "headers", or "redirect_query_params" + setting: Optional "on", "off" or "list_any_order" + + Returns: + StrictOption containing the parsed section and setting + + Raises: + InvalidConfigurationException: If the key format is invalid + """ regex = re.compile( - "(?P
{sections})(:(?P{switches}))?".format( - sections="|".join(valid_keys), switches="|".join(valid_switches) - ) + r""" + (?P
{sections}) # The section name (json/headers/redirect_query_params) + (?: # Optional non-capturing group for setting + : # Literal colon separator + (?P{switches}) # The setting value (on/off/list_any_order) + )? # End optional group + """.format(sections="|".join(valid_keys), switches="|".join(valid_switches)), + re.X, ) match = regex.fullmatch(key) diff --git a/tavern/_plugins/graphql/jsonschema.yaml b/tavern/_plugins/graphql/jsonschema.yaml index 46a20a907..cbb5b2484 100644 --- a/tavern/_plugins/graphql/jsonschema.yaml +++ b/tavern/_plugins/graphql/jsonschema.yaml @@ -8,6 +8,98 @@ description: Schema for GraphQL API testing type: object additionalProperties: false +definitions: + graphql_request: + type: object + description: GraphQL request to perform as part of stage + additionalProperties: false + required: + - url + - query + + properties: + url: + description: URL to make GraphQL request to + oneOf: + - type: string + - type: object + properties: + "$ext": + $ref: "#/definitions/verify_block" + + query: + type: string + description: GraphQL query string + + variables: + description: Variables for GraphQL query + oneOf: + - type: object + - type: array + - type: string + - type: number + - type: boolean + + files: + type: object + description: Files to send as part of the request. Mapping of graphql variable name to file path, or to 'long form' files + + headers: + description: Headers for GraphQL request + type: object + + operation_name: + description: Operation name for GraphQL request + type: string + + graphql_response: + type: object + description: Expected GraphQL response + additionalProperties: false + + properties: + data: + description: The data returned by the GraphQL query + type: object + + errors: + description: Errors returned by the GraphQL query + type: array + items: + type: object + + verify_response_with: + oneOf: + - $ref: "#/definitions/verify_block" + - type: array + items: + $ref: "#/definitions/verify_block" + + save: + type: object + description: Which objects to save from response + + subscription: + description: Name of subscription that the response is expected for + type: string + + timeout: + type: number + description: Request timeout in seconds + default: 5 + + stage: + properties: + graphql_request: + $ref: "#/definitions/graphql_request" + + graphql_response: + oneOf: + - $ref: "#/definitions/graphql_response" + - type: array + items: + $ref: "#/definitions/graphql_response" + properties: gql: description: Arguments to pass to the GQL HTTP transport constructor diff --git a/tavern/_plugins/grpc/jsonschema.yaml b/tavern/_plugins/grpc/jsonschema.yaml index f18c1131d..008c58766 100644 --- a/tavern/_plugins/grpc/jsonschema.yaml +++ b/tavern/_plugins/grpc/jsonschema.yaml @@ -9,6 +9,59 @@ additionalProperties: false required: - grpc +definitions: + grpc_request: + type: object + required: + - service + properties: + host: + type: string + + service: + type: string + + body: + type: object + + json: + type: object + + retain: + type: boolean + + grpc_response: + type: object + properties: + status: + oneOf: + - type: string + - type: integer + + details: + type: object + + proto_body: + type: object + + timeout: + type: number + + verify_response_with: + oneOf: + - $ref: "#/definitions/verify_block" + - type: array + items: + $ref: "#/definitions/verify_block" + + stage: + properties: + grpc_request: + $ref: "#/definitions/grpc_request" + + grpc_response: + $ref: "#/definitions/grpc_response" + properties: grpc: type: object diff --git a/tavern/_plugins/grpc/schema.yaml b/tavern/_plugins/grpc/schema.yaml deleted file mode 100644 index 771a0c212..000000000 --- a/tavern/_plugins/grpc/schema.yaml +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: GRPC schemas -desc: pykwalify schemas for 'grpc' plugin block, grpc_request, and grpc_response - -initialisation: - grpc: - required: false - type: map - mapping: - connect: - required: true - type: map - mapping: - host: - required: false - type: any - port: - required: false - type: any - func: int_variable - keepalive: - required: false - type: float - timeout: - required: false - type: float - tls: - required: false - type: any - func: bool_variable - - metadata: - required: false - type: any - - proto: - required: false - type: map - mapping: - source: - required: false - type: str - module: - required: false - type: str diff --git a/tavern/_plugins/mqtt/jsonschema.yaml b/tavern/_plugins/mqtt/jsonschema.yaml index 7d1fb7edf..de9629408 100644 --- a/tavern/_plugins/mqtt/jsonschema.yaml +++ b/tavern/_plugins/mqtt/jsonschema.yaml @@ -1,7 +1,7 @@ $schema: "http://json-schema.org/draft-07/schema#" title: Paho MQTT schema -description: Schema for paho-mqtt connection +description: Schema for paho-mqtt connection and requests/responses ### @@ -10,6 +10,94 @@ additionalProperties: false required: - paho-mqtt +definitions: + mqtt_publish: + type: object + description: Publish MQTT message + additionalProperties: false + + properties: + topic: + type: string + description: Topic to publish on + + payload: + type: string + description: Raw payload to post + + json: + description: JSON payload to post + $ref: "#/definitions/any_json" + + qos: + type: integer + description: QoS level to use for request + default: 0 + + retain: + type: boolean + description: Whether the message should be retained + default: false + + mqtt_response: + type: object + additionalProperties: false + description: Expected MQTT response + + properties: + unexpected: + type: boolean + description: Receiving this message fails the test + + topic: + type: string + description: Topic message should be received on + + payload: + description: Expected raw payload in response + oneOf: + - type: number + - type: integer + - type: string + - type: boolean + + json: + description: Expected JSON payload in response + $ref: "#/definitions/any_json" + + timeout: + type: number + description: How long to wait for response to arrive + + qos: + type: integer + description: QoS level that message should be received on + minimum: 0 + maximum: 2 + + verify_response_with: + oneOf: + - $ref: "#/definitions/verify_block" + - type: array + items: + $ref: "#/definitions/verify_block" + + save: + type: object + description: Which objects to save from the response + + stage: + properties: + mqtt_publish: + $ref: "#/definitions/mqtt_publish" + + mqtt_response: + oneOf: + - $ref: "#/definitions/mqtt_response" + - type: array + items: + $ref: "#/definitions/mqtt_response" + properties: paho-mqtt: type: object diff --git a/tavern/_plugins/mqtt/schema.yaml b/tavern/_plugins/mqtt/schema.yaml deleted file mode 100644 index 9dac6888a..000000000 --- a/tavern/_plugins/mqtt/schema.yaml +++ /dev/null @@ -1,139 +0,0 @@ ---- -name: MQTT schemas -desc: pykwalify schemas for 'mqtt' plugin block, mqtt_publish, and mqtt_response - -initialisation: - paho-mqtt: - required: false - type: map - mapping: - client: - required: false - type: map - mapping: - client_id: - type: str - required: false - clean_session: - type: bool - required: false - transport: - type: str - required: false - enum: - - tcp - - websockets - - connect: - required: true - type: map - mapping: - host: - required: true - type: str - port: - required: false - type: any - func: int_variable - keepalive: - required: false - type: float - timeout: - required: false - type: float - - tls: - required: false - type: map - mapping: - enable: - required: false - type: bool - - ca_certs: - required: false - type: str - - certfile: - required: false - type: str - - keyfile: - required: false - type: str - - cert_reqs: - required: false - type: str - enum: - - CERT_NONE - - CERT_OPTIONAL - - CERT_REQUIRED - - tls_version: - required: false - type: str - # This could be an enum but there's lots of them, and which ones are - # actually valid changes based on which version of python you're - # using. Just let any ssl errors propagate through - - ciphers: - required: false - type: str - - ssl_context: - required: false - type: map - mapping: - ca_certs: - required: false - type: str - - certfile: - required: false - type: str - - keyfile: - required: false - type: str - - password: - required: false - type: str - # This is the password for the keyfile, and is only needed if the keyfile is password encrypted - # If not supplied, but the keyfile is password protect, the ssl module will prompt for a password in terminal - - cert_reqs: - required: false - type: str - enum: - - CERT_NONE - - CERT_OPTIONAL - - CERT_REQUIRED - - tls_version: - required: false - type: str - # This could be an enum but there's lots of them, and which ones are - # actually valid changes based on which version of python you're - # using. Just let any ssl errors propagate through - - ciphers: - required: false - type: str - - alpn_protocols: - required: false - type: array - - auth: - required: false - type: map - mapping: - username: - type: str - required: true - - password: - type: str - required: false diff --git a/tavern/_plugins/rest/jsonschema.yaml b/tavern/_plugins/rest/jsonschema.yaml new file mode 100644 index 000000000..21ed563aa --- /dev/null +++ b/tavern/_plugins/rest/jsonschema.yaml @@ -0,0 +1,180 @@ +$schema: "http://json-schema.org/draft-07/schema#" + +title: REST schema +description: Schema for REST requests + +### + +definitions: + http_request: + type: object + additionalProperties: false + description: HTTP request to perform as part of stage + + required: + - url + + properties: + url: + description: URL to make request to + oneOf: + - type: string + - type: object + properties: + "$ext": + $ref: "#/definitions/verify_block" + + cert: + description: Certificate to use - either a path to a certificate and key in one file, or a two item list containing the certificate and key separately + oneOf: + - type: string + - type: array + minItems: 2 + maxItems: 2 + items: + type: string + + auth: + description: Authorisation to use for request - a list containing username and password + type: array + minItems: 2 + maxItems: 2 + items: + type: string + + verify: + description: Whether to verify the server's certificates + oneOf: + - type: boolean + default: false + - type: string + + method: + description: HTTP method to use for request + default: GET + type: string + + follow_redirects: + type: boolean + description: Whether to follow redirects from 3xx responses + default: false + + stream: + type: boolean + description: Whether to stream the download from the request + default: false + + cookies: + type: array + description: Which cookies to use in the request + + items: + oneOf: + - type: string + - type: object + + json: + description: JSON body to send in request body + $ref: "#/definitions/any_json" + + params: + description: Query parameters + type: object + + headers: + description: Headers for request + type: object + + data: + description: Form data to send in request + oneOf: + - type: object + - type: string + + timeout: + description: How long to wait for requests to time out + oneOf: + - type: number + - type: array + minItems: 2 + maxItems: 2 + items: + type: number + + file_body: + type: string + description: Path to a file to upload as the request body + + files: + oneOf: + - type: object + - type: array + description: Files to send as part of the request + + clear_session_cookies: + description: Whether to clear sesion cookies before running this request + type: boolean + + http_response: + type: object + additionalProperties: false + description: Expected HTTP response + + properties: + strict: + $ref: "#/definitions/strict_block" + + status_code: + description: Status code(s) to match + oneOf: + - type: integer + - type: array + minItems: 1 + items: + type: integer + + cookies: + type: array + description: Cookies expected to be returned + uniqueItems: true + minItems: 1 + + items: + type: string + + json: + description: Expected JSON response + $ref: "#/definitions/any_json" + + redirect_query_params: + description: Query parameters parsed from the 'location' of a redirect + type: object + + verify_response_with: + oneOf: + - $ref: "#/definitions/verify_block" + - type: array + items: + $ref: "#/definitions/verify_block" + + headers: + description: Headers expected in response + type: object + + save: + type: object + description: Which objects to save from the response + + stage: + type: object + description: One stage in a test + additionalProperties: false + required: + - name + + properties: + request: + $ref: "#/definitions/http_request" + + response: + $ref: "#/definitions/http_response" diff --git a/tavern/_plugins/rest/tavernhook.py b/tavern/_plugins/rest/tavernhook.py index 8f6a56e2b..881e45c8a 100644 --- a/tavern/_plugins/rest/tavernhook.py +++ b/tavern/_plugins/rest/tavernhook.py @@ -1,6 +1,8 @@ import logging +from os.path import abspath, dirname, join import requests +import yaml from tavern._core import exceptions from tavern._core.dict_util import format_keys @@ -19,6 +21,8 @@ class TavernRestPlugin(PluginHelperBase): request_type = RestRequest request_block_name = "request" + schema: dict + @staticmethod def get_expected_from_request( response_block: dict, test_block_config: TestConfig, session @@ -33,3 +37,10 @@ def get_expected_from_request( verifier_type = RestResponse response_block_name = "response" + + +schema_path: str = join(abspath(dirname(__file__)), "jsonschema.yaml") +with open(schema_path, encoding="utf-8") as schema_file: + schema = yaml.load(schema_file, Loader=yaml.SafeLoader) + +TavernRestPlugin.schema = schema diff --git a/tavern/response.py b/tavern/response.py index dcfee7039..a50eb2c76 100644 --- a/tavern/response.py +++ b/tavern/response.py @@ -23,6 +23,21 @@ def indent_err_text(err: str) -> str: @dataclasses.dataclass class BaseResponse: + """Base for all response verifiers. + + Subclasses must have an __init__ method like: + + def __init__( + self, + client: Any, + name: str, + expected: TestConfig, + test_block_config: TestConfig, + ) -> None: + super().__init__(name, expected, test_block_config) + # ...other setup + """ + name: str expected: Any test_block_config: TestConfig @@ -45,7 +60,7 @@ def _adderr(self, msg: str, *args, e=None) -> None: self.errors += [(msg % args)] @abstractmethod - def verify(self, response): + def verify(self, response) -> Mapping: """Verify response against expected values and returns any values that we wanted to save for use in future requests diff --git a/uv.lock b/uv.lock index 2dc83b350..4ee9e77ef 100644 --- a/uv.lock +++ b/uv.lock @@ -196,15 +196,15 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.0" +version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] [[package]] @@ -254,11 +254,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] @@ -502,14 +502,14 @@ toml = [ [[package]] name = "cross-web" -version = "0.4.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/93/4f/bdb62e969649ee76d4741ef8eee34384ec2bc21cc66eb7fd244e6ad62be8/cross_web-0.4.0.tar.gz", hash = "sha256:4ae65619ddfcd06d6803432c0366342d7e8aeba10194b4e144d73a662e75370c", size = 157111, upload-time = "2025-12-25T20:45:21.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/58/e688e99d1493c565d1587e64b499268d0a3129ae59f4efe440aac395f803/cross_web-0.4.1.tar.gz", hash = "sha256:0466295028dcae98c9ab3d18757f90b0e74fac2ff90efbe87e74657546d9993d", size = 157385, upload-time = "2026-01-09T18:17:41.534Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/d6/6c6a036655e5091b26b9f350dcf43821895325aa4727396b14c67679a957/cross_web-0.4.0-py3-none-any.whl", hash = "sha256:0c675bd26e91428cab31e3e927929b42da94aa96da92974e57c78f9a732d0e9b", size = 14200, upload-time = "2025-12-25T20:45:23.075Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/92b46b6e65f09b717a66c4e5a9bc47a45ebc83dd0e0ed126f8258363479d/cross_web-0.4.1-py3-none-any.whl", hash = "sha256:41b07c3a38253c517ec0603c1a366353aff77538946092b0f9a2235033f192c2", size = 14320, upload-time = "2026-01-09T18:17:40.325Z" }, ] [[package]] @@ -586,11 +586,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.2" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c1/e0/a75dbe4bca1e7d41307323dad5ea2efdd95408f74ab2de8bd7dba9b51a1a/filelock-3.20.2.tar.gz", hash = "sha256:a2241ff4ddde2a7cebddf78e39832509cb045d18ec1a09d7248d6bfc6bfbbe64", size = 19510, upload-time = "2026-01-02T15:33:32.582Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/30/ab407e2ec752aa541704ed8f93c11e2a5d92c168b8a755d818b74a3c5c2d/filelock-3.20.2-py3-none-any.whl", hash = "sha256:fbba7237d6ea277175a32c54bb71ef814a8546d8601269e1bfc388de333974e8", size = 16697, upload-time = "2026-01-02T15:33:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -754,7 +754,7 @@ wheels = [ [[package]] name = "google-api-core" -version = "2.28.1" +version = "2.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, @@ -763,9 +763,9 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/da/83d7043169ac2c8c7469f0e375610d78ae2160134bf1b80634c482fa079c/google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8", size = 176759, upload-time = "2025-10-28T21:34:51.529Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/10/05572d33273292bac49c2d1785925f7bc3ff2fe50e3044cf1062c1dde32e/google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", size = 177828, upload-time = "2026-01-08T22:21:39.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c", size = 173706, upload-time = "2025-10-28T21:34:50.151Z" }, + { url = "https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9", size = 173906, upload-time = "2026-01-08T22:21:36.093Z" }, ] [[package]] @@ -786,16 +786,15 @@ wheels = [ [[package]] name = "google-auth" -version = "2.45.0" +version = "2.47.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/00/3c794502a8b892c404b2dea5b3650eb21bfc7069612fbfd15c7f17c1cb0d/google_auth-2.45.0.tar.gz", hash = "sha256:90d3f41b6b72ea72dd9811e765699ee491ab24139f34ebf1ca2b9cc0c38708f3", size = 320708, upload-time = "2025-12-15T22:58:42.889Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/3c/ec64b9a275ca22fa1cd3b6e77fefcf837b0732c890aa32d2bd21313d9b33/google_auth-2.47.0.tar.gz", hash = "sha256:833229070a9dfee1a353ae9877dcd2dec069a8281a4e72e72f77d4a70ff945da", size = 323719, upload-time = "2026-01-06T21:55:31.045Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/97/451d55e05487a5cd6279a01a7e34921858b16f7dc8aa38a2c684743cd2b3/google_auth-2.45.0-py2.py3-none-any.whl", hash = "sha256:82344e86dc00410ef5382d99be677c6043d72e502b625aa4f4afa0bdacca0f36", size = 233312, upload-time = "2025-12-15T22:58:40.777Z" }, + { url = "https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl", hash = "sha256:c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498", size = 234867, upload-time = "2026-01-06T21:55:28.6Z" }, ] [[package]] @@ -1164,7 +1163,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1172,9 +1171,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [[package]] @@ -2515,7 +2514,7 @@ wheels = [ [[package]] name = "strawberry-graphql" -version = "0.288.2" +version = "0.288.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cross-web" }, @@ -2524,9 +2523,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/13/306dd2edb5f0c4aa51e2d071994a91ed1a0304e9feafa79ea0f04da51298/strawberry_graphql-0.288.2.tar.gz", hash = "sha256:853dbab407e3f5099f3a27dbf37786535894a0fbf150df5dde145fc290db607e", size = 215182, upload-time = "2026-01-01T20:01:19.277Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/a9/41f727b8c37e40ba746b509d41880311f1b391a661c8f230aa73ca44ebab/strawberry_graphql-0.288.3.tar.gz", hash = "sha256:985af4c33ea98343409892207d15b7d4a8491b1ba65ae3bb8ddcd674525069a6", size = 215302, upload-time = "2026-01-10T12:05:33.004Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/14/15abfa6d289048eeb1b1cca4a582db08a3c1f42784b485c21ef54617e2c7/strawberry_graphql-0.288.2-py3-none-any.whl", hash = "sha256:ad72d7904582db333158568751bb6186a872380a8cc6671159d011d279382542", size = 313137, upload-time = "2026-01-01T20:01:17.32Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e3/2d8362503956729e51350647adf43ac9691aef8542680691f61d2f1ec38d/strawberry_graphql-0.288.3-py3-none-any.whl", hash = "sha256:4cccc871d5545360bfea65a0b21ff823e4c66ab962da6eec812fb9e3a586316e", size = 313279, upload-time = "2026-01-10T12:05:30.771Z" }, ] [package.optional-dependencies] @@ -2872,7 +2871,7 @@ wheels = [ [[package]] name = "tox" -version = "4.33.0" +version = "4.34.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -2885,9 +2884,9 @@ dependencies = [ { name = "pyproject-api" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/d7/ccf2f7fb162170cd5bb4ac7c682dadf1159bae3c5c6d22dae0b2d5936336/tox-4.33.0.tar.gz", hash = "sha256:a29244bce3f514f94043e173366aa191c8cf0106ec8ddd18ba53f985acd73cc4", size = 204690, upload-time = "2026-01-02T22:52:53.904Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/9b/5909f40b281ebd37c2f83de5087b9cb8a9a64c33745f334be0aeaedadbbc/tox-4.34.1.tar.gz", hash = "sha256:ef1e82974c2f5ea02954d590ee0b967fad500c3879b264ea19efb9a554f3cc60", size = 205306, upload-time = "2026-01-09T17:42:59.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/cd/dd273f8896ce51014f106d133b79bdca6c650b9281271b247db2f693061c/tox-4.33.0-py3-none-any.whl", hash = "sha256:8582ac5c3ca97095ce88ae6bcd310d22614350ea9751b0e4ad39acad7874e270", size = 176556, upload-time = "2026-01-02T22:52:52.442Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0f/fe6629e277ce615e53d0a0b65dc23c88b15a402bb7dbf771f17bbd18f1c4/tox-4.34.1-py3-none-any.whl", hash = "sha256:5610d69708bab578d618959b023f8d7d5d3386ed14a2392aeebf9c583615af60", size = 176812, upload-time = "2026-01-09T17:42:58.629Z" }, ] [[package]] @@ -2915,14 +2914,14 @@ wheels = [ [[package]] name = "types-jsonschema" -version = "4.25.1.20251009" +version = "4.26.0.20260109" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/da/5b901088da5f710690b422137e8ae74197fb1ca471e4aa84dd3ef0d6e295/types_jsonschema-4.25.1.20251009.tar.gz", hash = "sha256:75d0f5c5dd18dc23b664437a0c1a625743e8d2e665ceaf3aecb29841f3a5f97f", size = 15661, upload-time = "2025-10-09T02:54:36.963Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/03/a1509b0c13fc7a1fca1494c84bde8cce8a5c0582b6255b9640ebd3034017/types_jsonschema-4.26.0.20260109.tar.gz", hash = "sha256:340fe91e6ea517900d6ababb6262a86c176473b5bf8455b96e85a89e3cfb5daa", size = 15886, upload-time = "2026-01-09T03:21:45.464Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/6a/e5146754c0dfc272f176db9c245bc43cc19030262d891a5a85d472797e60/types_jsonschema-4.25.1.20251009-py3-none-any.whl", hash = "sha256:f30b329037b78e7a60146b1146feb0b6fb0b71628637584409bada83968dad3e", size = 15925, upload-time = "2025-10-09T02:54:35.847Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d6/3db3134f35f1e4bf9a13517d759c1ae64086eddb8ad0047caee140021e64/types_jsonschema-4.26.0.20260109-py3-none-any.whl", hash = "sha256:e0276640d228732fb75d883905d607359b24a4ff745ba7f9a5f50e6fda891926", size = 15923, upload-time = "2026-01-09T03:21:43.828Z" }, ] [[package]] @@ -2954,14 +2953,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.32.4.20250913" +version = "2.32.4.20260107" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, ] [[package]] @@ -3014,37 +3013,37 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.2" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "uv" -version = "0.9.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/2b/4e2090bc3a6265b445b3d31ca6fff20c6458d11145069f7e48ade3e2d75b/uv-0.9.21.tar.gz", hash = "sha256:aa4ca6ccd68e81b5ebaa3684d3c4df2b51a982ac16211eadf0707741d36e6488", size = 3834762, upload-time = "2025-12-30T16:12:51.927Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/26/0750c5bb1637ebefe1db0936dc76ead8ce97f17368cda950642bfd90fa3f/uv-0.9.21-py3-none-linux_armv6l.whl", hash = "sha256:0b330eaced2fd9d94e2a70f3bb6c8fd7beadc9d9bf9f1227eb14da44039c413a", size = 21266556, upload-time = "2025-12-30T16:12:47.311Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ef/f019466c1e367ea68003cf35f4d44cc328694ed4a59b6004aa7dcacb2b35/uv-0.9.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1d8e0940bddd37a55f4479d61adaa6b302b780d473f037fc084e48b09a1678e7", size = 20485648, upload-time = "2025-12-30T16:12:15.746Z" }, - { url = "https://files.pythonhosted.org/packages/2a/41/f735bd9a5b4848b6f4f1028e6d768f581559d68eddb6403eb0f19ca4c843/uv-0.9.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cb420ddab7bcdd12c2352d4b551ced428d104311c0b98ce205675ab5c97072db", size = 18986976, upload-time = "2025-12-30T16:12:25.034Z" }, - { url = "https://files.pythonhosted.org/packages/9a/5f/01d537e05927594dc379ff8bc04f8cde26384d25108a9f63758eae2a7936/uv-0.9.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a36d164438a6310c9fceebd041d80f7cffcc63ba80a7c83ee98394fadf2b8545", size = 20819312, upload-time = "2025-12-30T16:12:41.802Z" }, - { url = "https://files.pythonhosted.org/packages/18/89/9497395f57e007a2daed8172042ecccade3ff5569fd367d093f49bd6a4a8/uv-0.9.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c0ad83ce874cbbf9eda569ba793a9fb70870db426e9862300db8cf2950a7fe3b", size = 20900227, upload-time = "2025-12-30T16:12:19.242Z" }, - { url = "https://files.pythonhosted.org/packages/04/61/a3f6dfc75d278cce96b370e00b6f03d73ec260e5304f622504848bad219d/uv-0.9.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9076191c934b813147060e4cd97e33a58999de0f9c46f8ac67f614e154dae5c8", size = 21965424, upload-time = "2025-12-30T16:12:01.589Z" }, - { url = "https://files.pythonhosted.org/packages/18/3e/344e8c1078cfea82159c6608b8694f24fdfe850ce329a4708c026cb8b0ff/uv-0.9.21-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2ce0f6aca91f7fbf1192e43c063f4de3666fd43126aacc71ff7d5a79f831af59", size = 23540343, upload-time = "2025-12-30T16:12:13.139Z" }, - { url = "https://files.pythonhosted.org/packages/7f/20/5826659a81526687c6e5b5507f3f79f4f4b7e3022f3efae2ba36b19864c3/uv-0.9.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b4817642d5ef248b74ca7be3505e5e012a06be050669b80d1f7ced5ad50d188", size = 23171564, upload-time = "2025-12-30T16:12:22.219Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8d/404c54e019bb99ce474dc21e6b96c8a1351ba3c06e5e19fd8dcae0ba1899/uv-0.9.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4fb42237fa309d79905fb73f653f63c1fe45a51193411c614b13512cf5506df3", size = 22202400, upload-time = "2025-12-30T16:12:04.612Z" }, - { url = "https://files.pythonhosted.org/packages/1a/f0/aa3d0081a2004050564364a1ef3277ddf889c9989a7278c0a9cce8284926/uv-0.9.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d22f0ac03635d661e811c69d7c0b292751f90699acc6a1fb1509e17c936474", size = 22206448, upload-time = "2025-12-30T16:12:30.626Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a9/7a375e723a588f31f305ddf9ae2097af0b9dc7f7813641788b5b9764a237/uv-0.9.21-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:cdd805909d360ad67640201376c8eb02de08dcf1680a1a81aebd9519daed6023", size = 20940568, upload-time = "2025-12-30T16:12:27.533Z" }, - { url = "https://files.pythonhosted.org/packages/18/d5/6187ffb7e1d24df34defe2718db8c4c3c08f153d3e7da22c250134b79cd1/uv-0.9.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82e438595a609cbe4e45c413a54bd5756d37c8c39108ce7b2799aff15f7d3337", size = 22085077, upload-time = "2025-12-30T16:12:10.153Z" }, - { url = "https://files.pythonhosted.org/packages/ee/fa/8e211167d0690d9f15a08da610a0383d2f43a6c838890878e14948472284/uv-0.9.21-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:fc1c06e1e5df423e1517e350ea2c9d85ecefd0919188a0a9f19bd239bbbdeeaf", size = 20862893, upload-time = "2025-12-30T16:12:49.87Z" }, - { url = "https://files.pythonhosted.org/packages/33/b2/9d24d84cb9a1a6a5ea98d03a29abf800d87e5710d25e53896dc73aeb63a5/uv-0.9.21-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9ef3d2a213c7720f4dae336e5123fe88427200d7523c78091c4ab7f849c3f13f", size = 21428397, upload-time = "2025-12-30T16:12:07.483Z" }, - { url = "https://files.pythonhosted.org/packages/4f/40/1e8e4c2e1308432c708eaa66dccdb83d2ee6120ea2b7d65e04fc06f48ff8/uv-0.9.21-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8da20914d92ba4cc35f071414d3da7365294fc0b7114da8ac2ab3a86c695096f", size = 22450537, upload-time = "2025-12-30T16:12:33.36Z" }, - { url = "https://files.pythonhosted.org/packages/18/b8/99c4731d001f512e844dfdc740db2bf2fea56d538749b639d21f5117a74a/uv-0.9.21-py3-none-win32.whl", hash = "sha256:e716e23bc0ec8cbb0811f99e653745e0cf15223e7ba5d8857d46be5b40b3045b", size = 20032654, upload-time = "2025-12-30T16:12:36.007Z" }, - { url = "https://files.pythonhosted.org/packages/29/6b/da441bf335f5e1c0c100b7dfb9702b6fed367ba703e543037bf1e70bf8c3/uv-0.9.21-py3-none-win_amd64.whl", hash = "sha256:64a7bb0e4e6a4c2d98c2d55f42aead7c2df0ceb17d5911d1a42b76228cab4525", size = 22206744, upload-time = "2025-12-30T16:12:38.953Z" }, - { url = "https://files.pythonhosted.org/packages/98/02/afbed8309fe07aaa9fa58a98941cebffbcd300fe70499a02a6806d93517b/uv-0.9.21-py3-none-win_arm64.whl", hash = "sha256:6c13c40966812f6bd6ecb6546e5d3e27e7fe9cefa07018f074f51d703cb29e1c", size = 20591604, upload-time = "2025-12-30T16:12:44.634Z" }, +version = "0.9.24" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/7f/6692596de7775b3059a55539aed2eec16a0642a2d6d3510baa5878287ce4/uv-0.9.24.tar.gz", hash = "sha256:d59d31c25fc530c68db9164174efac511a25fc882cec49cd48f75a18e7ebd6d5", size = 3852673, upload-time = "2026-01-09T22:34:31.635Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/51/10bb9541c40a5b4672527c357997a30fdf38b75e7bbaad0c37ed70889efa/uv-0.9.24-py3-none-linux_armv6l.whl", hash = "sha256:75a000f529ec92235b10fb5e16ca41f23f46c643308fd6c5b0d7b73ca056c5b9", size = 21395664, upload-time = "2026-01-09T22:34:05.887Z" }, + { url = "https://files.pythonhosted.org/packages/ec/dd/d7df524cb764ebc652e0c8bf9abe55fc34391adc2e4ab1d47375222b38a9/uv-0.9.24-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:207c8a2d4c4d55589feb63b4be74f6ff6ab92fa81b14a6515007ccec5a868ae0", size = 20547988, upload-time = "2026-01-09T22:34:16.21Z" }, + { url = "https://files.pythonhosted.org/packages/49/e4/7ca5e7eaed4b2b9d407aa5aeeb8f71cace7db77f30a63139bbbfdfe4770c/uv-0.9.24-py3-none-macosx_11_0_arm64.whl", hash = "sha256:44c0b8a78724e4cfa8e9c0266023c70fc792d0b39a5da17f5f847af2b530796b", size = 19033208, upload-time = "2026-01-09T22:33:50.91Z" }, + { url = "https://files.pythonhosted.org/packages/27/05/b7bab99541056537747bfdc55fdc97a4ba998e2b53cf855411ef176c412b/uv-0.9.24-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:841ede01d6dcf1676a21dce05f3647ba171c1d92768a03e8b8b6b7354b34a6d2", size = 20872212, upload-time = "2026-01-09T22:33:58.007Z" }, + { url = "https://files.pythonhosted.org/packages/d3/93/3a69cf481175766ee6018afb281666de12ccc04367d20a41dc070be8b422/uv-0.9.24-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:69531d9a8772afb2dff68fef2469f666e4f8a0132b2109e36541c423415835da", size = 21017966, upload-time = "2026-01-09T22:34:29.354Z" }, + { url = "https://files.pythonhosted.org/packages/17/40/7aec2d428e57a3ec992efc49bbc71e4a0ceece5a726751c661ddc3f41315/uv-0.9.24-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6720c9939cca7daff3cccc35dd896bbe139d7d463c62cba8dbbc474ff8eb93d1", size = 21943358, upload-time = "2026-01-09T22:34:08.63Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f4/2aa5b275aa8e5edb659036e94bae13ae294377384cf2a93a8d742a38050f/uv-0.9.24-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d7d1333d9c21088c89cb284ef29fdf48dc2015fe993174a823a3e7c991db90f9", size = 23672949, upload-time = "2026-01-09T22:34:03.113Z" }, + { url = "https://files.pythonhosted.org/packages/8e/24/2589bed4b39394c799472f841e0580318a8b7e69ef103a0ab50cf1c39dff/uv-0.9.24-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b610d89d6025000d08cd9bd458c6e264003a0ecfdaa8e4eba28938130cd1837", size = 23270210, upload-time = "2026-01-09T22:34:13.94Z" }, + { url = "https://files.pythonhosted.org/packages/80/3a/034494492a1ad1f95371c6fd735e4b7d180b8c1712c88b0f32a34d6352fd/uv-0.9.24-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:38c59e18fe5fa42f7baeb4f08c94914cee6d87ff8faa6fc95c994dbc0de26c90", size = 22282247, upload-time = "2026-01-09T22:33:53.362Z" }, + { url = "https://files.pythonhosted.org/packages/be/0e/d8ab2c4fa6c9410a8a37fa6608d460b0126cee2efed9eecf516cdec72a1a/uv-0.9.24-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009cc82cdfc48add6ec13a0c4ffbb788ae2cab53573b4218069ca626721a404b", size = 22348801, upload-time = "2026-01-09T22:34:00.46Z" }, + { url = "https://files.pythonhosted.org/packages/50/fa/7217764e4936d6fda1944d956452bf94f790ae8a02cb3e5aa496d23fcb25/uv-0.9.24-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1914d33e526167dc202ec4a59119c68467b31f7c71dcf8b1077571d091ca3e7c", size = 21000825, upload-time = "2026-01-09T22:34:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/94/8f/533db58a36895142b0c11eedf8bfe11c4724fb37deaa417bfb0c689d40b8/uv-0.9.24-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:aafe7dd9b633672054cf27f1a8e4127506324631f1af5edd051728f4f8085351", size = 22149066, upload-time = "2026-01-09T22:33:45.722Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c7/e6eccd96341a548f0405bffdf55e7f30b5c0757cd1b8f7578e0972a66002/uv-0.9.24-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:63a0a46693098cf8446e41bd5d9ce7d3bc9b775a63fe0c8405ab6ee328424d46", size = 20993489, upload-time = "2026-01-09T22:34:27.007Z" }, + { url = "https://files.pythonhosted.org/packages/46/07/32d852d2d40c003b52601c44202c9d9e655c485fae5d84e42f326814b0be/uv-0.9.24-py3-none-musllinux_1_1_i686.whl", hash = "sha256:15d3955bfb03a7b78aaf5afb639cedefdf0fc35ff844c92e3fe6e8700b94b84f", size = 21400775, upload-time = "2026-01-09T22:34:24.278Z" }, + { url = "https://files.pythonhosted.org/packages/b0/58/f8e94226126011ba2e2e9d59c6190dc7fe9e61fa7ef4ca720d7226c1482b/uv-0.9.24-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:488a07e59fb417bf86de5630197223b7a0223229e626afc124c26827db78cff8", size = 22554194, upload-time = "2026-01-09T22:34:18.504Z" }, + { url = "https://files.pythonhosted.org/packages/da/8e/b540c304039a6561ba8e9a673009cfe1451f989d2269fe40690901ddb233/uv-0.9.24-py3-none-win32.whl", hash = "sha256:68a3186074c03876ee06b68730d5ff69a430296760d917ebbbb8e3fb54fb4091", size = 20203184, upload-time = "2026-01-09T22:34:11.02Z" }, + { url = "https://files.pythonhosted.org/packages/16/59/dba7c5feec1f694183578435eaae0d759b8c459c5e4f91237a166841a116/uv-0.9.24-py3-none-win_amd64.whl", hash = "sha256:8cd626306b415491f839b1a9100da6795c82c44d4cf278dd7ace7a774af89df4", size = 22294050, upload-time = "2026-01-09T22:33:48.228Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ef/e58fb288bafb5a8b5d4994e73fa6e062e408680e5a20d0427d5f4f66d8b1/uv-0.9.24-py3-none-win_arm64.whl", hash = "sha256:8d3c0fec7aa17f936a5b258816e856647b21f978a81bcfb2dc8caf2892a4965e", size = 20620004, upload-time = "2026-01-09T22:33:55.62Z" }, ] [[package]] @@ -3111,16 +3110,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.35.4" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]] @@ -3212,56 +3211,73 @@ wheels = [ [[package]] name = "websockets" -version = "15.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, - { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, - { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, - { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, - { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] [[package]] name = "werkzeug" -version = "3.1.4" +version = "3.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, ] [[package]]