diff --git a/.github/workflows/shared.yml b/.github/workflows/shared.yml index f92725ce05..16ba250cde 100644 --- a/.github/workflows/shared.yml +++ b/.github/workflows/shared.yml @@ -35,7 +35,7 @@ jobs: continue-on-error: true strategy: matrix: - python-version: ["3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] dep-resolution: - name: lowest-direct install-flags: "--upgrade --resolution lowest-direct" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 379c47c125..2312c9465c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,7 @@ When bumping a dependency version manually, update the constraint in `pyproject. Security-relevant dependency updates (P0) are applied within 7 days of public disclosure and backported to active release branches. -The SDK currently supports Python 3.10 through 3.13. New CPython releases are supported within one minor SDK release of their stable release date. +The SDK currently supports Python 3.10 through 3.14. New CPython releases are supported within one minor SDK release of their stable release date. ## Triage Process diff --git a/README.md b/README.md index f6f66fd8de..9eaef01af8 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ +> **This documents v1.x, the stable release line of the MCP Python SDK. v2 is in alpha.** +> +> v2 pre-releases are published to PyPI as `2.0.0aN`. Installers never select a pre-release unless you opt in (for example `pip install mcp==2.0.0a1`), so v1.x users are unaffected. **If your package depends on `mcp`, add a `<2` upper bound to your version constraint (for example `mcp>=1.27,<2`) before the stable v2 release lands.** See the [v2 documentation](https://github.com/modelcontextprotocol/python-sdk/blob/main/README.v2.md) and the [migration guide](https://github.com/modelcontextprotocol/python-sdk/blob/main/docs/migration.md) for what's changing. +> +> v1.x remains recommended for production use. It is in maintenance mode and continues to receive critical bug fixes and security patches. + ## Table of Contents diff --git a/docs/experimental/index.md b/docs/experimental/index.md index 1d496b3f10..3b7f113ad7 100644 --- a/docs/experimental/index.md +++ b/docs/experimental/index.md @@ -1,12 +1,13 @@ # Experimental Features -!!! warning "Experimental APIs" +!!! warning "Deprecated" - The features in this section are experimental and may change without notice. - They track the evolving MCP specification and are not yet stable. + The experimental tasks API is deprecated and will be removed in mcp 2.0. + Tasks (SEP-1686) were removed from the MCP specification and are expected + to return as a separate MCP extension in a future release. This section documents experimental features in the MCP Python SDK. These features -implement draft specifications that are still being refined. +are deprecated and remain available on the 1.x line only for existing users. ## Available Experimental Features @@ -36,8 +37,10 @@ async def handle_get_task(request: GetTaskRequest) -> GetTaskResult: result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"}) ``` +Accessing the `.experimental` properties emits a `DeprecationWarning`. + ## Providing Feedback -Since these features are experimental, feedback is especially valuable. If you encounter -issues or have suggestions, please open an issue on the +If you rely on these features and have feedback on their deprecation or the planned +MCP extension, please open an issue on the [python-sdk repository](https://github.com/modelcontextprotocol/python-sdk/issues). diff --git a/docs/experimental/tasks-client.md b/docs/experimental/tasks-client.md index cfd23e4e14..acc8392026 100644 --- a/docs/experimental/tasks-client.md +++ b/docs/experimental/tasks-client.md @@ -1,8 +1,10 @@ # Client Task Usage -!!! warning "Experimental" +!!! warning "Deprecated" - Tasks are an experimental feature. The API may change without notice. + The experimental tasks API is deprecated and will be removed in mcp 2.0. + Tasks (SEP-1686) were removed from the MCP specification and are expected + to return as a separate MCP extension in a future release. This guide covers calling task-augmented tools from clients, handling the `input_required` status, and advanced patterns like receiving task requests from servers. diff --git a/docs/experimental/tasks-server.md b/docs/experimental/tasks-server.md index c6b94814cd..1760e197be 100644 --- a/docs/experimental/tasks-server.md +++ b/docs/experimental/tasks-server.md @@ -1,8 +1,10 @@ # Server Task Implementation -!!! warning "Experimental" +!!! warning "Deprecated" - Tasks are an experimental feature. The API may change without notice. + The experimental tasks API is deprecated and will be removed in mcp 2.0. + Tasks (SEP-1686) were removed from the MCP specification and are expected + to return as a separate MCP extension in a future release. This guide covers implementing task support in MCP servers, from basic setup to advanced patterns like elicitation and sampling within tasks. diff --git a/docs/experimental/tasks.md b/docs/experimental/tasks.md index 2d4d06a025..e5dd78fb20 100644 --- a/docs/experimental/tasks.md +++ b/docs/experimental/tasks.md @@ -1,9 +1,10 @@ # Tasks -!!! warning "Experimental" +!!! warning "Deprecated" - Tasks are an experimental feature tracking the draft MCP specification. - The API may change without notice. + The experimental tasks API is deprecated and will be removed in mcp 2.0. + Tasks (SEP-1686) were removed from the MCP specification and are expected + to return as a separate MCP extension in a future release. Tasks enable asynchronous request handling in MCP. Instead of blocking until an operation completes, the receiver creates a task, returns immediately, and the requestor polls for the result. diff --git a/pyproject.toml b/pyproject.toml index 6c88c8e789..769ddcf709 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,19 +20,23 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] dependencies = [ "anyio>=4.5", "httpx>=0.27.1,<1.0.0", "httpx-sse>=0.4", - "pydantic>=2.11.0,<3.0.0", - "starlette>=0.27", + "pydantic>=2.12.0,<3.0.0; python_version >= '3.14'", + "pydantic>=2.11.0,<3.0.0; python_version < '3.14'", + "starlette>=0.48.0; python_version >= '3.14'", + "starlette>=0.27; python_version < '3.14'", "python-multipart>=0.0.9", "sse-starlette>=1.6.1", "pydantic-settings>=2.5.2", "uvicorn>=0.31.1; sys_platform != 'emscripten'", "jsonschema>=4.20.0", - "pywin32>=310; sys_platform == 'win32'", + "pywin32>=311; sys_platform == 'win32' and python_version >= '3.14'", + "pywin32>=310; sys_platform == 'win32' and python_version < '3.14'", "pyjwt[crypto]>=2.10.1", "typing-extensions>=4.9.0", "typing-inspection>=0.4.1", @@ -67,7 +71,7 @@ dev = [ docs = [ "mkdocs>=1.6.1", "mkdocs-glightbox>=0.4.0", - "mkdocs-material[imaging]>=9.5.45", + "mkdocs-material[imaging]>=9.6.19", "mkdocstrings-python>=1.12.2", ] @@ -103,7 +107,13 @@ venv = ".venv" # those private functions instead of testing the private functions directly. It makes it easier to maintain the code source # and refactor code that is not public. executionEnvironments = [ + # The experimental tasks API and the WebSocket transport are deprecated; the suites and + # examples that cover them intentionally use the deprecated APIs. + { root = "tests/experimental", extraPaths = ["."], reportUnusedFunction = false, reportPrivateUsage = false, reportDeprecated = false }, + { root = "tests/shared/test_ws.py", extraPaths = ["."], reportUnusedFunction = false, reportPrivateUsage = false, reportDeprecated = false }, { root = "tests", extraPaths = ["."], reportUnusedFunction = false, reportPrivateUsage = false }, + { root = "examples/servers/simple-task", reportUnusedFunction = false, reportDeprecated = false }, + { root = "examples/servers/simple-task-interactive", reportUnusedFunction = false, reportDeprecated = false }, { root = "examples/servers", reportUnusedFunction = false }, { root = "examples/snippets/clients/logging_client.py", reportUndefinedVariable = false, reportUnknownArgumentType = false }, { root = "examples/snippets/clients/roots_example.py", reportUndefinedVariable = false, reportUnknownArgumentType = false, reportArgumentType = false }, diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index 8519f15cec..86f2676dcb 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -1,4 +1,5 @@ import logging +import warnings from datetime import timedelta from typing import Any, Protocol, overload @@ -17,6 +18,15 @@ DEFAULT_CLIENT_INFO = types.Implementation(name="mcp", version="0.1.0") +# Type checkers only surface `@deprecated` messages given as string literals, so the same text is +# repeated inline at each deprecated entry point (here, server/session.py, server/lowlevel/server.py). +# Keep those copies, the filterwarnings marks in the tasks test suites, and the pytest.warns match in +# tests/experimental/tasks/test_deprecations.py prefix-aligned when rewording. +_EXPERIMENTAL_TASKS_DEPRECATION = ( + "The experimental tasks API is deprecated and will be removed in mcp 2.0: tasks (SEP-1686) were removed" + " from the MCP specification and are expected to return as a separate MCP extension." +) + logger = logging.getLogger("client") @@ -143,6 +153,8 @@ def __init__( self._experimental_features: ExperimentalClientFeatures | None = None # Experimental: Task handlers (use defaults if not provided) + if experimental_task_handlers is not None: + warnings.warn(_EXPERIMENTAL_TASKS_DEPRECATION, DeprecationWarning, stacklevel=2) self._task_handlers = experimental_task_handlers or ExperimentalTaskHandlers() async def initialize(self) -> types.InitializeResult: @@ -204,10 +216,16 @@ def get_server_capabilities(self) -> types.ServerCapabilities | None: return self._server_capabilities @property + @deprecated( + "The experimental tasks API is deprecated and will be removed in mcp 2.0: tasks (SEP-1686) were removed" + " from the MCP specification and are expected to return as a separate MCP extension." + ) def experimental(self) -> ExperimentalClientFeatures: """Experimental APIs for tasks and other features. - WARNING: These APIs are experimental and may change without notice. + Deprecated: the experimental tasks API will be removed in mcp 2.0. Tasks + (SEP-1686) were removed from the MCP specification and are expected to + return as a separate MCP extension. Example: status = await session.experimental.get_task(task_id) diff --git a/src/mcp/client/websocket.py b/src/mcp/client/websocket.py index e8c8d9af87..a0e1b98801 100644 --- a/src/mcp/client/websocket.py +++ b/src/mcp/client/websocket.py @@ -6,6 +6,7 @@ import anyio from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from pydantic import ValidationError +from typing_extensions import deprecated from websockets.asyncio.client import connect as ws_connect from websockets.typing import Subprotocol @@ -15,6 +16,10 @@ logger = logging.getLogger(__name__) +@deprecated( + "The WebSocket client transport is deprecated and will be removed in mcp 2.0. WebSocket was never part of" + " the MCP specification; use the streamable HTTP transport (`streamable_http_client`) instead." +) @asynccontextmanager async def websocket_client( url: str, @@ -25,6 +30,9 @@ async def websocket_client( """ WebSocket client transport for MCP, symmetrical to the server version. + Deprecated: this transport will be removed in mcp 2.0. WebSocket was never + part of the MCP specification; use the streamable HTTP transport instead. + Connects to 'url' using the 'mcp' subprotocol, then yields: (read_stream, write_stream) diff --git a/src/mcp/server/experimental/request_context.py b/src/mcp/server/experimental/request_context.py index 0d69836355..9caad02074 100644 --- a/src/mcp/server/experimental/request_context.py +++ b/src/mcp/server/experimental/request_context.py @@ -252,7 +252,8 @@ async def work(task: ServerTaskContext) -> CallToolResult: task_group = support.task_group if task_id is None: - session_scope = self._session.experimental.task_session_scope + features = self._session._experimental # pyright: ignore[reportPrivateUsage] + session_scope = features.task_session_scope if session_scope is not None: task_id = scoped_task_id(session_scope) diff --git a/src/mcp/server/experimental/task_context.py b/src/mcp/server/experimental/task_context.py index e6e14fc938..c9af82be41 100644 --- a/src/mcp/server/experimental/task_context.py +++ b/src/mcp/server/experimental/task_context.py @@ -488,12 +488,13 @@ async def elicit_as_task( create_result = CreateTaskResult.model_validate(response_data) client_task_id = create_result.task.taskId - # Poll the client's task using session.experimental - async for _ in self._session.experimental.poll_task(client_task_id): + # Poll the client's task using the session's experimental features + features = self._session._experimental # pyright: ignore[reportPrivateUsage] + async for _ in features.poll_task(client_task_id): pass # Get final result from client - result = await self._session.experimental.get_task_result( + result = await features.get_task_result( client_task_id, ElicitResult, ) @@ -594,12 +595,13 @@ async def create_message_as_task( create_result = CreateTaskResult.model_validate(response_data) client_task_id = create_result.task.taskId - # Poll the client's task using session.experimental - async for _ in self._session.experimental.poll_task(client_task_id): + # Poll the client's task using the session's experimental features + features = self._session._experimental # pyright: ignore[reportPrivateUsage] + async for _ in features.poll_task(client_task_id): pass # Get final result from client - result = await self._session.experimental.get_task_result( + result = await features.get_task_result( client_task_id, CreateMessageResult, ) diff --git a/src/mcp/server/experimental/task_result_handler.py b/src/mcp/server/experimental/task_result_handler.py index 1cf7f69749..09f2a0a4e9 100644 --- a/src/mcp/server/experimental/task_result_handler.py +++ b/src/mcp/server/experimental/task_result_handler.py @@ -129,9 +129,15 @@ async def handle( # The stored result contains the actual payload data # Per spec: tasks/result MUST include _meta with related-task metadata related_task = RelatedTaskMetadata(taskId=task_id) - related_task_meta: dict[str, Any] = {RELATED_TASK_METADATA_KEY: related_task.model_dump(by_alias=True)} + related_task_meta: dict[str, Any] = { + RELATED_TASK_METADATA_KEY: related_task.model_dump( + by_alias=True, + mode="json", + exclude_none=True, + ) + } if result is not None: - result_data = result.model_dump(by_alias=True) + result_data = result.model_dump(by_alias=True, mode="json", exclude_none=True) existing_meta: dict[str, Any] = result_data.get("_meta") or {} result_data["_meta"] = {**existing_meta, **related_task_meta} return GetTaskPayloadResult.model_validate(result_data) diff --git a/src/mcp/server/experimental/task_support.py b/src/mcp/server/experimental/task_support.py index 8e91faf73b..80f4e1cf86 100644 --- a/src/mcp/server/experimental/task_support.py +++ b/src/mcp/server/experimental/task_support.py @@ -106,8 +106,10 @@ def configure_session(self, session: ServerSession, *, stateless: bool = False) stateless: Whether the session belongs to a stateless server run """ session.add_response_router(self.handler) - if not stateless and session.experimental.task_session_scope is None: - session.experimental.task_session_scope = new_session_scope() + if not stateless: + features = session._experimental # pyright: ignore[reportPrivateUsage] + if features.task_session_scope is None: + features.task_session_scope = new_session_scope() @classmethod def in_memory(cls) -> "TaskSupport": diff --git a/src/mcp/server/fastmcp/utilities/context_injection.py b/src/mcp/server/fastmcp/utilities/context_injection.py index 66d0cbaa0c..45e0aea755 100644 --- a/src/mcp/server/fastmcp/utilities/context_injection.py +++ b/src/mcp/server/fastmcp/utilities/context_injection.py @@ -25,7 +25,7 @@ def find_context_parameter(fn: Callable[..., Any]) -> str | None: # Get type hints to properly resolve string annotations try: hints = typing.get_type_hints(fn) - except Exception: + except Exception: # pragma: no cover # If we can't resolve type hints, we can't find the context parameter return None diff --git a/src/mcp/server/lowlevel/experimental.py b/src/mcp/server/lowlevel/experimental.py index 737d6bb2cd..546757bf28 100644 --- a/src/mcp/server/lowlevel/experimental.py +++ b/src/mcp/server/lowlevel/experimental.py @@ -129,7 +129,8 @@ def enable_tasks( def _requestor_session_scope(self) -> str | None: """Return the task session scope of the session making the current request.""" - return self._server.request_context.session.experimental.task_session_scope + session = self._server.request_context.session + return session._experimental.task_session_scope # pyright: ignore[reportPrivateUsage] def _require_task_in_requestor_scope(self, task_id: str) -> None: """Reject task IDs that belong to a different session. diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 7d925de32b..25a8fde37c 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -80,7 +80,7 @@ async def main(): import jsonschema from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from pydantic import AnyUrl -from typing_extensions import TypeVar +from typing_extensions import TypeVar, deprecated import mcp.types as types from mcp.server.experimental.request_context import Experimental @@ -244,10 +244,16 @@ def request_context( return request_ctx.get() @property + @deprecated( + "The experimental tasks API is deprecated and will be removed in mcp 2.0: tasks (SEP-1686) were removed" + " from the MCP specification and are expected to return as a separate MCP extension." + ) def experimental(self) -> ExperimentalHandlers: """Experimental APIs for tasks and other features. - WARNING: These APIs are experimental and may change without notice. + Deprecated: the experimental tasks API will be removed in mcp 2.0. Tasks + (SEP-1686) were removed from the MCP specification and are expected to + return as a separate MCP extension. """ # We create this inline so we only add these capabilities _if_ they're actually used diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index 8f0baa3e9c..f80971b012 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -44,6 +44,7 @@ async def handle_list_prompts(ctx: RequestContext) -> list[types.Prompt]: import anyio.lowlevel from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from pydantic import AnyUrl +from typing_extensions import deprecated import mcp.types as types from mcp.server.experimental.session_features import ExperimentalServerSessionFeatures @@ -108,14 +109,25 @@ def client_params(self) -> types.InitializeRequestParams | None: return self._client_params # pragma: no cover @property + def _experimental(self) -> ExperimentalServerSessionFeatures: + """Internal accessor for experimental features that skips the deprecation warning.""" + if self._experimental_features is None: + self._experimental_features = ExperimentalServerSessionFeatures(self) + return self._experimental_features + + @property + @deprecated( + "The experimental tasks API is deprecated and will be removed in mcp 2.0: tasks (SEP-1686) were removed" + " from the MCP specification and are expected to return as a separate MCP extension." + ) def experimental(self) -> ExperimentalServerSessionFeatures: """Experimental APIs for server→client task operations. - WARNING: These APIs are experimental and may change without notice. + Deprecated: the experimental tasks API will be removed in mcp 2.0. Tasks + (SEP-1686) were removed from the MCP specification and are expected to + return as a separate MCP extension. """ - if self._experimental_features is None: - self._experimental_features = ExperimentalServerSessionFeatures(self) - return self._experimental_features + return self._experimental def check_client_capability(self, capability: types.ClientCapabilities) -> bool: # pragma: no cover """Check if the client supports a specific capability.""" @@ -685,7 +697,5 @@ async def _handle_incoming(self, req: ServerRequestResponder) -> None: await self._incoming_message_stream_writer.send(req) @property - def incoming_messages( - self, - ) -> MemoryObjectReceiveStream[ServerRequestResponder]: + def incoming_messages(self) -> MemoryObjectReceiveStream[ServerRequestResponder]: return self._incoming_message_stream_reader diff --git a/src/mcp/server/websocket.py b/src/mcp/server/websocket.py index 5d5efd16e9..2b21604a76 100644 --- a/src/mcp/server/websocket.py +++ b/src/mcp/server/websocket.py @@ -6,6 +6,7 @@ from pydantic_core import ValidationError from starlette.types import Receive, Scope, Send from starlette.websockets import WebSocket +from typing_extensions import deprecated import mcp.types as types from mcp.shared.message import SessionMessage @@ -13,11 +14,18 @@ logger = logging.getLogger(__name__) -@asynccontextmanager # pragma: no cover +@deprecated( # pragma: no cover + "The WebSocket server transport is deprecated and will be removed in mcp 2.0. WebSocket was never part of" + " the MCP specification; use the streamable HTTP transport instead." +) +@asynccontextmanager async def websocket_server(scope: Scope, receive: Receive, send: Send): """ WebSocket server transport for MCP. This is an ASGI application, suitable to be used with a framework like Starlette and a server like Hypercorn. + + Deprecated: this transport will be removed in mcp 2.0. WebSocket was never + part of the MCP specification; use the streamable HTTP transport instead. """ websocket = WebSocket(scope, receive, send) diff --git a/tests/client/test_stdio.py b/tests/client/test_stdio.py index ba58da7321..987a3ca486 100644 --- a/tests/client/test_stdio.py +++ b/tests/client/test_stdio.py @@ -1,4 +1,5 @@ import errno +import gc import os import shutil import sys @@ -8,9 +9,16 @@ import anyio import pytest +from anyio.abc import Process from mcp.client.session import ClientSession -from mcp.client.stdio import StdioServerParameters, _create_platform_compatible_process, stdio_client +from mcp.client.stdio import ( + StdioServerParameters, + _create_platform_compatible_process, + _terminate_process_tree, + stdio_client, +) +from mcp.os.win32.utilities import FallbackProcess from mcp.shared.exceptions import McpError from mcp.shared.message import SessionMessage from mcp.types import CONNECTION_CLOSED, JSONRPCMessage, JSONRPCRequest, JSONRPCResponse @@ -219,6 +227,79 @@ def sigint_handler(signum, frame): raise +async def _wait_for_first_write(path: str) -> None: + """Poll until the file at *path* exists and has grown beyond its initial empty state. + + The marker files below are created empty before the writer is spawned, so any + growth proves the writing process booted and reached its write loop. Polling + replaces fixed startup sleeps, which flake on loaded machines where interpreter + startup can exceed any fixed window. Bounded so a writer that never starts + fails the test instead of hanging it. + """ + with anyio.fail_after(15): + while not os.path.exists(path) or os.path.getsize(path) == 0: + await anyio.sleep(0.05) + + +async def _wait_for_writes_to_stop(path: str) -> None: + """Poll until the file at *path* stops growing. + + Returns once the size is unchanged across three successive 0.3 second gaps + (each three times the writers' 0.1 second write interval), so a writer that + is merely starved of CPU for a single gap is not mistaken for a terminated + one. Any observed growth resets the consecutive-stable counter. The sentinel + forces at least one non-stable iteration before counting starts. If the file + never stops growing, the timeout fails the test: a writer that survives + _terminate_process_tree is a genuine cleanup failure that must not be masked. + """ + last_size = -1 + stable_pairs = 0 + with anyio.fail_after(15): + while True: + current_size = os.path.getsize(path) + if current_size == last_size: + stable_pairs += 1 + else: + stable_pairs = 0 + last_size = current_size + if stable_pairs == 3: + return + await anyio.sleep(0.3) + + +async def _dispose_process(proc: Process | FallbackProcess) -> None: + """Reap a dead process and close its pipe streams inside the test that spawned it. + + Without this, the subprocess transports stay referenced by the per-test event + loop, become garbage only after that loop closes, and their GC-time + ResourceWarnings fire during a later test on the same worker (on Windows + proactor the warning can itself die in __repr__ on a closed pipe). An in-test + gc.collect() cannot catch that, so the process is reaped and closed + deterministically here. Draining stdout to EOF guarantees the event loop has + observed the pipe closure (anyio's reader aclose alone does not close the + underlying transport), which lets asyncio close the subprocess transport + before the test returns. + + Precondition: the WHOLE process tree must already be confirmed dead. wait() + tolerates an already-exited process and returns promptly, but on the Windows + fallback path it runs popen.wait in a thread and is effectively uncancellable, + and stdout only reaches EOF once every tree member that inherited the pipe + handle is gone. The timeout fails the test rather than hanging it if that + precondition is ever violated. + """ + with anyio.fail_after(15): + await proc.wait() + assert proc.stdin is not None + await proc.stdin.aclose() + assert proc.stdout is not None + while True: + try: + await proc.stdout.receive() + except anyio.EndOfStream: + break + await proc.stdout.aclose() + + class TestChildProcessCleanup: """ Tests for child process cleanup functionality using _terminate_process_tree. @@ -259,84 +340,71 @@ async def test_basic_child_process_cleanup(self): with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: parent_marker = f.name - try: - # Parent script that spawns a child process - parent_script = textwrap.dedent( - f""" - import subprocess - import sys - import time - import os - - # Mark that parent started - with open({escape_path_for_python(parent_marker)}, 'w') as f: - f.write('parent started\\n') - - # Child script that writes continuously - child_script = f''' - import time - with open({escape_path_for_python(marker_file)}, 'a') as f: - while True: - f.write(f"{time.time()}") - f.flush() - time.sleep(0.1) - ''' - - # Start the child process - child = subprocess.Popen([sys.executable, '-c', child_script]) - - # Parent just sleeps + # Parent script that spawns a child process + parent_script = textwrap.dedent( + f""" + import subprocess + import sys + import time + import os + + # Mark that parent started + with open({escape_path_for_python(parent_marker)}, 'w') as f: + f.write('parent started\\n') + + # Child script that writes continuously + child_script = f''' + import time + with open({escape_path_for_python(marker_file)}, 'a') as f: while True: + f.write(f"{time.time()}") + f.flush() time.sleep(0.1) - """ - ) + ''' - print("\nStarting child process termination test...") + # Start the child process + child = subprocess.Popen([sys.executable, '-c', child_script]) - # Start the parent process - proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script]) + # Parent just sleeps + while True: + time.sleep(0.1) + """ + ) - # Wait for processes to start - await anyio.sleep(0.5) + # Start the parent process + proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script]) + tree_killed = False - # Verify parent started - assert os.path.exists(parent_marker), "Parent process didn't start" + try: + # Wait for the parent to start and the child to reach its write loop + await _wait_for_first_write(parent_marker) + assert os.path.getsize(parent_marker) > 0, "Parent process didn't start" - # Verify child is writing - if os.path.exists(marker_file): # pragma: no branch - initial_size = os.path.getsize(marker_file) - await anyio.sleep(0.3) - size_after_wait = os.path.getsize(marker_file) - assert size_after_wait > initial_size, "Child process should be writing" - print(f"Child is writing (file grew from {initial_size} to {size_after_wait} bytes)") + await _wait_for_first_write(marker_file) + assert os.path.getsize(marker_file) > 0, "Child process should be writing" # Terminate using our function - print("Terminating process and children...") - from mcp.client.stdio import _terminate_process_tree - await _terminate_process_tree(proc) + tree_killed = True - # Verify processes stopped - await anyio.sleep(0.5) - if os.path.exists(marker_file): # pragma: no branch - size_after_cleanup = os.path.getsize(marker_file) - await anyio.sleep(0.5) - final_size = os.path.getsize(marker_file) - - print(f"After cleanup: file size {size_after_cleanup} -> {final_size}") - assert final_size == size_after_cleanup, ( - f"Child process still running! File grew by {final_size - size_after_cleanup} bytes" - ) - - print("SUCCESS: Child process was properly terminated") + # Verify the child stopped writing; a survivor times out and fails the test + await _wait_for_writes_to_stop(marker_file) + # Tree is dead: reap and close the process so nothing leaks into later tests + await _dispose_process(proc) finally: + if not tree_killed: # pragma: no cover - cleanup only reached when the test failed mid-flight + await _terminate_process_tree(proc) + await _dispose_process(proc) # Clean up files for f in [marker_file, parent_marker]: try: os.unlink(f) except OSError: # pragma: no cover pass + # Collect subprocess transports now, while this test's warning filters + # are active, so GC-time ResourceWarnings cannot hit a later test + gc.collect() @pytest.mark.anyio @pytest.mark.filterwarnings("ignore::ResourceWarning" if sys.platform == "win32" else "default") @@ -353,88 +421,83 @@ async def test_nested_process_tree(self): with tempfile.NamedTemporaryFile(mode="w", delete=False) as f3: grandchild_file = f3.name - try: - # Simple nested process tree test - # We create parent -> child -> grandchild, each writing to a file - parent_script = textwrap.dedent( - f""" - import subprocess - import sys - import time - import os - - # Child will spawn grandchild and write to child file - child_script = f'''import subprocess - import sys - import time - - # Grandchild just writes to file - grandchild_script = \"\"\"import time - with open({escape_path_for_python(grandchild_file)}, 'a') as f: - while True: - f.write(f"gc {{time.time()}}") - f.flush() - time.sleep(0.1)\"\"\" - - # Spawn grandchild - subprocess.Popen([sys.executable, '-c', grandchild_script]) - - # Child writes to its file - with open({escape_path_for_python(child_file)}, 'a') as f: - while True: - f.write(f"c {time.time()}") - f.flush() - time.sleep(0.1)''' - - # Spawn child process - subprocess.Popen([sys.executable, '-c', child_script]) - - # Parent writes to its file - with open({escape_path_for_python(parent_file)}, 'a') as f: - while True: - f.write(f"p {time.time()}") - f.flush() - time.sleep(0.1) - """ - ) + # Simple nested process tree test + # We create parent -> child -> grandchild, each writing to a file + parent_script = textwrap.dedent( + f""" + import subprocess + import sys + import time + import os + + # Child will spawn grandchild and write to child file + child_script = f'''import subprocess + import sys + import time + + # Grandchild just writes to file + grandchild_script = \"\"\"import time + with open({escape_path_for_python(grandchild_file)}, 'a') as f: + while True: + f.write(f"gc {{time.time()}}") + f.flush() + time.sleep(0.1)\"\"\" - # Start the parent process - proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script]) + # Spawn grandchild + subprocess.Popen([sys.executable, '-c', grandchild_script]) - # Let all processes start - await anyio.sleep(1.0) + # Child writes to its file + with open({escape_path_for_python(child_file)}, 'a') as f: + while True: + f.write(f"c {time.time()}") + f.flush() + time.sleep(0.1)''' - # Verify all are writing - for file_path, name in [(parent_file, "parent"), (child_file, "child"), (grandchild_file, "grandchild")]: - if os.path.exists(file_path): # pragma: no branch - initial_size = os.path.getsize(file_path) - await anyio.sleep(0.3) - new_size = os.path.getsize(file_path) - assert new_size > initial_size, f"{name} process should be writing" + # Spawn child process + subprocess.Popen([sys.executable, '-c', child_script]) - # Terminate the whole tree - from mcp.client.stdio import _terminate_process_tree + # Parent writes to its file + with open({escape_path_for_python(parent_file)}, 'a') as f: + while True: + f.write(f"p {time.time()}") + f.flush() + time.sleep(0.1) + """ + ) - await _terminate_process_tree(proc) + # Start the parent process + proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script]) + tree_killed = False - # Verify all stopped - await anyio.sleep(0.5) + try: + # Wait for every level of the tree to reach its write loop for file_path, name in [(parent_file, "parent"), (child_file, "child"), (grandchild_file, "grandchild")]: - if os.path.exists(file_path): # pragma: no branch - size1 = os.path.getsize(file_path) - await anyio.sleep(0.3) - size2 = os.path.getsize(file_path) - assert size1 == size2, f"{name} still writing after cleanup!" + await _wait_for_first_write(file_path) + assert os.path.getsize(file_path) > 0, f"{name} process should be writing" + + # Terminate the whole tree + await _terminate_process_tree(proc) + tree_killed = True - print("SUCCESS: All processes in tree terminated") + # Verify every level stopped writing; a survivor times out and fails the test + for file_path in (parent_file, child_file, grandchild_file): + await _wait_for_writes_to_stop(file_path) + # Tree is dead: reap and close the process so nothing leaks into later tests + await _dispose_process(proc) finally: + if not tree_killed: # pragma: no cover - cleanup only reached when the test failed mid-flight + await _terminate_process_tree(proc) + await _dispose_process(proc) # Clean up all marker files for f in [parent_file, child_file, grandchild_file]: try: os.unlink(f) except OSError: # pragma: no cover pass + # Collect subprocess transports now, while this test's warning filters + # are active, so GC-time ResourceWarnings cannot hit a later test + gc.collect() @pytest.mark.anyio @pytest.mark.filterwarnings("ignore::ResourceWarning" if sys.platform == "win32" else "default") @@ -448,72 +511,67 @@ async def test_early_parent_exit(self): with tempfile.NamedTemporaryFile(mode="w", delete=False) as f: marker_file = f.name - try: - # Parent that spawns child and waits briefly - parent_script = textwrap.dedent( - f""" - import subprocess - import sys - import time - import signal - - # Child that continues running - child_script = f'''import time - with open({escape_path_for_python(marker_file)}, 'a') as f: - while True: - f.write(f"child {time.time()}") - f.flush() - time.sleep(0.1)''' - - # Start child in same process group - subprocess.Popen([sys.executable, '-c', child_script]) - - # Parent waits a bit then exits on SIGTERM - def handle_term(sig, frame): - sys.exit(0) - - signal.signal(signal.SIGTERM, handle_term) - - # Wait + # Parent that spawns child and waits briefly + parent_script = textwrap.dedent( + f""" + import subprocess + import sys + import time + import signal + + # Child that continues running + child_script = f'''import time + with open({escape_path_for_python(marker_file)}, 'a') as f: while True: - time.sleep(0.1) - """ - ) + f.write(f"child {time.time()}") + f.flush() + time.sleep(0.1)''' - # Start the parent process - proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script]) + # Start child in same process group + subprocess.Popen([sys.executable, '-c', child_script]) - # Let child start writing - await anyio.sleep(0.5) + # Parent waits a bit then exits on SIGTERM + def handle_term(sig, frame): + sys.exit(0) - # Verify child is writing - if os.path.exists(marker_file): # pragma: no cover - size1 = os.path.getsize(marker_file) - await anyio.sleep(0.3) - size2 = os.path.getsize(marker_file) - assert size2 > size1, "Child should be writing" + signal.signal(signal.SIGTERM, handle_term) - # Terminate - this will kill the process group even if parent exits first - from mcp.client.stdio import _terminate_process_tree + # Wait + while True: + time.sleep(0.1) + """ + ) - await _terminate_process_tree(proc) + # Start the parent process + proc = await _create_platform_compatible_process(sys.executable, ["-c", parent_script]) + tree_killed = False + + try: + # Wait for the child to reach its write loop + await _wait_for_first_write(marker_file) + assert os.path.getsize(marker_file) > 0, "Child should be writing" - # Verify child stopped - await anyio.sleep(0.5) - if os.path.exists(marker_file): # pragma: no branch - size3 = os.path.getsize(marker_file) - await anyio.sleep(0.3) - size4 = os.path.getsize(marker_file) - assert size3 == size4, "Child should be terminated" + # Terminate - this will kill the process group even if parent exits first + await _terminate_process_tree(proc) + tree_killed = True - print("SUCCESS: Child terminated even with parent exit during cleanup") + # Verify the child stopped writing; a survivor times out and fails the test + await _wait_for_writes_to_stop(marker_file) + # Tree is dead: reap and close the process so nothing leaks into later tests + await _dispose_process(proc) finally: + if not tree_killed: # pragma: no cover - cleanup only reached when the test failed mid-flight + await _terminate_process_tree(proc) + await _dispose_process(proc) # Clean up marker file try: os.unlink(marker_file) except OSError: # pragma: no cover pass + # Collect subprocess transports now, while this test's warning filters + # are active, so GC-time ResourceWarnings cannot hit a later test + gc.collect() @pytest.mark.anyio diff --git a/tests/experimental/tasks/conftest.py b/tests/experimental/tasks/conftest.py new file mode 100644 index 0000000000..77502190a2 --- /dev/null +++ b/tests/experimental/tasks/conftest.py @@ -0,0 +1,18 @@ +"""Shared configuration for the experimental tasks suite.""" + +from pathlib import Path + +import pytest + +_HERE = Path(__file__).parent + +# The tasks suite intentionally exercises the deprecated experimental tasks API. +_TASKS_DEPRECATION_IGNORE = pytest.mark.filterwarnings( + "ignore:The experimental tasks API is deprecated:DeprecationWarning" +) + + +def pytest_collection_modifyitems(items: list[pytest.Item]) -> None: + for item in items: + if _HERE in item.path.parents: + item.add_marker(_TASKS_DEPRECATION_IGNORE) diff --git a/tests/experimental/tasks/server/test_server.py b/tests/experimental/tasks/server/test_server.py index 64a1dabb04..08099fc507 100644 --- a/tests/experimental/tasks/server/test_server.py +++ b/tests/experimental/tasks/server/test_server.py @@ -312,7 +312,7 @@ async def run_server(): async with anyio.create_task_group() as tg: async def handle_messages(): - async for message in server_session.incoming_messages: + async for message in server_session.incoming_messages: # pragma: no cover await server._handle_message(message, server_session, {}, False) tg.start_soon(handle_messages) @@ -392,7 +392,7 @@ async def run_server(): ) as server_session: async with anyio.create_task_group() as tg: - async def handle_messages(): + async def handle_messages(): # pragma: no cover async for message in server_session.incoming_messages: await server._handle_message(message, server_session, {}, False) diff --git a/tests/experimental/tasks/server/test_task_result_handler.py b/tests/experimental/tasks/server/test_task_result_handler.py index db5b9edc70..411d318ed1 100644 --- a/tests/experimental/tasks/server/test_task_result_handler.py +++ b/tests/experimental/tasks/server/test_task_result_handler.py @@ -67,6 +67,29 @@ async def test_handle_returns_result_for_completed_task( assert "io.modelcontextprotocol/related-task" in response.meta +@pytest.mark.anyio +async def test_handle_omits_none_fields_from_completed_task_payload( + store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler +) -> None: + """Test task result payloads omit optional None fields instead of serializing null.""" + task = await store.create_task(TaskMetadata(ttl=60000), task_id="test-task") + result = CallToolResult(content=[TextContent(type="text", text="Done!")]) + await store.store_result(task.taskId, result) + await store.update_task(task.taskId, status="completed") + + mock_session = Mock() + mock_session.send_message = AsyncMock() + + request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(taskId=task.taskId)) + response = await handler.handle(request, mock_session, "req-1") + + payload = response.model_dump(by_alias=True, mode="json") + assert payload["content"] == [{"type": "text", "text": "Done!"}] + assert "annotations" not in payload["content"][0] + assert "_meta" not in payload["content"][0] + assert "io.modelcontextprotocol/related-task" in payload["_meta"] + + @pytest.mark.anyio async def test_handle_raises_for_nonexistent_task( store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler diff --git a/tests/experimental/tasks/test_deprecations.py b/tests/experimental/tasks/test_deprecations.py new file mode 100644 index 0000000000..1056e1fd94 --- /dev/null +++ b/tests/experimental/tasks/test_deprecations.py @@ -0,0 +1,72 @@ +"""Tests for the deprecation warnings on the experimental tasks entry points.""" + +import warnings + +import pytest + +import mcp.types as types +from mcp.client.experimental.task_handlers import ExperimentalTaskHandlers +from mcp.client.session import ClientSession +from mcp.server.lowlevel import Server +from mcp.server.models import InitializationOptions +from mcp.server.session import ServerSession +from mcp.shared.memory import create_client_server_memory_streams, create_connected_server_and_client_session + +_DEPRECATION_MATCH = "The experimental tasks API is deprecated" + + +@pytest.mark.anyio +async def test_client_session_experimental_property_is_deprecated() -> None: + async with create_client_server_memory_streams() as (client_streams, _): + read_stream, write_stream = client_streams + session = ClientSession(read_stream, write_stream) + with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH): + features = session.experimental + # The cached path warns as well + with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH): + assert session.experimental is features + + +@pytest.mark.anyio +async def test_client_session_experimental_task_handlers_kwarg_is_deprecated() -> None: + async with create_client_server_memory_streams() as (client_streams, _): + read_stream, write_stream = client_streams + with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH): + ClientSession(read_stream, write_stream, experimental_task_handlers=ExperimentalTaskHandlers()) + + +@pytest.mark.anyio +async def test_server_session_experimental_property_is_deprecated() -> None: + init_options = InitializationOptions( + server_name="test-server", + server_version="0.1.0", + capabilities=types.ServerCapabilities(), + ) + async with create_client_server_memory_streams() as (_, server_streams): + read_stream, write_stream = server_streams + async with ServerSession(read_stream, write_stream, init_options) as session: + with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH): + features = session.experimental + # The cached path warns as well. coverage.py misreports the branch arcs of the + # last statement in a nested `async with` body on Python 3.11+. + with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH): # pragma: no branch + assert session.experimental is features + + +def test_lowlevel_server_experimental_property_is_deprecated() -> None: + server: Server = Server("test-server") + with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH): + handlers = server.experimental + # The cached path warns as well + with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH): + assert server.experimental is handlers + + +@pytest.mark.anyio +async def test_plain_session_usage_does_not_warn() -> None: + """Clients and servers that don't touch the tasks API must not see deprecation warnings.""" + server: Server = Server("test-server") + with warnings.catch_warnings(): + warnings.simplefilter("error") + async with create_connected_server_and_client_session(server) as session: + await session.send_ping() diff --git a/tests/experimental/tasks/test_request_context.py b/tests/experimental/tasks/test_request_context.py index e52ec4403a..b204bec4f8 100644 --- a/tests/experimental/tasks/test_request_context.py +++ b/tests/experimental/tasks/test_request_context.py @@ -181,7 +181,7 @@ async def work(task: ServerTaskContext) -> Result: # deprecated argument has been reported. with pytest.raises(RuntimeError, match="Task support not enabled"): # The deliberate use of the deprecated overload is the point of this test. - await exp.run_task(work, task_id="explicitly-chosen") # pyright: ignore[reportDeprecated] + await exp.run_task(work, task_id="explicitly-chosen") @pytest.mark.anyio diff --git a/tests/server/test_lowlevel_input_validation.py b/tests/server/test_lowlevel_input_validation.py index 47cb57232d..0614ad7c46 100644 --- a/tests/server/test_lowlevel_input_validation.py +++ b/tests/server/test_lowlevel_input_validation.py @@ -70,7 +70,7 @@ async def run_server(): async with anyio.create_task_group() as tg: async def handle_messages(): - async for message in server_session.incoming_messages: + async for message in server_session.incoming_messages: # pragma: no cover await server._handle_message(message, server_session, {}, False) tg.start_soon(handle_messages) diff --git a/tests/server/test_lowlevel_output_validation.py b/tests/server/test_lowlevel_output_validation.py index f735445212..b53ddfc826 100644 --- a/tests/server/test_lowlevel_output_validation.py +++ b/tests/server/test_lowlevel_output_validation.py @@ -71,7 +71,7 @@ async def run_server(): async with anyio.create_task_group() as tg: async def handle_messages(): - async for message in server_session.incoming_messages: + async for message in server_session.incoming_messages: # pragma: no cover await server._handle_message(message, server_session, {}, False) tg.start_soon(handle_messages) diff --git a/tests/server/test_lowlevel_tool_annotations.py b/tests/server/test_lowlevel_tool_annotations.py index f812c48777..d968c5f35b 100644 --- a/tests/server/test_lowlevel_tool_annotations.py +++ b/tests/server/test_lowlevel_tool_annotations.py @@ -67,7 +67,7 @@ async def run_server(): async with anyio.create_task_group() as tg: async def handle_messages(): - async for message in server_session.incoming_messages: + async for message in server_session.incoming_messages: # pragma: no cover await server._handle_message(message, server_session, {}, False) tg.start_soon(handle_messages) diff --git a/tests/server/test_session.py b/tests/server/test_session.py index 34f9c6e28e..ba1b44126d 100644 --- a/tests/server/test_session.py +++ b/tests/server/test_session.py @@ -410,11 +410,8 @@ async def test_create_message_tool_result_validation(): # Case 8: empty messages list - skips validation entirely # Covers the `if messages:` branch (line 280->302) - with anyio.move_on_after(0.01): - await session.create_message( - messages=[], - max_tokens=100, - ) + with anyio.move_on_after(0.01): # pragma: no cover + await session.create_message(messages=[], max_tokens=100) @pytest.mark.anyio diff --git a/tests/server/test_sse_security.py b/tests/server/test_sse_security.py index 716a308a53..0978b8a150 100644 --- a/tests/server/test_sse_security.py +++ b/tests/server/test_sse_security.py @@ -409,7 +409,9 @@ async def hold_sse_connection() -> None: scope = _sse_scope("GET", "/sse", creator_user) with anyio.fail_after(5): async with transport.connect_sse(scope, get_receive, get_send) as (read_stream, write_stream): - async with read_stream, write_stream: + async with read_stream, write_stream: # pragma: no branch + # ^ coverage.py misses the ->exit arc on 3.11+ when the body + # is nested inside multiple async with blocks async for _ in read_stream: pass diff --git a/tests/shared/test_session.py b/tests/shared/test_session.py index e609397e5e..f4010141d8 100644 --- a/tests/shared/test_session.py +++ b/tests/shared/test_session.py @@ -124,7 +124,7 @@ async def make_request(client_session: ClientSession): ) # Give cancellation time to process - with anyio.fail_after(1): + with anyio.fail_after(1): # pragma: no cover await ev_cancelled.wait() @@ -176,7 +176,7 @@ async def make_request(client_session: ClientSession): tg.start_soon(mock_server) tg.start_soon(make_request, client_session) - with anyio.fail_after(2): + with anyio.fail_after(2): # pragma: no cover await ev_response_received.wait() assert len(result_holder) == 1 @@ -232,7 +232,7 @@ async def make_request(client_session: ClientSession): tg.start_soon(mock_server) tg.start_soon(make_request, client_session) - with anyio.fail_after(2): + with anyio.fail_after(2): # pragma: no cover await ev_error_received.wait() assert len(error_holder) == 1 @@ -289,7 +289,7 @@ async def make_request(client_session: ClientSession): tg.start_soon(mock_server) tg.start_soon(make_request, client_session) - with anyio.fail_after(2): + with anyio.fail_after(2): # pragma: no cover await ev_timeout.wait() @@ -335,7 +335,7 @@ async def mock_server(): tg.start_soon(make_request, client_session) tg.start_soon(mock_server) - with anyio.fail_after(1): + with anyio.fail_after(1): # pragma: no cover await ev_closed.wait() - with anyio.fail_after(1): + with anyio.fail_after(1): # pragma: no cover await ev_response.wait() diff --git a/tests/shared/test_ws.py b/tests/shared/test_ws.py index f093cb4927..64790e2b08 100644 --- a/tests/shared/test_ws.py +++ b/tests/shared/test_ws.py @@ -10,6 +10,7 @@ from pydantic import AnyUrl from starlette.applications import Starlette from starlette.routing import WebSocketRoute +from starlette.types import Message from starlette.websockets import WebSocket from mcp.client.session import ClientSession @@ -30,6 +31,11 @@ SERVER_NAME = "test_server_for_WS" +# This suite intentionally exercises the deprecated WebSocket transport. +pytestmark = pytest.mark.filterwarnings( + "ignore:The WebSocket (client|server) transport is deprecated:DeprecationWarning" +) + @pytest.fixture def server_port() -> int: @@ -198,3 +204,22 @@ async def test_ws_client_timeout( assert len(result.contents) > 0 assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "Read example" + + +def test_websocket_client_is_deprecated() -> None: + """Creating the websocket_client context manager emits a DeprecationWarning.""" + with pytest.warns(DeprecationWarning, match="The WebSocket client transport is deprecated"): + websocket_client("ws://127.0.0.1:1/ws") + + +def test_websocket_server_is_deprecated() -> None: + """Creating the websocket_server context manager emits a DeprecationWarning.""" + + async def receive() -> Message: + raise NotImplementedError + + async def send(message: Message) -> None: + raise NotImplementedError + + with pytest.warns(DeprecationWarning, match="The WebSocket server transport is deprecated"): + websocket_server({"type": "websocket"}, receive, send) diff --git a/tests/test_examples.py b/tests/test_examples.py index 6f5464e394..51a5c52dbf 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -100,7 +100,7 @@ async def test_desktop(monkeypatch: pytest.MonkeyPatch): assert "/fake/path/file2.txt" in content.text -@pytest.mark.parametrize("example", find_examples("README.md"), ids=str) +@pytest.mark.parametrize("example", list(find_examples("README.md")), ids=str) def test_docs_examples(example: CodeExample, eval_example: EvalExample): ruff_ignore: list[str] = ["F841", "I001", "F821"] # F821: undefined names (snippets lack imports) diff --git a/uv.lock b/uv.lock index 4b01712966..43e6218c0b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,12 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] [manifest] members = [ @@ -771,7 +777,8 @@ dependencies = [ { name = "httpx" }, { name = "httpx-sse" }, { name = "jsonschema" }, - { name = "pydantic" }, + { name = "pydantic", version = "2.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "pydantic", version = "2.13.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "pydantic-settings" }, { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, @@ -779,7 +786,8 @@ dependencies = [ { name = "sse-starlette" }, { name = "starlette" }, { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "typing-inspection", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "typing-inspection", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] @@ -822,15 +830,18 @@ requires-dist = [ { name = "httpx", specifier = ">=0.27.1,<1.0.0" }, { name = "httpx-sse", specifier = ">=0.4" }, { name = "jsonschema", specifier = ">=4.20.0" }, - { name = "pydantic", specifier = ">=2.11.0,<3.0.0" }, + { name = "pydantic", marker = "python_full_version < '3.14'", specifier = ">=2.11.0,<3.0.0" }, + { name = "pydantic", marker = "python_full_version >= '3.14'", specifier = ">=2.12.0,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "pyjwt", extras = ["crypto"], specifier = ">=2.10.1" }, { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" }, { name = "python-multipart", specifier = ">=0.0.9" }, - { name = "pywin32", marker = "sys_platform == 'win32'", specifier = ">=310" }, + { name = "pywin32", marker = "python_full_version < '3.14' and sys_platform == 'win32'", specifier = ">=310" }, + { name = "pywin32", marker = "python_full_version >= '3.14' and sys_platform == 'win32'", specifier = ">=311" }, { name = "rich", marker = "extra == 'rich'", specifier = ">=13.9.4" }, { name = "sse-starlette", specifier = ">=1.6.1" }, - { name = "starlette", specifier = ">=0.27" }, + { name = "starlette", marker = "python_full_version < '3.14'", specifier = ">=0.27" }, + { name = "starlette", marker = "python_full_version >= '3.14'", specifier = ">=0.48.0" }, { name = "typer", marker = "extra == 'cli'", specifier = ">=0.16.0" }, { name = "typing-extensions", specifier = ">=4.9.0" }, { name = "typing-inspection", specifier = ">=0.4.1" }, @@ -856,7 +867,7 @@ dev = [ docs = [ { name = "mkdocs", specifier = ">=1.6.1" }, { name = "mkdocs-glightbox", specifier = ">=0.4.0" }, - { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.5.45" }, + { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.6.19" }, { name = "mkdocstrings-python", specifier = ">=1.12.2" }, ] @@ -935,7 +946,8 @@ dependencies = [ { name = "click" }, { name = "httpx" }, { name = "mcp" }, - { name = "pydantic" }, + { name = "pydantic", version = "2.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "pydantic", version = "2.13.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "pydantic-settings" }, { name = "sse-starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, @@ -1783,23 +1795,50 @@ wheels = [ name = "pydantic" version = "2.11.7" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "python_full_version < '3.14'" }, + { name = "pydantic-core", version = "2.33.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, + { name = "typing-inspection", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, ] +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", +] +dependencies = [ + { name = "annotated-types", marker = "python_full_version >= '3.14'" }, + { name = "pydantic-core", version = "2.46.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, + { name = "typing-inspection", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + [[package]] name = "pydantic-core" version = "2.33.2" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } wheels = [ @@ -1881,14 +1920,136 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, ] +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" }, + { url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" }, + { url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" }, + { url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" }, + { url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + [[package]] name = "pydantic-settings" version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, + { name = "pydantic", version = "2.11.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "pydantic", version = "2.13.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, { name = "python-dotenv" }, - { name = "typing-inspection" }, + { name = "typing-inspection", version = "0.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, + { name = "typing-inspection", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } wheels = [ @@ -2531,14 +2692,34 @@ wheels = [ name = "typing-inspection" version = "0.4.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", +] +dependencies = [ + { name = "typing-extensions", marker = "python_full_version >= '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + [[package]] name = "urllib3" version = "2.5.0"