From 10c5af86cd3cf9f004456b891012a9c244830f01 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 8 Apr 2025 15:57:04 -0700 Subject: [PATCH 01/51] initial --- pyproject.toml | 3 +- src/mcp/server/fastmcp/server.py | 24 ++++- src/mcp/server/message_queue/__init__.py | 15 +++ src/mcp/server/message_queue/base.py | 128 +++++++++++++++++++++++ src/mcp/server/message_queue/redis.py | 117 +++++++++++++++++++++ src/mcp/server/sse.py | 49 ++++++--- 6 files changed, 321 insertions(+), 15 deletions(-) create mode 100644 src/mcp/server/message_queue/__init__.py create mode 100644 src/mcp/server/message_queue/base.py create mode 100644 src/mcp/server/message_queue/redis.py diff --git a/pyproject.toml b/pyproject.toml index 25514cd6b..504ee00b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ rich = ["rich>=13.9.4"] cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"] ws = ["websockets>=15.0.1"] +redis = ["redis>=4.5.0"] [project.scripts] mcp = "mcp.cli:app [cli]" @@ -57,7 +58,7 @@ dev = [ docs = [ "mkdocs>=1.6.1", "mkdocs-glightbox>=0.4.0", - "mkdocs-material[imaging]>=9.5.45", + "mkdocs-material[imaging]>=9.5.18", "mkdocstrings-python>=1.12.2", ] diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index bf0ce880a..0e04dca4e 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -75,6 +75,11 @@ class Settings(BaseSettings, Generic[LifespanResultT]): port: int = 8000 sse_path: str = "/sse" message_path: str = "/messages/" + + # SSE message queue settings + message_queue: Literal["memory", "redis"] = "memory" + redis_url: str = "redis://localhost:6379/0" + redis_prefix: str = "mcp:queue:" # resource settings warn_on_duplicate_resources: bool = True @@ -479,7 +484,24 @@ async def run_sse_async(self) -> None: def sse_app(self) -> Starlette: """Return an instance of the SSE server app.""" - sse = SseServerTransport(self.settings.message_path) + message_queue = None + if self.settings.message_queue == "redis": + try: + from mcp.server.message_queue import RedisMessageQueue + message_queue = RedisMessageQueue( + redis_url=self.settings.redis_url, + prefix=self.settings.redis_prefix + ) + logger.info(f"Using Redis message queue at {self.settings.redis_url}") + except ImportError: + logger.error("Redis message queue requested but 'redis' package not installed. ") + raise + else: + from mcp.server.message_queue import InMemoryMessageQueue + message_queue = InMemoryMessageQueue() + logger.info("Using in-memory message queue") + + sse = SseServerTransport(self.settings.message_path, message_queue=message_queue) async def handle_sse(request: Request) -> None: async with sse.connect_sse( diff --git a/src/mcp/server/message_queue/__init__.py b/src/mcp/server/message_queue/__init__.py new file mode 100644 index 000000000..033e7490d --- /dev/null +++ b/src/mcp/server/message_queue/__init__.py @@ -0,0 +1,15 @@ +""" +Message Queue Module for MCP Server + +This module implements queue interfaces for handling messages between clients and servers. +""" + +from mcp.server.message_queue.base import InMemoryMessageQueue, MessageQueue + +# Try to import Redis implementation if available +try: + from mcp.server.message_queue.redis import RedisMessageQueue +except ImportError: + RedisMessageQueue = None + +__all__ = ["MessageQueue", "InMemoryMessageQueue", "RedisMessageQueue"] \ No newline at end of file diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py new file mode 100644 index 000000000..9cfe13544 --- /dev/null +++ b/src/mcp/server/message_queue/base.py @@ -0,0 +1,128 @@ +""" +Base Message Queue Protocol and In-Memory Implementation + +This module defines the message queue protocol and provides a default in-memory implementation. +""" + +import logging +from typing import Protocol, runtime_checkable +from uuid import UUID + +import mcp.types as types + +logger = logging.getLogger(__name__) + + +@runtime_checkable +class MessageQueue(Protocol): + """Abstract interface for an SSE message queue. + + This interface allows messages to be queued and processed by any SSE server instance, + enabling multiple servers to handle requests for the same session. + """ + + async def add_message(self, session_id: UUID, message: types.JSONRPCMessage | Exception) -> bool: + """Add a message to the queue for the specified session. + + Args: + session_id: The UUID of the session this message is for + message: The message to queue + + Returns: + bool: True if message was accepted, False if session not found + """ + ... + + async def get_message(self, session_id: UUID, timeout: float = 0.1) -> types.JSONRPCMessage | Exception | None: + """Get the next message for the specified session. + + Args: + session_id: The UUID of the session to get messages for + timeout: Maximum time to wait for a message, in seconds + + Returns: + The next message or None if no message is available + """ + ... + + async def register_session(self, session_id: UUID) -> None: + """Register a new session with the queue. + + Args: + session_id: The UUID of the new session to register + """ + ... + + async def unregister_session(self, session_id: UUID) -> None: + """Unregister a session when it's closed. + + Args: + session_id: The UUID of the session to unregister + """ + ... + + async def session_exists(self, session_id: UUID) -> bool: + """Check if a session exists. + + Args: + session_id: The UUID of the session to check + + Returns: + bool: True if the session is active, False otherwise + """ + ... + + +class InMemoryMessageQueue: + """Default in-memory implementation of the MessageQueue interface. + + This implementation keeps messages in memory for each session until they're retrieved. + """ + + def __init__(self) -> None: + self._message_queues: dict[UUID, list[types.JSONRPCMessage | Exception]] = {} + self._active_sessions: set[UUID] = set() + + async def add_message(self, session_id: UUID, message: types.JSONRPCMessage | Exception) -> bool: + """Add a message to the queue for the specified session.""" + if session_id not in self._active_sessions: + logger.warning(f"Message received for unknown session {session_id}") + return False + + if session_id not in self._message_queues: + self._message_queues[session_id] = [] + + self._message_queues[session_id].append(message) + logger.debug(f"Added message to queue for session {session_id}") + return True + + async def get_message(self, session_id: UUID, timeout: float = 0.1) -> types.JSONRPCMessage | Exception | None: + """Get the next message for the specified session.""" + if session_id not in self._active_sessions: + return None + + queue = self._message_queues.get(session_id, []) + if not queue: + return None + + message = queue.pop(0) + if not queue: # Clean up empty queue + del self._message_queues[session_id] + + return message + + async def register_session(self, session_id: UUID) -> None: + """Register a new session with the queue.""" + self._active_sessions.add(session_id) + logger.debug(f"Registered session {session_id}") + + async def unregister_session(self, session_id: UUID) -> None: + """Unregister a session when it's closed.""" + self._active_sessions.discard(session_id) + if session_id in self._message_queues: + del self._message_queues[session_id] + logger.debug(f"Unregistered session {session_id}") + + async def session_exists(self, session_id: UUID) -> bool: + """Check if a session exists.""" + return session_id in self._active_sessions \ No newline at end of file diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py new file mode 100644 index 000000000..66255d2a4 --- /dev/null +++ b/src/mcp/server/message_queue/redis.py @@ -0,0 +1,117 @@ +""" +Redis Message Queue Module for MCP Server + +This module implements a Redis-backed message queue for handling messages between clients and servers. +""" + +import json +import logging +from uuid import UUID + +import mcp.types as types + +try: + import redis.asyncio as redis +except ImportError: + raise ImportError( + "Redis support requires the 'redis' package. " + "Install it with: 'uv add redis' or 'uv add \"mcp[redis]\"'" + ) + +logger = logging.getLogger(__name__) + + +class RedisMessageQueue: + """Redis implementation of the MessageQueue interface. + + This implementation uses Redis lists to store messages for each session. + Redis provides persistence and allows multiple servers to share the same queue. + """ + + def __init__(self, redis_url: str = "redis://localhost:6379/0", prefix: str = "mcp:queue:") -> None: + """Initialize Redis message queue. + + Args: + redis_url: Redis connection string + prefix: Key prefix for Redis keys to avoid collisions + """ + self._redis = redis.Redis.from_url(redis_url, decode_responses=True) + self._prefix = prefix + self._active_sessions_key = f"{prefix}active_sessions" + logger.debug(f"Initialized Redis message queue with URL: {redis_url}") + + def _session_queue_key(self, session_id: UUID) -> str: + """Get the Redis key for a session's message queue.""" + return f"{self._prefix}session:{session_id.hex}" + + async def add_message(self, session_id: UUID, message: types.JSONRPCMessage | Exception) -> bool: + """Add a message to the queue for the specified session.""" + # Check if session exists + if not await self.session_exists(session_id): + logger.warning(f"Message received for unknown session {session_id}") + return False + + # Serialize the message + if isinstance(message, Exception): + # For exceptions, store them as special format + data = json.dumps({ + "_exception": True, + "type": type(message).__name__, + "message": str(message) + }) + else: + data = message.model_dump_json(by_alias=True, exclude_none=True) + + # Push to the right side of the list (queue) + await self._redis.rpush(self._session_queue_key(session_id), data) + logger.debug(f"Added message to Redis queue for session {session_id}") + return True + + async def get_message(self, session_id: UUID, timeout: float = 0.1) -> types.JSONRPCMessage | Exception | None: + """Get the next message for the specified session.""" + # Check if session exists + if not await self.session_exists(session_id): + return None + + # Pop from the left side of the list (queue) + # Use BLPOP with timeout to avoid busy waiting + result = await self._redis.blpop([self._session_queue_key(session_id)], timeout) + + if not result: + return None + + # result is a tuple of (key, value) + _, data = result + + # Deserialize the message + json_data = json.loads(data) + + # Check if it's an exception + if isinstance(json_data, dict) and json_data.get("_exception"): + # Reconstitute a generic exception + return Exception(f"{json_data['type']}: {json_data['message']}") + + # Regular message + try: + return types.JSONRPCMessage.model_validate_json(data) + except Exception as e: + logger.error(f"Failed to deserialize message: {e}") + return None + + async def register_session(self, session_id: UUID) -> None: + """Register a new session with the queue.""" + # Add session ID to the set of active sessions + await self._redis.sadd(self._active_sessions_key, session_id.hex) + logger.debug(f"Registered session {session_id} in Redis") + + async def unregister_session(self, session_id: UUID) -> None: + """Unregister a session when it's closed.""" + # Remove session ID from active sessions + await self._redis.srem(self._active_sessions_key, session_id.hex) + # Delete the session's message queue + await self._redis.delete(self._session_queue_key(session_id)) + logger.debug(f"Unregistered session {session_id} from Redis") + + async def session_exists(self, session_id: UUID) -> bool: + """Check if a session exists.""" + return bool(await self._redis.sismember(self._active_sessions_key, session_id.hex)) \ No newline at end of file diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index d051c25bf..0993807b2 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -46,6 +46,7 @@ async def handle_sse(request): from starlette.types import Receive, Scope, Send import mcp.types as types +from mcp.server.message_queue import InMemoryMessageQueue, MessageQueue logger = logging.getLogger(__name__) @@ -63,19 +64,21 @@ class SseServerTransport: """ _endpoint: str - _read_stream_writers: dict[ - UUID, MemoryObjectSendStream[types.JSONRPCMessage | Exception] - ] + _message_queue: MessageQueue - def __init__(self, endpoint: str) -> None: + def __init__(self, endpoint: str, message_queue: MessageQueue | None = None) -> None: """ Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL given. + + Args: + endpoint: The endpoint URL for SSE connections + message_queue: Optional message queue to use. If None, creates an InMemoryMessageQueue. """ super().__init__() self._endpoint = endpoint - self._read_stream_writers = {} + self._message_queue = message_queue or InMemoryMessageQueue() logger.debug(f"SseServerTransport initialized with endpoint: {endpoint}") @asynccontextmanager @@ -96,13 +99,29 @@ async def connect_sse(self, scope: Scope, receive: Receive, send: Send): session_id = uuid4() session_uri = f"{quote(self._endpoint)}?session_id={session_id.hex}" - self._read_stream_writers[session_id] = read_stream_writer + await self._message_queue.register_session(session_id) logger.debug(f"Created new session with ID: {session_id}") sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[ dict[str, Any] ](0) + message_polling_active = True + async def poll_queue(): + """Background task to poll for messages in the queue""" + logger.debug(f"Starting queue polling for session {session_id}") + try: + while message_polling_active: + message = await self._message_queue.get_message(session_id) + if message: + logger.debug(f"Got message from queue for session {session_id}") + await read_stream_writer.send(message) + await anyio.sleep(0.01) + except Exception as e: + logger.error(f"Error in queue polling for session {session_id}: {e}") + finally: + logger.debug(f"Stopped queue polling for session {session_id}") + async def sse_writer(): logger.debug("Starting SSE writer") async with sse_stream_writer, write_stream_reader: @@ -126,9 +145,14 @@ async def sse_writer(): ) logger.debug("Starting SSE response task") tg.start_soon(response, scope, receive, send) + tg.start_soon(poll_queue) - logger.debug("Yielding read and write streams") - yield (read_stream, write_stream) + try: + logger.debug("Yielding read and write streams") + yield (read_stream, write_stream) + finally: + message_polling_active = False + await self._message_queue.unregister_session(session_id) async def handle_post_message( self, scope: Scope, receive: Receive, send: Send @@ -150,8 +174,7 @@ async def handle_post_message( response = Response("Invalid session ID", status_code=400) return await response(scope, receive, send) - writer = self._read_stream_writers.get(session_id) - if not writer: + if not await self._message_queue.session_exists(session_id): logger.warning(f"Could not find session for ID: {session_id}") response = Response("Could not find session", status_code=404) return await response(scope, receive, send) @@ -166,10 +189,10 @@ async def handle_post_message( logger.error(f"Failed to parse message: {err}") response = Response("Could not parse message", status_code=400) await response(scope, receive, send) - await writer.send(err) + await self._message_queue.add_message(session_id, err) return - logger.debug(f"Sending message to writer: {message}") + logger.debug(f"Adding message to queue for session {session_id}: {message}") response = Response("Accepted", status_code=202) await response(scope, receive, send) - await writer.send(message) + await self._message_queue.add_message(session_id, message) From c3d5efc0c064373518f04924c082f5d55463e1de Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 8 Apr 2025 16:01:34 -0700 Subject: [PATCH 02/51] readme update --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 05d607254..b85f2227b 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,13 @@ If you haven't created a uv-managed project yet, create one: ```bash uv add "mcp[cli]" ``` + + For optional features, you can add extras: + + ```bash + # For Redis support in message queue + uv add "mcp[redis]" + ``` Alternatively, for projects using pip for dependencies: ```bash @@ -169,6 +176,7 @@ mcp = FastMCP("My App") mcp = FastMCP("My App", dependencies=["pandas", "numpy"]) + @dataclass class AppContext: db: Database @@ -385,6 +393,27 @@ app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app())) For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes). +#### Message Queue Options + +By default, the SSE server uses an in-memory message queue for incoming POST messages. For production deployments or distributed scenarios, you can use Redis: + +```python +mcp = FastMCP( + "My App", + settings={ + "message_queue": "redis", + "redis_url": "redis://localhost:6379/0", + "redis_prefix": "mcp:queue:" + } +) +``` + +To use Redis, add the Redis dependency: + +```bash +uv add "mcp[redis]" +``` + ## Examples ### Echo Server From b2fce7dd1a2400ff011a752e01a79a9ac6107698 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 8 Apr 2025 16:35:27 -0700 Subject: [PATCH 03/51] ruff --- src/mcp/server/fastmcp/server.py | 17 +++-- src/mcp/server/message_queue/__init__.py | 5 +- src/mcp/server/message_queue/base.py | 81 ++++++++++++------------ src/mcp/server/message_queue/redis.py | 68 ++++++++++---------- src/mcp/server/sse.py | 9 ++- 5 files changed, 98 insertions(+), 82 deletions(-) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 0e04dca4e..a4d74962b 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -75,7 +75,7 @@ class Settings(BaseSettings, Generic[LifespanResultT]): port: int = 8000 sse_path: str = "/sse" message_path: str = "/messages/" - + # SSE message queue settings message_queue: Literal["memory", "redis"] = "memory" redis_url: str = "redis://localhost:6379/0" @@ -488,20 +488,25 @@ def sse_app(self) -> Starlette: if self.settings.message_queue == "redis": try: from mcp.server.message_queue import RedisMessageQueue + message_queue = RedisMessageQueue( - redis_url=self.settings.redis_url, - prefix=self.settings.redis_prefix + redis_url=self.settings.redis_url, prefix=self.settings.redis_prefix ) logger.info(f"Using Redis message queue at {self.settings.redis_url}") except ImportError: - logger.error("Redis message queue requested but 'redis' package not installed. ") + logger.error( + "Redis message queue requested but 'redis' package not installed. " + ) raise else: from mcp.server.message_queue import InMemoryMessageQueue + message_queue = InMemoryMessageQueue() logger.info("Using in-memory message queue") - - sse = SseServerTransport(self.settings.message_path, message_queue=message_queue) + + sse = SseServerTransport( + self.settings.message_path, message_queue=message_queue + ) async def handle_sse(request: Request) -> None: async with sse.connect_sse( diff --git a/src/mcp/server/message_queue/__init__.py b/src/mcp/server/message_queue/__init__.py index 033e7490d..cbc56a44d 100644 --- a/src/mcp/server/message_queue/__init__.py +++ b/src/mcp/server/message_queue/__init__.py @@ -1,7 +1,8 @@ """ Message Queue Module for MCP Server -This module implements queue interfaces for handling messages between clients and servers. +This module implements queue interfaces for handling +messages between clients and servers. """ from mcp.server.message_queue.base import InMemoryMessageQueue, MessageQueue @@ -12,4 +13,4 @@ except ImportError: RedisMessageQueue = None -__all__ = ["MessageQueue", "InMemoryMessageQueue", "RedisMessageQueue"] \ No newline at end of file +__all__ = ["MessageQueue", "InMemoryMessageQueue", "RedisMessageQueue"] diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index 9cfe13544..6dcaf7acc 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -1,9 +1,3 @@ -""" -Base Message Queue Protocol and In-Memory Implementation - -This module defines the message queue protocol and provides a default in-memory implementation. -""" - import logging from typing import Protocol, runtime_checkable from uuid import UUID @@ -16,57 +10,61 @@ @runtime_checkable class MessageQueue(Protocol): """Abstract interface for an SSE message queue. - - This interface allows messages to be queued and processed by any SSE server instance, + + This interface allows messages to be queued and processed by any SSE server instance enabling multiple servers to handle requests for the same session. """ - - async def add_message(self, session_id: UUID, message: types.JSONRPCMessage | Exception) -> bool: + + async def add_message( + self, session_id: UUID, message: types.JSONRPCMessage | Exception + ) -> bool: """Add a message to the queue for the specified session. - + Args: session_id: The UUID of the session this message is for message: The message to queue - + Returns: bool: True if message was accepted, False if session not found """ ... - - async def get_message(self, session_id: UUID, timeout: float = 0.1) -> types.JSONRPCMessage | Exception | None: + + async def get_message( + self, session_id: UUID, timeout: float = 0.1 + ) -> types.JSONRPCMessage | Exception | None: """Get the next message for the specified session. - + Args: session_id: The UUID of the session to get messages for timeout: Maximum time to wait for a message, in seconds - + Returns: The next message or None if no message is available """ ... - + async def register_session(self, session_id: UUID) -> None: """Register a new session with the queue. - + Args: session_id: The UUID of the new session to register """ ... - + async def unregister_session(self, session_id: UUID) -> None: """Unregister a session when it's closed. - + Args: session_id: The UUID of the session to unregister """ ... - + async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists. - + Args: session_id: The UUID of the session to check - + Returns: bool: True if the session is active, False otherwise """ @@ -75,54 +73,59 @@ async def session_exists(self, session_id: UUID) -> bool: class InMemoryMessageQueue: """Default in-memory implementation of the MessageQueue interface. - - This implementation keeps messages in memory for each session until they're retrieved. + + This implementation keeps messages in memory for + each session until they're retrieved. """ - + def __init__(self) -> None: self._message_queues: dict[UUID, list[types.JSONRPCMessage | Exception]] = {} self._active_sessions: set[UUID] = set() - - async def add_message(self, session_id: UUID, message: types.JSONRPCMessage | Exception) -> bool: + + async def add_message( + self, session_id: UUID, message: types.JSONRPCMessage | Exception + ) -> bool: """Add a message to the queue for the specified session.""" if session_id not in self._active_sessions: logger.warning(f"Message received for unknown session {session_id}") return False - + if session_id not in self._message_queues: self._message_queues[session_id] = [] - + self._message_queues[session_id].append(message) logger.debug(f"Added message to queue for session {session_id}") return True - - async def get_message(self, session_id: UUID, timeout: float = 0.1) -> types.JSONRPCMessage | Exception | None: + + async def get_message( + self, session_id: UUID, timeout: float = 0.1 + ) -> types.JSONRPCMessage | Exception | None: """Get the next message for the specified session.""" if session_id not in self._active_sessions: return None - + queue = self._message_queues.get(session_id, []) if not queue: return None - + message = queue.pop(0) if not queue: # Clean up empty queue del self._message_queues[session_id] - + return message - + async def register_session(self, session_id: UUID) -> None: """Register a new session with the queue.""" self._active_sessions.add(session_id) logger.debug(f"Registered session {session_id}") - + async def unregister_session(self, session_id: UUID) -> None: """Unregister a session when it's closed.""" self._active_sessions.discard(session_id) if session_id in self._message_queues: del self._message_queues[session_id] logger.debug(f"Unregistered session {session_id}") - + async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" - return session_id in self._active_sessions \ No newline at end of file + return session_id in self._active_sessions diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 66255d2a4..788c01489 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -1,9 +1,3 @@ -""" -Redis Message Queue Module for MCP Server - -This module implements a Redis-backed message queue for handling messages between clients and servers. -""" - import json import logging from uuid import UUID @@ -23,14 +17,16 @@ class RedisMessageQueue: """Redis implementation of the MessageQueue interface. - + This implementation uses Redis lists to store messages for each session. Redis provides persistence and allows multiple servers to share the same queue. """ - - def __init__(self, redis_url: str = "redis://localhost:6379/0", prefix: str = "mcp:queue:") -> None: + + def __init__( + self, redis_url: str = "redis://localhost:6379/0", prefix: str = "mcp:queue:" + ) -> None: """Initialize Redis message queue. - + Args: redis_url: Redis connection string prefix: Key prefix for Redis keys to avoid collisions @@ -39,71 +35,77 @@ def __init__(self, redis_url: str = "redis://localhost:6379/0", prefix: str = "m self._prefix = prefix self._active_sessions_key = f"{prefix}active_sessions" logger.debug(f"Initialized Redis message queue with URL: {redis_url}") - + def _session_queue_key(self, session_id: UUID) -> str: """Get the Redis key for a session's message queue.""" return f"{self._prefix}session:{session_id.hex}" - - async def add_message(self, session_id: UUID, message: types.JSONRPCMessage | Exception) -> bool: + + async def add_message( + self, session_id: UUID, message: types.JSONRPCMessage | Exception + ) -> bool: """Add a message to the queue for the specified session.""" # Check if session exists if not await self.session_exists(session_id): logger.warning(f"Message received for unknown session {session_id}") return False - + # Serialize the message if isinstance(message, Exception): # For exceptions, store them as special format - data = json.dumps({ - "_exception": True, - "type": type(message).__name__, - "message": str(message) - }) + data = json.dumps( + { + "_exception": True, + "type": type(message).__name__, + "message": str(message), + } + ) else: data = message.model_dump_json(by_alias=True, exclude_none=True) - + # Push to the right side of the list (queue) await self._redis.rpush(self._session_queue_key(session_id), data) logger.debug(f"Added message to Redis queue for session {session_id}") return True - - async def get_message(self, session_id: UUID, timeout: float = 0.1) -> types.JSONRPCMessage | Exception | None: + + async def get_message( + self, session_id: UUID, timeout: float = 0.1 + ) -> types.JSONRPCMessage | Exception | None: """Get the next message for the specified session.""" # Check if session exists if not await self.session_exists(session_id): return None - + # Pop from the left side of the list (queue) # Use BLPOP with timeout to avoid busy waiting result = await self._redis.blpop([self._session_queue_key(session_id)], timeout) - + if not result: return None - + # result is a tuple of (key, value) _, data = result - + # Deserialize the message json_data = json.loads(data) - + # Check if it's an exception if isinstance(json_data, dict) and json_data.get("_exception"): # Reconstitute a generic exception return Exception(f"{json_data['type']}: {json_data['message']}") - + # Regular message try: return types.JSONRPCMessage.model_validate_json(data) except Exception as e: logger.error(f"Failed to deserialize message: {e}") return None - + async def register_session(self, session_id: UUID) -> None: """Register a new session with the queue.""" # Add session ID to the set of active sessions await self._redis.sadd(self._active_sessions_key, session_id.hex) logger.debug(f"Registered session {session_id} in Redis") - + async def unregister_session(self, session_id: UUID) -> None: """Unregister a session when it's closed.""" # Remove session ID from active sessions @@ -111,7 +113,9 @@ async def unregister_session(self, session_id: UUID) -> None: # Delete the session's message queue await self._redis.delete(self._session_queue_key(session_id)) logger.debug(f"Unregistered session {session_id} from Redis") - + async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" - return bool(await self._redis.sismember(self._active_sessions_key, session_id.hex)) \ No newline at end of file + return bool( + await self._redis.sismember(self._active_sessions_key, session_id.hex) + ) diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 0993807b2..3efa9ba8c 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -66,14 +66,16 @@ class SseServerTransport: _endpoint: str _message_queue: MessageQueue - def __init__(self, endpoint: str, message_queue: MessageQueue | None = None) -> None: + def __init__( + self, endpoint: str, message_queue: MessageQueue | None = None + ) -> None: """ Creates a new SSE server transport, which will direct the client to POST messages to the relative or absolute URL given. - + Args: endpoint: The endpoint URL for SSE connections - message_queue: Optional message queue to use. If None, creates an InMemoryMessageQueue. + message_queue: Optional message queue to use """ super().__init__() @@ -107,6 +109,7 @@ async def connect_sse(self, scope: Scope, receive: Receive, send: Send): ](0) message_polling_active = True + async def poll_queue(): """Background task to poll for messages in the queue""" logger.debug(f"Starting queue polling for session {session_id}") From 7c82f36916e1b6bf1ae4405ebb6a09c343cc3599 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 8 Apr 2025 17:19:43 -0700 Subject: [PATCH 04/51] fix typing issues --- src/mcp/server/message_queue/redis.py | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 788c01489..c368050f5 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -5,7 +5,7 @@ import mcp.types as types try: - import redis.asyncio as redis + import redis.asyncio as redis # type: ignore[import] except ImportError: raise ImportError( "Redis support requires the 'redis' package. " @@ -31,7 +31,7 @@ def __init__( redis_url: Redis connection string prefix: Key prefix for Redis keys to avoid collisions """ - self._redis = redis.Redis.from_url(redis_url, decode_responses=True) + self._redis = redis.Redis.from_url(redis_url, decode_responses=True) # type: ignore[attr-defined] self._prefix = prefix self._active_sessions_key = f"{prefix}active_sessions" logger.debug(f"Initialized Redis message queue with URL: {redis_url}") @@ -63,7 +63,7 @@ async def add_message( data = message.model_dump_json(by_alias=True, exclude_none=True) # Push to the right side of the list (queue) - await self._redis.rpush(self._session_queue_key(session_id), data) + await self._redis.rpush(self._session_queue_key(session_id), data) # type: ignore[attr-defined] logger.debug(f"Added message to Redis queue for session {session_id}") return True @@ -77,25 +77,28 @@ async def get_message( # Pop from the left side of the list (queue) # Use BLPOP with timeout to avoid busy waiting - result = await self._redis.blpop([self._session_queue_key(session_id)], timeout) + result = await self._redis.blpop([self._session_queue_key(session_id)], timeout) # type: ignore[attr-defined] if not result: return None # result is a tuple of (key, value) - _, data = result + _, data = result # type: ignore[misc] # Deserialize the message - json_data = json.loads(data) + json_data = json.loads(data) # type: ignore[arg-type] # Check if it's an exception - if isinstance(json_data, dict) and json_data.get("_exception"): - # Reconstitute a generic exception - return Exception(f"{json_data['type']}: {json_data['message']}") + if isinstance(json_data, dict): + exception_dict: dict[str, object] = json_data + if exception_dict.get("_exception", False): + return Exception( + f"{exception_dict['type']}: {exception_dict['message']}" + ) # Regular message try: - return types.JSONRPCMessage.model_validate_json(data) + return types.JSONRPCMessage.model_validate_json(data) # type: ignore[arg-type] except Exception as e: logger.error(f"Failed to deserialize message: {e}") return None @@ -103,19 +106,21 @@ async def get_message( async def register_session(self, session_id: UUID) -> None: """Register a new session with the queue.""" # Add session ID to the set of active sessions - await self._redis.sadd(self._active_sessions_key, session_id.hex) + await self._redis.sadd(self._active_sessions_key, session_id.hex) # type: ignore[attr-defined] logger.debug(f"Registered session {session_id} in Redis") async def unregister_session(self, session_id: UUID) -> None: """Unregister a session when it's closed.""" # Remove session ID from active sessions - await self._redis.srem(self._active_sessions_key, session_id.hex) + await self._redis.srem(self._active_sessions_key, session_id.hex) # type: ignore[attr-defined] # Delete the session's message queue - await self._redis.delete(self._session_queue_key(session_id)) + await self._redis.delete(self._session_queue_key(session_id)) # type: ignore[attr-defined] logger.debug(f"Unregistered session {session_id} from Redis") async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" - return bool( - await self._redis.sismember(self._active_sessions_key, session_id.hex) + # Explicitly annotate the result as bool to help the type checker + result = bool( + await self._redis.sismember(self._active_sessions_key, session_id.hex) # type: ignore[attr-defined] ) + return result From b92f22f44d2be819bf5f8bd6e33aedf613aee87f Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 8 Apr 2025 17:22:48 -0700 Subject: [PATCH 05/51] update lock --- pyproject.toml | 2 +- uv.lock | 466 ++++++++++++++++++++++++------------------------- 2 files changed, 229 insertions(+), 239 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 504ee00b6..6d5e8065b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ dev = [ docs = [ "mkdocs>=1.6.1", "mkdocs-glightbox>=0.4.0", - "mkdocs-material[imaging]>=9.5.18", + "mkdocs-material[imaging]>=9.5.45", "mkdocstrings-python>=1.12.2", ] diff --git a/uv.lock b/uv.lock index 424e2d482..2a10680c8 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,6 @@ version = 1 -revision = 1 requires-python = ">=3.10" -[options] -resolution-mode = "lowest-direct" - [manifest] members = [ "mcp", @@ -24,26 +20,35 @@ wheels = [ [[package]] name = "anyio" -version = "4.5.0" +version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/44/66874c5256e9fbc30103b31927fd9341c8da6ccafd4721b2b3e81e6ef176/anyio-4.5.0.tar.gz", hash = "sha256:c5a275fe5ca0afd788001f58fca1e69e29ce706d746e317d660e21f70c530ef9", size = 169376 } +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/68/f9e9bf6324c46e6b8396610aef90ad423ec3e18c9079547ceafea3dce0ec/anyio-4.5.0-py3-none-any.whl", hash = "sha256:fdeb095b7cc5a5563175eedd926ec4ae55413bb4be5770c424af0ba46ccb4a78", size = 89250 }, + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, ] [[package]] name = "attrs" -version = "24.3.0" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, ] [[package]] @@ -55,6 +60,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, ] +[[package]] +name = "backrefs" +version = "5.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337 }, + { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142 }, + { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021 }, + { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915 }, + { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336 }, +] + [[package]] name = "black" version = "25.1.0" @@ -119,11 +137,11 @@ wheels = [ [[package]] name = "certifi" -version = "2024.12.14" +version = "2025.1.31" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, ] [[package]] @@ -246,14 +264,14 @@ wheels = [ [[package]] name = "click" -version = "8.1.0" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/2b/7ebad1e59a99207d417c0784f7fb67893465eef84b5b47c788324f1b4095/click-8.1.0.tar.gz", hash = "sha256:977c213473c7665d3aa092b41ff12063227751c41d7b17165013e10069cc5cd2", size = 329986 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/3e/3a523bdd24510288b1b850428e01172116a29268378b1da9a8d0b894a115/click-8.1.0-py3-none-any.whl", hash = "sha256:19a4baa64da924c5e0cd889aba8e947f280309f1a2ce0947a3e3a7bcb7cc72d6", size = 96400 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] [[package]] @@ -319,14 +337,14 @@ wheels = [ [[package]] name = "griffe" -version = "1.6.2" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/f2/b00eb72b853ecb5bf31dd47857cdf6767e380ca24ec2910d43b3fa7cc500/griffe-1.6.2.tar.gz", hash = "sha256:3a46fa7bd83280909b63c12b9a975732a927dd97809efe5b7972290b606c5d91", size = 392836 } +sdist = { url = "https://files.pythonhosted.org/packages/59/08/7df7e90e34d08ad890bd71d7ba19451052f88dc3d2c483d228d1331a4736/griffe-1.7.2.tar.gz", hash = "sha256:98d396d803fab3b680c2608f300872fd57019ed82f0672f5b5323a9ad18c540c", size = 394919 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/bc/bd8b7de5e748e078b6be648e76b47189a9182b1ac1eb7791ff7969f39f27/griffe-1.6.2-py3-none-any.whl", hash = "sha256:6399f7e663150e4278a312a8e8a14d2f3d7bd86e2ef2f8056a1058e38579c2ee", size = 128638 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/38b408f41064c9fcdbb0ea27c1bd13a1c8657c4846e04dab9f5ea770602c/griffe-1.7.2-py3-none-any.whl", hash = "sha256:1ed9c2e338a75741fc82083fe5a1bc89cb6142efe126194cc313e34ee6af5423", size = 129187 }, ] [[package]] @@ -353,18 +371,17 @@ wheels = [ [[package]] name = "httpx" -version = "0.27.0" +version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, - { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/3da5bdf4408b8b2800061c339f240c1802f2e82d55e50bd39c5a881f47f0/httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5", size = 126413 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/7b/ddacf6dcebb42466abd03f368782142baa82e08fc0c1f8eaa05b4bae87d5/httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5", size = 75590 }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] [[package]] @@ -387,11 +404,11 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] [[package]] @@ -504,6 +521,9 @@ cli = [ { name = "python-dotenv" }, { name = "typer" }, ] +redis = [ + { name = "redis" }, +] rich = [ { name = "rich" }, ] @@ -536,6 +556,7 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.7.2,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=4.5.0" }, { name = "rich", marker = "extra == 'rich'", specifier = ">=13.9.4" }, { name = "sse-starlette", specifier = ">=1.6.1" }, { name = "starlette", specifier = ">=0.27" }, @@ -543,7 +564,6 @@ requires-dist = [ { name = "uvicorn", specifier = ">=0.23.1" }, { name = "websockets", marker = "extra == 'ws'", specifier = ">=15.0.1" }, ] -provides-extras = ["cli", "rich", "ws"] [package.metadata.requires-dev] dev = [ @@ -742,10 +762,11 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.5.45" +version = "9.6.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, + { name = "backrefs" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, @@ -754,12 +775,11 @@ dependencies = [ { name = "paginate" }, { name = "pygments" }, { name = "pymdown-extensions" }, - { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/02/38f1f76252462b8e9652eb3778905206c1f3b9b4c25bf60aafc029675a2b/mkdocs_material-9.5.45.tar.gz", hash = "sha256:286489cf0beca4a129d91d59d6417419c63bceed1ce5cd0ec1fc7e1ebffb8189", size = 3906694 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/7e/c65e330e99daa5813e7594e57a09219ad041ed631604a72588ec7c11b34b/mkdocs_material-9.6.11.tar.gz", hash = "sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff", size = 3951595 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/43/f5f866cd840e14f82068831e53446ea1f66a128cd38a229c5b9c9243ed9e/mkdocs_material-9.5.45-py3-none-any.whl", hash = "sha256:a9be237cfd0be14be75f40f1726d83aa3a81ce44808dc3594d47a7a592f44547", size = 8615700 }, + { url = "https://files.pythonhosted.org/packages/19/91/79a15a772151aca0d505f901f6bbd4b85ee1fe54100256a6702056bab121/mkdocs_material-9.6.11-py3-none-any.whl", hash = "sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263", size = 8703720 }, ] [package.optional-dependencies] @@ -779,7 +799,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.29.0" +version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -789,23 +809,24 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/4d/a9484dc5d926295bdf308f1f6c4f07fcc99735b970591edc414d401fcc91/mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e", size = 1212185 } +sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686 } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/47/eb876dfd84e48f31ff60897d161b309cf6a04ca270155b0662aae562b3fb/mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d", size = 1630824 }, + { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075 }, ] [[package]] name = "mkdocstrings-python" -version = "1.12.2" +version = "1.16.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/ec/cb6debe2db77f1ef42b25b21d93b5021474de3037cd82385e586aee72545/mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3", size = 168207 } +sdist = { url = "https://files.pythonhosted.org/packages/44/c8/600c4201b6b9e72bab16802316d0c90ce04089f8e6bb5e064cd2a5abba7e/mkdocstrings_python-1.16.10.tar.gz", hash = "sha256:f9eedfd98effb612ab4d0ed6dd2b73aff6eba5215e0a65cea6d877717f75502e", size = 205771 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/c1/ac524e1026d9580cbc654b5d19f5843c8b364a66d30f956372cd09fd2f92/mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a", size = 111759 }, + { url = "https://files.pythonhosted.org/packages/53/37/19549c5e0179785308cc988a68e16aa7550e4e270ec8a9878334e86070c6/mkdocstrings_python-1.16.10-py3-none-any.whl", hash = "sha256:63bb9f01f8848a644bdb6289e86dc38ceddeaa63ecc2e291e3b2ca52702a6643", size = 124112 }, ] [[package]] @@ -926,11 +947,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, ] [[package]] @@ -953,113 +974,126 @@ wheels = [ [[package]] name = "pydantic" -version = "2.10.1" +version = "2.11.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/bd/7fc610993f616d2398958d0028d15eaf53bde5f80cb2edb7aa4f1feaf3a7/pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560", size = 783717 } +sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/fc/fda48d347bd50a788dd2a0f318a52160f911b86fc2d8b4c86f4d7c9bceea/pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e", size = 455329 }, + { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, ] [[package]] name = "pydantic-core" -version = "2.27.1" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/ce/60fd96895c09738648c83f3f00f595c807cb6735c70d3306b548cc96dd49/pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", size = 1897984 }, - { url = "https://files.pythonhosted.org/packages/fd/b9/84623d6b6be98cc209b06687d9bca5a7b966ffed008d15225dd0d20cce2e/pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b", size = 1807491 }, - { url = "https://files.pythonhosted.org/packages/01/72/59a70165eabbc93b1111d42df9ca016a4aa109409db04304829377947028/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", size = 1831953 }, - { url = "https://files.pythonhosted.org/packages/7c/0c/24841136476adafd26f94b45bb718a78cb0500bd7b4f8d667b67c29d7b0d/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", size = 1856071 }, - { url = "https://files.pythonhosted.org/packages/53/5e/c32957a09cceb2af10d7642df45d1e3dbd8596061f700eac93b801de53c0/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", size = 2038439 }, - { url = "https://files.pythonhosted.org/packages/e4/8f/979ab3eccd118b638cd6d8f980fea8794f45018255a36044dea40fe579d4/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", size = 2787416 }, - { url = "https://files.pythonhosted.org/packages/02/1d/00f2e4626565b3b6d3690dab4d4fe1a26edd6a20e53749eb21ca892ef2df/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", size = 2134548 }, - { url = "https://files.pythonhosted.org/packages/9d/46/3112621204128b90898adc2e721a3cd6cf5626504178d6f32c33b5a43b79/pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", size = 1989882 }, - { url = "https://files.pythonhosted.org/packages/49/ec/557dd4ff5287ffffdf16a31d08d723de6762bb1b691879dc4423392309bc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", size = 1995829 }, - { url = "https://files.pythonhosted.org/packages/6e/b2/610dbeb74d8d43921a7234555e4c091cb050a2bdb8cfea86d07791ce01c5/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", size = 2091257 }, - { url = "https://files.pythonhosted.org/packages/8c/7f/4bf8e9d26a9118521c80b229291fa9558a07cdd9a968ec2d5c1026f14fbc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", size = 2143894 }, - { url = "https://files.pythonhosted.org/packages/1f/1c/875ac7139c958f4390f23656fe696d1acc8edf45fb81e4831960f12cd6e4/pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", size = 1816081 }, - { url = "https://files.pythonhosted.org/packages/d7/41/55a117acaeda25ceae51030b518032934f251b1dac3704a53781383e3491/pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", size = 1981109 }, - { url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 }, - { url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 }, - { url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 }, - { url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 }, - { url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 }, - { url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 }, - { url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 }, - { url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 }, - { url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 }, - { url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 }, - { url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 }, - { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 }, - { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 }, - { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 }, - { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 }, - { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 }, - { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 }, - { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 }, - { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 }, - { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 }, - { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 }, - { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 }, - { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 }, - { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 }, - { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 }, - { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 }, - { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 }, - { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 }, - { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, - { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, - { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, - { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, - { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, - { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, - { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, - { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, - { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, - { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, - { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, - { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, - { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, - { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, - { url = "https://files.pythonhosted.org/packages/7c/60/e5eb2d462595ba1f622edbe7b1d19531e510c05c405f0b87c80c1e89d5b1/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", size = 1894016 }, - { url = "https://files.pythonhosted.org/packages/61/20/da7059855225038c1c4326a840908cc7ca72c7198cb6addb8b92ec81c1d6/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", size = 1771648 }, - { url = "https://files.pythonhosted.org/packages/8f/fc/5485cf0b0bb38da31d1d292160a4d123b5977841ddc1122c671a30b76cfd/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", size = 1826929 }, - { url = "https://files.pythonhosted.org/packages/a1/ff/fb1284a210e13a5f34c639efc54d51da136074ffbe25ec0c279cf9fbb1c4/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", size = 1980591 }, - { url = "https://files.pythonhosted.org/packages/f1/14/77c1887a182d05af74f6aeac7b740da3a74155d3093ccc7ee10b900cc6b5/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", size = 1981326 }, - { url = "https://files.pythonhosted.org/packages/06/aa/6f1b2747f811a9c66b5ef39d7f02fbb200479784c75e98290d70004b1253/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", size = 1989205 }, - { url = "https://files.pythonhosted.org/packages/7a/d2/8ce2b074d6835f3c88d85f6d8a399790043e9fdb3d0e43455e72d19df8cc/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", size = 2079616 }, - { url = "https://files.pythonhosted.org/packages/65/71/af01033d4e58484c3db1e5d13e751ba5e3d6b87cc3368533df4c50932c8b/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", size = 2133265 }, - { url = "https://files.pythonhosted.org/packages/33/72/f881b5e18fbb67cf2fb4ab253660de3c6899dbb2dba409d0b757e3559e3d/pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", size = 2001864 }, +sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 }, + { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 }, + { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 }, + { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 }, + { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 }, + { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 }, + { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 }, + { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 }, + { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 }, + { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 }, + { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 }, + { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 }, + { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 }, + { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 }, + { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 }, + { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 }, + { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 }, + { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 }, + { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 }, + { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 }, + { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 }, + { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 }, + { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 }, + { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 }, + { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, + { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, + { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, + { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, + { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, + { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, + { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, + { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, + { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, + { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, + { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, + { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, + { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, + { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, + { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, + { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, + { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, + { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 }, + { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 }, + { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 }, + { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 }, + { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 }, + { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 }, + { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 }, + { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 }, + { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 }, + { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 }, + { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 }, + { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, + { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, + { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, + { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 }, + { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 }, + { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, + { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 }, + { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 }, + { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 }, + { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 }, + { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 }, + { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 }, + { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 }, + { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 }, + { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 }, + { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 }, + { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 }, + { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 }, + { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 }, + { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 }, + { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 }, + { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 }, + { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 }, ] [[package]] name = "pydantic-settings" -version = "2.6.1" +version = "2.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } +sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, + { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, ] [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] [[package]] @@ -1077,20 +1111,20 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.391" +version = "1.1.398" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/05/4ea52a8a45cc28897edb485b4102d37cbfd5fce8445d679cdeb62bfad221/pyright-1.1.391.tar.gz", hash = "sha256:66b2d42cdf5c3cbab05f2f4b76e8bec8aa78e679bfa0b6ad7b923d9e027cadb2", size = 21965 } +sdist = { url = "https://files.pythonhosted.org/packages/24/d6/48740f1d029e9fc4194880d1ad03dcf0ba3a8f802e0e166b8f63350b3584/pyright-1.1.398.tar.gz", hash = "sha256:357a13edd9be8082dc73be51190913e475fa41a6efb6ec0d4b7aab3bc11638d8", size = 3892675 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/89/66f49552fbeb21944c8077d11834b2201514a56fd1b7747ffff9630f1bd9/pyright-1.1.391-py3-none-any.whl", hash = "sha256:54fa186f8b3e8a55a44ebfa842636635688670c6896dcf6cf4a7fc75062f4d15", size = 18579 }, + { url = "https://files.pythonhosted.org/packages/58/e0/5283593f61b3c525d6d7e94cfb6b3ded20b3df66e953acaf7bb4f23b3f6e/pyright-1.1.398-py3-none-any.whl", hash = "sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753", size = 5780235 }, ] [[package]] name = "pytest" -version = "8.3.4" +version = "8.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1100,23 +1134,23 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, ] [[package]] name = "pytest-examples" -version = "0.0.14" +version = "0.0.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "black" }, { name = "pytest" }, { name = "ruff" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/a7/b81d5cf26e9713a2d4c8e6863ee009360c5c07a0cfb880456ec8b09adab7/pytest_examples-0.0.14.tar.gz", hash = "sha256:776d1910709c0c5ce01b29bfe3651c5312d5cfe5c063e23ca6f65aed9af23f09", size = 20767 } +sdist = { url = "https://files.pythonhosted.org/packages/05/b2/555d866fc26d3ab7bc49e1be7cc982dd95b62bc33d60256506634d28fc5d/pytest_examples-0.0.17.tar.gz", hash = "sha256:3f02460c10de36646dab45825659fa4735441863af8c86388c22eb6113d038d8", size = 21211 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/99/f418071551ff2b5e8c06bd8b82b1f4fd472b5e4162f018773ba4ef52b6e8/pytest_examples-0.0.14-py3-none-any.whl", hash = "sha256:867a7ea105635d395df712a4b8d0df3bda4c3d78ae97a57b4f115721952b5e25", size = 17919 }, + { url = "https://files.pythonhosted.org/packages/9d/53/c2fc4d70d1999030b1f7bc6fe1441fa0a37a686a74a9a2109a4aa5e946ce/pytest_examples-0.0.17-py3-none-any.whl", hash = "sha256:3269b8b108e248d81edead269b2abf1cb76636bd49b7d5d3b41b194634cb10e6", size = 18168 }, ] [[package]] @@ -1158,11 +1192,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/06/1ef763af20d0572c032fa22882cfbfb005fba6e7300715a37840858c919e/python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", size = 37399 } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/2f/62ea1c8b593f4e093cc1a7768f0d46112107e790c3e478532329e434f00b/python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a", size = 19482 }, + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] [[package]] @@ -1222,72 +1256,15 @@ wheels = [ ] [[package]] -name = "regex" -version = "2024.11.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, - { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, - { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, - { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, - { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, - { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, - { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, - { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, - { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, - { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, - { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, - { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, - { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, - { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, - { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, - { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, - { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, - { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, - { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, - { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, - { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, - { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, - { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, - { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, - { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, - { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, - { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, - { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, - { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, - { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, - { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, - { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, - { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, - { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, - { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, - { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, - { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, - { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, - { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, - { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, - { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, - { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, - { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, - { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, - { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, - { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, - { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, - { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, - { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, - { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, - { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, - { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, - { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, - { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, - { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, - { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, - { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, - { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, - { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, - { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, - { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +name = "redis" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, ] [[package]] @@ -1307,41 +1284,41 @@ wheels = [ [[package]] name = "rich" -version = "13.9.4" +version = "14.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, ] [[package]] name = "ruff" -version = "0.8.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/5d/4b5403f3e89837decfd54c51bea7f94b7d3fae77e08858603d0e04d7ad17/ruff-0.8.5.tar.gz", hash = "sha256:1098d36f69831f7ff2a1da3e6407d5fbd6dfa2559e4f74ff2d260c5588900317", size = 3454835 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/f8/03391745a703ce11678eb37c48ae89ec60396ea821e9d0bcea7c8e88fd91/ruff-0.8.5-py3-none-linux_armv6l.whl", hash = "sha256:5ad11a5e3868a73ca1fa4727fe7e33735ea78b416313f4368c504dbeb69c0f88", size = 10626889 }, - { url = "https://files.pythonhosted.org/packages/55/74/83bb74a44183b904216f3edfb9995b89830c83aaa6ce84627f74da0e0cf8/ruff-0.8.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f69ab37771ea7e0715fead8624ec42996d101269a96e31f4d31be6fc33aa19b7", size = 10398233 }, - { url = "https://files.pythonhosted.org/packages/e8/7a/a162a4feb3ef85d594527165e366dde09d7a1e534186ff4ba5d127eda850/ruff-0.8.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b5462d7804558ccff9c08fe8cbf6c14b7efe67404316696a2dde48297b1925bb", size = 10001843 }, - { url = "https://files.pythonhosted.org/packages/e7/9f/5ee5dcd135411402e35b6ec6a8dfdadbd31c5cd1c36a624d356a38d76090/ruff-0.8.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d56de7220a35607f9fe59f8a6d018e14504f7b71d784d980835e20fc0611cd50", size = 10872507 }, - { url = "https://files.pythonhosted.org/packages/b6/67/db2df2dd4a34b602d7f6ebb1b3744c8157f0d3579973ffc58309c9c272e8/ruff-0.8.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d99cf80b0429cbebf31cbbf6f24f05a29706f0437c40413d950e67e2d4faca4", size = 10377200 }, - { url = "https://files.pythonhosted.org/packages/fe/ff/fe3a6a73006bced73e60d171d154a82430f61d97e787f511a24bd6302611/ruff-0.8.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b75ac29715ac60d554a049dbb0ef3b55259076181c3369d79466cb130eb5afd", size = 11433155 }, - { url = "https://files.pythonhosted.org/packages/e3/95/c1d1a1fe36658c1f3e1b47e1cd5f688b72d5786695b9e621c2c38399a95e/ruff-0.8.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c9d526a62c9eda211b38463528768fd0ada25dad524cb33c0e99fcff1c67b5dc", size = 12139227 }, - { url = "https://files.pythonhosted.org/packages/1b/fe/644b70d473a27b5112ac7a3428edcc1ce0db775c301ff11aa146f71886e0/ruff-0.8.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:587c5e95007612c26509f30acc506c874dab4c4abbacd0357400bd1aa799931b", size = 11697941 }, - { url = "https://files.pythonhosted.org/packages/00/39/4f83e517ec173e16a47c6d102cd22a1aaebe80e1208a1f2e83ab9a0e4134/ruff-0.8.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:622b82bf3429ff0e346835ec213aec0a04d9730480cbffbb6ad9372014e31bbd", size = 12967686 }, - { url = "https://files.pythonhosted.org/packages/1a/f6/52a2973ff108d74b5da706a573379eea160bece098f7cfa3f35dc4622710/ruff-0.8.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99be814d77a5dac8a8957104bdd8c359e85c86b0ee0e38dca447cb1095f70fb", size = 11253788 }, - { url = "https://files.pythonhosted.org/packages/ce/1f/3b30f3c65b1303cb8e268ec3b046b77ab21ed8e26921cfc7e8232aa57f2c/ruff-0.8.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01c048f9c3385e0fd7822ad0fd519afb282af9cf1778f3580e540629df89725", size = 10860360 }, - { url = "https://files.pythonhosted.org/packages/a5/a8/2a3ea6bacead963f7aeeba0c61815d9b27b0d638e6a74984aa5cc5d27733/ruff-0.8.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7512e8cb038db7f5db6aae0e24735ff9ea03bb0ed6ae2ce534e9baa23c1dc9ea", size = 10457922 }, - { url = "https://files.pythonhosted.org/packages/17/47/8f9514b670969aab57c5fc826fb500a16aee8feac1bcf8a91358f153a5ba/ruff-0.8.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:762f113232acd5b768d6b875d16aad6b00082add40ec91c927f0673a8ec4ede8", size = 10958347 }, - { url = "https://files.pythonhosted.org/packages/0d/d6/78a9af8209ad99541816d74f01ce678fc01ebb3f37dd7ab8966646dcd92b/ruff-0.8.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:03a90200c5dfff49e4c967b405f27fdfa81594cbb7c5ff5609e42d7fe9680da5", size = 11328882 }, - { url = "https://files.pythonhosted.org/packages/54/77/5c8072ec7afdfdf42c7a4019044486a2b6c85ee73617f8875ec94b977fed/ruff-0.8.5-py3-none-win32.whl", hash = "sha256:8710ffd57bdaa6690cbf6ecff19884b8629ec2a2a2a2f783aa94b1cc795139ed", size = 8802515 }, - { url = "https://files.pythonhosted.org/packages/bc/b6/47d2b06784de8ae992c45cceb2a30f3f205b3236a629d7ca4c0c134839a2/ruff-0.8.5-py3-none-win_amd64.whl", hash = "sha256:4020d8bf8d3a32325c77af452a9976a9ad6455773bcb94991cf15bd66b347e47", size = 9684231 }, - { url = "https://files.pythonhosted.org/packages/bf/5e/ffee22bf9f9e4b2669d1f0179ae8804584939fb6502b51f2401e26b1e028/ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb", size = 9124741 }, +version = "0.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5b/3ae20f89777115944e89c2d8c2e795dcc5b9e04052f76d5347e35e0da66e/ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407", size = 3933063 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/db/baee59ac88f57527fcbaad3a7b309994e42329c6bc4d4d2b681a3d7b5426/ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2", size = 10106493 }, + { url = "https://files.pythonhosted.org/packages/c1/d6/9a0962cbb347f4ff98b33d699bf1193ff04ca93bed4b4222fd881b502154/ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc", size = 10876382 }, + { url = "https://files.pythonhosted.org/packages/3a/8f/62bab0c7d7e1ae3707b69b157701b41c1ccab8f83e8501734d12ea8a839f/ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906", size = 10237050 }, + { url = "https://files.pythonhosted.org/packages/09/96/e296965ae9705af19c265d4d441958ed65c0c58fc4ec340c27cc9d2a1f5b/ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f", size = 10424984 }, + { url = "https://files.pythonhosted.org/packages/e5/56/644595eb57d855afed6e54b852e2df8cd5ca94c78043b2f29bdfb29882d5/ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e", size = 9957438 }, + { url = "https://files.pythonhosted.org/packages/86/83/9d3f3bed0118aef3e871ded9e5687fb8c5776bde233427fd9ce0a45db2d4/ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223", size = 11547282 }, + { url = "https://files.pythonhosted.org/packages/40/e6/0c6e4f5ae72fac5ccb44d72c0111f294a5c2c8cc5024afcb38e6bda5f4b3/ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e", size = 12182020 }, + { url = "https://files.pythonhosted.org/packages/b5/92/4aed0e460aeb1df5ea0c2fbe8d04f9725cccdb25d8da09a0d3f5b8764bf8/ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d", size = 11679154 }, + { url = "https://files.pythonhosted.org/packages/1b/d3/7316aa2609f2c592038e2543483eafbc62a0e1a6a6965178e284808c095c/ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99", size = 13905985 }, + { url = "https://files.pythonhosted.org/packages/63/80/734d3d17546e47ff99871f44ea7540ad2bbd7a480ed197fe8a1c8a261075/ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222", size = 11348343 }, + { url = "https://files.pythonhosted.org/packages/04/7b/70fc7f09a0161dce9613a4671d198f609e653d6f4ff9eee14d64c4c240fb/ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304", size = 10308487 }, + { url = "https://files.pythonhosted.org/packages/1a/22/1cdd62dabd678d75842bf4944fd889cf794dc9e58c18cc547f9eb28f95ed/ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019", size = 9929091 }, + { url = "https://files.pythonhosted.org/packages/9f/20/40e0563506332313148e783bbc1e4276d657962cc370657b2fff20e6e058/ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896", size = 10924659 }, + { url = "https://files.pythonhosted.org/packages/b5/41/eef9b7aac8819d9e942f617f9db296f13d2c4576806d604aba8db5a753f1/ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751", size = 11428160 }, + { url = "https://files.pythonhosted.org/packages/ff/61/c488943414fb2b8754c02f3879de003e26efdd20f38167ded3fb3fc1cda3/ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270", size = 10311496 }, + { url = "https://files.pythonhosted.org/packages/b6/2b/2a1c8deb5f5dfa3871eb7daa41492c4d2b2824a74d2b38e788617612a66d/ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb", size = 11399146 }, + { url = "https://files.pythonhosted.org/packages/4f/03/3aec4846226d54a37822e4c7ea39489e4abd6f88388fba74e3d4abe77300/ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc", size = 10450306 }, ] [[package]] @@ -1382,26 +1359,27 @@ wheels = [ [[package]] name = "sse-starlette" -version = "1.6.1" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/88/0af7f586894cfe61bd212f33e571785c4570085711b24fb7445425a5eeb0/sse-starlette-1.6.1.tar.gz", hash = "sha256:6208af2bd7d0887c92f1379da14bd1f4db56bd1274cc5d36670c683d2aa1de6a", size = 14555 } +sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f7/499e5d0c181a52a205d5b0982fd71cf162d1e070c97dca90c60520bbf8bf/sse_starlette-1.6.1-py3-none-any.whl", hash = "sha256:d8f18f1c633e355afe61cc5e9c92eea85badcb8b2d56ec8cfb0a006994aa55da", size = 9553 }, + { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, ] [[package]] name = "starlette" -version = "0.27.0" +version = "0.46.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/68/559bed5484e746f1ab2ebbe22312f2c25ec62e4b534916d41a8c21147bf8/starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75", size = 51394 } +sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/f8/e2cca22387965584a409795913b774235752be4176d276714e15e1a58884/starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91", size = 66978 }, + { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, ] [[package]] @@ -1457,7 +1435,7 @@ wheels = [ [[package]] name = "trio" -version = "0.26.2" +version = "0.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1468,14 +1446,14 @@ dependencies = [ { name = "sniffio" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/03/ab0e9509be0c6465e2773768ec25ee0cb8053c0b91471ab3854bbf2294b2/trio-0.26.2.tar.gz", hash = "sha256:0346c3852c15e5c7d40ea15972c4805689ef2cb8b5206f794c9c19450119f3a4", size = 561156 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/70/efa56ce2271c44a7f4f43533a0477e6854a0948e9f7b76491de1fd3be7c9/trio-0.26.2-py3-none-any.whl", hash = "sha256:c5237e8133eb0a1d72f09a971a55c28ebe69e351c783fc64bc37db8db8bbe1d0", size = 475996 }, + { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 }, ] [[package]] name = "typer" -version = "0.12.4" +version = "0.15.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1483,18 +1461,30 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/f7/f174a1cae84848ae8b27170a96187b91937b743f0580ff968078fe16930a/typer-0.12.4.tar.gz", hash = "sha256:c9c1613ed6a166162705b3347b8d10b661ccc5d95692654d0fb628118f2c34e6", size = 97945 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/cc/15083dcde1252a663398b1b2a173637a3ec65adadfb95137dc95df1e6adc/typer-0.12.4-py3-none-any.whl", hash = "sha256:819aa03699f438397e876aa12b0d63766864ecba1b579092cc9fe35d886e34b6", size = 47402 }, + { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, ] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, ] [[package]] @@ -1508,16 +1498,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.30.0" +version = "0.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/f7/4ad826703a49b320a4adf2470fdd2a3481ea13f4460cb615ad12c75be003/uvicorn-0.30.0.tar.gz", hash = "sha256:f678dec4fa3a39706bbf49b9ec5fc40049d42418716cea52b53f07828a60aa37", size = 42560 } +sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/a1/d57e38417a8dabb22df02b6aebc209dc73485792e6c5620e501547133d0b/uvicorn-0.30.0-py3-none-any.whl", hash = "sha256:78fa0b5f56abb8562024a59041caeb555c86e48d0efdd23c3fe7de7a4075bdab", size = 62388 }, + { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, ] [[package]] @@ -1618,4 +1608,4 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, -] \ No newline at end of file +] From 5dbca6eb164d06239ceb88cfa9f0c10ee8d18865 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 8 Apr 2025 17:24:28 -0700 Subject: [PATCH 06/51] retrigger tests? From badc1e2f2c48acc14322096b714e76e488b6068c Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 8 Apr 2025 17:26:33 -0700 Subject: [PATCH 07/51] revert --- uv.lock | 466 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 238 insertions(+), 228 deletions(-) diff --git a/uv.lock b/uv.lock index 2a10680c8..424e2d482 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 +revision = 1 requires-python = ">=3.10" +[options] +resolution-mode = "lowest-direct" + [manifest] members = [ "mcp", @@ -20,35 +24,26 @@ wheels = [ [[package]] name = "anyio" -version = "4.9.0" +version = "4.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] - -[[package]] -name = "async-timeout" -version = "5.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/44/66874c5256e9fbc30103b31927fd9341c8da6ccafd4721b2b3e81e6ef176/anyio-4.5.0.tar.gz", hash = "sha256:c5a275fe5ca0afd788001f58fca1e69e29ce706d746e317d660e21f70c530ef9", size = 169376 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, + { url = "https://files.pythonhosted.org/packages/3b/68/f9e9bf6324c46e6b8396610aef90ad423ec3e18c9079547ceafea3dce0ec/anyio-4.5.0-py3-none-any.whl", hash = "sha256:fdeb095b7cc5a5563175eedd926ec4ae55413bb4be5770c424af0ba46ccb4a78", size = 89250 }, ] [[package]] name = "attrs" -version = "25.3.0" +version = "24.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, + { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, ] [[package]] @@ -60,19 +55,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, ] -[[package]] -name = "backrefs" -version = "5.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337 }, - { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142 }, - { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021 }, - { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915 }, - { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336 }, -] - [[package]] name = "black" version = "25.1.0" @@ -137,11 +119,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.1.31" +version = "2024.12.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, ] [[package]] @@ -264,14 +246,14 @@ wheels = [ [[package]] name = "click" -version = "8.1.8" +version = "8.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +sdist = { url = "https://files.pythonhosted.org/packages/45/2b/7ebad1e59a99207d417c0784f7fb67893465eef84b5b47c788324f1b4095/click-8.1.0.tar.gz", hash = "sha256:977c213473c7665d3aa092b41ff12063227751c41d7b17165013e10069cc5cd2", size = 329986 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, + { url = "https://files.pythonhosted.org/packages/86/3e/3a523bdd24510288b1b850428e01172116a29268378b1da9a8d0b894a115/click-8.1.0-py3-none-any.whl", hash = "sha256:19a4baa64da924c5e0cd889aba8e947f280309f1a2ce0947a3e3a7bcb7cc72d6", size = 96400 }, ] [[package]] @@ -337,14 +319,14 @@ wheels = [ [[package]] name = "griffe" -version = "1.7.2" +version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/08/7df7e90e34d08ad890bd71d7ba19451052f88dc3d2c483d228d1331a4736/griffe-1.7.2.tar.gz", hash = "sha256:98d396d803fab3b680c2608f300872fd57019ed82f0672f5b5323a9ad18c540c", size = 394919 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/f2/b00eb72b853ecb5bf31dd47857cdf6767e380ca24ec2910d43b3fa7cc500/griffe-1.6.2.tar.gz", hash = "sha256:3a46fa7bd83280909b63c12b9a975732a927dd97809efe5b7972290b606c5d91", size = 392836 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/5e/38b408f41064c9fcdbb0ea27c1bd13a1c8657c4846e04dab9f5ea770602c/griffe-1.7.2-py3-none-any.whl", hash = "sha256:1ed9c2e338a75741fc82083fe5a1bc89cb6142efe126194cc313e34ee6af5423", size = 129187 }, + { url = "https://files.pythonhosted.org/packages/4e/bc/bd8b7de5e748e078b6be648e76b47189a9182b1ac1eb7791ff7969f39f27/griffe-1.6.2-py3-none-any.whl", hash = "sha256:6399f7e663150e4278a312a8e8a14d2f3d7bd86e2ef2f8056a1058e38579c2ee", size = 128638 }, ] [[package]] @@ -371,17 +353,18 @@ wheels = [ [[package]] name = "httpx" -version = "0.28.1" +version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, + { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/3da5bdf4408b8b2800061c339f240c1802f2e82d55e50bd39c5a881f47f0/httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5", size = 126413 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, + { url = "https://files.pythonhosted.org/packages/41/7b/ddacf6dcebb42466abd03f368782142baa82e08fc0c1f8eaa05b4bae87d5/httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5", size = 75590 }, ] [[package]] @@ -404,11 +387,11 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] @@ -521,9 +504,6 @@ cli = [ { name = "python-dotenv" }, { name = "typer" }, ] -redis = [ - { name = "redis" }, -] rich = [ { name = "rich" }, ] @@ -556,7 +536,6 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.7.2,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" }, - { name = "redis", marker = "extra == 'redis'", specifier = ">=4.5.0" }, { name = "rich", marker = "extra == 'rich'", specifier = ">=13.9.4" }, { name = "sse-starlette", specifier = ">=1.6.1" }, { name = "starlette", specifier = ">=0.27" }, @@ -564,6 +543,7 @@ requires-dist = [ { name = "uvicorn", specifier = ">=0.23.1" }, { name = "websockets", marker = "extra == 'ws'", specifier = ">=15.0.1" }, ] +provides-extras = ["cli", "rich", "ws"] [package.metadata.requires-dev] dev = [ @@ -762,11 +742,10 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.6.11" +version = "9.5.45" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, - { name = "backrefs" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, @@ -775,11 +754,12 @@ dependencies = [ { name = "paginate" }, { name = "pygments" }, { name = "pymdown-extensions" }, + { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/7e/c65e330e99daa5813e7594e57a09219ad041ed631604a72588ec7c11b34b/mkdocs_material-9.6.11.tar.gz", hash = "sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff", size = 3951595 } +sdist = { url = "https://files.pythonhosted.org/packages/02/02/38f1f76252462b8e9652eb3778905206c1f3b9b4c25bf60aafc029675a2b/mkdocs_material-9.5.45.tar.gz", hash = "sha256:286489cf0beca4a129d91d59d6417419c63bceed1ce5cd0ec1fc7e1ebffb8189", size = 3906694 } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/91/79a15a772151aca0d505f901f6bbd4b85ee1fe54100256a6702056bab121/mkdocs_material-9.6.11-py3-none-any.whl", hash = "sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263", size = 8703720 }, + { url = "https://files.pythonhosted.org/packages/5c/43/f5f866cd840e14f82068831e53446ea1f66a128cd38a229c5b9c9243ed9e/mkdocs_material-9.5.45-py3-none-any.whl", hash = "sha256:a9be237cfd0be14be75f40f1726d83aa3a81ce44808dc3594d47a7a592f44547", size = 8615700 }, ] [package.optional-dependencies] @@ -799,7 +779,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.29.1" +version = "0.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -809,24 +789,23 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/4d/a9484dc5d926295bdf308f1f6c4f07fcc99735b970591edc414d401fcc91/mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e", size = 1212185 } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075 }, + { url = "https://files.pythonhosted.org/packages/15/47/eb876dfd84e48f31ff60897d161b309cf6a04ca270155b0662aae562b3fb/mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d", size = 1630824 }, ] [[package]] name = "mkdocstrings-python" -version = "1.16.10" +version = "1.12.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/c8/600c4201b6b9e72bab16802316d0c90ce04089f8e6bb5e064cd2a5abba7e/mkdocstrings_python-1.16.10.tar.gz", hash = "sha256:f9eedfd98effb612ab4d0ed6dd2b73aff6eba5215e0a65cea6d877717f75502e", size = 205771 } +sdist = { url = "https://files.pythonhosted.org/packages/23/ec/cb6debe2db77f1ef42b25b21d93b5021474de3037cd82385e586aee72545/mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3", size = 168207 } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/37/19549c5e0179785308cc988a68e16aa7550e4e270ec8a9878334e86070c6/mkdocstrings_python-1.16.10-py3-none-any.whl", hash = "sha256:63bb9f01f8848a644bdb6289e86dc38ceddeaa63ecc2e291e3b2ca52702a6643", size = 124112 }, + { url = "https://files.pythonhosted.org/packages/5b/c1/ac524e1026d9580cbc654b5d19f5843c8b364a66d30f956372cd09fd2f92/mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a", size = 111759 }, ] [[package]] @@ -947,11 +926,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.7" +version = "4.3.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, ] [[package]] @@ -974,126 +953,113 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.3" +version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, - { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } +sdist = { url = "https://files.pythonhosted.org/packages/c4/bd/7fc610993f616d2398958d0028d15eaf53bde5f80cb2edb7aa4f1feaf3a7/pydantic-2.10.1.tar.gz", hash = "sha256:a4daca2dc0aa429555e0656d6bf94873a7dc5f54ee42b1f5873d666fb3f35560", size = 783717 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, + { url = "https://files.pythonhosted.org/packages/e0/fc/fda48d347bd50a788dd2a0f318a52160f911b86fc2d8b4c86f4d7c9bceea/pydantic-2.10.1-py3-none-any.whl", hash = "sha256:a8d20db84de64cf4a7d59e899c2caf0fe9d660c7cfc482528e7020d7dd189a7e", size = 455329 }, ] [[package]] name = "pydantic-core" -version = "2.33.1" +version = "2.27.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/ea/5f572806ab4d4223d11551af814d243b0e3e02cc6913def4d1fe4a5ca41c/pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26", size = 2044021 }, - { url = "https://files.pythonhosted.org/packages/8c/d1/f86cc96d2aa80e3881140d16d12ef2b491223f90b28b9a911346c04ac359/pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927", size = 1861742 }, - { url = "https://files.pythonhosted.org/packages/37/08/fbd2cd1e9fc735a0df0142fac41c114ad9602d1c004aea340169ae90973b/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db", size = 1910414 }, - { url = "https://files.pythonhosted.org/packages/7f/73/3ac217751decbf8d6cb9443cec9b9eb0130eeada6ae56403e11b486e277e/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48", size = 1996848 }, - { url = "https://files.pythonhosted.org/packages/9a/f5/5c26b265cdcff2661e2520d2d1e9db72d117ea00eb41e00a76efe68cb009/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969", size = 2141055 }, - { url = "https://files.pythonhosted.org/packages/5d/14/a9c3cee817ef2f8347c5ce0713e91867a0dceceefcb2973942855c917379/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e", size = 2753806 }, - { url = "https://files.pythonhosted.org/packages/f2/68/866ce83a51dd37e7c604ce0050ff6ad26de65a7799df89f4db87dd93d1d6/pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89", size = 2007777 }, - { url = "https://files.pythonhosted.org/packages/b6/a8/36771f4404bb3e49bd6d4344da4dede0bf89cc1e01f3b723c47248a3761c/pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde", size = 2122803 }, - { url = "https://files.pythonhosted.org/packages/18/9c/730a09b2694aa89360d20756369822d98dc2f31b717c21df33b64ffd1f50/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65", size = 2086755 }, - { url = "https://files.pythonhosted.org/packages/54/8e/2dccd89602b5ec31d1c58138d02340ecb2ebb8c2cac3cc66b65ce3edb6ce/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc", size = 2257358 }, - { url = "https://files.pythonhosted.org/packages/d1/9c/126e4ac1bfad8a95a9837acdd0963695d69264179ba4ede8b8c40d741702/pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091", size = 2257916 }, - { url = "https://files.pythonhosted.org/packages/7d/ba/91eea2047e681a6853c81c20aeca9dcdaa5402ccb7404a2097c2adf9d038/pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383", size = 1923823 }, - { url = "https://files.pythonhosted.org/packages/94/c0/fcdf739bf60d836a38811476f6ecd50374880b01e3014318b6e809ddfd52/pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504", size = 1952494 }, - { url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 }, - { url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 }, - { url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 }, - { url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 }, - { url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 }, - { url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 }, - { url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 }, - { url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 }, - { url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 }, - { url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 }, - { url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 }, - { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 }, - { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 }, - { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 }, - { url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 }, - { url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 }, - { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, - { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, - { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, - { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, - { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, - { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, - { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, - { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, - { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, - { url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 }, - { url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 }, - { url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 }, - { url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 }, - { url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 }, - { url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 }, - { url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 }, - { url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 }, - { url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 }, - { url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 }, - { url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 }, - { url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 }, - { url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 }, - { url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 }, - { url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 }, - { url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 }, - { url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 }, - { url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 }, - { url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 }, - { url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 }, - { url = "https://files.pythonhosted.org/packages/9c/c7/8b311d5adb0fe00a93ee9b4e92a02b0ec08510e9838885ef781ccbb20604/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02", size = 2041659 }, - { url = "https://files.pythonhosted.org/packages/8a/d6/4f58d32066a9e26530daaf9adc6664b01875ae0691570094968aaa7b8fcc/pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068", size = 1873294 }, - { url = "https://files.pythonhosted.org/packages/f7/3f/53cc9c45d9229da427909c751f8ed2bf422414f7664ea4dde2d004f596ba/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e", size = 1903771 }, - { url = "https://files.pythonhosted.org/packages/f0/49/bf0783279ce674eb9903fb9ae43f6c614cb2f1c4951370258823f795368b/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe", size = 2083558 }, - { url = "https://files.pythonhosted.org/packages/9c/5b/0d998367687f986c7d8484a2c476d30f07bf5b8b1477649a6092bd4c540e/pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1", size = 2118038 }, - { url = "https://files.pythonhosted.org/packages/b3/33/039287d410230ee125daee57373ac01940d3030d18dba1c29cd3089dc3ca/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7", size = 2079315 }, - { url = "https://files.pythonhosted.org/packages/1f/85/6d8b2646d99c062d7da2d0ab2faeb0d6ca9cca4c02da6076376042a20da3/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde", size = 2249063 }, - { url = "https://files.pythonhosted.org/packages/17/d7/c37d208d5738f7b9ad8f22ae8a727d88ebf9c16c04ed2475122cc3f7224a/pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add", size = 2254631 }, - { url = "https://files.pythonhosted.org/packages/13/e0/bafa46476d328e4553b85ab9b2f7409e7aaef0ce4c937c894821c542d347/pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c", size = 2080877 }, - { url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 }, - { url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 }, - { url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 }, - { url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 }, - { url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 }, - { url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 }, - { url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 }, - { url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 }, - { url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 }, +sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/ce/60fd96895c09738648c83f3f00f595c807cb6735c70d3306b548cc96dd49/pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a", size = 1897984 }, + { url = "https://files.pythonhosted.org/packages/fd/b9/84623d6b6be98cc209b06687d9bca5a7b966ffed008d15225dd0d20cce2e/pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b", size = 1807491 }, + { url = "https://files.pythonhosted.org/packages/01/72/59a70165eabbc93b1111d42df9ca016a4aa109409db04304829377947028/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278", size = 1831953 }, + { url = "https://files.pythonhosted.org/packages/7c/0c/24841136476adafd26f94b45bb718a78cb0500bd7b4f8d667b67c29d7b0d/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05", size = 1856071 }, + { url = "https://files.pythonhosted.org/packages/53/5e/c32957a09cceb2af10d7642df45d1e3dbd8596061f700eac93b801de53c0/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4", size = 2038439 }, + { url = "https://files.pythonhosted.org/packages/e4/8f/979ab3eccd118b638cd6d8f980fea8794f45018255a36044dea40fe579d4/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f", size = 2787416 }, + { url = "https://files.pythonhosted.org/packages/02/1d/00f2e4626565b3b6d3690dab4d4fe1a26edd6a20e53749eb21ca892ef2df/pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08", size = 2134548 }, + { url = "https://files.pythonhosted.org/packages/9d/46/3112621204128b90898adc2e721a3cd6cf5626504178d6f32c33b5a43b79/pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6", size = 1989882 }, + { url = "https://files.pythonhosted.org/packages/49/ec/557dd4ff5287ffffdf16a31d08d723de6762bb1b691879dc4423392309bc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807", size = 1995829 }, + { url = "https://files.pythonhosted.org/packages/6e/b2/610dbeb74d8d43921a7234555e4c091cb050a2bdb8cfea86d07791ce01c5/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c", size = 2091257 }, + { url = "https://files.pythonhosted.org/packages/8c/7f/4bf8e9d26a9118521c80b229291fa9558a07cdd9a968ec2d5c1026f14fbc/pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206", size = 2143894 }, + { url = "https://files.pythonhosted.org/packages/1f/1c/875ac7139c958f4390f23656fe696d1acc8edf45fb81e4831960f12cd6e4/pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c", size = 1816081 }, + { url = "https://files.pythonhosted.org/packages/d7/41/55a117acaeda25ceae51030b518032934f251b1dac3704a53781383e3491/pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17", size = 1981109 }, + { url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 }, + { url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 }, + { url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 }, + { url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 }, + { url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 }, + { url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 }, + { url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 }, + { url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 }, + { url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 }, + { url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 }, + { url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 }, + { url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 }, + { url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 }, + { url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 }, + { url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 }, + { url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 }, + { url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 }, + { url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 }, + { url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 }, + { url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 }, + { url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 }, + { url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 }, + { url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 }, + { url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 }, + { url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 }, + { url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 }, + { url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 }, + { url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 }, + { url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 }, + { url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 }, + { url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 }, + { url = "https://files.pythonhosted.org/packages/e3/f3/363652651779113189cefdbbb619b7b07b7a67ebb6840325117cc8cc3460/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737", size = 1857389 }, + { url = "https://files.pythonhosted.org/packages/5f/97/be804aed6b479af5a945daec7538d8bf358d668bdadde4c7888a2506bdfb/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2", size = 2037934 }, + { url = "https://files.pythonhosted.org/packages/42/01/295f0bd4abf58902917e342ddfe5f76cf66ffabfc57c2e23c7681a1a1197/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107", size = 2735176 }, + { url = "https://files.pythonhosted.org/packages/9d/a0/cd8e9c940ead89cc37812a1a9f310fef59ba2f0b22b4e417d84ab09fa970/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51", size = 2160720 }, + { url = "https://files.pythonhosted.org/packages/73/ae/9d0980e286627e0aeca4c352a60bd760331622c12d576e5ea4441ac7e15e/pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a", size = 1992972 }, + { url = "https://files.pythonhosted.org/packages/bf/ba/ae4480bc0292d54b85cfb954e9d6bd226982949f8316338677d56541b85f/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc", size = 2001477 }, + { url = "https://files.pythonhosted.org/packages/55/b7/e26adf48c2f943092ce54ae14c3c08d0d221ad34ce80b18a50de8ed2cba8/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960", size = 2091186 }, + { url = "https://files.pythonhosted.org/packages/ba/cc/8491fff5b608b3862eb36e7d29d36a1af1c945463ca4c5040bf46cc73f40/pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23", size = 2154429 }, + { url = "https://files.pythonhosted.org/packages/78/d8/c080592d80edd3441ab7f88f865f51dae94a157fc64283c680e9f32cf6da/pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05", size = 1833713 }, + { url = "https://files.pythonhosted.org/packages/83/84/5ab82a9ee2538ac95a66e51f6838d6aba6e0a03a42aa185ad2fe404a4e8f/pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337", size = 1987897 }, + { url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 }, + { url = "https://files.pythonhosted.org/packages/7c/60/e5eb2d462595ba1f622edbe7b1d19531e510c05c405f0b87c80c1e89d5b1/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6", size = 1894016 }, + { url = "https://files.pythonhosted.org/packages/61/20/da7059855225038c1c4326a840908cc7ca72c7198cb6addb8b92ec81c1d6/pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676", size = 1771648 }, + { url = "https://files.pythonhosted.org/packages/8f/fc/5485cf0b0bb38da31d1d292160a4d123b5977841ddc1122c671a30b76cfd/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d", size = 1826929 }, + { url = "https://files.pythonhosted.org/packages/a1/ff/fb1284a210e13a5f34c639efc54d51da136074ffbe25ec0c279cf9fbb1c4/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c", size = 1980591 }, + { url = "https://files.pythonhosted.org/packages/f1/14/77c1887a182d05af74f6aeac7b740da3a74155d3093ccc7ee10b900cc6b5/pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27", size = 1981326 }, + { url = "https://files.pythonhosted.org/packages/06/aa/6f1b2747f811a9c66b5ef39d7f02fbb200479784c75e98290d70004b1253/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f", size = 1989205 }, + { url = "https://files.pythonhosted.org/packages/7a/d2/8ce2b074d6835f3c88d85f6d8a399790043e9fdb3d0e43455e72d19df8cc/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed", size = 2079616 }, + { url = "https://files.pythonhosted.org/packages/65/71/af01033d4e58484c3db1e5d13e751ba5e3d6b87cc3368533df4c50932c8b/pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f", size = 2133265 }, + { url = "https://files.pythonhosted.org/packages/33/72/f881b5e18fbb67cf2fb4ab253660de3c6899dbb2dba409d0b757e3559e3d/pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c", size = 2001864 }, ] [[package]] name = "pydantic-settings" -version = "2.8.1" +version = "2.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } +sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, + { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, ] [[package]] name = "pygments" -version = "2.19.1" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, ] [[package]] @@ -1111,20 +1077,20 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.398" +version = "1.1.391" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/d6/48740f1d029e9fc4194880d1ad03dcf0ba3a8f802e0e166b8f63350b3584/pyright-1.1.398.tar.gz", hash = "sha256:357a13edd9be8082dc73be51190913e475fa41a6efb6ec0d4b7aab3bc11638d8", size = 3892675 } +sdist = { url = "https://files.pythonhosted.org/packages/11/05/4ea52a8a45cc28897edb485b4102d37cbfd5fce8445d679cdeb62bfad221/pyright-1.1.391.tar.gz", hash = "sha256:66b2d42cdf5c3cbab05f2f4b76e8bec8aa78e679bfa0b6ad7b923d9e027cadb2", size = 21965 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/e0/5283593f61b3c525d6d7e94cfb6b3ded20b3df66e953acaf7bb4f23b3f6e/pyright-1.1.398-py3-none-any.whl", hash = "sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753", size = 5780235 }, + { url = "https://files.pythonhosted.org/packages/ad/89/66f49552fbeb21944c8077d11834b2201514a56fd1b7747ffff9630f1bd9/pyright-1.1.391-py3-none-any.whl", hash = "sha256:54fa186f8b3e8a55a44ebfa842636635688670c6896dcf6cf4a7fc75062f4d15", size = 18579 }, ] [[package]] name = "pytest" -version = "8.3.5" +version = "8.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1134,23 +1100,23 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, ] [[package]] name = "pytest-examples" -version = "0.0.17" +version = "0.0.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "black" }, { name = "pytest" }, { name = "ruff" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/b2/555d866fc26d3ab7bc49e1be7cc982dd95b62bc33d60256506634d28fc5d/pytest_examples-0.0.17.tar.gz", hash = "sha256:3f02460c10de36646dab45825659fa4735441863af8c86388c22eb6113d038d8", size = 21211 } +sdist = { url = "https://files.pythonhosted.org/packages/d2/a7/b81d5cf26e9713a2d4c8e6863ee009360c5c07a0cfb880456ec8b09adab7/pytest_examples-0.0.14.tar.gz", hash = "sha256:776d1910709c0c5ce01b29bfe3651c5312d5cfe5c063e23ca6f65aed9af23f09", size = 20767 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/53/c2fc4d70d1999030b1f7bc6fe1441fa0a37a686a74a9a2109a4aa5e946ce/pytest_examples-0.0.17-py3-none-any.whl", hash = "sha256:3269b8b108e248d81edead269b2abf1cb76636bd49b7d5d3b41b194634cb10e6", size = 18168 }, + { url = "https://files.pythonhosted.org/packages/2b/99/f418071551ff2b5e8c06bd8b82b1f4fd472b5e4162f018773ba4ef52b6e8/pytest_examples-0.0.14-py3-none-any.whl", hash = "sha256:867a7ea105635d395df712a4b8d0df3bda4c3d78ae97a57b4f115721952b5e25", size = 17919 }, ] [[package]] @@ -1192,11 +1158,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.1.0" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } +sdist = { url = "https://files.pythonhosted.org/packages/31/06/1ef763af20d0572c032fa22882cfbfb005fba6e7300715a37840858c919e/python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", size = 37399 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, + { url = "https://files.pythonhosted.org/packages/44/2f/62ea1c8b593f4e093cc1a7768f0d46112107e790c3e478532329e434f00b/python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a", size = 19482 }, ] [[package]] @@ -1256,15 +1222,72 @@ wheels = [ ] [[package]] -name = "redis" -version = "5.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, + { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, + { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, + { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, + { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, + { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, + { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, + { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, + { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, + { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, + { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, + { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, + { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, + { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, ] [[package]] @@ -1284,41 +1307,41 @@ wheels = [ [[package]] name = "rich" -version = "14.0.0" +version = "13.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, ] [[package]] name = "ruff" -version = "0.11.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/5b/3ae20f89777115944e89c2d8c2e795dcc5b9e04052f76d5347e35e0da66e/ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407", size = 3933063 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/db/baee59ac88f57527fcbaad3a7b309994e42329c6bc4d4d2b681a3d7b5426/ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2", size = 10106493 }, - { url = "https://files.pythonhosted.org/packages/c1/d6/9a0962cbb347f4ff98b33d699bf1193ff04ca93bed4b4222fd881b502154/ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc", size = 10876382 }, - { url = "https://files.pythonhosted.org/packages/3a/8f/62bab0c7d7e1ae3707b69b157701b41c1ccab8f83e8501734d12ea8a839f/ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906", size = 10237050 }, - { url = "https://files.pythonhosted.org/packages/09/96/e296965ae9705af19c265d4d441958ed65c0c58fc4ec340c27cc9d2a1f5b/ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f", size = 10424984 }, - { url = "https://files.pythonhosted.org/packages/e5/56/644595eb57d855afed6e54b852e2df8cd5ca94c78043b2f29bdfb29882d5/ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e", size = 9957438 }, - { url = "https://files.pythonhosted.org/packages/86/83/9d3f3bed0118aef3e871ded9e5687fb8c5776bde233427fd9ce0a45db2d4/ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223", size = 11547282 }, - { url = "https://files.pythonhosted.org/packages/40/e6/0c6e4f5ae72fac5ccb44d72c0111f294a5c2c8cc5024afcb38e6bda5f4b3/ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e", size = 12182020 }, - { url = "https://files.pythonhosted.org/packages/b5/92/4aed0e460aeb1df5ea0c2fbe8d04f9725cccdb25d8da09a0d3f5b8764bf8/ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d", size = 11679154 }, - { url = "https://files.pythonhosted.org/packages/1b/d3/7316aa2609f2c592038e2543483eafbc62a0e1a6a6965178e284808c095c/ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99", size = 13905985 }, - { url = "https://files.pythonhosted.org/packages/63/80/734d3d17546e47ff99871f44ea7540ad2bbd7a480ed197fe8a1c8a261075/ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222", size = 11348343 }, - { url = "https://files.pythonhosted.org/packages/04/7b/70fc7f09a0161dce9613a4671d198f609e653d6f4ff9eee14d64c4c240fb/ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304", size = 10308487 }, - { url = "https://files.pythonhosted.org/packages/1a/22/1cdd62dabd678d75842bf4944fd889cf794dc9e58c18cc547f9eb28f95ed/ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019", size = 9929091 }, - { url = "https://files.pythonhosted.org/packages/9f/20/40e0563506332313148e783bbc1e4276d657962cc370657b2fff20e6e058/ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896", size = 10924659 }, - { url = "https://files.pythonhosted.org/packages/b5/41/eef9b7aac8819d9e942f617f9db296f13d2c4576806d604aba8db5a753f1/ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751", size = 11428160 }, - { url = "https://files.pythonhosted.org/packages/ff/61/c488943414fb2b8754c02f3879de003e26efdd20f38167ded3fb3fc1cda3/ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270", size = 10311496 }, - { url = "https://files.pythonhosted.org/packages/b6/2b/2a1c8deb5f5dfa3871eb7daa41492c4d2b2824a74d2b38e788617612a66d/ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb", size = 11399146 }, - { url = "https://files.pythonhosted.org/packages/4f/03/3aec4846226d54a37822e4c7ea39489e4abd6f88388fba74e3d4abe77300/ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc", size = 10450306 }, +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/5d/4b5403f3e89837decfd54c51bea7f94b7d3fae77e08858603d0e04d7ad17/ruff-0.8.5.tar.gz", hash = "sha256:1098d36f69831f7ff2a1da3e6407d5fbd6dfa2559e4f74ff2d260c5588900317", size = 3454835 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f8/03391745a703ce11678eb37c48ae89ec60396ea821e9d0bcea7c8e88fd91/ruff-0.8.5-py3-none-linux_armv6l.whl", hash = "sha256:5ad11a5e3868a73ca1fa4727fe7e33735ea78b416313f4368c504dbeb69c0f88", size = 10626889 }, + { url = "https://files.pythonhosted.org/packages/55/74/83bb74a44183b904216f3edfb9995b89830c83aaa6ce84627f74da0e0cf8/ruff-0.8.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f69ab37771ea7e0715fead8624ec42996d101269a96e31f4d31be6fc33aa19b7", size = 10398233 }, + { url = "https://files.pythonhosted.org/packages/e8/7a/a162a4feb3ef85d594527165e366dde09d7a1e534186ff4ba5d127eda850/ruff-0.8.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b5462d7804558ccff9c08fe8cbf6c14b7efe67404316696a2dde48297b1925bb", size = 10001843 }, + { url = "https://files.pythonhosted.org/packages/e7/9f/5ee5dcd135411402e35b6ec6a8dfdadbd31c5cd1c36a624d356a38d76090/ruff-0.8.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d56de7220a35607f9fe59f8a6d018e14504f7b71d784d980835e20fc0611cd50", size = 10872507 }, + { url = "https://files.pythonhosted.org/packages/b6/67/db2df2dd4a34b602d7f6ebb1b3744c8157f0d3579973ffc58309c9c272e8/ruff-0.8.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d99cf80b0429cbebf31cbbf6f24f05a29706f0437c40413d950e67e2d4faca4", size = 10377200 }, + { url = "https://files.pythonhosted.org/packages/fe/ff/fe3a6a73006bced73e60d171d154a82430f61d97e787f511a24bd6302611/ruff-0.8.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b75ac29715ac60d554a049dbb0ef3b55259076181c3369d79466cb130eb5afd", size = 11433155 }, + { url = "https://files.pythonhosted.org/packages/e3/95/c1d1a1fe36658c1f3e1b47e1cd5f688b72d5786695b9e621c2c38399a95e/ruff-0.8.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c9d526a62c9eda211b38463528768fd0ada25dad524cb33c0e99fcff1c67b5dc", size = 12139227 }, + { url = "https://files.pythonhosted.org/packages/1b/fe/644b70d473a27b5112ac7a3428edcc1ce0db775c301ff11aa146f71886e0/ruff-0.8.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:587c5e95007612c26509f30acc506c874dab4c4abbacd0357400bd1aa799931b", size = 11697941 }, + { url = "https://files.pythonhosted.org/packages/00/39/4f83e517ec173e16a47c6d102cd22a1aaebe80e1208a1f2e83ab9a0e4134/ruff-0.8.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:622b82bf3429ff0e346835ec213aec0a04d9730480cbffbb6ad9372014e31bbd", size = 12967686 }, + { url = "https://files.pythonhosted.org/packages/1a/f6/52a2973ff108d74b5da706a573379eea160bece098f7cfa3f35dc4622710/ruff-0.8.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99be814d77a5dac8a8957104bdd8c359e85c86b0ee0e38dca447cb1095f70fb", size = 11253788 }, + { url = "https://files.pythonhosted.org/packages/ce/1f/3b30f3c65b1303cb8e268ec3b046b77ab21ed8e26921cfc7e8232aa57f2c/ruff-0.8.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01c048f9c3385e0fd7822ad0fd519afb282af9cf1778f3580e540629df89725", size = 10860360 }, + { url = "https://files.pythonhosted.org/packages/a5/a8/2a3ea6bacead963f7aeeba0c61815d9b27b0d638e6a74984aa5cc5d27733/ruff-0.8.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7512e8cb038db7f5db6aae0e24735ff9ea03bb0ed6ae2ce534e9baa23c1dc9ea", size = 10457922 }, + { url = "https://files.pythonhosted.org/packages/17/47/8f9514b670969aab57c5fc826fb500a16aee8feac1bcf8a91358f153a5ba/ruff-0.8.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:762f113232acd5b768d6b875d16aad6b00082add40ec91c927f0673a8ec4ede8", size = 10958347 }, + { url = "https://files.pythonhosted.org/packages/0d/d6/78a9af8209ad99541816d74f01ce678fc01ebb3f37dd7ab8966646dcd92b/ruff-0.8.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:03a90200c5dfff49e4c967b405f27fdfa81594cbb7c5ff5609e42d7fe9680da5", size = 11328882 }, + { url = "https://files.pythonhosted.org/packages/54/77/5c8072ec7afdfdf42c7a4019044486a2b6c85ee73617f8875ec94b977fed/ruff-0.8.5-py3-none-win32.whl", hash = "sha256:8710ffd57bdaa6690cbf6ecff19884b8629ec2a2a2a2f783aa94b1cc795139ed", size = 8802515 }, + { url = "https://files.pythonhosted.org/packages/bc/b6/47d2b06784de8ae992c45cceb2a30f3f205b3236a629d7ca4c0c134839a2/ruff-0.8.5-py3-none-win_amd64.whl", hash = "sha256:4020d8bf8d3a32325c77af452a9976a9ad6455773bcb94991cf15bd66b347e47", size = 9684231 }, + { url = "https://files.pythonhosted.org/packages/bf/5e/ffee22bf9f9e4b2669d1f0179ae8804584939fb6502b51f2401e26b1e028/ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb", size = 9124741 }, ] [[package]] @@ -1359,27 +1382,26 @@ wheels = [ [[package]] name = "sse-starlette" -version = "2.2.1" +version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } +sdist = { url = "https://files.pythonhosted.org/packages/40/88/0af7f586894cfe61bd212f33e571785c4570085711b24fb7445425a5eeb0/sse-starlette-1.6.1.tar.gz", hash = "sha256:6208af2bd7d0887c92f1379da14bd1f4db56bd1274cc5d36670c683d2aa1de6a", size = 14555 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, + { url = "https://files.pythonhosted.org/packages/5e/f7/499e5d0c181a52a205d5b0982fd71cf162d1e070c97dca90c60520bbf8bf/sse_starlette-1.6.1-py3-none-any.whl", hash = "sha256:d8f18f1c633e355afe61cc5e9c92eea85badcb8b2d56ec8cfb0a006994aa55da", size = 9553 }, ] [[package]] name = "starlette" -version = "0.46.1" +version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } +sdist = { url = "https://files.pythonhosted.org/packages/06/68/559bed5484e746f1ab2ebbe22312f2c25ec62e4b534916d41a8c21147bf8/starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75", size = 51394 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, + { url = "https://files.pythonhosted.org/packages/58/f8/e2cca22387965584a409795913b774235752be4176d276714e15e1a58884/starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91", size = 66978 }, ] [[package]] @@ -1435,7 +1457,7 @@ wheels = [ [[package]] name = "trio" -version = "0.29.0" +version = "0.26.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1446,14 +1468,14 @@ dependencies = [ { name = "sniffio" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/03/ab0e9509be0c6465e2773768ec25ee0cb8053c0b91471ab3854bbf2294b2/trio-0.26.2.tar.gz", hash = "sha256:0346c3852c15e5c7d40ea15972c4805689ef2cb8b5206f794c9c19450119f3a4", size = 561156 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 }, + { url = "https://files.pythonhosted.org/packages/1c/70/efa56ce2271c44a7f4f43533a0477e6854a0948e9f7b76491de1fd3be7c9/trio-0.26.2-py3-none-any.whl", hash = "sha256:c5237e8133eb0a1d72f09a971a55c28ebe69e351c783fc64bc37db8db8bbe1d0", size = 475996 }, ] [[package]] name = "typer" -version = "0.15.2" +version = "0.12.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1461,30 +1483,18 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } +sdist = { url = "https://files.pythonhosted.org/packages/d4/f7/f174a1cae84848ae8b27170a96187b91937b743f0580ff968078fe16930a/typer-0.12.4.tar.gz", hash = "sha256:c9c1613ed6a166162705b3347b8d10b661ccc5d95692654d0fb628118f2c34e6", size = 97945 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, + { url = "https://files.pythonhosted.org/packages/ae/cc/15083dcde1252a663398b1b2a173637a3ec65adadfb95137dc95df1e6adc/typer-0.12.4-py3-none-any.whl", hash = "sha256:819aa03699f438397e876aa12b0d63766864ecba1b579092cc9fe35d886e34b6", size = 47402 }, ] [[package]] name = "typing-extensions" -version = "4.13.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.0" +version = "4.12.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] [[package]] @@ -1498,16 +1508,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/f7/4ad826703a49b320a4adf2470fdd2a3481ea13f4460cb615ad12c75be003/uvicorn-0.30.0.tar.gz", hash = "sha256:f678dec4fa3a39706bbf49b9ec5fc40049d42418716cea52b53f07828a60aa37", size = 42560 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, + { url = "https://files.pythonhosted.org/packages/2a/a1/d57e38417a8dabb22df02b6aebc209dc73485792e6c5620e501547133d0b/uvicorn-0.30.0-py3-none-any.whl", hash = "sha256:78fa0b5f56abb8562024a59041caeb555c86e48d0efdd23c3fe7de7a4075bdab", size = 62388 }, ] [[package]] @@ -1608,4 +1618,4 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, -] +] \ No newline at end of file From 23665db2b8e6256addd8390a92f9b53bdfd4419c Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 8 Apr 2025 17:33:21 -0700 Subject: [PATCH 08/51] clean up test stuff --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b85f2227b..686586611 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,6 @@ mcp = FastMCP("My App") mcp = FastMCP("My App", dependencies=["pandas", "numpy"]) - @dataclass class AppContext: db: Database @@ -398,13 +397,15 @@ For more information on mounting applications in Starlette, see the [Starlette d By default, the SSE server uses an in-memory message queue for incoming POST messages. For production deployments or distributed scenarios, you can use Redis: ```python +from mcp.server.fastmcp import FastMCP + mcp = FastMCP( - "My App", + "My App", settings={ "message_queue": "redis", "redis_url": "redis://localhost:6379/0", - "redis_prefix": "mcp:queue:" - } + "redis_prefix": "mcp:queue:", + }, ) ``` From ccd5a139cbadd634daadbb838f488e0cd78cf76c Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 8 Apr 2025 17:39:06 -0700 Subject: [PATCH 09/51] lock pydantic version --- pyproject.toml | 2 +- uv.lock | 303 ++++++++++++++++++++++--------------------------- 2 files changed, 135 insertions(+), 170 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6d5e8065b..09d2197e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dependencies = [ "anyio>=4.5", "httpx>=0.27", "httpx-sse>=0.4", - "pydantic>=2.7.2,<3.0.0", + "pydantic>=2.7.2,<=2.10.1", "starlette>=0.27", "sse-starlette>=1.6.1", "pydantic-settings>=2.5.2", diff --git a/uv.lock b/uv.lock index 424e2d482..6574871c2 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,6 @@ version = 1 -revision = 1 requires-python = ">=3.10" -[options] -resolution-mode = "lowest-direct" - [manifest] members = [ "mcp", @@ -24,26 +20,35 @@ wheels = [ [[package]] name = "anyio" -version = "4.5.0" +version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/44/66874c5256e9fbc30103b31927fd9341c8da6ccafd4721b2b3e81e6ef176/anyio-4.5.0.tar.gz", hash = "sha256:c5a275fe5ca0afd788001f58fca1e69e29ce706d746e317d660e21f70c530ef9", size = 169376 } + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/68/f9e9bf6324c46e6b8396610aef90ad423ec3e18c9079547ceafea3dce0ec/anyio-4.5.0-py3-none-any.whl", hash = "sha256:fdeb095b7cc5a5563175eedd926ec4ae55413bb4be5770c424af0ba46ccb4a78", size = 89250 }, + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, ] [[package]] name = "attrs" -version = "24.3.0" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, ] [[package]] @@ -55,6 +60,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, ] +[[package]] +name = "backrefs" +version = "5.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337 }, + { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142 }, + { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021 }, + { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915 }, + { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336 }, +] + [[package]] name = "black" version = "25.1.0" @@ -119,11 +137,11 @@ wheels = [ [[package]] name = "certifi" -version = "2024.12.14" +version = "2025.1.31" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, ] [[package]] @@ -246,14 +264,14 @@ wheels = [ [[package]] name = "click" -version = "8.1.0" +version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/2b/7ebad1e59a99207d417c0784f7fb67893465eef84b5b47c788324f1b4095/click-8.1.0.tar.gz", hash = "sha256:977c213473c7665d3aa092b41ff12063227751c41d7b17165013e10069cc5cd2", size = 329986 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/3e/3a523bdd24510288b1b850428e01172116a29268378b1da9a8d0b894a115/click-8.1.0-py3-none-any.whl", hash = "sha256:19a4baa64da924c5e0cd889aba8e947f280309f1a2ce0947a3e3a7bcb7cc72d6", size = 96400 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, ] [[package]] @@ -319,14 +337,14 @@ wheels = [ [[package]] name = "griffe" -version = "1.6.2" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/f2/b00eb72b853ecb5bf31dd47857cdf6767e380ca24ec2910d43b3fa7cc500/griffe-1.6.2.tar.gz", hash = "sha256:3a46fa7bd83280909b63c12b9a975732a927dd97809efe5b7972290b606c5d91", size = 392836 } +sdist = { url = "https://files.pythonhosted.org/packages/59/08/7df7e90e34d08ad890bd71d7ba19451052f88dc3d2c483d228d1331a4736/griffe-1.7.2.tar.gz", hash = "sha256:98d396d803fab3b680c2608f300872fd57019ed82f0672f5b5323a9ad18c540c", size = 394919 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/bc/bd8b7de5e748e078b6be648e76b47189a9182b1ac1eb7791ff7969f39f27/griffe-1.6.2-py3-none-any.whl", hash = "sha256:6399f7e663150e4278a312a8e8a14d2f3d7bd86e2ef2f8056a1058e38579c2ee", size = 128638 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/38b408f41064c9fcdbb0ea27c1bd13a1c8657c4846e04dab9f5ea770602c/griffe-1.7.2-py3-none-any.whl", hash = "sha256:1ed9c2e338a75741fc82083fe5a1bc89cb6142efe126194cc313e34ee6af5423", size = 129187 }, ] [[package]] @@ -353,18 +371,17 @@ wheels = [ [[package]] name = "httpx" -version = "0.27.0" +version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, - { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/3da5bdf4408b8b2800061c339f240c1802f2e82d55e50bd39c5a881f47f0/httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5", size = 126413 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/7b/ddacf6dcebb42466abd03f368782142baa82e08fc0c1f8eaa05b4bae87d5/httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5", size = 75590 }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] [[package]] @@ -387,11 +404,11 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] [[package]] @@ -504,6 +521,9 @@ cli = [ { name = "python-dotenv" }, { name = "typer" }, ] +redis = [ + { name = "redis" }, +] rich = [ { name = "rich" }, ] @@ -533,9 +553,10 @@ requires-dist = [ { name = "anyio", specifier = ">=4.5" }, { name = "httpx", specifier = ">=0.27" }, { name = "httpx-sse", specifier = ">=0.4" }, - { name = "pydantic", specifier = ">=2.7.2,<3.0.0" }, + { name = "pydantic", specifier = ">=2.7.2,<=2.10.1" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=4.5.0" }, { name = "rich", marker = "extra == 'rich'", specifier = ">=13.9.4" }, { name = "sse-starlette", specifier = ">=1.6.1" }, { name = "starlette", specifier = ">=0.27" }, @@ -543,7 +564,6 @@ requires-dist = [ { name = "uvicorn", specifier = ">=0.23.1" }, { name = "websockets", marker = "extra == 'ws'", specifier = ">=15.0.1" }, ] -provides-extras = ["cli", "rich", "ws"] [package.metadata.requires-dev] dev = [ @@ -742,10 +762,11 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.5.45" +version = "9.6.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, + { name = "backrefs" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, @@ -754,12 +775,11 @@ dependencies = [ { name = "paginate" }, { name = "pygments" }, { name = "pymdown-extensions" }, - { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/02/38f1f76252462b8e9652eb3778905206c1f3b9b4c25bf60aafc029675a2b/mkdocs_material-9.5.45.tar.gz", hash = "sha256:286489cf0beca4a129d91d59d6417419c63bceed1ce5cd0ec1fc7e1ebffb8189", size = 3906694 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/7e/c65e330e99daa5813e7594e57a09219ad041ed631604a72588ec7c11b34b/mkdocs_material-9.6.11.tar.gz", hash = "sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff", size = 3951595 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/43/f5f866cd840e14f82068831e53446ea1f66a128cd38a229c5b9c9243ed9e/mkdocs_material-9.5.45-py3-none-any.whl", hash = "sha256:a9be237cfd0be14be75f40f1726d83aa3a81ce44808dc3594d47a7a592f44547", size = 8615700 }, + { url = "https://files.pythonhosted.org/packages/19/91/79a15a772151aca0d505f901f6bbd4b85ee1fe54100256a6702056bab121/mkdocs_material-9.6.11-py3-none-any.whl", hash = "sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263", size = 8703720 }, ] [package.optional-dependencies] @@ -779,7 +799,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.29.0" +version = "0.29.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -789,23 +809,24 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/4d/a9484dc5d926295bdf308f1f6c4f07fcc99735b970591edc414d401fcc91/mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e", size = 1212185 } +sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686 } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/47/eb876dfd84e48f31ff60897d161b309cf6a04ca270155b0662aae562b3fb/mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d", size = 1630824 }, + { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075 }, ] [[package]] name = "mkdocstrings-python" -version = "1.12.2" +version = "1.16.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/ec/cb6debe2db77f1ef42b25b21d93b5021474de3037cd82385e586aee72545/mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3", size = 168207 } +sdist = { url = "https://files.pythonhosted.org/packages/44/c8/600c4201b6b9e72bab16802316d0c90ce04089f8e6bb5e064cd2a5abba7e/mkdocstrings_python-1.16.10.tar.gz", hash = "sha256:f9eedfd98effb612ab4d0ed6dd2b73aff6eba5215e0a65cea6d877717f75502e", size = 205771 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/c1/ac524e1026d9580cbc654b5d19f5843c8b364a66d30f956372cd09fd2f92/mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a", size = 111759 }, + { url = "https://files.pythonhosted.org/packages/53/37/19549c5e0179785308cc988a68e16aa7550e4e270ec8a9878334e86070c6/mkdocstrings_python-1.16.10-py3-none-any.whl", hash = "sha256:63bb9f01f8848a644bdb6289e86dc38ceddeaa63ecc2e291e3b2ca52702a6643", size = 124112 }, ] [[package]] @@ -926,11 +947,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, ] [[package]] @@ -1042,24 +1063,24 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.6.1" +version = "2.8.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } +sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, + { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, ] [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] [[package]] @@ -1077,20 +1098,20 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.391" +version = "1.1.398" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/05/4ea52a8a45cc28897edb485b4102d37cbfd5fce8445d679cdeb62bfad221/pyright-1.1.391.tar.gz", hash = "sha256:66b2d42cdf5c3cbab05f2f4b76e8bec8aa78e679bfa0b6ad7b923d9e027cadb2", size = 21965 } +sdist = { url = "https://files.pythonhosted.org/packages/24/d6/48740f1d029e9fc4194880d1ad03dcf0ba3a8f802e0e166b8f63350b3584/pyright-1.1.398.tar.gz", hash = "sha256:357a13edd9be8082dc73be51190913e475fa41a6efb6ec0d4b7aab3bc11638d8", size = 3892675 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/89/66f49552fbeb21944c8077d11834b2201514a56fd1b7747ffff9630f1bd9/pyright-1.1.391-py3-none-any.whl", hash = "sha256:54fa186f8b3e8a55a44ebfa842636635688670c6896dcf6cf4a7fc75062f4d15", size = 18579 }, + { url = "https://files.pythonhosted.org/packages/58/e0/5283593f61b3c525d6d7e94cfb6b3ded20b3df66e953acaf7bb4f23b3f6e/pyright-1.1.398-py3-none-any.whl", hash = "sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753", size = 5780235 }, ] [[package]] name = "pytest" -version = "8.3.4" +version = "8.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1100,23 +1121,23 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, ] [[package]] name = "pytest-examples" -version = "0.0.14" +version = "0.0.17" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "black" }, { name = "pytest" }, { name = "ruff" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/a7/b81d5cf26e9713a2d4c8e6863ee009360c5c07a0cfb880456ec8b09adab7/pytest_examples-0.0.14.tar.gz", hash = "sha256:776d1910709c0c5ce01b29bfe3651c5312d5cfe5c063e23ca6f65aed9af23f09", size = 20767 } +sdist = { url = "https://files.pythonhosted.org/packages/05/b2/555d866fc26d3ab7bc49e1be7cc982dd95b62bc33d60256506634d28fc5d/pytest_examples-0.0.17.tar.gz", hash = "sha256:3f02460c10de36646dab45825659fa4735441863af8c86388c22eb6113d038d8", size = 21211 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/99/f418071551ff2b5e8c06bd8b82b1f4fd472b5e4162f018773ba4ef52b6e8/pytest_examples-0.0.14-py3-none-any.whl", hash = "sha256:867a7ea105635d395df712a4b8d0df3bda4c3d78ae97a57b4f115721952b5e25", size = 17919 }, + { url = "https://files.pythonhosted.org/packages/9d/53/c2fc4d70d1999030b1f7bc6fe1441fa0a37a686a74a9a2109a4aa5e946ce/pytest_examples-0.0.17-py3-none-any.whl", hash = "sha256:3269b8b108e248d81edead269b2abf1cb76636bd49b7d5d3b41b194634cb10e6", size = 18168 }, ] [[package]] @@ -1158,11 +1179,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/31/06/1ef763af20d0572c032fa22882cfbfb005fba6e7300715a37840858c919e/python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", size = 37399 } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/2f/62ea1c8b593f4e093cc1a7768f0d46112107e790c3e478532329e434f00b/python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a", size = 19482 }, + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] [[package]] @@ -1222,72 +1243,15 @@ wheels = [ ] [[package]] -name = "regex" -version = "2024.11.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, - { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, - { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, - { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, - { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, - { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, - { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, - { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, - { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, - { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, - { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, - { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, - { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, - { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, - { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, - { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, - { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, - { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, - { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, - { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, - { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, - { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, - { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, - { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, - { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, - { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, - { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, - { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, - { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, - { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, - { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, - { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, - { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, - { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, - { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, - { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, - { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, - { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, - { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, - { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, - { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, - { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, - { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, - { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, - { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, - { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, - { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, - { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, - { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, - { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, - { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, - { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, - { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, - { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, - { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, - { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, - { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, - { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, - { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, - { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, - { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +name = "redis" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, ] [[package]] @@ -1307,41 +1271,41 @@ wheels = [ [[package]] name = "rich" -version = "13.9.4" +version = "14.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, ] [[package]] name = "ruff" -version = "0.8.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/5d/4b5403f3e89837decfd54c51bea7f94b7d3fae77e08858603d0e04d7ad17/ruff-0.8.5.tar.gz", hash = "sha256:1098d36f69831f7ff2a1da3e6407d5fbd6dfa2559e4f74ff2d260c5588900317", size = 3454835 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/f8/03391745a703ce11678eb37c48ae89ec60396ea821e9d0bcea7c8e88fd91/ruff-0.8.5-py3-none-linux_armv6l.whl", hash = "sha256:5ad11a5e3868a73ca1fa4727fe7e33735ea78b416313f4368c504dbeb69c0f88", size = 10626889 }, - { url = "https://files.pythonhosted.org/packages/55/74/83bb74a44183b904216f3edfb9995b89830c83aaa6ce84627f74da0e0cf8/ruff-0.8.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f69ab37771ea7e0715fead8624ec42996d101269a96e31f4d31be6fc33aa19b7", size = 10398233 }, - { url = "https://files.pythonhosted.org/packages/e8/7a/a162a4feb3ef85d594527165e366dde09d7a1e534186ff4ba5d127eda850/ruff-0.8.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b5462d7804558ccff9c08fe8cbf6c14b7efe67404316696a2dde48297b1925bb", size = 10001843 }, - { url = "https://files.pythonhosted.org/packages/e7/9f/5ee5dcd135411402e35b6ec6a8dfdadbd31c5cd1c36a624d356a38d76090/ruff-0.8.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d56de7220a35607f9fe59f8a6d018e14504f7b71d784d980835e20fc0611cd50", size = 10872507 }, - { url = "https://files.pythonhosted.org/packages/b6/67/db2df2dd4a34b602d7f6ebb1b3744c8157f0d3579973ffc58309c9c272e8/ruff-0.8.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d99cf80b0429cbebf31cbbf6f24f05a29706f0437c40413d950e67e2d4faca4", size = 10377200 }, - { url = "https://files.pythonhosted.org/packages/fe/ff/fe3a6a73006bced73e60d171d154a82430f61d97e787f511a24bd6302611/ruff-0.8.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b75ac29715ac60d554a049dbb0ef3b55259076181c3369d79466cb130eb5afd", size = 11433155 }, - { url = "https://files.pythonhosted.org/packages/e3/95/c1d1a1fe36658c1f3e1b47e1cd5f688b72d5786695b9e621c2c38399a95e/ruff-0.8.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c9d526a62c9eda211b38463528768fd0ada25dad524cb33c0e99fcff1c67b5dc", size = 12139227 }, - { url = "https://files.pythonhosted.org/packages/1b/fe/644b70d473a27b5112ac7a3428edcc1ce0db775c301ff11aa146f71886e0/ruff-0.8.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:587c5e95007612c26509f30acc506c874dab4c4abbacd0357400bd1aa799931b", size = 11697941 }, - { url = "https://files.pythonhosted.org/packages/00/39/4f83e517ec173e16a47c6d102cd22a1aaebe80e1208a1f2e83ab9a0e4134/ruff-0.8.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:622b82bf3429ff0e346835ec213aec0a04d9730480cbffbb6ad9372014e31bbd", size = 12967686 }, - { url = "https://files.pythonhosted.org/packages/1a/f6/52a2973ff108d74b5da706a573379eea160bece098f7cfa3f35dc4622710/ruff-0.8.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99be814d77a5dac8a8957104bdd8c359e85c86b0ee0e38dca447cb1095f70fb", size = 11253788 }, - { url = "https://files.pythonhosted.org/packages/ce/1f/3b30f3c65b1303cb8e268ec3b046b77ab21ed8e26921cfc7e8232aa57f2c/ruff-0.8.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01c048f9c3385e0fd7822ad0fd519afb282af9cf1778f3580e540629df89725", size = 10860360 }, - { url = "https://files.pythonhosted.org/packages/a5/a8/2a3ea6bacead963f7aeeba0c61815d9b27b0d638e6a74984aa5cc5d27733/ruff-0.8.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7512e8cb038db7f5db6aae0e24735ff9ea03bb0ed6ae2ce534e9baa23c1dc9ea", size = 10457922 }, - { url = "https://files.pythonhosted.org/packages/17/47/8f9514b670969aab57c5fc826fb500a16aee8feac1bcf8a91358f153a5ba/ruff-0.8.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:762f113232acd5b768d6b875d16aad6b00082add40ec91c927f0673a8ec4ede8", size = 10958347 }, - { url = "https://files.pythonhosted.org/packages/0d/d6/78a9af8209ad99541816d74f01ce678fc01ebb3f37dd7ab8966646dcd92b/ruff-0.8.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:03a90200c5dfff49e4c967b405f27fdfa81594cbb7c5ff5609e42d7fe9680da5", size = 11328882 }, - { url = "https://files.pythonhosted.org/packages/54/77/5c8072ec7afdfdf42c7a4019044486a2b6c85ee73617f8875ec94b977fed/ruff-0.8.5-py3-none-win32.whl", hash = "sha256:8710ffd57bdaa6690cbf6ecff19884b8629ec2a2a2a2f783aa94b1cc795139ed", size = 8802515 }, - { url = "https://files.pythonhosted.org/packages/bc/b6/47d2b06784de8ae992c45cceb2a30f3f205b3236a629d7ca4c0c134839a2/ruff-0.8.5-py3-none-win_amd64.whl", hash = "sha256:4020d8bf8d3a32325c77af452a9976a9ad6455773bcb94991cf15bd66b347e47", size = 9684231 }, - { url = "https://files.pythonhosted.org/packages/bf/5e/ffee22bf9f9e4b2669d1f0179ae8804584939fb6502b51f2401e26b1e028/ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb", size = 9124741 }, +version = "0.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5b/3ae20f89777115944e89c2d8c2e795dcc5b9e04052f76d5347e35e0da66e/ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407", size = 3933063 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/db/baee59ac88f57527fcbaad3a7b309994e42329c6bc4d4d2b681a3d7b5426/ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2", size = 10106493 }, + { url = "https://files.pythonhosted.org/packages/c1/d6/9a0962cbb347f4ff98b33d699bf1193ff04ca93bed4b4222fd881b502154/ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc", size = 10876382 }, + { url = "https://files.pythonhosted.org/packages/3a/8f/62bab0c7d7e1ae3707b69b157701b41c1ccab8f83e8501734d12ea8a839f/ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906", size = 10237050 }, + { url = "https://files.pythonhosted.org/packages/09/96/e296965ae9705af19c265d4d441958ed65c0c58fc4ec340c27cc9d2a1f5b/ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f", size = 10424984 }, + { url = "https://files.pythonhosted.org/packages/e5/56/644595eb57d855afed6e54b852e2df8cd5ca94c78043b2f29bdfb29882d5/ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e", size = 9957438 }, + { url = "https://files.pythonhosted.org/packages/86/83/9d3f3bed0118aef3e871ded9e5687fb8c5776bde233427fd9ce0a45db2d4/ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223", size = 11547282 }, + { url = "https://files.pythonhosted.org/packages/40/e6/0c6e4f5ae72fac5ccb44d72c0111f294a5c2c8cc5024afcb38e6bda5f4b3/ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e", size = 12182020 }, + { url = "https://files.pythonhosted.org/packages/b5/92/4aed0e460aeb1df5ea0c2fbe8d04f9725cccdb25d8da09a0d3f5b8764bf8/ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d", size = 11679154 }, + { url = "https://files.pythonhosted.org/packages/1b/d3/7316aa2609f2c592038e2543483eafbc62a0e1a6a6965178e284808c095c/ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99", size = 13905985 }, + { url = "https://files.pythonhosted.org/packages/63/80/734d3d17546e47ff99871f44ea7540ad2bbd7a480ed197fe8a1c8a261075/ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222", size = 11348343 }, + { url = "https://files.pythonhosted.org/packages/04/7b/70fc7f09a0161dce9613a4671d198f609e653d6f4ff9eee14d64c4c240fb/ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304", size = 10308487 }, + { url = "https://files.pythonhosted.org/packages/1a/22/1cdd62dabd678d75842bf4944fd889cf794dc9e58c18cc547f9eb28f95ed/ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019", size = 9929091 }, + { url = "https://files.pythonhosted.org/packages/9f/20/40e0563506332313148e783bbc1e4276d657962cc370657b2fff20e6e058/ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896", size = 10924659 }, + { url = "https://files.pythonhosted.org/packages/b5/41/eef9b7aac8819d9e942f617f9db296f13d2c4576806d604aba8db5a753f1/ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751", size = 11428160 }, + { url = "https://files.pythonhosted.org/packages/ff/61/c488943414fb2b8754c02f3879de003e26efdd20f38167ded3fb3fc1cda3/ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270", size = 10311496 }, + { url = "https://files.pythonhosted.org/packages/b6/2b/2a1c8deb5f5dfa3871eb7daa41492c4d2b2824a74d2b38e788617612a66d/ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb", size = 11399146 }, + { url = "https://files.pythonhosted.org/packages/4f/03/3aec4846226d54a37822e4c7ea39489e4abd6f88388fba74e3d4abe77300/ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc", size = 10450306 }, ] [[package]] @@ -1382,26 +1346,27 @@ wheels = [ [[package]] name = "sse-starlette" -version = "1.6.1" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/88/0af7f586894cfe61bd212f33e571785c4570085711b24fb7445425a5eeb0/sse-starlette-1.6.1.tar.gz", hash = "sha256:6208af2bd7d0887c92f1379da14bd1f4db56bd1274cc5d36670c683d2aa1de6a", size = 14555 } +sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f7/499e5d0c181a52a205d5b0982fd71cf162d1e070c97dca90c60520bbf8bf/sse_starlette-1.6.1-py3-none-any.whl", hash = "sha256:d8f18f1c633e355afe61cc5e9c92eea85badcb8b2d56ec8cfb0a006994aa55da", size = 9553 }, + { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, ] [[package]] name = "starlette" -version = "0.27.0" +version = "0.46.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/68/559bed5484e746f1ab2ebbe22312f2c25ec62e4b534916d41a8c21147bf8/starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75", size = 51394 } +sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/f8/e2cca22387965584a409795913b774235752be4176d276714e15e1a58884/starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91", size = 66978 }, + { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, ] [[package]] @@ -1457,7 +1422,7 @@ wheels = [ [[package]] name = "trio" -version = "0.26.2" +version = "0.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1468,14 +1433,14 @@ dependencies = [ { name = "sniffio" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/03/ab0e9509be0c6465e2773768ec25ee0cb8053c0b91471ab3854bbf2294b2/trio-0.26.2.tar.gz", hash = "sha256:0346c3852c15e5c7d40ea15972c4805689ef2cb8b5206f794c9c19450119f3a4", size = 561156 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/70/efa56ce2271c44a7f4f43533a0477e6854a0948e9f7b76491de1fd3be7c9/trio-0.26.2-py3-none-any.whl", hash = "sha256:c5237e8133eb0a1d72f09a971a55c28ebe69e351c783fc64bc37db8db8bbe1d0", size = 475996 }, + { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 }, ] [[package]] name = "typer" -version = "0.12.4" +version = "0.15.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1483,18 +1448,18 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d4/f7/f174a1cae84848ae8b27170a96187b91937b743f0580ff968078fe16930a/typer-0.12.4.tar.gz", hash = "sha256:c9c1613ed6a166162705b3347b8d10b661ccc5d95692654d0fb628118f2c34e6", size = 97945 } +sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/cc/15083dcde1252a663398b1b2a173637a3ec65adadfb95137dc95df1e6adc/typer-0.12.4-py3-none-any.whl", hash = "sha256:819aa03699f438397e876aa12b0d63766864ecba1b579092cc9fe35d886e34b6", size = 47402 }, + { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, ] [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 }, ] [[package]] @@ -1508,16 +1473,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.30.0" +version = "0.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/f7/4ad826703a49b320a4adf2470fdd2a3481ea13f4460cb615ad12c75be003/uvicorn-0.30.0.tar.gz", hash = "sha256:f678dec4fa3a39706bbf49b9ec5fc40049d42418716cea52b53f07828a60aa37", size = 42560 } +sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/a1/d57e38417a8dabb22df02b6aebc209dc73485792e6c5620e501547133d0b/uvicorn-0.30.0-py3-none-any.whl", hash = "sha256:78fa0b5f56abb8562024a59041caeb555c86e48d0efdd23c3fe7de7a4075bdab", size = 62388 }, + { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, ] [[package]] @@ -1618,4 +1583,4 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, -] \ No newline at end of file +] From fb44020b1961cc4da861e697f44e5f2aa4774511 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 8 Apr 2025 17:41:39 -0700 Subject: [PATCH 10/51] fix lock --- uv.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/uv.lock b/uv.lock index 6574871c2..a29ae6b0d 100644 --- a/uv.lock +++ b/uv.lock @@ -504,6 +504,7 @@ wheels = [ [[package]] name = "mcp" +version = "1.6.1.dev15+23665db" source = { editable = "." } dependencies = [ { name = "anyio" }, From efe6da9edc5c5203c55b83740e47dbaf9f61c2da Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 14 Apr 2025 12:30:26 -0400 Subject: [PATCH 11/51] wip --- pyproject.toml | 5 +- src/mcp/server/message_queue/base.py | 116 +++----- src/mcp/server/message_queue/redis.py | 153 +++++----- src/mcp/server/sse.py | 43 +-- uv.lock | 393 ++++++++++++++++++-------- 5 files changed, 423 insertions(+), 287 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 09d2197e7..10d728904 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,18 +25,19 @@ dependencies = [ "anyio>=4.5", "httpx>=0.27", "httpx-sse>=0.4", - "pydantic>=2.7.2,<=2.10.1", + "pydantic>=2.7.2,<3.0.0", "starlette>=0.27", "sse-starlette>=1.6.1", "pydantic-settings>=2.5.2", "uvicorn>=0.23.1", + "redis==5.2.1", + "types-redis==4.6.0.20241004", ] [project.optional-dependencies] rich = ["rich>=13.9.4"] cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"] ws = ["websockets>=15.0.1"] -redis = ["redis>=4.5.0"] [project.scripts] mcp = "mcp.cli:app [cli]" diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index 6dcaf7acc..b119f4b63 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -1,63 +1,46 @@ import logging -from typing import Protocol, runtime_checkable +from typing import Protocol, runtime_checkable, Callable, Awaitable from uuid import UUID +from contextlib import asynccontextmanager import mcp.types as types logger = logging.getLogger(__name__) +MessageCallback = Callable[[types.JSONRPCMessage | Exception], Awaitable[None]] + @runtime_checkable class MessageQueue(Protocol): - """Abstract interface for an SSE message queue. + """Abstract interface for SSE messaging. - This interface allows messages to be queued and processed by any SSE server instance - enabling multiple servers to handle requests for the same session. + This interface allows messages to be published to sessions and callbacks to be + registered for message handling, enabling multiple servers to handle requests. """ - async def add_message( + async def publish_message( self, session_id: UUID, message: types.JSONRPCMessage | Exception ) -> bool: - """Add a message to the queue for the specified session. + """Publish a message for the specified session. Args: session_id: The UUID of the session this message is for - message: The message to queue - - Returns: - bool: True if message was accepted, False if session not found - """ - ... - - async def get_message( - self, session_id: UUID, timeout: float = 0.1 - ) -> types.JSONRPCMessage | Exception | None: - """Get the next message for the specified session. - - Args: - session_id: The UUID of the session to get messages for - timeout: Maximum time to wait for a message, in seconds + message: The message to publish Returns: - The next message or None if no message is available - """ - ... - - async def register_session(self, session_id: UUID) -> None: - """Register a new session with the queue. - - Args: - session_id: The UUID of the new session to register + bool: True if message was published, False if session not found """ ... - async def unregister_session(self, session_id: UUID) -> None: - """Unregister a session when it's closed. - + @asynccontextmanager + async def active_for_request(self, session_id: UUID, callback: MessageCallback): + """Request-scoped context manager that ensures the listener is active. + Args: - session_id: The UUID of the session to unregister + session_id: The UUID of the session to activate + callback: Async callback function to handle messages for this session """ - ... + yield async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists. @@ -74,57 +57,44 @@ async def session_exists(self, session_id: UUID) -> bool: class InMemoryMessageQueue: """Default in-memory implementation of the MessageQueue interface. - This implementation keeps messages in memory for - each session until they're retrieved. + This implementation immediately calls registered callbacks when messages are received. """ def __init__(self) -> None: - self._message_queues: dict[UUID, list[types.JSONRPCMessage | Exception]] = {} + self._callbacks: dict[UUID, MessageCallback] = {} self._active_sessions: set[UUID] = set() - async def add_message( + async def publish_message( self, session_id: UUID, message: types.JSONRPCMessage | Exception ) -> bool: - """Add a message to the queue for the specified session.""" - if session_id not in self._active_sessions: + """Publish a message for the specified session.""" + if not await self.session_exists(session_id): logger.warning(f"Message received for unknown session {session_id}") return False - if session_id not in self._message_queues: - self._message_queues[session_id] = [] - - self._message_queues[session_id].append(message) - logger.debug(f"Added message to queue for session {session_id}") + # Call the callback directly if registered + if session_id in self._callbacks: + await self._callbacks[session_id](message) + logger.debug(f"Called callback for session {session_id}") + else: + logger.warning(f"No callback registered for session {session_id}") + return True - async def get_message( - self, session_id: UUID, timeout: float = 0.1 - ) -> types.JSONRPCMessage | Exception | None: - """Get the next message for the specified session.""" - if session_id not in self._active_sessions: - return None - - queue = self._message_queues.get(session_id, []) - if not queue: - return None - - message = queue.pop(0) - if not queue: # Clean up empty queue - del self._message_queues[session_id] - - return message - - async def register_session(self, session_id: UUID) -> None: - """Register a new session with the queue.""" + @asynccontextmanager + async def active_for_request(self, session_id: UUID, callback: MessageCallback): + """Request-scoped context manager that ensures the listener is active.""" self._active_sessions.add(session_id) - logger.debug(f"Registered session {session_id}") - - async def unregister_session(self, session_id: UUID) -> None: - """Unregister a session when it's closed.""" - self._active_sessions.discard(session_id) - if session_id in self._message_queues: - del self._message_queues[session_id] - logger.debug(f"Unregistered session {session_id}") + self._callbacks[session_id] = callback + logger.debug(f"Registered session {session_id} with callback") + + try: + yield + finally: + self._active_sessions.discard(session_id) + if session_id in self._callbacks: + del self._callbacks[session_id] + logger.debug(f"Unregistered session {session_id}") async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index c368050f5..e2c5b6c01 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -2,10 +2,17 @@ import logging from uuid import UUID +import anyio +from anyio import CapacityLimiter import mcp.types as types +from mcp.server.message_queue.base import MessageCallback +from typing import Any, cast +from anyio import from_thread +from contextlib import asynccontextmanager + try: - import redis.asyncio as redis # type: ignore[import] + import redis.asyncio as redis except ImportError: raise ImportError( "Redis support requires the 'redis' package. " @@ -16,42 +23,102 @@ class RedisMessageQueue: - """Redis implementation of the MessageQueue interface. + """Redis implementation of the MessageQueue interface using pubsub. - This implementation uses Redis lists to store messages for each session. - Redis provides persistence and allows multiple servers to share the same queue. + This implementation uses Redis pubsub for real-time message distribution across + multiple servers handling the same sessions. """ def __init__( - self, redis_url: str = "redis://localhost:6379/0", prefix: str = "mcp:queue:" + self, redis_url: str = "redis://localhost:6379/0", prefix: str = "mcp:pubsub:" ) -> None: """Initialize Redis message queue. Args: redis_url: Redis connection string - prefix: Key prefix for Redis keys to avoid collisions + prefix: Key prefix for Redis channels to avoid collisions """ - self._redis = redis.Redis.from_url(redis_url, decode_responses=True) # type: ignore[attr-defined] + self._redis = redis.from_url(redis_url, decode_responses=True) # type: ignore + self._pubsub = self._redis.pubsub(ignore_subscribe_messages=True) # type: ignore self._prefix = prefix self._active_sessions_key = f"{prefix}active_sessions" + self._callbacks: dict[UUID, MessageCallback] = {} + self._limiter = CapacityLimiter(1) logger.debug(f"Initialized Redis message queue with URL: {redis_url}") - def _session_queue_key(self, session_id: UUID) -> str: - """Get the Redis key for a session's message queue.""" + def _session_channel(self, session_id: UUID) -> str: + """Get the Redis channel for a session.""" return f"{self._prefix}session:{session_id.hex}" - async def add_message( + @asynccontextmanager + async def active_for_request(self, session_id: UUID, callback: MessageCallback): + """Request-scoped context manager that ensures the listener task is running.""" + + await self._redis.sadd(self._active_sessions_key, session_id.hex) + self._callbacks[session_id] = callback + channel = self._session_channel(session_id) + await self._pubsub.subscribe(channel) # type: ignore + + logger.debug(f"Registered session {session_id} in Redis with callback") + async with anyio.create_task_group() as tg: + tg.start_soon(self._listen_for_messages) + try: + yield + finally: + tg.cancel_scope.cancel() + await self._pubsub.unsubscribe(channel) # type: ignore + await self._redis.srem(self._active_sessions_key, session_id.hex) + del self._callbacks[session_id] + logger.debug(f"Unregistered session {session_id} from Redis") + + async def _listen_for_messages(self) -> None: + """Background task that listens for messages on subscribed channels.""" + async with self._limiter: + while True: + message: None | dict[str, Any] = await self._pubsub.get_message( # type: ignore + ignore_subscribe_messages=True + ) + if message is not None: + # Extract session ID from channel name + channel: str = cast(str, message["channel"]) + if not channel.startswith(self._prefix): + continue + + session_hex = channel.split(":")[-1] + try: + session_id = UUID(hex=session_hex) + except ValueError: + logger.error(f"Invalid session channel: {channel}") + continue + + # Deserialize the message + data: str = cast(str, message["data"]) + msg: None | types.JSONRPCMessage | Exception = None + try: + json_data = json.loads(data) + if isinstance(json_data, dict): + json_dict: dict[str, Any] = json_data + if json_dict.get("_exception", False): + msg = Exception( + f"{json_dict['type']}: {json_dict['message']}" + ) + else: + msg = types.JSONRPCMessage.model_validate_json(data) + + if msg and session_id in self._callbacks: + from_thread.run(self._callbacks[session_id], msg) + except Exception as e: + logger.error(f"Failed to process message: {e}") + + async def publish_message( self, session_id: UUID, message: types.JSONRPCMessage | Exception ) -> bool: - """Add a message to the queue for the specified session.""" - # Check if session exists + """Publish a message for the specified session.""" if not await self.session_exists(session_id): logger.warning(f"Message received for unknown session {session_id}") return False - # Serialize the message if isinstance(message, Exception): - # For exceptions, store them as special format data = json.dumps( { "_exception": True, @@ -60,63 +127,13 @@ async def add_message( } ) else: - data = message.model_dump_json(by_alias=True, exclude_none=True) + data = message.model_dump_json() - # Push to the right side of the list (queue) - await self._redis.rpush(self._session_queue_key(session_id), data) # type: ignore[attr-defined] - logger.debug(f"Added message to Redis queue for session {session_id}") + channel = self._session_channel(session_id) + await self._redis.publish(channel, data) # type: ignore[attr-defined] + logger.debug(f"Published message to Redis channel for session {session_id}") return True - async def get_message( - self, session_id: UUID, timeout: float = 0.1 - ) -> types.JSONRPCMessage | Exception | None: - """Get the next message for the specified session.""" - # Check if session exists - if not await self.session_exists(session_id): - return None - - # Pop from the left side of the list (queue) - # Use BLPOP with timeout to avoid busy waiting - result = await self._redis.blpop([self._session_queue_key(session_id)], timeout) # type: ignore[attr-defined] - - if not result: - return None - - # result is a tuple of (key, value) - _, data = result # type: ignore[misc] - - # Deserialize the message - json_data = json.loads(data) # type: ignore[arg-type] - - # Check if it's an exception - if isinstance(json_data, dict): - exception_dict: dict[str, object] = json_data - if exception_dict.get("_exception", False): - return Exception( - f"{exception_dict['type']}: {exception_dict['message']}" - ) - - # Regular message - try: - return types.JSONRPCMessage.model_validate_json(data) # type: ignore[arg-type] - except Exception as e: - logger.error(f"Failed to deserialize message: {e}") - return None - - async def register_session(self, session_id: UUID) -> None: - """Register a new session with the queue.""" - # Add session ID to the set of active sessions - await self._redis.sadd(self._active_sessions_key, session_id.hex) # type: ignore[attr-defined] - logger.debug(f"Registered session {session_id} in Redis") - - async def unregister_session(self, session_id: UUID) -> None: - """Unregister a session when it's closed.""" - # Remove session ID from active sessions - await self._redis.srem(self._active_sessions_key, session_id.hex) # type: ignore[attr-defined] - # Delete the session's message queue - await self._redis.delete(self._session_queue_key(session_id)) # type: ignore[attr-defined] - logger.debug(f"Unregistered session {session_id} from Redis") - async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" # Explicitly annotate the result as bool to help the type checker diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 3efa9ba8c..8a0cf2751 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -101,30 +101,18 @@ async def connect_sse(self, scope: Scope, receive: Receive, send: Send): session_id = uuid4() session_uri = f"{quote(self._endpoint)}?session_id={session_id.hex}" - await self._message_queue.register_session(session_id) + + async def message_callback(message: types.JSONRPCMessage | Exception) -> None: + """Callback that receives messages from the message queue""" + logger.debug(f"Got message from queue for session {session_id}") + await read_stream_writer.send(message) + logger.debug(f"Created new session with ID: {session_id}") sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[ dict[str, Any] ](0) - message_polling_active = True - - async def poll_queue(): - """Background task to poll for messages in the queue""" - logger.debug(f"Starting queue polling for session {session_id}") - try: - while message_polling_active: - message = await self._message_queue.get_message(session_id) - if message: - logger.debug(f"Got message from queue for session {session_id}") - await read_stream_writer.send(message) - await anyio.sleep(0.01) - except Exception as e: - logger.error(f"Error in queue polling for session {session_id}: {e}") - finally: - logger.debug(f"Stopped queue polling for session {session_id}") - async def sse_writer(): logger.debug("Starting SSE writer") async with sse_stream_writer, write_stream_reader: @@ -148,14 +136,13 @@ async def sse_writer(): ) logger.debug("Starting SSE response task") tg.start_soon(response, scope, receive, send) - tg.start_soon(poll_queue) - try: - logger.debug("Yielding read and write streams") - yield (read_stream, write_stream) - finally: - message_polling_active = False - await self._message_queue.unregister_session(session_id) + async with self._message_queue.active_for_request(session_id, message_callback): + try: + logger.debug("Yielding read and write streams") + yield (read_stream, write_stream) + finally: + logger.debug(f"Closing SSE connection for session {session_id}") async def handle_post_message( self, scope: Scope, receive: Receive, send: Send @@ -192,10 +179,10 @@ async def handle_post_message( logger.error(f"Failed to parse message: {err}") response = Response("Could not parse message", status_code=400) await response(scope, receive, send) - await self._message_queue.add_message(session_id, err) + await self._message_queue.publish_message(session_id, err) return - logger.debug(f"Adding message to queue for session {session_id}: {message}") + logger.debug(f"Publishing message for session {session_id}: {message}") response = Response("Accepted", status_code=202) await response(scope, receive, send) - await self._message_queue.add_message(session_id, message) + await self._message_queue.publish_message(session_id, message) diff --git a/uv.lock b/uv.lock index a29ae6b0d..83b387549 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,9 @@ version = 1 requires-python = ">=3.10" +[options] +resolution-mode = "lowest-direct" + [manifest] members = [ "mcp", @@ -20,17 +23,17 @@ wheels = [ [[package]] name = "anyio" -version = "4.9.0" +version = "4.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/44/66874c5256e9fbc30103b31927fd9341c8da6ccafd4721b2b3e81e6ef176/anyio-4.5.0.tar.gz", hash = "sha256:c5a275fe5ca0afd788001f58fca1e69e29ce706d746e317d660e21f70c530ef9", size = 169376 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, + { url = "https://files.pythonhosted.org/packages/3b/68/f9e9bf6324c46e6b8396610aef90ad423ec3e18c9079547ceafea3dce0ec/anyio-4.5.0-py3-none-any.whl", hash = "sha256:fdeb095b7cc5a5563175eedd926ec4ae55413bb4be5770c424af0ba46ccb4a78", size = 89250 }, ] [[package]] @@ -44,11 +47,11 @@ wheels = [ [[package]] name = "attrs" -version = "25.3.0" +version = "24.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, + { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, ] [[package]] @@ -60,19 +63,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, ] -[[package]] -name = "backrefs" -version = "5.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337 }, - { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142 }, - { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021 }, - { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915 }, - { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336 }, -] - [[package]] name = "black" version = "25.1.0" @@ -137,11 +127,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.1.31" +version = "2024.12.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, ] [[package]] @@ -264,14 +254,14 @@ wheels = [ [[package]] name = "click" -version = "8.1.8" +version = "8.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +sdist = { url = "https://files.pythonhosted.org/packages/45/2b/7ebad1e59a99207d417c0784f7fb67893465eef84b5b47c788324f1b4095/click-8.1.0.tar.gz", hash = "sha256:977c213473c7665d3aa092b41ff12063227751c41d7b17165013e10069cc5cd2", size = 329986 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, + { url = "https://files.pythonhosted.org/packages/86/3e/3a523bdd24510288b1b850428e01172116a29268378b1da9a8d0b894a115/click-8.1.0-py3-none-any.whl", hash = "sha256:19a4baa64da924c5e0cd889aba8e947f280309f1a2ce0947a3e3a7bcb7cc72d6", size = 96400 }, ] [[package]] @@ -283,6 +273,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "cryptography" +version = "44.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 }, + { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 }, + { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 }, + { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 }, + { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 }, + { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 }, + { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 }, + { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 }, + { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 }, + { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 }, + { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 }, + { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 }, + { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 }, + { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 }, + { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 }, + { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 }, + { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 }, + { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 }, + { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 }, + { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 }, + { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 }, + { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 }, + { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 }, + { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 }, + { url = "https://files.pythonhosted.org/packages/99/10/173be140714d2ebaea8b641ff801cbcb3ef23101a2981cbf08057876f89e/cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", size = 3396886 }, + { url = "https://files.pythonhosted.org/packages/2f/b4/424ea2d0fce08c24ede307cead3409ecbfc2f566725d4701b9754c0a1174/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", size = 3892387 }, + { url = "https://files.pythonhosted.org/packages/28/20/8eaa1a4f7c68a1cb15019dbaad59c812d4df4fac6fd5f7b0b9c5177f1edd/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", size = 4109922 }, + { url = "https://files.pythonhosted.org/packages/11/25/5ed9a17d532c32b3bc81cc294d21a36c772d053981c22bd678396bc4ae30/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", size = 3895715 }, + { url = "https://files.pythonhosted.org/packages/63/31/2aac03b19c6329b62c45ba4e091f9de0b8f687e1b0cd84f101401bece343/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", size = 4109876 }, + { url = "https://files.pythonhosted.org/packages/99/ec/6e560908349843718db1a782673f36852952d52a55ab14e46c42c8a7690a/cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", size = 3131719 }, + { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513 }, + { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432 }, + { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421 }, + { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081 }, +] + [[package]] name = "cssselect2" version = "0.8.0" @@ -337,14 +372,14 @@ wheels = [ [[package]] name = "griffe" -version = "1.7.2" +version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/08/7df7e90e34d08ad890bd71d7ba19451052f88dc3d2c483d228d1331a4736/griffe-1.7.2.tar.gz", hash = "sha256:98d396d803fab3b680c2608f300872fd57019ed82f0672f5b5323a9ad18c540c", size = 394919 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/f2/b00eb72b853ecb5bf31dd47857cdf6767e380ca24ec2910d43b3fa7cc500/griffe-1.6.2.tar.gz", hash = "sha256:3a46fa7bd83280909b63c12b9a975732a927dd97809efe5b7972290b606c5d91", size = 392836 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/5e/38b408f41064c9fcdbb0ea27c1bd13a1c8657c4846e04dab9f5ea770602c/griffe-1.7.2-py3-none-any.whl", hash = "sha256:1ed9c2e338a75741fc82083fe5a1bc89cb6142efe126194cc313e34ee6af5423", size = 129187 }, + { url = "https://files.pythonhosted.org/packages/4e/bc/bd8b7de5e748e078b6be648e76b47189a9182b1ac1eb7791ff7969f39f27/griffe-1.6.2-py3-none-any.whl", hash = "sha256:6399f7e663150e4278a312a8e8a14d2f3d7bd86e2ef2f8056a1058e38579c2ee", size = 128638 }, ] [[package]] @@ -371,17 +406,18 @@ wheels = [ [[package]] name = "httpx" -version = "0.28.1" +version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, + { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/3da5bdf4408b8b2800061c339f240c1802f2e82d55e50bd39c5a881f47f0/httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5", size = 126413 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, + { url = "https://files.pythonhosted.org/packages/41/7b/ddacf6dcebb42466abd03f368782142baa82e08fc0c1f8eaa05b4bae87d5/httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5", size = 75590 }, ] [[package]] @@ -404,11 +440,11 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, ] [[package]] @@ -504,7 +540,6 @@ wheels = [ [[package]] name = "mcp" -version = "1.6.1.dev15+23665db" source = { editable = "." } dependencies = [ { name = "anyio" }, @@ -512,8 +547,10 @@ dependencies = [ { name = "httpx-sse" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "redis" }, { name = "sse-starlette" }, { name = "starlette" }, + { name = "types-redis" }, { name = "uvicorn" }, ] @@ -522,9 +559,6 @@ cli = [ { name = "python-dotenv" }, { name = "typer" }, ] -redis = [ - { name = "redis" }, -] rich = [ { name = "rich" }, ] @@ -554,14 +588,15 @@ requires-dist = [ { name = "anyio", specifier = ">=4.5" }, { name = "httpx", specifier = ">=0.27" }, { name = "httpx-sse", specifier = ">=0.4" }, - { name = "pydantic", specifier = ">=2.7.2,<=2.10.1" }, + { name = "pydantic", specifier = ">=2.7.2,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" }, - { name = "redis", marker = "extra == 'redis'", specifier = ">=4.5.0" }, + { name = "redis", specifier = "==5.2.1" }, { name = "rich", marker = "extra == 'rich'", specifier = ">=13.9.4" }, { name = "sse-starlette", specifier = ">=1.6.1" }, { name = "starlette", specifier = ">=0.27" }, { name = "typer", marker = "extra == 'cli'", specifier = ">=0.12.4" }, + { name = "types-redis", specifier = "==4.6.0.20241004" }, { name = "uvicorn", specifier = ">=0.23.1" }, { name = "websockets", marker = "extra == 'ws'", specifier = ">=15.0.1" }, ] @@ -763,11 +798,10 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.6.11" +version = "9.5.45" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, - { name = "backrefs" }, { name = "colorama" }, { name = "jinja2" }, { name = "markdown" }, @@ -776,11 +810,12 @@ dependencies = [ { name = "paginate" }, { name = "pygments" }, { name = "pymdown-extensions" }, + { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/7e/c65e330e99daa5813e7594e57a09219ad041ed631604a72588ec7c11b34b/mkdocs_material-9.6.11.tar.gz", hash = "sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff", size = 3951595 } +sdist = { url = "https://files.pythonhosted.org/packages/02/02/38f1f76252462b8e9652eb3778905206c1f3b9b4c25bf60aafc029675a2b/mkdocs_material-9.5.45.tar.gz", hash = "sha256:286489cf0beca4a129d91d59d6417419c63bceed1ce5cd0ec1fc7e1ebffb8189", size = 3906694 } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/91/79a15a772151aca0d505f901f6bbd4b85ee1fe54100256a6702056bab121/mkdocs_material-9.6.11-py3-none-any.whl", hash = "sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263", size = 8703720 }, + { url = "https://files.pythonhosted.org/packages/5c/43/f5f866cd840e14f82068831e53446ea1f66a128cd38a229c5b9c9243ed9e/mkdocs_material-9.5.45-py3-none-any.whl", hash = "sha256:a9be237cfd0be14be75f40f1726d83aa3a81ce44808dc3594d47a7a592f44547", size = 8615700 }, ] [package.optional-dependencies] @@ -800,7 +835,7 @@ wheels = [ [[package]] name = "mkdocstrings" -version = "0.29.1" +version = "0.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -810,24 +845,23 @@ dependencies = [ { name = "mkdocs-autorefs" }, { name = "pymdown-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/4d/a9484dc5d926295bdf308f1f6c4f07fcc99735b970591edc414d401fcc91/mkdocstrings-0.29.0.tar.gz", hash = "sha256:3657be1384543ce0ee82112c3e521bbf48e41303aa0c229b9ffcccba057d922e", size = 1212185 } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075 }, + { url = "https://files.pythonhosted.org/packages/15/47/eb876dfd84e48f31ff60897d161b309cf6a04ca270155b0662aae562b3fb/mkdocstrings-0.29.0-py3-none-any.whl", hash = "sha256:8ea98358d2006f60befa940fdebbbc88a26b37ecbcded10be726ba359284f73d", size = 1630824 }, ] [[package]] name = "mkdocstrings-python" -version = "1.16.10" +version = "1.12.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "griffe" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/44/c8/600c4201b6b9e72bab16802316d0c90ce04089f8e6bb5e064cd2a5abba7e/mkdocstrings_python-1.16.10.tar.gz", hash = "sha256:f9eedfd98effb612ab4d0ed6dd2b73aff6eba5215e0a65cea6d877717f75502e", size = 205771 } +sdist = { url = "https://files.pythonhosted.org/packages/23/ec/cb6debe2db77f1ef42b25b21d93b5021474de3037cd82385e586aee72545/mkdocstrings_python-1.12.2.tar.gz", hash = "sha256:7a1760941c0b52a2cd87b960a9e21112ffe52e7df9d0b9583d04d47ed2e186f3", size = 168207 } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/37/19549c5e0179785308cc988a68e16aa7550e4e270ec8a9878334e86070c6/mkdocstrings_python-1.16.10-py3-none-any.whl", hash = "sha256:63bb9f01f8848a644bdb6289e86dc38ceddeaa63ecc2e291e3b2ca52702a6643", size = 124112 }, + { url = "https://files.pythonhosted.org/packages/5b/c1/ac524e1026d9580cbc654b5d19f5843c8b364a66d30f956372cd09fd2f92/mkdocstrings_python-1.12.2-py3-none-any.whl", hash = "sha256:7f7d40d6db3cb1f5d19dbcd80e3efe4d0ba32b073272c0c0de9de2e604eda62a", size = 111759 }, ] [[package]] @@ -948,11 +982,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.7" +version = "4.3.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, ] [[package]] @@ -1064,24 +1098,24 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.8.1" +version = "2.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 } +sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 }, + { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, ] [[package]] name = "pygments" -version = "2.19.1" +version = "2.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, ] [[package]] @@ -1099,20 +1133,20 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.398" +version = "1.1.391" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/24/d6/48740f1d029e9fc4194880d1ad03dcf0ba3a8f802e0e166b8f63350b3584/pyright-1.1.398.tar.gz", hash = "sha256:357a13edd9be8082dc73be51190913e475fa41a6efb6ec0d4b7aab3bc11638d8", size = 3892675 } +sdist = { url = "https://files.pythonhosted.org/packages/11/05/4ea52a8a45cc28897edb485b4102d37cbfd5fce8445d679cdeb62bfad221/pyright-1.1.391.tar.gz", hash = "sha256:66b2d42cdf5c3cbab05f2f4b76e8bec8aa78e679bfa0b6ad7b923d9e027cadb2", size = 21965 } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/e0/5283593f61b3c525d6d7e94cfb6b3ded20b3df66e953acaf7bb4f23b3f6e/pyright-1.1.398-py3-none-any.whl", hash = "sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753", size = 5780235 }, + { url = "https://files.pythonhosted.org/packages/ad/89/66f49552fbeb21944c8077d11834b2201514a56fd1b7747ffff9630f1bd9/pyright-1.1.391-py3-none-any.whl", hash = "sha256:54fa186f8b3e8a55a44ebfa842636635688670c6896dcf6cf4a7fc75062f4d15", size = 18579 }, ] [[package]] name = "pytest" -version = "8.3.5" +version = "8.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1122,23 +1156,23 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, ] [[package]] name = "pytest-examples" -version = "0.0.17" +version = "0.0.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "black" }, { name = "pytest" }, { name = "ruff" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/b2/555d866fc26d3ab7bc49e1be7cc982dd95b62bc33d60256506634d28fc5d/pytest_examples-0.0.17.tar.gz", hash = "sha256:3f02460c10de36646dab45825659fa4735441863af8c86388c22eb6113d038d8", size = 21211 } +sdist = { url = "https://files.pythonhosted.org/packages/d2/a7/b81d5cf26e9713a2d4c8e6863ee009360c5c07a0cfb880456ec8b09adab7/pytest_examples-0.0.14.tar.gz", hash = "sha256:776d1910709c0c5ce01b29bfe3651c5312d5cfe5c063e23ca6f65aed9af23f09", size = 20767 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/53/c2fc4d70d1999030b1f7bc6fe1441fa0a37a686a74a9a2109a4aa5e946ce/pytest_examples-0.0.17-py3-none-any.whl", hash = "sha256:3269b8b108e248d81edead269b2abf1cb76636bd49b7d5d3b41b194634cb10e6", size = 18168 }, + { url = "https://files.pythonhosted.org/packages/2b/99/f418071551ff2b5e8c06bd8b82b1f4fd472b5e4162f018773ba4ef52b6e8/pytest_examples-0.0.14-py3-none-any.whl", hash = "sha256:867a7ea105635d395df712a4b8d0df3bda4c3d78ae97a57b4f115721952b5e25", size = 17919 }, ] [[package]] @@ -1180,11 +1214,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.1.0" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } +sdist = { url = "https://files.pythonhosted.org/packages/31/06/1ef763af20d0572c032fa22882cfbfb005fba6e7300715a37840858c919e/python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", size = 37399 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, + { url = "https://files.pythonhosted.org/packages/44/2f/62ea1c8b593f4e093cc1a7768f0d46112107e790c3e478532329e434f00b/python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a", size = 19482 }, ] [[package]] @@ -1255,6 +1289,75 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, ] +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/3c/4651f6b130c6842a8f3df82461a8950f923925db8b6961063e82744bddcc/regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91", size = 482674 }, + { url = "https://files.pythonhosted.org/packages/15/51/9f35d12da8434b489c7b7bffc205c474a0a9432a889457026e9bc06a297a/regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/bd/18/b731f5510d1b8fb63c6b6d3484bfa9a59b84cc578ac8b5172970e05ae07c/regex-2024.11.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164d8b7b3b4bcb2068b97428060b2a53be050085ef94eca7f240e7947f1b080e", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/78/a2/6dd36e16341ab95e4c6073426561b9bfdeb1a9c9b63ab1b579c2e96cb105/regex-2024.11.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3660c82f209655a06b587d55e723f0b813d3a7db2e32e5e7dc64ac2a9e86fde", size = 782511 }, + { url = "https://files.pythonhosted.org/packages/1b/2b/323e72d5d2fd8de0d9baa443e1ed70363ed7e7b2fb526f5950c5cb99c364/regex-2024.11.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d22326fcdef5e08c154280b71163ced384b428343ae16a5ab2b3354aed12436e", size = 821149 }, + { url = "https://files.pythonhosted.org/packages/90/30/63373b9ea468fbef8a907fd273e5c329b8c9535fee36fc8dba5fecac475d/regex-2024.11.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1ac758ef6aebfc8943560194e9fd0fa18bcb34d89fd8bd2af18183afd8da3a2", size = 809707 }, + { url = "https://files.pythonhosted.org/packages/f2/98/26d3830875b53071f1f0ae6d547f1d98e964dd29ad35cbf94439120bb67a/regex-2024.11.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:997d6a487ff00807ba810e0f8332c18b4eb8d29463cfb7c820dc4b6e7562d0cf", size = 781702 }, + { url = "https://files.pythonhosted.org/packages/87/55/eb2a068334274db86208ab9d5599ffa63631b9f0f67ed70ea7c82a69bbc8/regex-2024.11.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a02d2bb04fec86ad61f3ea7f49c015a0681bf76abb9857f945d26159d2968c", size = 771976 }, + { url = "https://files.pythonhosted.org/packages/74/c0/be707bcfe98254d8f9d2cff55d216e946f4ea48ad2fd8cf1428f8c5332ba/regex-2024.11.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f02f93b92358ee3f78660e43b4b0091229260c5d5c408d17d60bf26b6c900e86", size = 697397 }, + { url = "https://files.pythonhosted.org/packages/49/dc/bb45572ceb49e0f6509f7596e4ba7031f6819ecb26bc7610979af5a77f45/regex-2024.11.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06eb1be98df10e81ebaded73fcd51989dcf534e3c753466e4b60c4697a003b67", size = 768726 }, + { url = "https://files.pythonhosted.org/packages/5a/db/f43fd75dc4c0c2d96d0881967897926942e935d700863666f3c844a72ce6/regex-2024.11.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:040df6fe1a5504eb0f04f048e6d09cd7c7110fef851d7c567a6b6e09942feb7d", size = 775098 }, + { url = "https://files.pythonhosted.org/packages/99/d7/f94154db29ab5a89d69ff893159b19ada89e76b915c1293e98603d39838c/regex-2024.11.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabbfc59f2c6edba2a6622c647b716e34e8e3867e0ab975412c5c2f79b82da2", size = 839325 }, + { url = "https://files.pythonhosted.org/packages/f7/17/3cbfab1f23356fbbf07708220ab438a7efa1e0f34195bf857433f79f1788/regex-2024.11.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8447d2d39b5abe381419319f942de20b7ecd60ce86f16a23b0698f22e1b70008", size = 843277 }, + { url = "https://files.pythonhosted.org/packages/7e/f2/48b393b51900456155de3ad001900f94298965e1cad1c772b87f9cfea011/regex-2024.11.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da8f5fc57d1933de22a9e23eec290a0d8a5927a5370d24bda9a6abe50683fe62", size = 773197 }, + { url = "https://files.pythonhosted.org/packages/45/3f/ef9589aba93e084cd3f8471fded352826dcae8489b650d0b9b27bc5bba8a/regex-2024.11.6-cp310-cp310-win32.whl", hash = "sha256:b489578720afb782f6ccf2840920f3a32e31ba28a4b162e13900c3e6bd3f930e", size = 261714 }, + { url = "https://files.pythonhosted.org/packages/42/7e/5f1b92c8468290c465fd50c5318da64319133231415a8aa6ea5ab995a815/regex-2024.11.6-cp310-cp310-win_amd64.whl", hash = "sha256:5071b2093e793357c9d8b2929dfc13ac5f0a6c650559503bb81189d0a3814519", size = 274042 }, + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + [[package]] name = "requests" version = "2.32.3" @@ -1272,41 +1375,50 @@ wheels = [ [[package]] name = "rich" -version = "14.0.0" +version = "13.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, ] [[package]] name = "ruff" -version = "0.11.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/5b/3ae20f89777115944e89c2d8c2e795dcc5b9e04052f76d5347e35e0da66e/ruff-0.11.4.tar.gz", hash = "sha256:f45bd2fb1a56a5a85fae3b95add03fb185a0b30cf47f5edc92aa0355ca1d7407", size = 3933063 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/db/baee59ac88f57527fcbaad3a7b309994e42329c6bc4d4d2b681a3d7b5426/ruff-0.11.4-py3-none-linux_armv6l.whl", hash = "sha256:d9f4a761ecbde448a2d3e12fb398647c7f0bf526dbc354a643ec505965824ed2", size = 10106493 }, - { url = "https://files.pythonhosted.org/packages/c1/d6/9a0962cbb347f4ff98b33d699bf1193ff04ca93bed4b4222fd881b502154/ruff-0.11.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8c1747d903447d45ca3d40c794d1a56458c51e5cc1bc77b7b64bd2cf0b1626cc", size = 10876382 }, - { url = "https://files.pythonhosted.org/packages/3a/8f/62bab0c7d7e1ae3707b69b157701b41c1ccab8f83e8501734d12ea8a839f/ruff-0.11.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:51a6494209cacca79e121e9b244dc30d3414dac8cc5afb93f852173a2ecfc906", size = 10237050 }, - { url = "https://files.pythonhosted.org/packages/09/96/e296965ae9705af19c265d4d441958ed65c0c58fc4ec340c27cc9d2a1f5b/ruff-0.11.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f171605f65f4fc49c87f41b456e882cd0c89e4ac9d58e149a2b07930e1d466f", size = 10424984 }, - { url = "https://files.pythonhosted.org/packages/e5/56/644595eb57d855afed6e54b852e2df8cd5ca94c78043b2f29bdfb29882d5/ruff-0.11.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebf99ea9af918878e6ce42098981fc8c1db3850fef2f1ada69fb1dcdb0f8e79e", size = 9957438 }, - { url = "https://files.pythonhosted.org/packages/86/83/9d3f3bed0118aef3e871ded9e5687fb8c5776bde233427fd9ce0a45db2d4/ruff-0.11.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edad2eac42279df12e176564a23fc6f4aaeeb09abba840627780b1bb11a9d223", size = 11547282 }, - { url = "https://files.pythonhosted.org/packages/40/e6/0c6e4f5ae72fac5ccb44d72c0111f294a5c2c8cc5024afcb38e6bda5f4b3/ruff-0.11.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f103a848be9ff379fc19b5d656c1f911d0a0b4e3e0424f9532ececf319a4296e", size = 12182020 }, - { url = "https://files.pythonhosted.org/packages/b5/92/4aed0e460aeb1df5ea0c2fbe8d04f9725cccdb25d8da09a0d3f5b8764bf8/ruff-0.11.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:193e6fac6eb60cc97b9f728e953c21cc38a20077ed64f912e9d62b97487f3f2d", size = 11679154 }, - { url = "https://files.pythonhosted.org/packages/1b/d3/7316aa2609f2c592038e2543483eafbc62a0e1a6a6965178e284808c095c/ruff-0.11.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7af4e5f69b7c138be8dcffa5b4a061bf6ba6a3301f632a6bce25d45daff9bc99", size = 13905985 }, - { url = "https://files.pythonhosted.org/packages/63/80/734d3d17546e47ff99871f44ea7540ad2bbd7a480ed197fe8a1c8a261075/ruff-0.11.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126b1bf13154aa18ae2d6c3c5efe144ec14b97c60844cfa6eb960c2a05188222", size = 11348343 }, - { url = "https://files.pythonhosted.org/packages/04/7b/70fc7f09a0161dce9613a4671d198f609e653d6f4ff9eee14d64c4c240fb/ruff-0.11.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8806daaf9dfa881a0ed603f8a0e364e4f11b6ed461b56cae2b1c0cab0645304", size = 10308487 }, - { url = "https://files.pythonhosted.org/packages/1a/22/1cdd62dabd678d75842bf4944fd889cf794dc9e58c18cc547f9eb28f95ed/ruff-0.11.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5d94bb1cc2fc94a769b0eb975344f1b1f3d294da1da9ddbb5a77665feb3a3019", size = 9929091 }, - { url = "https://files.pythonhosted.org/packages/9f/20/40e0563506332313148e783bbc1e4276d657962cc370657b2fff20e6e058/ruff-0.11.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:995071203d0fe2183fc7a268766fd7603afb9996785f086b0d76edee8755c896", size = 10924659 }, - { url = "https://files.pythonhosted.org/packages/b5/41/eef9b7aac8819d9e942f617f9db296f13d2c4576806d604aba8db5a753f1/ruff-0.11.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a37ca937e307ea18156e775a6ac6e02f34b99e8c23fe63c1996185a4efe0751", size = 11428160 }, - { url = "https://files.pythonhosted.org/packages/ff/61/c488943414fb2b8754c02f3879de003e26efdd20f38167ded3fb3fc1cda3/ruff-0.11.4-py3-none-win32.whl", hash = "sha256:0e9365a7dff9b93af933dab8aebce53b72d8f815e131796268709890b4a83270", size = 10311496 }, - { url = "https://files.pythonhosted.org/packages/b6/2b/2a1c8deb5f5dfa3871eb7daa41492c4d2b2824a74d2b38e788617612a66d/ruff-0.11.4-py3-none-win_amd64.whl", hash = "sha256:5a9fa1c69c7815e39fcfb3646bbfd7f528fa8e2d4bebdcf4c2bd0fa037a255fb", size = 11399146 }, - { url = "https://files.pythonhosted.org/packages/4f/03/3aec4846226d54a37822e4c7ea39489e4abd6f88388fba74e3d4abe77300/ruff-0.11.4-py3-none-win_arm64.whl", hash = "sha256:d435db6b9b93d02934cf61ef332e66af82da6d8c69aefdea5994c89997c7a0fc", size = 10450306 }, +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/5d/4b5403f3e89837decfd54c51bea7f94b7d3fae77e08858603d0e04d7ad17/ruff-0.8.5.tar.gz", hash = "sha256:1098d36f69831f7ff2a1da3e6407d5fbd6dfa2559e4f74ff2d260c5588900317", size = 3454835 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f8/03391745a703ce11678eb37c48ae89ec60396ea821e9d0bcea7c8e88fd91/ruff-0.8.5-py3-none-linux_armv6l.whl", hash = "sha256:5ad11a5e3868a73ca1fa4727fe7e33735ea78b416313f4368c504dbeb69c0f88", size = 10626889 }, + { url = "https://files.pythonhosted.org/packages/55/74/83bb74a44183b904216f3edfb9995b89830c83aaa6ce84627f74da0e0cf8/ruff-0.8.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f69ab37771ea7e0715fead8624ec42996d101269a96e31f4d31be6fc33aa19b7", size = 10398233 }, + { url = "https://files.pythonhosted.org/packages/e8/7a/a162a4feb3ef85d594527165e366dde09d7a1e534186ff4ba5d127eda850/ruff-0.8.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b5462d7804558ccff9c08fe8cbf6c14b7efe67404316696a2dde48297b1925bb", size = 10001843 }, + { url = "https://files.pythonhosted.org/packages/e7/9f/5ee5dcd135411402e35b6ec6a8dfdadbd31c5cd1c36a624d356a38d76090/ruff-0.8.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d56de7220a35607f9fe59f8a6d018e14504f7b71d784d980835e20fc0611cd50", size = 10872507 }, + { url = "https://files.pythonhosted.org/packages/b6/67/db2df2dd4a34b602d7f6ebb1b3744c8157f0d3579973ffc58309c9c272e8/ruff-0.8.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d99cf80b0429cbebf31cbbf6f24f05a29706f0437c40413d950e67e2d4faca4", size = 10377200 }, + { url = "https://files.pythonhosted.org/packages/fe/ff/fe3a6a73006bced73e60d171d154a82430f61d97e787f511a24bd6302611/ruff-0.8.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b75ac29715ac60d554a049dbb0ef3b55259076181c3369d79466cb130eb5afd", size = 11433155 }, + { url = "https://files.pythonhosted.org/packages/e3/95/c1d1a1fe36658c1f3e1b47e1cd5f688b72d5786695b9e621c2c38399a95e/ruff-0.8.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c9d526a62c9eda211b38463528768fd0ada25dad524cb33c0e99fcff1c67b5dc", size = 12139227 }, + { url = "https://files.pythonhosted.org/packages/1b/fe/644b70d473a27b5112ac7a3428edcc1ce0db775c301ff11aa146f71886e0/ruff-0.8.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:587c5e95007612c26509f30acc506c874dab4c4abbacd0357400bd1aa799931b", size = 11697941 }, + { url = "https://files.pythonhosted.org/packages/00/39/4f83e517ec173e16a47c6d102cd22a1aaebe80e1208a1f2e83ab9a0e4134/ruff-0.8.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:622b82bf3429ff0e346835ec213aec0a04d9730480cbffbb6ad9372014e31bbd", size = 12967686 }, + { url = "https://files.pythonhosted.org/packages/1a/f6/52a2973ff108d74b5da706a573379eea160bece098f7cfa3f35dc4622710/ruff-0.8.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99be814d77a5dac8a8957104bdd8c359e85c86b0ee0e38dca447cb1095f70fb", size = 11253788 }, + { url = "https://files.pythonhosted.org/packages/ce/1f/3b30f3c65b1303cb8e268ec3b046b77ab21ed8e26921cfc7e8232aa57f2c/ruff-0.8.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c01c048f9c3385e0fd7822ad0fd519afb282af9cf1778f3580e540629df89725", size = 10860360 }, + { url = "https://files.pythonhosted.org/packages/a5/a8/2a3ea6bacead963f7aeeba0c61815d9b27b0d638e6a74984aa5cc5d27733/ruff-0.8.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7512e8cb038db7f5db6aae0e24735ff9ea03bb0ed6ae2ce534e9baa23c1dc9ea", size = 10457922 }, + { url = "https://files.pythonhosted.org/packages/17/47/8f9514b670969aab57c5fc826fb500a16aee8feac1bcf8a91358f153a5ba/ruff-0.8.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:762f113232acd5b768d6b875d16aad6b00082add40ec91c927f0673a8ec4ede8", size = 10958347 }, + { url = "https://files.pythonhosted.org/packages/0d/d6/78a9af8209ad99541816d74f01ce678fc01ebb3f37dd7ab8966646dcd92b/ruff-0.8.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:03a90200c5dfff49e4c967b405f27fdfa81594cbb7c5ff5609e42d7fe9680da5", size = 11328882 }, + { url = "https://files.pythonhosted.org/packages/54/77/5c8072ec7afdfdf42c7a4019044486a2b6c85ee73617f8875ec94b977fed/ruff-0.8.5-py3-none-win32.whl", hash = "sha256:8710ffd57bdaa6690cbf6ecff19884b8629ec2a2a2a2f783aa94b1cc795139ed", size = 8802515 }, + { url = "https://files.pythonhosted.org/packages/bc/b6/47d2b06784de8ae992c45cceb2a30f3f205b3236a629d7ca4c0c134839a2/ruff-0.8.5-py3-none-win_amd64.whl", hash = "sha256:4020d8bf8d3a32325c77af452a9976a9ad6455773bcb94991cf15bd66b347e47", size = 9684231 }, + { url = "https://files.pythonhosted.org/packages/bf/5e/ffee22bf9f9e4b2669d1f0179ae8804584939fb6502b51f2401e26b1e028/ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb", size = 9124741 }, +] + +[[package]] +name = "setuptools" +version = "78.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/0db4da3bc908df06e5efae42b44e75c81dd52716e10192ff36d0c1c8e379/setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54", size = 1367827 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 }, ] [[package]] @@ -1347,27 +1459,26 @@ wheels = [ [[package]] name = "sse-starlette" -version = "2.2.1" +version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } +sdist = { url = "https://files.pythonhosted.org/packages/40/88/0af7f586894cfe61bd212f33e571785c4570085711b24fb7445425a5eeb0/sse-starlette-1.6.1.tar.gz", hash = "sha256:6208af2bd7d0887c92f1379da14bd1f4db56bd1274cc5d36670c683d2aa1de6a", size = 14555 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, + { url = "https://files.pythonhosted.org/packages/5e/f7/499e5d0c181a52a205d5b0982fd71cf162d1e070c97dca90c60520bbf8bf/sse_starlette-1.6.1-py3-none-any.whl", hash = "sha256:d8f18f1c633e355afe61cc5e9c92eea85badcb8b2d56ec8cfb0a006994aa55da", size = 9553 }, ] [[package]] name = "starlette" -version = "0.46.1" +version = "0.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 } +sdist = { url = "https://files.pythonhosted.org/packages/06/68/559bed5484e746f1ab2ebbe22312f2c25ec62e4b534916d41a8c21147bf8/starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75", size = 51394 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 }, + { url = "https://files.pythonhosted.org/packages/58/f8/e2cca22387965584a409795913b774235752be4176d276714e15e1a58884/starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91", size = 66978 }, ] [[package]] @@ -1423,7 +1534,7 @@ wheels = [ [[package]] name = "trio" -version = "0.29.0" +version = "0.26.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1434,14 +1545,14 @@ dependencies = [ { name = "sniffio" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/03/ab0e9509be0c6465e2773768ec25ee0cb8053c0b91471ab3854bbf2294b2/trio-0.26.2.tar.gz", hash = "sha256:0346c3852c15e5c7d40ea15972c4805689ef2cb8b5206f794c9c19450119f3a4", size = 561156 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 }, + { url = "https://files.pythonhosted.org/packages/1c/70/efa56ce2271c44a7f4f43533a0477e6854a0948e9f7b76491de1fd3be7c9/trio-0.26.2-py3-none-any.whl", hash = "sha256:c5237e8133eb0a1d72f09a971a55c28ebe69e351c783fc64bc37db8db8bbe1d0", size = 475996 }, ] [[package]] name = "typer" -version = "0.15.2" +version = "0.12.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1449,18 +1560,68 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 } +sdist = { url = "https://files.pythonhosted.org/packages/d4/f7/f174a1cae84848ae8b27170a96187b91937b743f0580ff968078fe16930a/typer-0.12.4.tar.gz", hash = "sha256:c9c1613ed6a166162705b3347b8d10b661ccc5d95692654d0fb628118f2c34e6", size = 97945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/cc/15083dcde1252a663398b1b2a173637a3ec65adadfb95137dc95df1e6adc/typer-0.12.4-py3-none-any.whl", hash = "sha256:819aa03699f438397e876aa12b0d63766864ecba1b579092cc9fe35d886e34b6", size = 47402 }, +] + +[[package]] +name = "types-cffi" +version = "1.17.0.20250326" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/3b/d29491d754b9e42edd4890648311ffa5d4d000b7d97b92ac4d04faad40d8/types_cffi-1.17.0.20250326.tar.gz", hash = "sha256:6c8fea2c2f34b55e5fb77b1184c8ad849d57cf0ddccbc67a62121ac4b8b32254", size = 16887 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/49/ce473d7fbc2c80931ef9f7530fd3ddf31b8a5bca56340590334ce6ffbfb1/types_cffi-1.17.0.20250326-py3-none-any.whl", hash = "sha256:5af4ecd7374ae0d5fa9e80864e8d4b31088cc32c51c544e3af7ed5b5ed681447", size = 20133 }, +] + +[[package]] +name = "types-pyopenssl" +version = "24.1.0.20240722" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499 }, +] + +[[package]] +name = "types-redis" +version = "4.6.0.20241004" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-pyopenssl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/95/c054d3ac940e8bac4ca216470c80c26688a0e79e09f520a942bb27da3386/types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", size = 49679 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/82/7d25dce10aad92d2226b269bce2f85cfd843b4477cd50245d7d40ecf8f89/types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed", size = 58737 }, +] + +[[package]] +name = "types-setuptools" +version = "78.1.0.20250329" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/6e/c54e6705e5fe67c3606e4c7c91123ecf10d7e1e6d7a9c11b52970cf2196c/types_setuptools-78.1.0.20250329.tar.gz", hash = "sha256:31e62950c38b8cc1c5114b077504e36426860a064287cac11b9666ab3a483234", size = 43942 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 }, + { url = "https://files.pythonhosted.org/packages/7d/31/85d0264705d8ef47680d28f4dc9bb1e27d8cace785fbe3f8d009fad6cb88/types_setuptools-78.1.0.20250329-py3-none-any.whl", hash = "sha256:ea47eab891afb506f470eee581dcde44d64dc99796665da794da6f83f50f6776", size = 66985 }, ] [[package]] name = "typing-extensions" -version = "4.13.1" +version = "4.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/ad/cd3e3465232ec2416ae9b983f27b9e94dc8171d56ac99b345319a9475967/typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff", size = 106633 } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/c5/e7a0b0f5ed69f94c8ab7379c599e6036886bffcde609969a5325f47f1332/typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69", size = 45739 }, + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, ] [[package]] @@ -1474,16 +1635,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.30.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/f7/4ad826703a49b320a4adf2470fdd2a3481ea13f4460cb615ad12c75be003/uvicorn-0.30.0.tar.gz", hash = "sha256:f678dec4fa3a39706bbf49b9ec5fc40049d42418716cea52b53f07828a60aa37", size = 42560 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, + { url = "https://files.pythonhosted.org/packages/2a/a1/d57e38417a8dabb22df02b6aebc209dc73485792e6c5620e501547133d0b/uvicorn-0.30.0-py3-none-any.whl", hash = "sha256:78fa0b5f56abb8562024a59041caeb555c86e48d0efdd23c3fe7de7a4075bdab", size = 62388 }, ] [[package]] From d625782ade81dc458e415d59f09a1cf58576f462 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 14 Apr 2025 12:51:21 -0400 Subject: [PATCH 12/51] fixes --- README.md | 17 ++-- pyproject.toml | 2 - src/mcp/server/fastmcp/server.py | 28 ++---- src/mcp/server/message_queue/redis.py | 85 ++++++++-------- uv.lock | 133 +------------------------- 5 files changed, 61 insertions(+), 204 deletions(-) diff --git a/README.md b/README.md index 686586611..fb78e0588 100644 --- a/README.md +++ b/README.md @@ -397,22 +397,23 @@ For more information on mounting applications in Starlette, see the [Starlette d By default, the SSE server uses an in-memory message queue for incoming POST messages. For production deployments or distributed scenarios, you can use Redis: ```python +# Using the built-in Redis message queue from mcp.server.fastmcp import FastMCP +from mcp.server.message_queue import RedisMessageQueue -mcp = FastMCP( - "My App", - settings={ - "message_queue": "redis", - "redis_url": "redis://localhost:6379/0", - "redis_prefix": "mcp:queue:", - }, +# Create a Redis message queue +redis_queue = RedisMessageQueue( + redis_url="redis://localhost:6379/0", prefix="mcp:pubsub:" ) + +# Pass the message queue instance to the server +mcp = FastMCP("My App", message_queue=redis_queue) ``` To use Redis, add the Redis dependency: ```bash -uv add "mcp[redis]" +uv add redis types-redis ``` ## Examples diff --git a/pyproject.toml b/pyproject.toml index 10d728904..25514cd6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,6 @@ dependencies = [ "sse-starlette>=1.6.1", "pydantic-settings>=2.5.2", "uvicorn>=0.23.1", - "redis==5.2.1", - "types-redis==4.6.0.20241004", ] [project.optional-dependencies] diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index a4d74962b..1e9b2d80b 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -49,6 +49,7 @@ from mcp.types import Resource as MCPResource from mcp.types import ResourceTemplate as MCPResourceTemplate from mcp.types import Tool as MCPTool +from mcp.server.message_queue import MessageQueue logger = get_logger(__name__) @@ -77,9 +78,7 @@ class Settings(BaseSettings, Generic[LifespanResultT]): message_path: str = "/messages/" # SSE message queue settings - message_queue: Literal["memory", "redis"] = "memory" - redis_url: str = "redis://localhost:6379/0" - redis_prefix: str = "mcp:queue:" + message_queue: MessageQueue | None = Field(None, description="Custom message queue instance") # resource settings warn_on_duplicate_resources: bool = True @@ -484,25 +483,14 @@ async def run_sse_async(self) -> None: def sse_app(self) -> Starlette: """Return an instance of the SSE server app.""" - message_queue = None - if self.settings.message_queue == "redis": - try: - from mcp.server.message_queue import RedisMessageQueue - - message_queue = RedisMessageQueue( - redis_url=self.settings.redis_url, prefix=self.settings.redis_prefix - ) - logger.info(f"Using Redis message queue at {self.settings.redis_url}") - except ImportError: - logger.error( - "Redis message queue requested but 'redis' package not installed. " - ) - raise - else: + # Use a custom provided message queue if available + message_queue = self.settings.message_queue + + # If no message queue is provided, create an in-memory queue as default + if message_queue is None: from mcp.server.message_queue import InMemoryMessageQueue - message_queue = InMemoryMessageQueue() - logger.info("Using in-memory message queue") + logger.info("Using default in-memory message queue") sse = SseServerTransport( self.settings.message_path, message_queue=message_queue diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index e2c5b6c01..65512f7b6 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -1,14 +1,13 @@ import json import logging +from contextlib import asynccontextmanager +from typing import Any, cast from uuid import UUID import anyio -from anyio import CapacityLimiter +from anyio import CapacityLimiter, from_thread import mcp.types as types from mcp.server.message_queue.base import MessageCallback -from typing import Any, cast -from anyio import from_thread -from contextlib import asynccontextmanager try: @@ -38,8 +37,8 @@ def __init__( redis_url: Redis connection string prefix: Key prefix for Redis channels to avoid collisions """ - self._redis = redis.from_url(redis_url, decode_responses=True) # type: ignore - self._pubsub = self._redis.pubsub(ignore_subscribe_messages=True) # type: ignore + self._redis = redis.from_url(redis_url, decode_responses=True) # type: ignore + self._pubsub = self._redis.pubsub(ignore_subscribe_messages=True) # type: ignore self._prefix = prefix self._active_sessions_key = f"{prefix}active_sessions" self._callbacks: dict[UUID, MessageCallback] = {} @@ -53,11 +52,10 @@ def _session_channel(self, session_id: UUID) -> str: @asynccontextmanager async def active_for_request(self, session_id: UUID, callback: MessageCallback): """Request-scoped context manager that ensures the listener task is running.""" - await self._redis.sadd(self._active_sessions_key, session_id.hex) self._callbacks[session_id] = callback channel = self._session_channel(session_id) - await self._pubsub.subscribe(channel) # type: ignore + await self._pubsub.subscribe(channel) # type: ignore logger.debug(f"Registered session {session_id} in Redis with callback") async with anyio.create_task_group() as tg: @@ -66,7 +64,7 @@ async def active_for_request(self, session_id: UUID, callback: MessageCallback): yield finally: tg.cancel_scope.cancel() - await self._pubsub.unsubscribe(channel) # type: ignore + await self._pubsub.unsubscribe(channel) # type: ignore await self._redis.srem(self._active_sessions_key, session_id.hex) del self._callbacks[session_id] logger.debug(f"Unregistered session {session_id} from Redis") @@ -75,40 +73,41 @@ async def _listen_for_messages(self) -> None: """Background task that listens for messages on subscribed channels.""" async with self._limiter: while True: - message: None | dict[str, Any] = await self._pubsub.get_message( # type: ignore + message: None | dict[str, Any] = await self._pubsub.get_message( # type: ignore ignore_subscribe_messages=True ) - if message is not None: - # Extract session ID from channel name - channel: str = cast(str, message["channel"]) - if not channel.startswith(self._prefix): - continue - - session_hex = channel.split(":")[-1] - try: - session_id = UUID(hex=session_hex) - except ValueError: - logger.error(f"Invalid session channel: {channel}") - continue - - # Deserialize the message - data: str = cast(str, message["data"]) - msg: None | types.JSONRPCMessage | Exception = None - try: - json_data = json.loads(data) - if isinstance(json_data, dict): - json_dict: dict[str, Any] = json_data - if json_dict.get("_exception", False): - msg = Exception( - f"{json_dict['type']}: {json_dict['message']}" - ) - else: - msg = types.JSONRPCMessage.model_validate_json(data) - - if msg and session_id in self._callbacks: - from_thread.run(self._callbacks[session_id], msg) - except Exception as e: - logger.error(f"Failed to process message: {e}") + if message is None: + continue + + # Extract session ID from channel name + channel: str = cast(str, message["channel"]) + if not channel.startswith(self._prefix): + continue + + session_hex = channel.split(":")[-1] + try: + session_id = UUID(hex=session_hex) + except ValueError: + logger.error(f"Invalid session channel: {channel}") + continue + + data: str = cast(str, message["data"]) + msg: None | types.JSONRPCMessage | Exception = None + try: + json_data = json.loads(data) + if isinstance(json_data, dict): + json_dict: dict[str, Any] = json_data + if json_dict.get("_exception", False): + msg = Exception( + f"{json_dict['type']}: {json_dict['message']}" + ) + else: + msg = types.JSONRPCMessage.model_validate_json(data) + + if msg and session_id in self._callbacks: + from_thread.run(self._callbacks[session_id], msg) + except Exception as e: + logger.error(f"Failed to process message: {e}") async def publish_message( self, session_id: UUID, message: types.JSONRPCMessage | Exception @@ -136,8 +135,6 @@ async def publish_message( async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" - # Explicitly annotate the result as bool to help the type checker - result = bool( + return bool( await self._redis.sismember(self._active_sessions_key, session_id.hex) # type: ignore[attr-defined] ) - return result diff --git a/uv.lock b/uv.lock index 83b387549..424e2d482 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10" [options] @@ -36,15 +37,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/68/f9e9bf6324c46e6b8396610aef90ad423ec3e18c9079547ceafea3dce0ec/anyio-4.5.0-py3-none-any.whl", hash = "sha256:fdeb095b7cc5a5563175eedd926ec4ae55413bb4be5770c424af0ba46ccb4a78", size = 89250 }, ] -[[package]] -name = "async-timeout" -version = "5.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, -] - [[package]] name = "attrs" version = "24.3.0" @@ -273,51 +265,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] -[[package]] -name = "cryptography" -version = "44.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 }, - { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 }, - { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 }, - { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 }, - { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 }, - { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 }, - { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 }, - { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 }, - { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 }, - { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 }, - { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 }, - { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 }, - { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 }, - { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 }, - { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 }, - { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 }, - { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 }, - { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 }, - { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 }, - { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 }, - { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 }, - { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 }, - { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 }, - { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 }, - { url = "https://files.pythonhosted.org/packages/99/10/173be140714d2ebaea8b641ff801cbcb3ef23101a2981cbf08057876f89e/cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", size = 3396886 }, - { url = "https://files.pythonhosted.org/packages/2f/b4/424ea2d0fce08c24ede307cead3409ecbfc2f566725d4701b9754c0a1174/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", size = 3892387 }, - { url = "https://files.pythonhosted.org/packages/28/20/8eaa1a4f7c68a1cb15019dbaad59c812d4df4fac6fd5f7b0b9c5177f1edd/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", size = 4109922 }, - { url = "https://files.pythonhosted.org/packages/11/25/5ed9a17d532c32b3bc81cc294d21a36c772d053981c22bd678396bc4ae30/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", size = 3895715 }, - { url = "https://files.pythonhosted.org/packages/63/31/2aac03b19c6329b62c45ba4e091f9de0b8f687e1b0cd84f101401bece343/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", size = 4109876 }, - { url = "https://files.pythonhosted.org/packages/99/ec/6e560908349843718db1a782673f36852952d52a55ab14e46c42c8a7690a/cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", size = 3131719 }, - { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513 }, - { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432 }, - { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421 }, - { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081 }, -] - [[package]] name = "cssselect2" version = "0.8.0" @@ -547,10 +494,8 @@ dependencies = [ { name = "httpx-sse" }, { name = "pydantic" }, { name = "pydantic-settings" }, - { name = "redis" }, { name = "sse-starlette" }, { name = "starlette" }, - { name = "types-redis" }, { name = "uvicorn" }, ] @@ -591,15 +536,14 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.7.2,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" }, - { name = "redis", specifier = "==5.2.1" }, { name = "rich", marker = "extra == 'rich'", specifier = ">=13.9.4" }, { name = "sse-starlette", specifier = ">=1.6.1" }, { name = "starlette", specifier = ">=0.27" }, { name = "typer", marker = "extra == 'cli'", specifier = ">=0.12.4" }, - { name = "types-redis", specifier = "==4.6.0.20241004" }, { name = "uvicorn", specifier = ">=0.23.1" }, { name = "websockets", marker = "extra == 'ws'", specifier = ">=15.0.1" }, ] +provides-extras = ["cli", "rich", "ws"] [package.metadata.requires-dev] dev = [ @@ -1277,18 +1221,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, ] -[[package]] -name = "redis" -version = "5.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, -] - [[package]] name = "regex" version = "2024.11.6" @@ -1412,15 +1344,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/5e/ffee22bf9f9e4b2669d1f0179ae8804584939fb6502b51f2401e26b1e028/ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb", size = 9124741 }, ] -[[package]] -name = "setuptools" -version = "78.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/0db4da3bc908df06e5efae42b44e75c81dd52716e10192ff36d0c1c8e379/setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54", size = 1367827 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 }, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -1565,56 +1488,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/cc/15083dcde1252a663398b1b2a173637a3ec65adadfb95137dc95df1e6adc/typer-0.12.4-py3-none-any.whl", hash = "sha256:819aa03699f438397e876aa12b0d63766864ecba1b579092cc9fe35d886e34b6", size = 47402 }, ] -[[package]] -name = "types-cffi" -version = "1.17.0.20250326" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "types-setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3f/3b/d29491d754b9e42edd4890648311ffa5d4d000b7d97b92ac4d04faad40d8/types_cffi-1.17.0.20250326.tar.gz", hash = "sha256:6c8fea2c2f34b55e5fb77b1184c8ad849d57cf0ddccbc67a62121ac4b8b32254", size = 16887 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/49/ce473d7fbc2c80931ef9f7530fd3ddf31b8a5bca56340590334ce6ffbfb1/types_cffi-1.17.0.20250326-py3-none-any.whl", hash = "sha256:5af4ecd7374ae0d5fa9e80864e8d4b31088cc32c51c544e3af7ed5b5ed681447", size = 20133 }, -] - -[[package]] -name = "types-pyopenssl" -version = "24.1.0.20240722" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "types-cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499 }, -] - -[[package]] -name = "types-redis" -version = "4.6.0.20241004" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "types-pyopenssl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/95/c054d3ac940e8bac4ca216470c80c26688a0e79e09f520a942bb27da3386/types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", size = 49679 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/82/7d25dce10aad92d2226b269bce2f85cfd843b4477cd50245d7d40ecf8f89/types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed", size = 58737 }, -] - -[[package]] -name = "types-setuptools" -version = "78.1.0.20250329" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/6e/c54e6705e5fe67c3606e4c7c91123ecf10d7e1e6d7a9c11b52970cf2196c/types_setuptools-78.1.0.20250329.tar.gz", hash = "sha256:31e62950c38b8cc1c5114b077504e36426860a064287cac11b9666ab3a483234", size = 43942 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/31/85d0264705d8ef47680d28f4dc9bb1e27d8cace785fbe3f8d009fad6cb88/types_setuptools-78.1.0.20250329-py3-none-any.whl", hash = "sha256:ea47eab891afb506f470eee581dcde44d64dc99796665da794da6f83f50f6776", size = 66985 }, -] - [[package]] name = "typing-extensions" version = "4.12.2" @@ -1745,4 +1618,4 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, -] +] \ No newline at end of file From 78c6aef94cd2a45d69da8106ec462c6afd4af6a7 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 14 Apr 2025 12:53:32 -0400 Subject: [PATCH 13/51] Add optional redis dep --- pyproject.toml | 1 + uv.lock | 29 ++++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 25514cd6b..c84ce151c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ rich = ["rich>=13.9.4"] cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"] ws = ["websockets>=15.0.1"] +redis = ["redis>=5.0.0"] [project.scripts] mcp = "mcp.cli:app [cli]" diff --git a/uv.lock b/uv.lock index 424e2d482..bcb44345d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,4 @@ version = 1 -revision = 1 requires-python = ">=3.10" [options] @@ -37,6 +36,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/68/f9e9bf6324c46e6b8396610aef90ad423ec3e18c9079547ceafea3dce0ec/anyio-4.5.0-py3-none-any.whl", hash = "sha256:fdeb095b7cc5a5563175eedd926ec4ae55413bb4be5770c424af0ba46ccb4a78", size = 89250 }, ] +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + [[package]] name = "attrs" version = "24.3.0" @@ -504,6 +512,9 @@ cli = [ { name = "python-dotenv" }, { name = "typer" }, ] +redis = [ + { name = "redis" }, +] rich = [ { name = "rich" }, ] @@ -536,6 +547,7 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.7.2,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.0" }, { name = "rich", marker = "extra == 'rich'", specifier = ">=13.9.4" }, { name = "sse-starlette", specifier = ">=1.6.1" }, { name = "starlette", specifier = ">=0.27" }, @@ -543,7 +555,6 @@ requires-dist = [ { name = "uvicorn", specifier = ">=0.23.1" }, { name = "websockets", marker = "extra == 'ws'", specifier = ">=15.0.1" }, ] -provides-extras = ["cli", "rich", "ws"] [package.metadata.requires-dev] dev = [ @@ -1221,6 +1232,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, ] +[[package]] +name = "redis" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version <= '3.11.2'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/b9/b6eeedcbcf487b000f96aa085c842a46d24eab99a5bb05ba6fd917e0ea14/redis-5.0.0.tar.gz", hash = "sha256:5cea6c0d335c9a7332a460ed8729ceabb4d0c489c7285b0a86dbbf8a017bd120", size = 4576790 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/dfdc17f701f7b587f6c89c2b9b6b5978c87a8a785555efc810b064c875de/redis-5.0.0-py3-none-any.whl", hash = "sha256:06570d0b2d84d46c21defc550afbaada381af82f5b83e5b3777600e05d8e2ed0", size = 250068 }, +] + [[package]] name = "regex" version = "2024.11.6" @@ -1618,4 +1641,4 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, -] \ No newline at end of file +] From fad836c38b8f8f8cf1ad123a4b058153fef601ae Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 14 Apr 2025 16:04:32 -0400 Subject: [PATCH 14/51] changes --- examples/fastmcp/unicode_example.py | 10 ++- pyproject.toml | 3 +- src/mcp/server/fastmcp/server.py | 1 - src/mcp/server/message_queue/redis.py | 8 +- uv.lock | 118 ++++++++++++++++++++++++-- 5 files changed, 127 insertions(+), 13 deletions(-) diff --git a/examples/fastmcp/unicode_example.py b/examples/fastmcp/unicode_example.py index a69f586a5..1d7c846c7 100644 --- a/examples/fastmcp/unicode_example.py +++ b/examples/fastmcp/unicode_example.py @@ -4,8 +4,14 @@ """ from mcp.server.fastmcp import FastMCP +from mcp.server.message_queue import RedisMessageQueue -mcp = FastMCP() +# Create a Redis message queue +redis_queue = RedisMessageQueue( + redis_url="redis://localhost:6379/0", prefix="mcp:pubsub:" +) + +mcp = FastMCP(message_queue=redis_queue) @mcp.tool( @@ -61,4 +67,4 @@ def multilingual_hello() -> str: if __name__ == "__main__": - mcp.run() + mcp.run(transport="sse") diff --git a/pyproject.toml b/pyproject.toml index c84ce151c..353d2e568 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,8 @@ dependencies = [ rich = ["rich>=13.9.4"] cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"] ws = ["websockets>=15.0.1"] -redis = ["redis>=5.0.0"] +redis = ["redis>=5.2.1"] +types-redis = ["types-redis>=4.6.0.20241004"] [project.scripts] mcp = "mcp.cli:app [cli]" diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 1e9b2d80b..9665a36bc 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -485,7 +485,6 @@ def sse_app(self) -> Starlette: """Return an instance of the SSE server app.""" # Use a custom provided message queue if available message_queue = self.settings.message_queue - # If no message queue is provided, create an in-memory queue as default if message_queue is None: from mcp.server.message_queue import InMemoryMessageQueue diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 65512f7b6..0ad7e5ae0 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -5,7 +5,7 @@ from uuid import UUID import anyio -from anyio import CapacityLimiter, from_thread +from anyio import CapacityLimiter import mcp.types as types from mcp.server.message_queue.base import MessageCallback @@ -74,8 +74,8 @@ async def _listen_for_messages(self) -> None: async with self._limiter: while True: message: None | dict[str, Any] = await self._pubsub.get_message( # type: ignore - ignore_subscribe_messages=True - ) + ignore_subscribe_messages=True, timeout=None # type: ignore + ) if message is None: continue @@ -105,7 +105,7 @@ async def _listen_for_messages(self) -> None: msg = types.JSONRPCMessage.model_validate_json(data) if msg and session_id in self._callbacks: - from_thread.run(self._callbacks[session_id], msg) + await self._callbacks[session_id](msg) except Exception as e: logger.error(f"Failed to process message: {e}") diff --git a/uv.lock b/uv.lock index bcb44345d..20a599051 100644 --- a/uv.lock +++ b/uv.lock @@ -273,6 +273,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "cryptography" +version = "44.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 }, + { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 }, + { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 }, + { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 }, + { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 }, + { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 }, + { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 }, + { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 }, + { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 }, + { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 }, + { url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 }, + { url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 }, + { url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 }, + { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 }, + { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 }, + { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 }, + { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 }, + { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 }, + { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 }, + { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 }, + { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 }, + { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 }, + { url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 }, + { url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 }, + { url = "https://files.pythonhosted.org/packages/99/10/173be140714d2ebaea8b641ff801cbcb3ef23101a2981cbf08057876f89e/cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb", size = 3396886 }, + { url = "https://files.pythonhosted.org/packages/2f/b4/424ea2d0fce08c24ede307cead3409ecbfc2f566725d4701b9754c0a1174/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41", size = 3892387 }, + { url = "https://files.pythonhosted.org/packages/28/20/8eaa1a4f7c68a1cb15019dbaad59c812d4df4fac6fd5f7b0b9c5177f1edd/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562", size = 4109922 }, + { url = "https://files.pythonhosted.org/packages/11/25/5ed9a17d532c32b3bc81cc294d21a36c772d053981c22bd678396bc4ae30/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5", size = 3895715 }, + { url = "https://files.pythonhosted.org/packages/63/31/2aac03b19c6329b62c45ba4e091f9de0b8f687e1b0cd84f101401bece343/cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa", size = 4109876 }, + { url = "https://files.pythonhosted.org/packages/99/ec/6e560908349843718db1a782673f36852952d52a55ab14e46c42c8a7690a/cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d", size = 3131719 }, + { url = "https://files.pythonhosted.org/packages/d6/d7/f30e75a6aa7d0f65031886fa4a1485c2fbfe25a1896953920f6a9cfe2d3b/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d", size = 3887513 }, + { url = "https://files.pythonhosted.org/packages/9c/b4/7a494ce1032323ca9db9a3661894c66e0d7142ad2079a4249303402d8c71/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471", size = 4107432 }, + { url = "https://files.pythonhosted.org/packages/45/f8/6b3ec0bc56123b344a8d2b3264a325646d2dcdbdd9848b5e6f3d37db90b3/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615", size = 3891421 }, + { url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081 }, +] + [[package]] name = "cssselect2" version = "0.8.0" @@ -518,6 +563,9 @@ redis = [ rich = [ { name = "rich" }, ] +types-redis = [ + { name = "types-redis" }, +] ws = [ { name = "websockets" }, ] @@ -547,11 +595,12 @@ requires-dist = [ { name = "pydantic", specifier = ">=2.7.2,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" }, - { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.0" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=5.2.1" }, { name = "rich", marker = "extra == 'rich'", specifier = ">=13.9.4" }, { name = "sse-starlette", specifier = ">=1.6.1" }, { name = "starlette", specifier = ">=0.27" }, { name = "typer", marker = "extra == 'cli'", specifier = ">=0.12.4" }, + { name = "types-redis", marker = "extra == 'types-redis'", specifier = ">=4.6.0.20241004" }, { name = "uvicorn", specifier = ">=0.23.1" }, { name = "websockets", marker = "extra == 'ws'", specifier = ">=15.0.1" }, ] @@ -1234,14 +1283,14 @@ wheels = [ [[package]] name = "redis" -version = "5.0.0" +version = "5.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "async-timeout", marker = "python_full_version <= '3.11.2'" }, + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/b9/b6eeedcbcf487b000f96aa085c842a46d24eab99a5bb05ba6fd917e0ea14/redis-5.0.0.tar.gz", hash = "sha256:5cea6c0d335c9a7332a460ed8729ceabb4d0c489c7285b0a86dbbf8a017bd120", size = 4576790 } +sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/dfdc17f701f7b587f6c89c2b9b6b5978c87a8a785555efc810b064c875de/redis-5.0.0-py3-none-any.whl", hash = "sha256:06570d0b2d84d46c21defc550afbaada381af82f5b83e5b3777600e05d8e2ed0", size = 250068 }, + { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, ] [[package]] @@ -1367,6 +1416,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bf/5e/ffee22bf9f9e4b2669d1f0179ae8804584939fb6502b51f2401e26b1e028/ruff-0.8.5-py3-none-win_arm64.whl", hash = "sha256:134ae019ef13e1b060ab7136e7828a6d83ea727ba123381307eb37c6bd5e01cb", size = 9124741 }, ] +[[package]] +name = "setuptools" +version = "78.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/0db4da3bc908df06e5efae42b44e75c81dd52716e10192ff36d0c1c8e379/setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54", size = 1367827 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 }, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -1511,6 +1569,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/cc/15083dcde1252a663398b1b2a173637a3ec65adadfb95137dc95df1e6adc/typer-0.12.4-py3-none-any.whl", hash = "sha256:819aa03699f438397e876aa12b0d63766864ecba1b579092cc9fe35d886e34b6", size = 47402 }, ] +[[package]] +name = "types-cffi" +version = "1.17.0.20250326" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/3b/d29491d754b9e42edd4890648311ffa5d4d000b7d97b92ac4d04faad40d8/types_cffi-1.17.0.20250326.tar.gz", hash = "sha256:6c8fea2c2f34b55e5fb77b1184c8ad849d57cf0ddccbc67a62121ac4b8b32254", size = 16887 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/49/ce473d7fbc2c80931ef9f7530fd3ddf31b8a5bca56340590334ce6ffbfb1/types_cffi-1.17.0.20250326-py3-none-any.whl", hash = "sha256:5af4ecd7374ae0d5fa9e80864e8d4b31088cc32c51c544e3af7ed5b5ed681447", size = 20133 }, +] + +[[package]] +name = "types-pyopenssl" +version = "24.1.0.20240722" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/29/47a346550fd2020dac9a7a6d033ea03fccb92fa47c726056618cc889745e/types-pyOpenSSL-24.1.0.20240722.tar.gz", hash = "sha256:47913b4678a01d879f503a12044468221ed8576263c1540dcb0484ca21b08c39", size = 8458 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/05/c868a850b6fbb79c26f5f299b768ee0adc1f9816d3461dcf4287916f655b/types_pyOpenSSL-24.1.0.20240722-py3-none-any.whl", hash = "sha256:6a7a5d2ec042537934cfb4c9d4deb0e16c4c6250b09358df1f083682fe6fda54", size = 7499 }, +] + +[[package]] +name = "types-redis" +version = "4.6.0.20241004" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "types-pyopenssl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/95/c054d3ac940e8bac4ca216470c80c26688a0e79e09f520a942bb27da3386/types-redis-4.6.0.20241004.tar.gz", hash = "sha256:5f17d2b3f9091ab75384153bfa276619ffa1cf6a38da60e10d5e6749cc5b902e", size = 49679 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/82/7d25dce10aad92d2226b269bce2f85cfd843b4477cd50245d7d40ecf8f89/types_redis-4.6.0.20241004-py3-none-any.whl", hash = "sha256:ef5da68cb827e5f606c8f9c0b49eeee4c2669d6d97122f301d3a55dc6a63f6ed", size = 58737 }, +] + +[[package]] +name = "types-setuptools" +version = "78.1.0.20250329" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/6e/c54e6705e5fe67c3606e4c7c91123ecf10d7e1e6d7a9c11b52970cf2196c/types_setuptools-78.1.0.20250329.tar.gz", hash = "sha256:31e62950c38b8cc1c5114b077504e36426860a064287cac11b9666ab3a483234", size = 43942 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/31/85d0264705d8ef47680d28f4dc9bb1e27d8cace785fbe3f8d009fad6cb88/types_setuptools-78.1.0.20250329-py3-none-any.whl", hash = "sha256:ea47eab891afb506f470eee581dcde44d64dc99796665da794da6f83f50f6776", size = 66985 }, +] + [[package]] name = "typing-extensions" version = "4.12.2" From fd975014745c7c83f35f3e83d8509b9b9b38af29 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 14 Apr 2025 16:08:29 -0400 Subject: [PATCH 15/51] format / lint --- src/mcp/server/fastmcp/server.py | 7 +++++-- src/mcp/server/message_queue/base.py | 14 ++++++++------ src/mcp/server/message_queue/redis.py | 9 +++++---- src/mcp/server/sse.py | 8 +++++--- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 9665a36bc..d2a9c2f99 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -33,6 +33,7 @@ from mcp.server.lowlevel.server import LifespanResultT from mcp.server.lowlevel.server import Server as MCPServer from mcp.server.lowlevel.server import lifespan as default_lifespan +from mcp.server.message_queue import MessageQueue from mcp.server.session import ServerSession, ServerSessionT from mcp.server.sse import SseServerTransport from mcp.server.stdio import stdio_server @@ -49,7 +50,6 @@ from mcp.types import Resource as MCPResource from mcp.types import ResourceTemplate as MCPResourceTemplate from mcp.types import Tool as MCPTool -from mcp.server.message_queue import MessageQueue logger = get_logger(__name__) @@ -78,7 +78,9 @@ class Settings(BaseSettings, Generic[LifespanResultT]): message_path: str = "/messages/" # SSE message queue settings - message_queue: MessageQueue | None = Field(None, description="Custom message queue instance") + message_queue: MessageQueue | None = Field( + None, description="Custom message queue instance" + ) # resource settings warn_on_duplicate_resources: bool = True @@ -488,6 +490,7 @@ def sse_app(self) -> Starlette: # If no message queue is provided, create an in-memory queue as default if message_queue is None: from mcp.server.message_queue import InMemoryMessageQueue + message_queue = InMemoryMessageQueue() logger.info("Using default in-memory message queue") diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index b119f4b63..ceea99334 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -1,7 +1,8 @@ import logging -from typing import Protocol, runtime_checkable, Callable, Awaitable -from uuid import UUID +from collections.abc import Awaitable, Callable from contextlib import asynccontextmanager +from typing import Protocol, runtime_checkable +from uuid import UUID import mcp.types as types @@ -35,7 +36,7 @@ async def publish_message( @asynccontextmanager async def active_for_request(self, session_id: UUID, callback: MessageCallback): """Request-scoped context manager that ensures the listener is active. - + Args: session_id: The UUID of the session to activate callback: Async callback function to handle messages for this session @@ -57,7 +58,8 @@ async def session_exists(self, session_id: UUID) -> bool: class InMemoryMessageQueue: """Default in-memory implementation of the MessageQueue interface. - This implementation immediately calls registered callbacks when messages are received. + This implementation immediately calls registered callbacks when messages + are received. """ def __init__(self) -> None: @@ -78,7 +80,7 @@ async def publish_message( logger.debug(f"Called callback for session {session_id}") else: logger.warning(f"No callback registered for session {session_id}") - + return True @asynccontextmanager @@ -87,7 +89,7 @@ async def active_for_request(self, session_id: UUID, callback: MessageCallback): self._active_sessions.add(session_id) self._callbacks[session_id] = callback logger.debug(f"Registered session {session_id} with callback") - + try: yield finally: diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 0ad7e5ae0..d16fd1b0d 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -6,10 +6,10 @@ import anyio from anyio import CapacityLimiter + import mcp.types as types from mcp.server.message_queue.base import MessageCallback - try: import redis.asyncio as redis except ImportError: @@ -56,7 +56,7 @@ async def active_for_request(self, session_id: UUID, callback: MessageCallback): self._callbacks[session_id] = callback channel = self._session_channel(session_id) await self._pubsub.subscribe(channel) # type: ignore - + logger.debug(f"Registered session {session_id} in Redis with callback") async with anyio.create_task_group() as tg: tg.start_soon(self._listen_for_messages) @@ -74,11 +74,12 @@ async def _listen_for_messages(self) -> None: async with self._limiter: while True: message: None | dict[str, Any] = await self._pubsub.get_message( # type: ignore - ignore_subscribe_messages=True, timeout=None # type: ignore + ignore_subscribe_messages=True, + timeout=None, # type: ignore ) if message is None: continue - + # Extract session ID from channel name channel: str = cast(str, message["channel"]) if not channel.startswith(self._prefix): diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 8a0cf2751..664adcd4f 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -101,12 +101,12 @@ async def connect_sse(self, scope: Scope, receive: Receive, send: Send): session_id = uuid4() session_uri = f"{quote(self._endpoint)}?session_id={session_id.hex}" - + async def message_callback(message: types.JSONRPCMessage | Exception) -> None: """Callback that receives messages from the message queue""" logger.debug(f"Got message from queue for session {session_id}") await read_stream_writer.send(message) - + logger.debug(f"Created new session with ID: {session_id}") sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[ @@ -137,7 +137,9 @@ async def sse_writer(): logger.debug("Starting SSE response task") tg.start_soon(response, scope, receive, send) - async with self._message_queue.active_for_request(session_id, message_callback): + async with self._message_queue.active_for_request( + session_id, message_callback + ): try: logger.debug("Yielding read and write streams") yield (read_stream, write_stream) From 4bce7d88cf89dc13d8226720d41cbd8ab12515ce Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 14 Apr 2025 16:17:45 -0400 Subject: [PATCH 16/51] cleanup --- README.md | 9 +-------- pyproject.toml | 3 +-- src/mcp/server/sse.py | 7 ++----- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index fb78e0588..9f6c3fa39 100644 --- a/README.md +++ b/README.md @@ -87,13 +87,6 @@ If you haven't created a uv-managed project yet, create one: ```bash uv add "mcp[cli]" ``` - - For optional features, you can add extras: - - ```bash - # For Redis support in message queue - uv add "mcp[redis]" - ``` Alternatively, for projects using pip for dependencies: ```bash @@ -413,7 +406,7 @@ mcp = FastMCP("My App", message_queue=redis_queue) To use Redis, add the Redis dependency: ```bash -uv add redis types-redis +uv add "mcp[redis]" ``` ## Examples diff --git a/pyproject.toml b/pyproject.toml index 353d2e568..c66f1e667 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,7 @@ dependencies = [ rich = ["rich>=13.9.4"] cli = ["typer>=0.12.4", "python-dotenv>=1.0.0"] ws = ["websockets>=15.0.1"] -redis = ["redis>=5.2.1"] -types-redis = ["types-redis>=4.6.0.20241004"] +redis = ["redis>=5.2.1", "types-redis>=4.6.0.20241004"] [project.scripts] mcp = "mcp.cli:app [cli]" diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 664adcd4f..0c6daf8e0 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -140,11 +140,8 @@ async def sse_writer(): async with self._message_queue.active_for_request( session_id, message_callback ): - try: - logger.debug("Yielding read and write streams") - yield (read_stream, write_stream) - finally: - logger.debug(f"Closing SSE connection for session {session_id}") + logger.debug("Yielding read and write streams") + yield (read_stream, write_stream) async def handle_post_message( self, scope: Scope, receive: Receive, send: Send From d6075bb6d64453888757196d4cad1945ebf6e80d Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 14 Apr 2025 16:18:51 -0400 Subject: [PATCH 17/51] update lock --- uv.lock | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/uv.lock b/uv.lock index 20a599051..750d9b02c 100644 --- a/uv.lock +++ b/uv.lock @@ -559,13 +559,11 @@ cli = [ ] redis = [ { name = "redis" }, + { name = "types-redis" }, ] rich = [ { name = "rich" }, ] -types-redis = [ - { name = "types-redis" }, -] ws = [ { name = "websockets" }, ] @@ -600,7 +598,7 @@ requires-dist = [ { name = "sse-starlette", specifier = ">=1.6.1" }, { name = "starlette", specifier = ">=0.27" }, { name = "typer", marker = "extra == 'cli'", specifier = ">=0.12.4" }, - { name = "types-redis", marker = "extra == 'types-redis'", specifier = ">=4.6.0.20241004" }, + { name = "types-redis", marker = "extra == 'redis'", specifier = ">=4.6.0.20241004" }, { name = "uvicorn", specifier = ">=0.23.1" }, { name = "websockets", marker = "extra == 'ws'", specifier = ">=15.0.1" }, ] From 8ee3a7ee48b1158ab29a7ff0ebe74e04a7850308 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 14 Apr 2025 16:20:56 -0400 Subject: [PATCH 18/51] remove redundant comment --- src/mcp/server/fastmcp/server.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index d2a9c2f99..a03129ed7 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -485,9 +485,7 @@ async def run_sse_async(self) -> None: def sse_app(self) -> Starlette: """Return an instance of the SSE server app.""" - # Use a custom provided message queue if available message_queue = self.settings.message_queue - # If no message queue is provided, create an in-memory queue as default if message_queue is None: from mcp.server.message_queue import InMemoryMessageQueue From 7cabcea26a02d02b3a2f964c45f70681a5dd6ded Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 14 Apr 2025 16:25:31 -0400 Subject: [PATCH 19/51] add a checkpoint --- src/mcp/server/message_queue/redis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index d16fd1b0d..29371b9f5 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -5,7 +5,7 @@ from uuid import UUID import anyio -from anyio import CapacityLimiter +from anyio import CapacityLimiter, lowlevel import mcp.types as types from mcp.server.message_queue.base import MessageCallback @@ -42,6 +42,7 @@ def __init__( self._prefix = prefix self._active_sessions_key = f"{prefix}active_sessions" self._callbacks: dict[UUID, MessageCallback] = {} + # Ensures only one polling task runs at a time for message handling self._limiter = CapacityLimiter(1) logger.debug(f"Initialized Redis message queue with URL: {redis_url}") @@ -73,6 +74,7 @@ async def _listen_for_messages(self) -> None: """Background task that listens for messages on subscribed channels.""" async with self._limiter: while True: + await lowlevel.checkpoint() message: None | dict[str, Any] = await self._pubsub.get_message( # type: ignore ignore_subscribe_messages=True, timeout=None, # type: ignore From 5111c92c879409b1ede77e72373462a7d5f41706 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 15 Apr 2025 15:08:54 -0400 Subject: [PATCH 20/51] naming changes --- README.md | 16 +++++------ src/mcp/server/message_queue/base.py | 39 ++++++++++++--------------- src/mcp/server/message_queue/redis.py | 38 +++++++++++++++----------- src/mcp/server/sse.py | 2 +- 4 files changed, 48 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 9f6c3fa39..55ba083b2 100644 --- a/README.md +++ b/README.md @@ -385,22 +385,22 @@ app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app())) For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes). -#### Message Queue Options +#### Message Dispatch Options -By default, the SSE server uses an in-memory message queue for incoming POST messages. For production deployments or distributed scenarios, you can use Redis: +By default, the SSE server uses an in-memory message dispatch system for incoming POST messages. For production deployments or distributed scenarios, you can use Redis or implement your own message dispatch system that conforms to the `MessageDispatch` protocol: ```python -# Using the built-in Redis message queue +# Using the built-in Redis message dispatch from mcp.server.fastmcp import FastMCP -from mcp.server.message_queue import RedisMessageQueue +from mcp.server.message_queue import RedisMessageDispatch -# Create a Redis message queue -redis_queue = RedisMessageQueue( +# Create a Redis message dispatch +redis_dispatch = RedisMessageDispatch( redis_url="redis://localhost:6379/0", prefix="mcp:pubsub:" ) -# Pass the message queue instance to the server -mcp = FastMCP("My App", message_queue=redis_queue) +# Pass the message dispatch instance to the server +mcp = FastMCP("My App", message_queue=redis_dispatch) ``` To use Redis, add the Redis dependency: diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index ceea99334..821a26429 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -12,8 +12,8 @@ @runtime_checkable -class MessageQueue(Protocol): - """Abstract interface for SSE messaging. +class MessageDispatch(Protocol): + """Abstract interface for SSE message dispatching. This interface allows messages to be published to sessions and callbacks to be registered for message handling, enabling multiple servers to handle requests. @@ -34,11 +34,11 @@ async def publish_message( ... @asynccontextmanager - async def active_for_request(self, session_id: UUID, callback: MessageCallback): - """Request-scoped context manager that ensures the listener is active. + async def subscribe(self, session_id: UUID, callback: MessageCallback): + """Request-scoped context manager that subscribes to messages for a session. Args: - session_id: The UUID of the session to activate + session_id: The UUID of the session to subscribe to callback: Async callback function to handle messages for this session """ yield @@ -55,49 +55,44 @@ async def session_exists(self, session_id: UUID) -> bool: ... -class InMemoryMessageQueue: - """Default in-memory implementation of the MessageQueue interface. +class InMemoryMessageDispatch: + """Default in-memory implementation of the MessageDispatch interface. - This implementation immediately calls registered callbacks when messages - are received. + This implementation immediately dispatches messages to registered callbacks when + messages are received without any queuing behavior. """ def __init__(self) -> None: self._callbacks: dict[UUID, MessageCallback] = {} - self._active_sessions: set[UUID] = set() + # We don't need a separate _active_sessions set since _callbacks already tracks this async def publish_message( self, session_id: UUID, message: types.JSONRPCMessage | Exception ) -> bool: """Publish a message for the specified session.""" - if not await self.session_exists(session_id): + if session_id not in self._callbacks: logger.warning(f"Message received for unknown session {session_id}") return False - # Call the callback directly if registered - if session_id in self._callbacks: - await self._callbacks[session_id](message) - logger.debug(f"Called callback for session {session_id}") - else: - logger.warning(f"No callback registered for session {session_id}") + # Call the callback directly + await self._callbacks[session_id](message) + logger.debug(f"Called callback for session {session_id}") return True @asynccontextmanager - async def active_for_request(self, session_id: UUID, callback: MessageCallback): - """Request-scoped context manager that ensures the listener is active.""" - self._active_sessions.add(session_id) + async def subscribe(self, session_id: UUID, callback: MessageCallback): + """Request-scoped context manager that subscribes to messages for a session.""" self._callbacks[session_id] = callback logger.debug(f"Registered session {session_id} with callback") try: yield finally: - self._active_sessions.discard(session_id) if session_id in self._callbacks: del self._callbacks[session_id] logger.debug(f"Unregistered session {session_id}") async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" - return session_id in self._active_sessions + return session_id in self._callbacks diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 29371b9f5..46fe911d4 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -21,8 +21,8 @@ logger = logging.getLogger(__name__) -class RedisMessageQueue: - """Redis implementation of the MessageQueue interface using pubsub. +class RedisMessageDispatch: + """Redis implementation of the MessageDispatch interface using pubsub. This implementation uses Redis pubsub for real-time message distribution across multiple servers handling the same sessions. @@ -31,7 +31,7 @@ class RedisMessageQueue: def __init__( self, redis_url: str = "redis://localhost:6379/0", prefix: str = "mcp:pubsub:" ) -> None: - """Initialize Redis message queue. + """Initialize Redis message dispatch. Args: redis_url: Redis connection string @@ -44,15 +44,15 @@ def __init__( self._callbacks: dict[UUID, MessageCallback] = {} # Ensures only one polling task runs at a time for message handling self._limiter = CapacityLimiter(1) - logger.debug(f"Initialized Redis message queue with URL: {redis_url}") + logger.debug(f"Initialized Redis message dispatch with URL: {redis_url}") def _session_channel(self, session_id: UUID) -> str: """Get the Redis channel for a session.""" return f"{self._prefix}session:{session_id.hex}" @asynccontextmanager - async def active_for_request(self, session_id: UUID, callback: MessageCallback): - """Request-scoped context manager that ensures the listener task is running.""" + async def subscribe(self, session_id: UUID, callback: MessageCallback): + """Request-scoped context manager that subscribes to messages for a session.""" await self._redis.sadd(self._active_sessions_key, session_id.hex) self._callbacks[session_id] = callback channel = self._session_channel(session_id) @@ -98,17 +98,23 @@ async def _listen_for_messages(self) -> None: msg: None | types.JSONRPCMessage | Exception = None try: json_data = json.loads(data) - if isinstance(json_data, dict): - json_dict: dict[str, Any] = json_data - if json_dict.get("_exception", False): - msg = Exception( - f"{json_dict['type']}: {json_dict['message']}" - ) + if not isinstance(json_data, dict): + logger.error(f"Received non-dict JSON data: {type(json_data)}") + continue + + json_dict: dict[str, Any] = json_data + if json_dict.get("_exception", False): + msg = Exception( + f"{json_dict['type']}: {json_dict['message']}" + ) + else: + msg = types.JSONRPCMessage.model_validate_json(data) + + if msg: + if session_id in self._callbacks: + await self._callbacks[session_id](msg) else: - msg = types.JSONRPCMessage.model_validate_json(data) - - if msg and session_id in self._callbacks: - await self._callbacks[session_id](msg) + logger.warning(f"No callback registered for session {session_id}") except Exception as e: logger.error(f"Failed to process message: {e}") diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 0c6daf8e0..fb0484d1b 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -80,7 +80,7 @@ def __init__( super().__init__() self._endpoint = endpoint - self._message_queue = message_queue or InMemoryMessageQueue() + self._message_dispatch = message_queue or InMemoryMessageDispatch() logger.debug(f"SseServerTransport initialized with endpoint: {endpoint}") @asynccontextmanager From 09e0cabe00e07109f0b9d1dab9cb29d50675e438 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 15 Apr 2025 15:46:12 -0400 Subject: [PATCH 21/51] logging improvements --- src/mcp/server/message_queue/__init__.py | 12 +++--- src/mcp/server/message_queue/base.py | 32 +++++++++----- src/mcp/server/message_queue/redis.py | 54 +++++++++--------------- src/mcp/server/sse.py | 20 +++++---- 4 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/mcp/server/message_queue/__init__.py b/src/mcp/server/message_queue/__init__.py index cbc56a44d..f4a8b9dfa 100644 --- a/src/mcp/server/message_queue/__init__.py +++ b/src/mcp/server/message_queue/__init__.py @@ -1,16 +1,16 @@ """ -Message Queue Module for MCP Server +Message Dispatch Module for MCP Server -This module implements queue interfaces for handling +This module implements dispatch interfaces for handling messages between clients and servers. """ -from mcp.server.message_queue.base import InMemoryMessageQueue, MessageQueue +from mcp.server.message_queue.base import InMemoryMessageDispatch, MessageDispatch # Try to import Redis implementation if available try: - from mcp.server.message_queue.redis import RedisMessageQueue + from mcp.server.message_queue.redis import RedisMessageDispatch except ImportError: - RedisMessageQueue = None + RedisMessageDispatch = None -__all__ = ["MessageQueue", "InMemoryMessageQueue", "RedisMessageQueue"] +__all__ = ["MessageDispatch", "InMemoryMessageDispatch", "RedisMessageDispatch"] diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index 821a26429..236aac05d 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -3,6 +3,7 @@ from contextlib import asynccontextmanager from typing import Protocol, runtime_checkable from uuid import UUID +from pydantic import ValidationError import mcp.types as types @@ -20,13 +21,13 @@ class MessageDispatch(Protocol): """ async def publish_message( - self, session_id: UUID, message: types.JSONRPCMessage | Exception + self, session_id: UUID, message: types.JSONRPCMessage | str ) -> bool: """Publish a message for the specified session. Args: session_id: The UUID of the session this message is for - message: The message to publish + message: The message to publish (JSONRPCMessage or str for invalid JSON) Returns: bool: True if message was published, False if session not found @@ -67,31 +68,40 @@ def __init__(self) -> None: # We don't need a separate _active_sessions set since _callbacks already tracks this async def publish_message( - self, session_id: UUID, message: types.JSONRPCMessage | Exception + self, session_id: UUID, message: types.JSONRPCMessage | str ) -> bool: """Publish a message for the specified session.""" if session_id not in self._callbacks: - logger.warning(f"Message received for unknown session {session_id}") + logger.warning(f"Message dropped: unknown session {session_id}") return False - - # Call the callback directly - await self._callbacks[session_id](message) - logger.debug(f"Called callback for session {session_id}") - + + # For string messages, attempt parsing and recreate original ValidationError if invalid + if isinstance(message, str): + try: + callback_argument = types.JSONRPCMessage.model_validate_json(message) + except ValidationError as exc: + callback_argument = exc + else: + callback_argument = message + + # Call the callback with either valid message or recreated ValidationError + await self._callbacks[session_id](callback_argument) + + logger.debug(f"Message dispatched to session {session_id}") return True @asynccontextmanager async def subscribe(self, session_id: UUID, callback: MessageCallback): """Request-scoped context manager that subscribes to messages for a session.""" self._callbacks[session_id] = callback - logger.debug(f"Registered session {session_id} with callback") + logger.debug(f"Subscribing to messages for session {session_id}") try: yield finally: if session_id in self._callbacks: del self._callbacks[session_id] - logger.debug(f"Unregistered session {session_id}") + logger.debug(f"Unsubscribed from session {session_id}") async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 46fe911d4..62f3b38d2 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -1,8 +1,8 @@ -import json import logging from contextlib import asynccontextmanager from typing import Any, cast from uuid import UUID +from pydantic import ValidationError import anyio from anyio import CapacityLimiter, lowlevel @@ -44,7 +44,7 @@ def __init__( self._callbacks: dict[UUID, MessageCallback] = {} # Ensures only one polling task runs at a time for message handling self._limiter = CapacityLimiter(1) - logger.debug(f"Initialized Redis message dispatch with URL: {redis_url}") + logger.debug(f"Redis message dispatch initialized: {redis_url}") def _session_channel(self, session_id: UUID) -> str: """Get the Redis channel for a session.""" @@ -58,7 +58,7 @@ async def subscribe(self, session_id: UUID, callback: MessageCallback): channel = self._session_channel(session_id) await self._pubsub.subscribe(channel) # type: ignore - logger.debug(f"Registered session {session_id} in Redis with callback") + logger.debug(f"Subscribing to Redis channel for session {session_id}") async with anyio.create_task_group() as tg: tg.start_soon(self._listen_for_messages) try: @@ -68,7 +68,7 @@ async def subscribe(self, session_id: UUID, callback: MessageCallback): await self._pubsub.unsubscribe(channel) # type: ignore await self._redis.srem(self._active_sessions_key, session_id.hex) del self._callbacks[session_id] - logger.debug(f"Unregistered session {session_id} from Redis") + logger.debug(f"Unsubscribed from Redis channel for session {session_id}") async def _listen_for_messages(self) -> None: """Background task that listens for messages on subscribed channels.""" @@ -85,61 +85,49 @@ async def _listen_for_messages(self) -> None: # Extract session ID from channel name channel: str = cast(str, message["channel"]) if not channel.startswith(self._prefix): + logger.debug(f"Ignoring message from non-MCP channel: {channel}") continue session_hex = channel.split(":")[-1] try: session_id = UUID(hex=session_hex) except ValueError: - logger.error(f"Invalid session channel: {channel}") + logger.error(f"Received message for invalid session channel: {channel}") continue data: str = cast(str, message["data"]) - msg: None | types.JSONRPCMessage | Exception = None try: - json_data = json.loads(data) - if not isinstance(json_data, dict): - logger.error(f"Received non-dict JSON data: {type(json_data)}") + if session_id not in self._callbacks: + logger.warning(f"Message dropped: no callback for session {session_id}") continue - json_dict: dict[str, Any] = json_data - if json_dict.get("_exception", False): - msg = Exception( - f"{json_dict['type']}: {json_dict['message']}" - ) - else: + # Try to parse as valid message or recreate original ValidationError + try: msg = types.JSONRPCMessage.model_validate_json(data) - - if msg: - if session_id in self._callbacks: - await self._callbacks[session_id](msg) - else: - logger.warning(f"No callback registered for session {session_id}") + await self._callbacks[session_id](msg) + except ValidationError as exc: + # Pass the identical validation error that would have occurred originally + await self._callbacks[session_id](exc) except Exception as e: - logger.error(f"Failed to process message: {e}") + logger.error(f"Error processing message for session {session_id}: {e}") async def publish_message( - self, session_id: UUID, message: types.JSONRPCMessage | Exception + self, session_id: UUID, message: types.JSONRPCMessage | str ) -> bool: """Publish a message for the specified session.""" if not await self.session_exists(session_id): - logger.warning(f"Message received for unknown session {session_id}") + logger.warning(f"Message dropped: unknown session {session_id}") return False - if isinstance(message, Exception): - data = json.dumps( - { - "_exception": True, - "type": type(message).__name__, - "message": str(message), - } - ) + # Pass raw JSON strings directly, preserving validation errors + if isinstance(message, str): + data = message else: data = message.model_dump_json() channel = self._session_channel(session_id) await self._redis.publish(channel, data) # type: ignore[attr-defined] - logger.debug(f"Published message to Redis channel for session {session_id}") + logger.debug(f"Message published to Redis channel for session {session_id}") return True async def session_exists(self, session_id: UUID) -> bool: diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index fb0484d1b..533e23634 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -46,7 +46,7 @@ async def handle_sse(request): from starlette.types import Receive, Scope, Send import mcp.types as types -from mcp.server.message_queue import InMemoryMessageQueue, MessageQueue +from mcp.server.message_queue import InMemoryMessageDispatch, MessageDispatch logger = logging.getLogger(__name__) @@ -64,10 +64,10 @@ class SseServerTransport: """ _endpoint: str - _message_queue: MessageQueue + _message_dispatch: MessageDispatch def __init__( - self, endpoint: str, message_queue: MessageQueue | None = None + self, endpoint: str, message_dispatch: MessageDispatch | None = None ) -> None: """ Creates a new SSE server transport, which will direct the client to POST @@ -75,12 +75,12 @@ def __init__( Args: endpoint: The endpoint URL for SSE connections - message_queue: Optional message queue to use + message_dispatch: Optional message dispatch to use """ super().__init__() self._endpoint = endpoint - self._message_dispatch = message_queue or InMemoryMessageDispatch() + self._message_dispatch = message_dispatch or InMemoryMessageDispatch() logger.debug(f"SseServerTransport initialized with endpoint: {endpoint}") @asynccontextmanager @@ -137,7 +137,7 @@ async def sse_writer(): logger.debug("Starting SSE response task") tg.start_soon(response, scope, receive, send) - async with self._message_queue.active_for_request( + async with self._message_dispatch.subscribe( session_id, message_callback ): logger.debug("Yielding read and write streams") @@ -163,7 +163,7 @@ async def handle_post_message( response = Response("Invalid session ID", status_code=400) return await response(scope, receive, send) - if not await self._message_queue.session_exists(session_id): + if not await self._message_dispatch.session_exists(session_id): logger.warning(f"Could not find session for ID: {session_id}") response = Response("Could not find session", status_code=404) return await response(scope, receive, send) @@ -178,10 +178,12 @@ async def handle_post_message( logger.error(f"Failed to parse message: {err}") response = Response("Could not parse message", status_code=400) await response(scope, receive, send) - await self._message_queue.publish_message(session_id, err) + # Pass raw JSON string through dispatch; original ValidationError will be recreated when + # the receiver tries to validate the same invalid JSON + await self._message_dispatch.publish_message(session_id, body.decode()) return logger.debug(f"Publishing message for session {session_id}: {message}") response = Response("Accepted", status_code=202) await response(scope, receive, send) - await self._message_queue.publish_message(session_id, message) + await self._message_dispatch.publish_message(session_id, message) From 8d280d8bba0711b5ce00facbb3d0f00674d18694 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 15 Apr 2025 15:49:20 -0400 Subject: [PATCH 22/51] better channel validation --- src/mcp/server/message_queue/redis.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 62f3b38d2..d5e53afb4 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -82,17 +82,22 @@ async def _listen_for_messages(self) -> None: if message is None: continue - # Extract session ID from channel name channel: str = cast(str, message["channel"]) - if not channel.startswith(self._prefix): + expected_prefix = f"{self._prefix}session:" + + if not channel.startswith(expected_prefix): logger.debug(f"Ignoring message from non-MCP channel: {channel}") continue - - session_hex = channel.split(":")[-1] + + session_hex = channel[len(expected_prefix):] try: session_id = UUID(hex=session_hex) + expected_channel = self._session_channel(session_id) + if channel != expected_channel: + logger.error(f"Channel format mismatch: {channel}") + continue except ValueError: - logger.error(f"Received message for invalid session channel: {channel}") + logger.error(f"Received message with invalid UUID in channel: {channel}") continue data: str = cast(str, message["data"]) From 87e07b8ed18334be251c62d04b348c31d49e6b50 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 15 Apr 2025 16:16:11 -0400 Subject: [PATCH 23/51] formatting and linting --- src/mcp/server/fastmcp/server.py | 1 + src/mcp/server/message_queue/base.py | 13 ++++++------ src/mcp/server/message_queue/redis.py | 20 +++++++++---------- src/mcp/server/sse.py | 8 +++----- .../fastmcp/servers/test_file_server.py | 8 ++------ 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 5925a4d0d..1522638c6 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -472,6 +472,7 @@ async def run_stdio_async(self) -> None: async def run_sse_async(self) -> None: """Run the server using SSE transport.""" import uvicorn + starlette_app = self.sse_app() config = uvicorn.Config( diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index 236aac05d..856f97632 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -3,6 +3,7 @@ from contextlib import asynccontextmanager from typing import Protocol, runtime_checkable from uuid import UUID + from pydantic import ValidationError import mcp.types as types @@ -59,13 +60,13 @@ async def session_exists(self, session_id: UUID) -> bool: class InMemoryMessageDispatch: """Default in-memory implementation of the MessageDispatch interface. - This implementation immediately dispatches messages to registered callbacks when + This implementation immediately dispatches messages to registered callbacks when messages are received without any queuing behavior. """ def __init__(self) -> None: self._callbacks: dict[UUID, MessageCallback] = {} - # We don't need a separate _active_sessions set since _callbacks already tracks this + # _callbacks tracks active sessions, no need for separate _active_sessions set async def publish_message( self, session_id: UUID, message: types.JSONRPCMessage | str @@ -74,8 +75,8 @@ async def publish_message( if session_id not in self._callbacks: logger.warning(f"Message dropped: unknown session {session_id}") return False - - # For string messages, attempt parsing and recreate original ValidationError if invalid + + # Parse string messages or recreate original ValidationError if isinstance(message, str): try: callback_argument = types.JSONRPCMessage.model_validate_json(message) @@ -83,10 +84,10 @@ async def publish_message( callback_argument = exc else: callback_argument = message - + # Call the callback with either valid message or recreated ValidationError await self._callbacks[session_id](callback_argument) - + logger.debug(f"Message dispatched to session {session_id}") return True diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index d5e53afb4..d03d10304 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -2,10 +2,10 @@ from contextlib import asynccontextmanager from typing import Any, cast from uuid import UUID -from pydantic import ValidationError import anyio from anyio import CapacityLimiter, lowlevel +from pydantic import ValidationError import mcp.types as types from mcp.server.message_queue.base import MessageCallback @@ -68,7 +68,7 @@ async def subscribe(self, session_id: UUID, callback: MessageCallback): await self._pubsub.unsubscribe(channel) # type: ignore await self._redis.srem(self._active_sessions_key, session_id.hex) del self._callbacks[session_id] - logger.debug(f"Unsubscribed from Redis channel for session {session_id}") + logger.debug(f"Unsubscribed from Redis channel: {session_id}") async def _listen_for_messages(self) -> None: """Background task that listens for messages on subscribed channels.""" @@ -84,12 +84,12 @@ async def _listen_for_messages(self) -> None: channel: str = cast(str, message["channel"]) expected_prefix = f"{self._prefix}session:" - + if not channel.startswith(expected_prefix): logger.debug(f"Ignoring message from non-MCP channel: {channel}") continue - - session_hex = channel[len(expected_prefix):] + + session_hex = channel[len(expected_prefix) :] try: session_id = UUID(hex=session_hex) expected_channel = self._session_channel(session_id) @@ -97,24 +97,24 @@ async def _listen_for_messages(self) -> None: logger.error(f"Channel format mismatch: {channel}") continue except ValueError: - logger.error(f"Received message with invalid UUID in channel: {channel}") + logger.error(f"Invalid UUID in channel: {channel}") continue data: str = cast(str, message["data"]) try: if session_id not in self._callbacks: - logger.warning(f"Message dropped: no callback for session {session_id}") + logger.warning(f"Message dropped: no callback for {session_id}") continue - + # Try to parse as valid message or recreate original ValidationError try: msg = types.JSONRPCMessage.model_validate_json(data) await self._callbacks[session_id](msg) except ValidationError as exc: - # Pass the identical validation error that would have occurred originally + # Pass the identical validation error that would have occurred await self._callbacks[session_id](exc) except Exception as e: - logger.error(f"Error processing message for session {session_id}: {e}") + logger.error(f"Error processing message for {session_id}: {e}") async def publish_message( self, session_id: UUID, message: types.JSONRPCMessage | str diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 533e23634..e3b00bd97 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -137,9 +137,7 @@ async def sse_writer(): logger.debug("Starting SSE response task") tg.start_soon(response, scope, receive, send) - async with self._message_dispatch.subscribe( - session_id, message_callback - ): + async with self._message_dispatch.subscribe(session_id, message_callback): logger.debug("Yielding read and write streams") yield (read_stream, write_stream) @@ -178,8 +176,8 @@ async def handle_post_message( logger.error(f"Failed to parse message: {err}") response = Response("Could not parse message", status_code=400) await response(scope, receive, send) - # Pass raw JSON string through dispatch; original ValidationError will be recreated when - # the receiver tries to validate the same invalid JSON + # Pass raw JSON string; receiver will recreate identical ValidationError + # when parsing the same invalid JSON await self._message_dispatch.publish_message(session_id, body.decode()) return diff --git a/tests/server/fastmcp/servers/test_file_server.py b/tests/server/fastmcp/servers/test_file_server.py index c1f51cabe..b40778ea8 100644 --- a/tests/server/fastmcp/servers/test_file_server.py +++ b/tests/server/fastmcp/servers/test_file_server.py @@ -114,17 +114,13 @@ async def test_read_resource_file(mcp: FastMCP): @pytest.mark.anyio async def test_delete_file(mcp: FastMCP, test_dir: Path): - await mcp.call_tool( - "delete_file", arguments={"path": str(test_dir / "example.py")} - ) + await mcp.call_tool("delete_file", arguments={"path": str(test_dir / "example.py")}) assert not (test_dir / "example.py").exists() @pytest.mark.anyio async def test_delete_file_and_check_resources(mcp: FastMCP, test_dir: Path): - await mcp.call_tool( - "delete_file", arguments={"path": str(test_dir / "example.py")} - ) + await mcp.call_tool("delete_file", arguments={"path": str(test_dir / "example.py")}) res_iter = await mcp.read_resource("file://test_dir/example.py") res_list = list(res_iter) assert len(res_list) == 1 From b48428486aa90f7529c36e5a78074ac2a2d813bc Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 15 Apr 2025 16:17:26 -0400 Subject: [PATCH 24/51] fix naming in server.py --- src/mcp/server/fastmcp/server.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 1522638c6..d160ed67b 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -32,7 +32,7 @@ from mcp.server.lowlevel.server import LifespanResultT from mcp.server.lowlevel.server import Server as MCPServer from mcp.server.lowlevel.server import lifespan as default_lifespan -from mcp.server.message_queue import MessageQueue +from mcp.server.message_queue import MessageDispatch from mcp.server.session import ServerSession, ServerSessionT from mcp.server.sse import SseServerTransport from mcp.server.stdio import stdio_server @@ -77,8 +77,8 @@ class Settings(BaseSettings, Generic[LifespanResultT]): message_path: str = "/messages/" # SSE message queue settings - message_queue: MessageQueue | None = Field( - None, description="Custom message queue instance" + message_dispatch: MessageDispatch | None = Field( + None, description="Custom message dispatch instance" ) # resource settings @@ -486,15 +486,15 @@ async def run_sse_async(self) -> None: def sse_app(self) -> Starlette: """Return an instance of the SSE server app.""" - message_queue = self.settings.message_queue - if message_queue is None: - from mcp.server.message_queue import InMemoryMessageQueue + message_dispatch = self.settings.message_dispatch + if message_dispatch is None: + from mcp.server.message_queue import InMemoryMessageDispatch - message_queue = InMemoryMessageQueue() - logger.info("Using default in-memory message queue") + message_dispatch = InMemoryMessageDispatch() + logger.info("Using default in-memory message dispatch") sse = SseServerTransport( - self.settings.message_path, message_queue=message_queue + self.settings.message_path, message_dispatch=message_dispatch ) async def handle_sse(request: Request) -> None: From 0bfd800dfce2c93ff34ac2642113abd1e2aba9f6 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 21 Apr 2025 13:27:59 -0700 Subject: [PATCH 25/51] Rework to fix POST blocking issue --- src/mcp/server/message_queue/base.py | 45 ++++- src/mcp/server/message_queue/redis.py | 240 +++++++++++++++++++------- src/mcp/server/sse.py | 16 +- 3 files changed, 234 insertions(+), 67 deletions(-) diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index 856f97632..d9a0beb57 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -4,7 +4,7 @@ from typing import Protocol, runtime_checkable from uuid import UUID -from pydantic import ValidationError +from pydantic import BaseModel, ValidationError import mcp.types as types @@ -13,6 +13,18 @@ MessageCallback = Callable[[types.JSONRPCMessage | Exception], Awaitable[None]] +class MessageWrapper(BaseModel): + message_id: str + payload: str + + def get_json_rpc_message(self) -> types.JSONRPCMessage | ValidationError: + """Parse the payload into a JSONRPCMessage or return ValidationError.""" + try: + return types.JSONRPCMessage.model_validate_json(self.payload) + except ValidationError as exc: + return exc + + @runtime_checkable class MessageDispatch(Protocol): """Abstract interface for SSE message dispatching. @@ -35,6 +47,25 @@ async def publish_message( """ ... + async def publish_message_sync( + self, session_id: UUID, message: types.JSONRPCMessage | str, timeout: float = 30.0 + ) -> bool: + """Publish a message for the specified session and wait for consumption confirmation. + + This method blocks until the message has been fully consumed by the subscriber, + or until the timeout is reached. + + Args: + session_id: The UUID of the session this message is for + message: The message to publish (JSONRPCMessage or str for invalid JSON) + timeout: Maximum time to wait for consumption in seconds + + Returns: + bool: True if message was published and consumed, False otherwise + """ + # Default implementation falls back to standard publish + return await self.publish_message(session_id, message) + @asynccontextmanager async def subscribe(self, session_id: UUID, callback: MessageCallback): """Request-scoped context manager that subscribes to messages for a session. @@ -90,6 +121,18 @@ async def publish_message( logger.debug(f"Message dispatched to session {session_id}") return True + + async def publish_message_sync( + self, session_id: UUID, message: types.JSONRPCMessage | str, timeout: float = 30.0 + ) -> bool: + """Publish a message for the specified session and wait for consumption. + + For InMemoryMessageDispatch, this is the same as publish_message since + the callback is executed synchronously. + """ + # For in-memory dispatch, the message is processed immediately + # so we can just call the regular publish method + return await self.publish_message(session_id, message) @asynccontextmanager async def subscribe(self, session_id: UUID, callback: MessageCallback): diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index d03d10304..e405d68c7 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -1,14 +1,14 @@ import logging from contextlib import asynccontextmanager from typing import Any, cast -from uuid import UUID +from uuid import UUID, uuid4 import anyio -from anyio import CapacityLimiter, lowlevel -from pydantic import ValidationError +from anyio import CancelScope, CapacityLimiter, Event, lowlevel +from anyio.abc import TaskGroup import mcp.types as types -from mcp.server.message_queue.base import MessageCallback +from mcp.server.message_queue.base import MessageCallback, MessageWrapper try: import redis.asyncio as redis @@ -42,98 +42,212 @@ def __init__( self._prefix = prefix self._active_sessions_key = f"{prefix}active_sessions" self._callbacks: dict[UUID, MessageCallback] = {} - # Ensures only one polling task runs at a time for message handling self._limiter = CapacityLimiter(1) + self._ack_events: dict[str, Event] = {} + logger.debug(f"Redis message dispatch initialized: {redis_url}") def _session_channel(self, session_id: UUID) -> str: """Get the Redis channel for a session.""" return f"{self._prefix}session:{session_id.hex}" + def _ack_channel(self, session_id: UUID) -> str: + """Get the acknowledgment channel for a session.""" + return f"{self._prefix}ack:{session_id.hex}" + @asynccontextmanager async def subscribe(self, session_id: UUID, callback: MessageCallback): """Request-scoped context manager that subscribes to messages for a session.""" await self._redis.sadd(self._active_sessions_key, session_id.hex) self._callbacks[session_id] = callback - channel = self._session_channel(session_id) - await self._pubsub.subscribe(channel) # type: ignore - - logger.debug(f"Subscribing to Redis channel for session {session_id}") - async with anyio.create_task_group() as tg: - tg.start_soon(self._listen_for_messages) - try: - yield - finally: - tg.cancel_scope.cancel() - await self._pubsub.unsubscribe(channel) # type: ignore - await self._redis.srem(self._active_sessions_key, session_id.hex) - del self._callbacks[session_id] - logger.debug(f"Unsubscribed from Redis channel: {session_id}") - - async def _listen_for_messages(self) -> None: + + session_channel = self._session_channel(session_id) + ack_channel = self._ack_channel(session_id) + + await self._pubsub.subscribe(session_channel) # type: ignore + await self._pubsub.subscribe(ack_channel) # type: ignore + + logger.debug(f"Subscribing to Redis channels for session {session_id}") + + # Two nested task groups ensure proper cleanup: the inner one cancels the + # listener, while the outer one allows any handlers to complete before exiting. + async with anyio.create_task_group() as tg_handler: + async with anyio.create_task_group() as tg: + tg.start_soon(self._listen_for_messages, tg_handler) + try: + yield + finally: + tg.cancel_scope.cancel() + await self._pubsub.unsubscribe(session_channel) # type: ignore + await self._pubsub.unsubscribe(ack_channel) # type: ignore + await self._redis.srem(self._active_sessions_key, session_id.hex) + del self._callbacks[session_id] + logger.debug( + f"Unsubscribed from Redis channels for session {session_id}" + ) + + async def _listen_for_messages(self, tg_handler: TaskGroup) -> None: """Background task that listens for messages on subscribed channels.""" async with self._limiter: while True: await lowlevel.checkpoint() - message: None | dict[str, Any] = await self._pubsub.get_message( # type: ignore - ignore_subscribe_messages=True, - timeout=None, # type: ignore - ) - if message is None: - continue - - channel: str = cast(str, message["channel"]) - expected_prefix = f"{self._prefix}session:" + # Shield message retrieval from cancellation to ensure no messages are + # lost when a session disconnects during processing. + with CancelScope(shield=True): + redis_message: ( # type: ignore + None | dict[str, Any] + ) = await self._pubsub.get_message( # type: ignore + ignore_subscribe_messages=True, + timeout=0.1, # type: ignore + ) + if redis_message is None: + continue - if not channel.startswith(expected_prefix): - logger.debug(f"Ignoring message from non-MCP channel: {channel}") - continue + channel: str = cast(str, redis_message["channel"]) + data: str = cast(str, redis_message["data"]) - session_hex = channel[len(expected_prefix) :] - try: - session_id = UUID(hex=session_hex) - expected_channel = self._session_channel(session_id) - if channel != expected_channel: - logger.error(f"Channel format mismatch: {channel}") + # Handle acknowledgment messages + if channel.startswith(f"{self._prefix}ack:"): + tg_handler.start_soon(self._handle_ack_message, channel, data) continue - except ValueError: - logger.error(f"Invalid UUID in channel: {channel}") - continue - data: str = cast(str, message["data"]) - try: - if session_id not in self._callbacks: - logger.warning(f"Message dropped: no callback for {session_id}") + # Handle session messages + elif channel.startswith(f"{self._prefix}session:"): + tg_handler.start_soon( + self._handle_session_message, channel, data + ) continue - # Try to parse as valid message or recreate original ValidationError - try: - msg = types.JSONRPCMessage.model_validate_json(data) - await self._callbacks[session_id](msg) - except ValidationError as exc: - # Pass the identical validation error that would have occurred - await self._callbacks[session_id](exc) - except Exception as e: - logger.error(f"Error processing message for {session_id}: {e}") + # Ignore other channels + else: + logger.debug( + f"Ignoring message from non-MCP channel: {channel}" + ) + + async def _handle_ack_message(self, channel: str, data: str) -> None: + """Handle acknowledgment messages received on ack channels.""" + ack_prefix = f"{self._prefix}ack:" + if not channel.startswith(ack_prefix): + return + + # Validate channel format exactly matches our expected format + session_hex = channel[len(ack_prefix) :] + try: + # Validate this is a valid UUID hex and channel has correct format + session_id = UUID(hex=session_hex) + expected_channel = self._ack_channel(session_id) + if channel != expected_channel: + logger.error( + f"Channel mismatch: got {channel}, expected {expected_channel}" + ) + return + except ValueError: + logger.error(f"Invalid UUID hex in ack channel: {channel}") + return + + # Extract message ID from data + message_id = data.strip() + if message_id in self._ack_events: + logger.debug(f"Received acknowledgment for message: {message_id}") + self._ack_events[message_id].set() + + async def _handle_session_message(self, channel: str, data: str) -> None: + """Handle regular messages received on session channels.""" + session_prefix = f"{self._prefix}session:" + if not channel.startswith(session_prefix): + return + + session_hex = channel[len(session_prefix) :] + try: + session_id = UUID(hex=session_hex) + expected_channel = self._session_channel(session_id) + if channel != expected_channel: + logger.error( + f"Channel mismatch: got {channel}, expected {expected_channel}" + ) + return + except ValueError: + logger.error(f"Invalid UUID hex in session channel: {channel}") + return + + if session_id not in self._callbacks: + logger.warning(f"Message dropped: no callback for {session_id}") + return + + try: + wrapper = MessageWrapper.model_validate_json(data) + result = wrapper.get_json_rpc_message() + await self._callbacks[session_id](result) + await self._send_acknowledgment(session_id, wrapper.message_id) + + except Exception as e: + logger.error(f"Error processing message for {session_id}: {e}") + + async def _send_acknowledgment(self, session_id: UUID, message_id: str) -> None: + """Send an acknowledgment for a message that was successfully processed.""" + ack_channel = self._ack_channel(session_id) + await self._redis.publish(ack_channel, message_id) # type: ignore + logger.debug( + f"Sent acknowledgment for message {message_id} to session {session_id}" + ) async def publish_message( - self, session_id: UUID, message: types.JSONRPCMessage | str - ) -> bool: + self, + session_id: UUID, + message: types.JSONRPCMessage | str, + message_id: str | None = None, + ) -> str | None: """Publish a message for the specified session.""" if not await self.session_exists(session_id): logger.warning(f"Message dropped: unknown session {session_id}") - return False + return None # Pass raw JSON strings directly, preserving validation errors + message_id = message_id or str(uuid4()) if isinstance(message, str): - data = message + wrapper = MessageWrapper(message_id=message_id, payload=message) else: - data = message.model_dump_json() + wrapper = MessageWrapper( + message_id=message_id, payload=message.model_dump_json() + ) channel = self._session_channel(session_id) - await self._redis.publish(channel, data) # type: ignore[attr-defined] - logger.debug(f"Message published to Redis channel for session {session_id}") - return True + await self._redis.publish(channel, wrapper.model_dump_json()) # type: ignore + logger.debug( + f"Message {message_id} published to Redis channel for session {session_id}" + ) + return message_id + + async def publish_message_sync( + self, + session_id: UUID, + message: types.JSONRPCMessage | str, + timeout: float = 120.0, + ) -> bool: + """Publish a message and wait for acknowledgment of processing.""" + message_id = str(uuid4()) + ack_event = Event() + self._ack_events[message_id] = ack_event + + try: + published_id = await self.publish_message(session_id, message, message_id) + if published_id is None: + return False + + with anyio.fail_after(timeout): + await ack_event.wait() + logger.debug(f"Received acknowledgment for message {message_id}") + return True + + except TimeoutError: + logger.warning( + f"Timed out waiting for acknowledgment of message {message_id}" + ) + return False + + finally: + if message_id in self._ack_events: + del self._ack_events[message_id] async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index e3b00bd97..395067ed2 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -174,14 +174,24 @@ async def handle_post_message( logger.debug(f"Validated client message: {message}") except ValidationError as err: logger.error(f"Failed to parse message: {err}") + # Still publish the invalid message, but using synchronized version response = Response("Could not parse message", status_code=400) await response(scope, receive, send) # Pass raw JSON string; receiver will recreate identical ValidationError # when parsing the same invalid JSON - await self._message_dispatch.publish_message(session_id, body.decode()) + await self._message_dispatch.publish_message_sync(session_id, body.decode()) return logger.debug(f"Publishing message for session {session_id}: {message}") - response = Response("Accepted", status_code=202) + + # Use sync publish to block until the message is processed + result = await self._message_dispatch.publish_message_sync(session_id, message) + + if result: + # Message was successfully processed + response = Response("OK", status_code=200) + else: + # Message timed out or failed to be processed + response = Response("Processing Timeout", status_code=504) + await response(scope, receive, send) - await self._message_dispatch.publish_message(session_id, message) From 1e81f36723c8a01fdcb31a59974c02f8a79dbd1e Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 21 Apr 2025 13:32:30 -0700 Subject: [PATCH 26/51] comments fix --- src/mcp/server/message_queue/base.py | 18 ++++++++++++------ src/mcp/server/message_queue/redis.py | 2 -- src/mcp/server/sse.py | 3 +-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index d9a0beb57..2a5d4af03 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -16,7 +16,7 @@ class MessageWrapper(BaseModel): message_id: str payload: str - + def get_json_rpc_message(self) -> types.JSONRPCMessage | ValidationError: """Parse the payload into a JSONRPCMessage or return ValidationError.""" try: @@ -48,9 +48,12 @@ async def publish_message( ... async def publish_message_sync( - self, session_id: UUID, message: types.JSONRPCMessage | str, timeout: float = 30.0 + self, + session_id: UUID, + message: types.JSONRPCMessage | str, + timeout: float = 120.0, ) -> bool: - """Publish a message for the specified session and wait for consumption confirmation. + """Publish a message for the specified session and wait for confirmation. This method blocks until the message has been fully consumed by the subscriber, or until the timeout is reached. @@ -121,12 +124,15 @@ async def publish_message( logger.debug(f"Message dispatched to session {session_id}") return True - + async def publish_message_sync( - self, session_id: UUID, message: types.JSONRPCMessage | str, timeout: float = 30.0 + self, + session_id: UUID, + message: types.JSONRPCMessage | str, + timeout: float = 30.0, ) -> bool: """Publish a message for the specified session and wait for consumption. - + For InMemoryMessageDispatch, this is the same as publish_message since the callback is executed synchronously. """ diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index e405d68c7..9952dfa1d 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -130,10 +130,8 @@ async def _handle_ack_message(self, channel: str, data: str) -> None: if not channel.startswith(ack_prefix): return - # Validate channel format exactly matches our expected format session_hex = channel[len(ack_prefix) :] try: - # Validate this is a valid UUID hex and channel has correct format session_id = UUID(hex=session_hex) expected_channel = self._ack_channel(session_id) if channel != expected_channel: diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 395067ed2..1e9214b4a 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -174,7 +174,6 @@ async def handle_post_message( logger.debug(f"Validated client message: {message}") except ValidationError as err: logger.error(f"Failed to parse message: {err}") - # Still publish the invalid message, but using synchronized version response = Response("Could not parse message", status_code=400) await response(scope, receive, send) # Pass raw JSON string; receiver will recreate identical ValidationError @@ -184,7 +183,7 @@ async def handle_post_message( logger.debug(f"Publishing message for session {session_id}: {message}") - # Use sync publish to block until the message is processed + # Use sync publish, block POST response until the message is processed result = await self._message_dispatch.publish_message_sync(session_id, message) if result: From 215cc42159bc60063a2b6e4c9297cdc5ac2dc667 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 22 Apr 2025 14:05:24 -0700 Subject: [PATCH 27/51] wip --- src/mcp/server/message_queue/redis.py | 144 +++++++++++++++----------- 1 file changed, 84 insertions(+), 60 deletions(-) diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 9952dfa1d..060e37cba 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -42,6 +42,7 @@ def __init__( self._prefix = prefix self._active_sessions_key = f"{prefix}active_sessions" self._callbacks: dict[UUID, MessageCallback] = {} + self._handlers: dict[UUID, TaskGroup] = {} self._limiter = CapacityLimiter(1) self._ack_events: dict[str, Event] = {} @@ -69,24 +70,70 @@ async def subscribe(self, session_id: UUID, callback: MessageCallback): logger.debug(f"Subscribing to Redis channels for session {session_id}") - # Two nested task groups ensure proper cleanup: the inner one cancels the - # listener, while the outer one allows any handlers to complete before exiting. - async with anyio.create_task_group() as tg_handler: - async with anyio.create_task_group() as tg: - tg.start_soon(self._listen_for_messages, tg_handler) - try: - yield - finally: - tg.cancel_scope.cancel() - await self._pubsub.unsubscribe(session_channel) # type: ignore - await self._pubsub.unsubscribe(ack_channel) # type: ignore - await self._redis.srem(self._active_sessions_key, session_id.hex) - del self._callbacks[session_id] - logger.debug( - f"Unsubscribed from Redis channels for session {session_id}" - ) + # Store the task group for the session + async with anyio.create_task_group() as tg: + self._handlers[session_id] = tg + tg.start_soon(self._listen_for_messages) + try: + yield + finally: + tg.cancel_scope.cancel() + await self._pubsub.unsubscribe(session_channel) # type: ignore + await self._pubsub.unsubscribe(ack_channel) # type: ignore + await self._redis.srem(self._active_sessions_key, session_id.hex) + del self._callbacks[session_id] + logger.debug( + f"Unsubscribed from Redis channels for session {session_id}" + ) + del self._handlers[session_id] + + def _parse_ack_channel(self, channel: str) -> UUID | None: + """Parse and validate an acknowledgment channel, returning session_id.""" + ack_prefix = f"{self._prefix}ack:" + if not channel.startswith(ack_prefix): + return None + + # Extract exactly what should be a UUID hex after the prefix + session_hex = channel[len(ack_prefix):] + if len(session_hex) != 32: # Standard UUID hex length + logger.error(f"Invalid UUID length in ack channel: {channel}") + return None + + try: + session_id = UUID(hex=session_hex) + expected_channel = self._ack_channel(session_id) + if channel != expected_channel: + logger.error(f"Channel mismatch: got {channel}, expected {expected_channel}") + return None + return session_id + except ValueError: + logger.error(f"Invalid UUID hex in ack channel: {channel}") + return None + + def _parse_session_channel(self, channel: str) -> UUID | None: + """Parse and validate a session channel, returning session_id.""" + session_prefix = f"{self._prefix}session:" + if not channel.startswith(session_prefix): + return None + + # Extract exactly what should be a UUID hex after the prefix + session_hex = channel[len(session_prefix):] + if len(session_hex) != 32: # Standard UUID hex length + logger.error(f"Invalid UUID length in session channel: {channel}") + return None + + try: + session_id = UUID(hex=session_hex) + expected_channel = self._session_channel(session_id) + if channel != expected_channel: + logger.error(f"Channel mismatch: got {channel}, expected {expected_channel}") + return None + return session_id + except ValueError: + logger.error(f"Invalid UUID hex in session channel: {channel}") + return None - async def _listen_for_messages(self, tg_handler: TaskGroup) -> None: + async def _listen_for_messages(self) -> None: """Background task that listens for messages on subscribed channels.""" async with self._limiter: while True: @@ -106,41 +153,31 @@ async def _listen_for_messages(self, tg_handler: TaskGroup) -> None: channel: str = cast(str, redis_message["channel"]) data: str = cast(str, redis_message["data"]) - # Handle acknowledgment messages + # Determine which session this message is for + session_id = None if channel.startswith(f"{self._prefix}ack:"): - tg_handler.start_soon(self._handle_ack_message, channel, data) - continue - - # Handle session messages + session_id = self._parse_ack_channel(channel) elif channel.startswith(f"{self._prefix}session:"): - tg_handler.start_soon( - self._handle_session_message, channel, data - ) + session_id = self._parse_session_channel(channel) + + if session_id is None: + logger.debug(f"Ignoring message from channel: {channel}") continue - - # Ignore other channels + + if session_id not in self._handlers: + logger.warning(f"Dropping message for non-existent session: {session_id}") + continue + + session_tg = self._handlers[session_id] + if channel.startswith(f"{self._prefix}ack:"): + session_tg.start_soon(self._handle_ack_message, channel, data) else: - logger.debug( - f"Ignoring message from non-MCP channel: {channel}" - ) + session_tg.start_soon(self._handle_session_message, channel, data) async def _handle_ack_message(self, channel: str, data: str) -> None: """Handle acknowledgment messages received on ack channels.""" - ack_prefix = f"{self._prefix}ack:" - if not channel.startswith(ack_prefix): - return - - session_hex = channel[len(ack_prefix) :] - try: - session_id = UUID(hex=session_hex) - expected_channel = self._ack_channel(session_id) - if channel != expected_channel: - logger.error( - f"Channel mismatch: got {channel}, expected {expected_channel}" - ) - return - except ValueError: - logger.error(f"Invalid UUID hex in ack channel: {channel}") + session_id = self._parse_ack_channel(channel) + if session_id is None: return # Extract message ID from data @@ -151,21 +188,8 @@ async def _handle_ack_message(self, channel: str, data: str) -> None: async def _handle_session_message(self, channel: str, data: str) -> None: """Handle regular messages received on session channels.""" - session_prefix = f"{self._prefix}session:" - if not channel.startswith(session_prefix): - return - - session_hex = channel[len(session_prefix) :] - try: - session_id = UUID(hex=session_hex) - expected_channel = self._session_channel(session_id) - if channel != expected_channel: - logger.error( - f"Channel mismatch: got {channel}, expected {expected_channel}" - ) - return - except ValueError: - logger.error(f"Invalid UUID hex in session channel: {channel}") + session_id = self._parse_session_channel(channel) + if session_id is None: return if session_id not in self._callbacks: From 8fce8e68686b299f5e6f0fa9aa0cb710086a4d3c Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 22 Apr 2025 14:15:53 -0700 Subject: [PATCH 28/51] back to b48428486aa90f7529c36e5a78074ac2a2d813bc --- src/mcp/server/message_queue/base.py | 51 +----- src/mcp/server/message_queue/redis.py | 246 ++++++-------------------- src/mcp/server/sse.py | 15 +- 3 files changed, 59 insertions(+), 253 deletions(-) diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index 2a5d4af03..856f97632 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -4,7 +4,7 @@ from typing import Protocol, runtime_checkable from uuid import UUID -from pydantic import BaseModel, ValidationError +from pydantic import ValidationError import mcp.types as types @@ -13,18 +13,6 @@ MessageCallback = Callable[[types.JSONRPCMessage | Exception], Awaitable[None]] -class MessageWrapper(BaseModel): - message_id: str - payload: str - - def get_json_rpc_message(self) -> types.JSONRPCMessage | ValidationError: - """Parse the payload into a JSONRPCMessage or return ValidationError.""" - try: - return types.JSONRPCMessage.model_validate_json(self.payload) - except ValidationError as exc: - return exc - - @runtime_checkable class MessageDispatch(Protocol): """Abstract interface for SSE message dispatching. @@ -47,28 +35,6 @@ async def publish_message( """ ... - async def publish_message_sync( - self, - session_id: UUID, - message: types.JSONRPCMessage | str, - timeout: float = 120.0, - ) -> bool: - """Publish a message for the specified session and wait for confirmation. - - This method blocks until the message has been fully consumed by the subscriber, - or until the timeout is reached. - - Args: - session_id: The UUID of the session this message is for - message: The message to publish (JSONRPCMessage or str for invalid JSON) - timeout: Maximum time to wait for consumption in seconds - - Returns: - bool: True if message was published and consumed, False otherwise - """ - # Default implementation falls back to standard publish - return await self.publish_message(session_id, message) - @asynccontextmanager async def subscribe(self, session_id: UUID, callback: MessageCallback): """Request-scoped context manager that subscribes to messages for a session. @@ -125,21 +91,6 @@ async def publish_message( logger.debug(f"Message dispatched to session {session_id}") return True - async def publish_message_sync( - self, - session_id: UUID, - message: types.JSONRPCMessage | str, - timeout: float = 30.0, - ) -> bool: - """Publish a message for the specified session and wait for consumption. - - For InMemoryMessageDispatch, this is the same as publish_message since - the callback is executed synchronously. - """ - # For in-memory dispatch, the message is processed immediately - # so we can just call the regular publish method - return await self.publish_message(session_id, message) - @asynccontextmanager async def subscribe(self, session_id: UUID, callback: MessageCallback): """Request-scoped context manager that subscribes to messages for a session.""" diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 060e37cba..d03d10304 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -1,14 +1,14 @@ import logging from contextlib import asynccontextmanager from typing import Any, cast -from uuid import UUID, uuid4 +from uuid import UUID import anyio -from anyio import CancelScope, CapacityLimiter, Event, lowlevel -from anyio.abc import TaskGroup +from anyio import CapacityLimiter, lowlevel +from pydantic import ValidationError import mcp.types as types -from mcp.server.message_queue.base import MessageCallback, MessageWrapper +from mcp.server.message_queue.base import MessageCallback try: import redis.asyncio as redis @@ -42,234 +42,98 @@ def __init__( self._prefix = prefix self._active_sessions_key = f"{prefix}active_sessions" self._callbacks: dict[UUID, MessageCallback] = {} - self._handlers: dict[UUID, TaskGroup] = {} + # Ensures only one polling task runs at a time for message handling self._limiter = CapacityLimiter(1) - self._ack_events: dict[str, Event] = {} - logger.debug(f"Redis message dispatch initialized: {redis_url}") def _session_channel(self, session_id: UUID) -> str: """Get the Redis channel for a session.""" return f"{self._prefix}session:{session_id.hex}" - def _ack_channel(self, session_id: UUID) -> str: - """Get the acknowledgment channel for a session.""" - return f"{self._prefix}ack:{session_id.hex}" - @asynccontextmanager async def subscribe(self, session_id: UUID, callback: MessageCallback): """Request-scoped context manager that subscribes to messages for a session.""" await self._redis.sadd(self._active_sessions_key, session_id.hex) self._callbacks[session_id] = callback + channel = self._session_channel(session_id) + await self._pubsub.subscribe(channel) # type: ignore - session_channel = self._session_channel(session_id) - ack_channel = self._ack_channel(session_id) - - await self._pubsub.subscribe(session_channel) # type: ignore - await self._pubsub.subscribe(ack_channel) # type: ignore - - logger.debug(f"Subscribing to Redis channels for session {session_id}") - - # Store the task group for the session + logger.debug(f"Subscribing to Redis channel for session {session_id}") async with anyio.create_task_group() as tg: - self._handlers[session_id] = tg tg.start_soon(self._listen_for_messages) try: yield finally: tg.cancel_scope.cancel() - await self._pubsub.unsubscribe(session_channel) # type: ignore - await self._pubsub.unsubscribe(ack_channel) # type: ignore + await self._pubsub.unsubscribe(channel) # type: ignore await self._redis.srem(self._active_sessions_key, session_id.hex) del self._callbacks[session_id] - logger.debug( - f"Unsubscribed from Redis channels for session {session_id}" - ) - del self._handlers[session_id] - - def _parse_ack_channel(self, channel: str) -> UUID | None: - """Parse and validate an acknowledgment channel, returning session_id.""" - ack_prefix = f"{self._prefix}ack:" - if not channel.startswith(ack_prefix): - return None - - # Extract exactly what should be a UUID hex after the prefix - session_hex = channel[len(ack_prefix):] - if len(session_hex) != 32: # Standard UUID hex length - logger.error(f"Invalid UUID length in ack channel: {channel}") - return None - - try: - session_id = UUID(hex=session_hex) - expected_channel = self._ack_channel(session_id) - if channel != expected_channel: - logger.error(f"Channel mismatch: got {channel}, expected {expected_channel}") - return None - return session_id - except ValueError: - logger.error(f"Invalid UUID hex in ack channel: {channel}") - return None - - def _parse_session_channel(self, channel: str) -> UUID | None: - """Parse and validate a session channel, returning session_id.""" - session_prefix = f"{self._prefix}session:" - if not channel.startswith(session_prefix): - return None - - # Extract exactly what should be a UUID hex after the prefix - session_hex = channel[len(session_prefix):] - if len(session_hex) != 32: # Standard UUID hex length - logger.error(f"Invalid UUID length in session channel: {channel}") - return None - - try: - session_id = UUID(hex=session_hex) - expected_channel = self._session_channel(session_id) - if channel != expected_channel: - logger.error(f"Channel mismatch: got {channel}, expected {expected_channel}") - return None - return session_id - except ValueError: - logger.error(f"Invalid UUID hex in session channel: {channel}") - return None + logger.debug(f"Unsubscribed from Redis channel: {session_id}") async def _listen_for_messages(self) -> None: """Background task that listens for messages on subscribed channels.""" async with self._limiter: while True: await lowlevel.checkpoint() - # Shield message retrieval from cancellation to ensure no messages are - # lost when a session disconnects during processing. - with CancelScope(shield=True): - redis_message: ( # type: ignore - None | dict[str, Any] - ) = await self._pubsub.get_message( # type: ignore - ignore_subscribe_messages=True, - timeout=0.1, # type: ignore - ) - if redis_message is None: - continue - - channel: str = cast(str, redis_message["channel"]) - data: str = cast(str, redis_message["data"]) - - # Determine which session this message is for - session_id = None - if channel.startswith(f"{self._prefix}ack:"): - session_id = self._parse_ack_channel(channel) - elif channel.startswith(f"{self._prefix}session:"): - session_id = self._parse_session_channel(channel) - - if session_id is None: - logger.debug(f"Ignoring message from channel: {channel}") + message: None | dict[str, Any] = await self._pubsub.get_message( # type: ignore + ignore_subscribe_messages=True, + timeout=None, # type: ignore + ) + if message is None: + continue + + channel: str = cast(str, message["channel"]) + expected_prefix = f"{self._prefix}session:" + + if not channel.startswith(expected_prefix): + logger.debug(f"Ignoring message from non-MCP channel: {channel}") + continue + + session_hex = channel[len(expected_prefix) :] + try: + session_id = UUID(hex=session_hex) + expected_channel = self._session_channel(session_id) + if channel != expected_channel: + logger.error(f"Channel format mismatch: {channel}") continue - - if session_id not in self._handlers: - logger.warning(f"Dropping message for non-existent session: {session_id}") + except ValueError: + logger.error(f"Invalid UUID in channel: {channel}") + continue + + data: str = cast(str, message["data"]) + try: + if session_id not in self._callbacks: + logger.warning(f"Message dropped: no callback for {session_id}") continue - - session_tg = self._handlers[session_id] - if channel.startswith(f"{self._prefix}ack:"): - session_tg.start_soon(self._handle_ack_message, channel, data) - else: - session_tg.start_soon(self._handle_session_message, channel, data) - - async def _handle_ack_message(self, channel: str, data: str) -> None: - """Handle acknowledgment messages received on ack channels.""" - session_id = self._parse_ack_channel(channel) - if session_id is None: - return - - # Extract message ID from data - message_id = data.strip() - if message_id in self._ack_events: - logger.debug(f"Received acknowledgment for message: {message_id}") - self._ack_events[message_id].set() - - async def _handle_session_message(self, channel: str, data: str) -> None: - """Handle regular messages received on session channels.""" - session_id = self._parse_session_channel(channel) - if session_id is None: - return - if session_id not in self._callbacks: - logger.warning(f"Message dropped: no callback for {session_id}") - return - - try: - wrapper = MessageWrapper.model_validate_json(data) - result = wrapper.get_json_rpc_message() - await self._callbacks[session_id](result) - await self._send_acknowledgment(session_id, wrapper.message_id) - - except Exception as e: - logger.error(f"Error processing message for {session_id}: {e}") - - async def _send_acknowledgment(self, session_id: UUID, message_id: str) -> None: - """Send an acknowledgment for a message that was successfully processed.""" - ack_channel = self._ack_channel(session_id) - await self._redis.publish(ack_channel, message_id) # type: ignore - logger.debug( - f"Sent acknowledgment for message {message_id} to session {session_id}" - ) + # Try to parse as valid message or recreate original ValidationError + try: + msg = types.JSONRPCMessage.model_validate_json(data) + await self._callbacks[session_id](msg) + except ValidationError as exc: + # Pass the identical validation error that would have occurred + await self._callbacks[session_id](exc) + except Exception as e: + logger.error(f"Error processing message for {session_id}: {e}") async def publish_message( - self, - session_id: UUID, - message: types.JSONRPCMessage | str, - message_id: str | None = None, - ) -> str | None: + self, session_id: UUID, message: types.JSONRPCMessage | str + ) -> bool: """Publish a message for the specified session.""" if not await self.session_exists(session_id): logger.warning(f"Message dropped: unknown session {session_id}") - return None + return False # Pass raw JSON strings directly, preserving validation errors - message_id = message_id or str(uuid4()) if isinstance(message, str): - wrapper = MessageWrapper(message_id=message_id, payload=message) + data = message else: - wrapper = MessageWrapper( - message_id=message_id, payload=message.model_dump_json() - ) + data = message.model_dump_json() channel = self._session_channel(session_id) - await self._redis.publish(channel, wrapper.model_dump_json()) # type: ignore - logger.debug( - f"Message {message_id} published to Redis channel for session {session_id}" - ) - return message_id - - async def publish_message_sync( - self, - session_id: UUID, - message: types.JSONRPCMessage | str, - timeout: float = 120.0, - ) -> bool: - """Publish a message and wait for acknowledgment of processing.""" - message_id = str(uuid4()) - ack_event = Event() - self._ack_events[message_id] = ack_event - - try: - published_id = await self.publish_message(session_id, message, message_id) - if published_id is None: - return False - - with anyio.fail_after(timeout): - await ack_event.wait() - logger.debug(f"Received acknowledgment for message {message_id}") - return True - - except TimeoutError: - logger.warning( - f"Timed out waiting for acknowledgment of message {message_id}" - ) - return False - - finally: - if message_id in self._ack_events: - del self._ack_events[message_id] + await self._redis.publish(channel, data) # type: ignore[attr-defined] + logger.debug(f"Message published to Redis channel for session {session_id}") + return True async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index 1e9214b4a..e3b00bd97 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -178,19 +178,10 @@ async def handle_post_message( await response(scope, receive, send) # Pass raw JSON string; receiver will recreate identical ValidationError # when parsing the same invalid JSON - await self._message_dispatch.publish_message_sync(session_id, body.decode()) + await self._message_dispatch.publish_message(session_id, body.decode()) return logger.debug(f"Publishing message for session {session_id}: {message}") - - # Use sync publish, block POST response until the message is processed - result = await self._message_dispatch.publish_message_sync(session_id, message) - - if result: - # Message was successfully processed - response = Response("OK", status_code=200) - else: - # Message timed out or failed to be processed - response = Response("Processing Timeout", status_code=504) - + response = Response("Accepted", status_code=202) await response(scope, receive, send) + await self._message_dispatch.publish_message(session_id, message) From b2893e60eba9988dde30c0fe4f9e4756869aa9cf Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 22 Apr 2025 14:38:03 -0700 Subject: [PATCH 29/51] push message handling onto corresponding SSE session task group --- src/mcp/server/message_queue/redis.py | 99 +++++++++++++++++---------- 1 file changed, 62 insertions(+), 37 deletions(-) diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index d03d10304..7c03c4613 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -4,7 +4,8 @@ from uuid import UUID import anyio -from anyio import CapacityLimiter, lowlevel +from anyio import CancelScope, CapacityLimiter, lowlevel +from anyio.abc import TaskGroup from pydantic import ValidationError import mcp.types as types @@ -42,6 +43,7 @@ def __init__( self._prefix = prefix self._active_sessions_key = f"{prefix}active_sessions" self._callbacks: dict[UUID, MessageCallback] = {} + self._task_groups: dict[UUID, TaskGroup] = {} # Ensures only one polling task runs at a time for message handling self._limiter = CapacityLimiter(1) logger.debug(f"Redis message dispatch initialized: {redis_url}") @@ -60,6 +62,7 @@ async def subscribe(self, session_id: UUID, callback: MessageCallback): logger.debug(f"Subscribing to Redis channel for session {session_id}") async with anyio.create_task_group() as tg: + self._task_groups[session_id] = tg tg.start_soon(self._listen_for_messages) try: yield @@ -68,53 +71,75 @@ async def subscribe(self, session_id: UUID, callback: MessageCallback): await self._pubsub.unsubscribe(channel) # type: ignore await self._redis.srem(self._active_sessions_key, session_id.hex) del self._callbacks[session_id] + del self._task_groups[session_id] logger.debug(f"Unsubscribed from Redis channel: {session_id}") + def _extract_session_id(self, channel: str) -> UUID | None: + """Extract and validate session ID from channel.""" + expected_prefix = f"{self._prefix}session:" + if not channel.startswith(expected_prefix): + return None + + session_hex = channel[len(expected_prefix) :] + try: + session_id = UUID(hex=session_hex) + if channel != self._session_channel(session_id): + logger.error(f"Channel format mismatch: {channel}") + return None + return session_id + except ValueError: + logger.error(f"Invalid UUID in channel: {channel}") + return None + async def _listen_for_messages(self) -> None: """Background task that listens for messages on subscribed channels.""" async with self._limiter: while True: await lowlevel.checkpoint() - message: None | dict[str, Any] = await self._pubsub.get_message( # type: ignore - ignore_subscribe_messages=True, - timeout=None, # type: ignore - ) - if message is None: - continue - - channel: str = cast(str, message["channel"]) - expected_prefix = f"{self._prefix}session:" - - if not channel.startswith(expected_prefix): - logger.debug(f"Ignoring message from non-MCP channel: {channel}") - continue - - session_hex = channel[len(expected_prefix) :] - try: - session_id = UUID(hex=session_hex) - expected_channel = self._session_channel(session_id) - if channel != expected_channel: - logger.error(f"Channel format mismatch: {channel}") + with CancelScope(shield=True): + message: None | dict[str, Any] = await self._pubsub.get_message( # type: ignore + ignore_subscribe_messages=True, + timeout=0.1, # type: ignore + ) + if message is None: continue - except ValueError: - logger.error(f"Invalid UUID in channel: {channel}") - continue - - data: str = cast(str, message["data"]) - try: - if session_id not in self._callbacks: - logger.warning(f"Message dropped: no callback for {session_id}") + + channel: str = cast(str, message["channel"]) + session_id = self._extract_session_id(channel) + if session_id is None: + logger.debug(f"Ignoring message from non-MCP channel: {channel}") continue - # Try to parse as valid message or recreate original ValidationError + data: str = cast(str, message["data"]) try: - msg = types.JSONRPCMessage.model_validate_json(data) - await self._callbacks[session_id](msg) - except ValidationError as exc: - # Pass the identical validation error that would have occurred - await self._callbacks[session_id](exc) - except Exception as e: - logger.error(f"Error processing message for {session_id}: {e}") + if session_id in self._task_groups: + self._task_groups[session_id].start_soon( + self._handle_message, session_id, data + ) + else: + logger.warning( + f"Message dropped: no task group for session: {session_id}" + ) + except Exception as e: + logger.error(f"Error processing message for {session_id}: {e}") + + async def _handle_message(self, session_id: UUID, data: str) -> None: + """Process a message from Redis in the session's task group.""" + if session_id not in self._callbacks: + logger.warning(f"Message dropped: callback removed for {session_id}") + return + + try: + # Parse message or pass validation error to callback + msg_or_error = None + try: + msg_or_error = types.JSONRPCMessage.model_validate_json(data) + except ValidationError as exc: + msg_or_error = exc + + await self._callbacks[session_id](msg_or_error) + except Exception as e: + logger.error(f"Error in message handler for {session_id}: {e}") async def publish_message( self, session_id: UUID, message: types.JSONRPCMessage | str From e5938d478e2255a879732448d84bb287b38fd5a4 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 22 Apr 2025 14:42:40 -0700 Subject: [PATCH 30/51] format --- src/mcp/server/message_queue/redis.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 7c03c4613..162081329 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -107,7 +107,9 @@ async def _listen_for_messages(self) -> None: channel: str = cast(str, message["channel"]) session_id = self._extract_session_id(channel) if session_id is None: - logger.debug(f"Ignoring message from non-MCP channel: {channel}") + logger.debug( + f"Ignoring message from non-MCP channel: {channel}" + ) continue data: str = cast(str, message["data"]) From a151f1c69fde4fb254786a6f08891f472bb3a163 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 22 Apr 2025 14:48:18 -0700 Subject: [PATCH 31/51] clean up comment and session state --- src/mcp/server/message_queue/base.py | 1 - src/mcp/server/message_queue/redis.py | 18 ++++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index 856f97632..1322efa25 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -66,7 +66,6 @@ class InMemoryMessageDispatch: def __init__(self) -> None: self._callbacks: dict[UUID, MessageCallback] = {} - # _callbacks tracks active sessions, no need for separate _active_sessions set async def publish_message( self, session_id: UUID, message: types.JSONRPCMessage | str diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 162081329..a5a835d66 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -42,8 +42,8 @@ def __init__( self._pubsub = self._redis.pubsub(ignore_subscribe_messages=True) # type: ignore self._prefix = prefix self._active_sessions_key = f"{prefix}active_sessions" - self._callbacks: dict[UUID, MessageCallback] = {} - self._task_groups: dict[UUID, TaskGroup] = {} + # Maps session IDs to the callback and task group for that SSE session. + self._session_state: dict[UUID, tuple[MessageCallback, TaskGroup]] = {} # Ensures only one polling task runs at a time for message handling self._limiter = CapacityLimiter(1) logger.debug(f"Redis message dispatch initialized: {redis_url}") @@ -56,13 +56,12 @@ def _session_channel(self, session_id: UUID) -> str: async def subscribe(self, session_id: UUID, callback: MessageCallback): """Request-scoped context manager that subscribes to messages for a session.""" await self._redis.sadd(self._active_sessions_key, session_id.hex) - self._callbacks[session_id] = callback channel = self._session_channel(session_id) await self._pubsub.subscribe(channel) # type: ignore logger.debug(f"Subscribing to Redis channel for session {session_id}") async with anyio.create_task_group() as tg: - self._task_groups[session_id] = tg + self._session_state[session_id] = (callback, tg) tg.start_soon(self._listen_for_messages) try: yield @@ -70,8 +69,7 @@ async def subscribe(self, session_id: UUID, callback: MessageCallback): tg.cancel_scope.cancel() await self._pubsub.unsubscribe(channel) # type: ignore await self._redis.srem(self._active_sessions_key, session_id.hex) - del self._callbacks[session_id] - del self._task_groups[session_id] + del self._session_state[session_id] logger.debug(f"Unsubscribed from Redis channel: {session_id}") def _extract_session_id(self, channel: str) -> UUID | None: @@ -114,8 +112,8 @@ async def _listen_for_messages(self) -> None: data: str = cast(str, message["data"]) try: - if session_id in self._task_groups: - self._task_groups[session_id].start_soon( + if session_state := self._session_state.get(session_id): + session_state[1].start_soon( self._handle_message, session_id, data ) else: @@ -127,7 +125,7 @@ async def _listen_for_messages(self) -> None: async def _handle_message(self, session_id: UUID, data: str) -> None: """Process a message from Redis in the session's task group.""" - if session_id not in self._callbacks: + if (session_state := self._session_state.get(session_id)) is None: logger.warning(f"Message dropped: callback removed for {session_id}") return @@ -139,7 +137,7 @@ async def _handle_message(self, session_id: UUID, data: str) -> None: except ValidationError as exc: msg_or_error = exc - await self._callbacks[session_id](msg_or_error) + await session_state[0](msg_or_error) except Exception as e: logger.error(f"Error in message handler for {session_id}: {e}") From d22f46b5fce8565555706e380db38e3874b4cee3 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 22 Apr 2025 14:50:00 -0700 Subject: [PATCH 32/51] shorten comment --- src/mcp/server/message_queue/redis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index a5a835d66..923f5eca4 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -118,7 +118,7 @@ async def _listen_for_messages(self) -> None: ) else: logger.warning( - f"Message dropped: no task group for session: {session_id}" + f"Message dropped: unknown session {session_id}" ) except Exception as e: logger.error(f"Error processing message for {session_id}: {e}") From 8d6a20dfe1bb81ce4b077ab84c8a340f667153f7 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Wed, 23 Apr 2025 14:01:51 -0700 Subject: [PATCH 33/51] remove extra change --- examples/fastmcp/unicode_example.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/examples/fastmcp/unicode_example.py b/examples/fastmcp/unicode_example.py index 1d7c846c7..a69f586a5 100644 --- a/examples/fastmcp/unicode_example.py +++ b/examples/fastmcp/unicode_example.py @@ -4,14 +4,8 @@ """ from mcp.server.fastmcp import FastMCP -from mcp.server.message_queue import RedisMessageQueue -# Create a Redis message queue -redis_queue = RedisMessageQueue( - redis_url="redis://localhost:6379/0", prefix="mcp:pubsub:" -) - -mcp = FastMCP(message_queue=redis_queue) +mcp = FastMCP() @mcp.tool( @@ -67,4 +61,4 @@ def multilingual_hello() -> str: if __name__ == "__main__": - mcp.run(transport="sse") + mcp.run() From bb248817da7c0734bd8d2c9cbdaaaf56991ee9f8 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Thu, 24 Apr 2025 13:44:47 -0700 Subject: [PATCH 34/51] testing --- .../simple-prompt/mcp_simple_prompt/server.py | 6 +- pyproject.toml | 1 + src/mcp/server/fastmcp/server.py | 11 +- src/mcp/server/message_queue/base.py | 8 + src/mcp/server/message_queue/redis.py | 4 + src/mcp/server/sse.py | 3 +- tests/server/message_queue/__init__.py | 1 + tests/server/message_queue/test_redis.py | 354 ++++++++++++++++++ .../message_queue/test_redis_integration.py | 263 +++++++++++++ uv.lock | 16 + 10 files changed, 664 insertions(+), 3 deletions(-) create mode 100644 tests/server/message_queue/__init__.py create mode 100644 tests/server/message_queue/test_redis.py create mode 100644 tests/server/message_queue/test_redis_integration.py diff --git a/examples/servers/simple-prompt/mcp_simple_prompt/server.py b/examples/servers/simple-prompt/mcp_simple_prompt/server.py index 0552f2770..72517204d 100644 --- a/examples/servers/simple-prompt/mcp_simple_prompt/server.py +++ b/examples/servers/simple-prompt/mcp_simple_prompt/server.py @@ -92,7 +92,11 @@ async def get_prompt( from starlette.applications import Starlette from starlette.routing import Mount, Route - sse = SseServerTransport("/messages/") + from mcp.server.message_queue.redis import RedisMessageDispatch + + message_dispatch = RedisMessageDispatch("redis://localhost:6379/0") + + sse = SseServerTransport("/messages/", message_dispatch=message_dispatch) async def handle_sse(request): async with sse.connect_sse( diff --git a/pyproject.toml b/pyproject.toml index 014e3fb28..5acf6e639 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "sse-starlette>=1.6.1", "pydantic-settings>=2.5.2", "uvicorn>=0.23.1; sys_platform != 'emscripten'", + "fakeredis==2.28.1", ] [project.optional-dependencies] diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index d160ed67b..424d4a65f 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -2,6 +2,7 @@ from __future__ import annotations as _annotations +from email import message import inspect import json import re @@ -507,7 +508,14 @@ async def handle_sse(request: Request) -> None: streams[0], streams[1], self._mcp_server.create_initialization_options(), - ) + ) + + @asynccontextmanager + async def lifespan(app: Starlette): + try: + yield + finally: + await message_dispatch.close() return Starlette( debug=self.settings.debug, @@ -515,6 +523,7 @@ async def handle_sse(request: Request) -> None: Route(self.settings.sse_path, endpoint=handle_sse), Mount(self.settings.message_path, app=sse.handle_post_message), ], + lifespan=lifespan, ) async def list_prompts(self) -> list[MCPPrompt]: diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index 1322efa25..d54367207 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -56,6 +56,10 @@ async def session_exists(self, session_id: UUID) -> bool: """ ... + async def close(self) -> None: + """Close the message dispatch.""" + ... + class InMemoryMessageDispatch: """Default in-memory implementation of the MessageDispatch interface. @@ -106,3 +110,7 @@ async def subscribe(self, session_id: UUID, callback: MessageCallback): async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" return session_id in self._callbacks + + async def close(self) -> None: + """Close the message dispatch.""" + pass \ No newline at end of file diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 923f5eca4..be79cfd76 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -48,6 +48,10 @@ def __init__( self._limiter = CapacityLimiter(1) logger.debug(f"Redis message dispatch initialized: {redis_url}") + async def close(self): + await self._pubsub.aclose() # type: ignore + await self._redis.aclose() # type: ignore + def _session_channel(self, session_id: UUID) -> str: """Get the Redis channel for a session.""" return f"{self._prefix}session:{session_id.hex}" diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index e3b00bd97..30af31617 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -49,6 +49,7 @@ async def handle_sse(request): from mcp.server.message_queue import InMemoryMessageDispatch, MessageDispatch logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) class SseServerTransport: @@ -184,4 +185,4 @@ async def handle_post_message( logger.debug(f"Publishing message for session {session_id}: {message}") response = Response("Accepted", status_code=202) await response(scope, receive, send) - await self._message_dispatch.publish_message(session_id, message) + await self._message_dispatch.publish_message(session_id, message) \ No newline at end of file diff --git a/tests/server/message_queue/__init__.py b/tests/server/message_queue/__init__.py new file mode 100644 index 000000000..dd6692718 --- /dev/null +++ b/tests/server/message_queue/__init__.py @@ -0,0 +1 @@ +# Message queue tests module \ No newline at end of file diff --git a/tests/server/message_queue/test_redis.py b/tests/server/message_queue/test_redis.py new file mode 100644 index 000000000..ce9a104c0 --- /dev/null +++ b/tests/server/message_queue/test_redis.py @@ -0,0 +1,354 @@ +from unittest.mock import AsyncMock, patch +from uuid import uuid4 + +import anyio +from pydantic import ValidationError +import pytest + +import mcp.types as types + +# Set up fakeredis for testing +try: + from fakeredis import aioredis as fake_redis +except ImportError: + pytest.skip( + "fakeredis is required for testing Redis functionality", allow_module_level=True + ) + + +@pytest.fixture +async def redis_dispatch(): + """Create a Redis message dispatch with a fake Redis client.""" + # Mock the redis module entirely within RedisMessageDispatch + with patch("mcp.server.message_queue.redis.redis", fake_redis.FakeRedis): + from mcp.server.message_queue.redis import RedisMessageDispatch + + dispatch = RedisMessageDispatch() + try: + yield dispatch + finally: + await dispatch.close() + + +@pytest.mark.anyio +async def test_session_exists(redis_dispatch): + """Test session existence check.""" + session_id = uuid4() + + # Initially session should not exist + assert not await redis_dispatch.session_exists(session_id) + + # After subscribing, session should exist + async with redis_dispatch.subscribe(session_id, AsyncMock()): + assert await redis_dispatch.session_exists(session_id) + + # After unsubscribing, session should not exist + assert not await redis_dispatch.session_exists(session_id) + + +@pytest.mark.anyio +async def test_subscribe_unsubscribe(redis_dispatch): + """Test subscribing and unsubscribing from a session.""" + session_id = uuid4() + callback = AsyncMock() + + # Subscribe + async with redis_dispatch.subscribe(session_id, callback): + # Check that session is tracked + assert session_id in redis_dispatch._session_state + assert await redis_dispatch.session_exists(session_id) + + # After context exit, session should be cleaned up + assert session_id not in redis_dispatch._session_state + assert not await redis_dispatch.session_exists(session_id) + + +@pytest.mark.anyio +async def test_publish_message_valid_json(redis_dispatch): + """Test publishing a valid JSON-RPC message.""" + session_id = uuid4() + callback = AsyncMock() + message = types.JSONRPCMessage.model_validate( + {"jsonrpc": "2.0", "method": "test", "params": {}, "id": 1} + ) + + # Subscribe to messages + async with redis_dispatch.subscribe(session_id, callback): + # Publish message + published = await redis_dispatch.publish_message(session_id, message) + assert published + + # Give some time for the message to be processed + await anyio.sleep(0.1) + + # Callback should have been called with the message + callback.assert_called_once() + call_args = callback.call_args[0][0] + assert isinstance(call_args, types.JSONRPCMessage) + assert isinstance(call_args.root, types.JSONRPCRequest) + assert call_args.root.method == "test" # Access method through root attribute + + +@pytest.mark.anyio +async def test_publish_message_invalid_json(redis_dispatch): + """Test publishing an invalid JSON string.""" + session_id = uuid4() + callback = AsyncMock() + invalid_json = '{"invalid": "json",,}' # Invalid JSON + + # Subscribe to messages + async with redis_dispatch.subscribe(session_id, callback): + # Publish invalid message + published = await redis_dispatch.publish_message(session_id, invalid_json) + assert published + + # Give some time for the message to be processed + await anyio.sleep(0.1) + + # Callback should have been called with a ValidationError + callback.assert_called_once() + error = callback.call_args[0][0] + assert isinstance(error, ValidationError) + + +@pytest.mark.anyio +async def test_publish_to_nonexistent_session(redis_dispatch): + """Test publishing to a session that doesn't exist.""" + session_id = uuid4() + message = types.JSONRPCMessage.model_validate( + {"jsonrpc": "2.0", "method": "test", "params": {}, "id": 1} + ) + + published = await redis_dispatch.publish_message(session_id, message) + assert not published + + +@pytest.mark.anyio +async def test_extract_session_id(redis_dispatch): + """Test extracting session ID from channel name.""" + session_id = uuid4() + channel = redis_dispatch._session_channel(session_id) + + # Valid channel + extracted_id = redis_dispatch._extract_session_id(channel) + assert extracted_id == session_id + + # Invalid channel format + extracted_id = redis_dispatch._extract_session_id("invalid_channel_name") + assert extracted_id is None + + # Invalid UUID in channel + invalid_channel = f"{redis_dispatch._prefix}session:invalid_uuid" + extracted_id = redis_dispatch._extract_session_id(invalid_channel) + assert extracted_id is None + + +@pytest.mark.anyio +async def test_multiple_sessions(redis_dispatch): + """Test handling multiple concurrent sessions.""" + session1 = uuid4() + session2 = uuid4() + callback1 = AsyncMock() + callback2 = AsyncMock() + + async with redis_dispatch.subscribe(session1, callback1): + async with redis_dispatch.subscribe(session2, callback2): + # Both sessions should exist + assert await redis_dispatch.session_exists(session1) + assert await redis_dispatch.session_exists(session2) + + # Publish to session1 + message1 = types.JSONRPCMessage.model_validate( + {"jsonrpc": "2.0", "method": "test1", "params": {}, "id": 1} + ) + await redis_dispatch.publish_message(session1, message1) + + # Publish to session2 + message2 = types.JSONRPCMessage.model_validate( + {"jsonrpc": "2.0", "method": "test2", "params": {}, "id": 2} + ) + await redis_dispatch.publish_message(session2, message2) + + # Give some time for messages to be processed + await anyio.sleep(0.1) + + # Check callbacks + callback1.assert_called_once() + callback2.assert_called_once() + + call1_args = callback1.call_args[0][0] + assert call1_args.root.method == "test1" + + call2_args = callback2.call_args[0][0] + assert call2_args.root.method == "test2" + + +@pytest.mark.anyio +async def test_task_group_cancellation(redis_dispatch): + """Test that task group is properly cancelled when context exits.""" + session_id = uuid4() + callback = AsyncMock() + + async with redis_dispatch.subscribe(session_id, callback): + # Check that task group is active + _, task_group = redis_dispatch._session_state[session_id] + assert task_group.cancel_scope.cancel_called is False + + # After context exit, task group should be cancelled + # And session state should be cleaned up + assert session_id not in redis_dispatch._session_state + + +@pytest.mark.anyio +async def test_session_cancellation_isolation(redis_dispatch): + """Test that cancelling one session doesn't affect other sessions.""" + session1 = uuid4() + session2 = uuid4() + + # Create a blocking callback for session1 to ensure it's running when cancelled + session1_event = anyio.Event() + session1_started = anyio.Event() + session1_cancelled = False + + async def blocking_callback1(msg): + session1_started.set() + try: + await session1_event.wait() + except anyio.get_cancelled_exc_class(): + nonlocal session1_cancelled + session1_cancelled = True + raise + + callback2 = AsyncMock() + + # Start session2 first + async with redis_dispatch.subscribe(session2, callback2): + # Start session1 with a blocking callback + async with anyio.create_task_group() as tg: + async def session1_runner(): + async with redis_dispatch.subscribe(session1, blocking_callback1): + # Publish a message to trigger the blocking callback + message = types.JSONRPCMessage.model_validate( + {"jsonrpc": "2.0", "method": "test", "params": {}, "id": 1} + ) + await redis_dispatch.publish_message(session1, message) + + # Wait for the callback to start + await session1_started.wait() + + # Keep the context alive while we test cancellation + await anyio.sleep_forever() + + tg.start_soon(session1_runner) + + # Wait for session1's callback to start + await session1_started.wait() + + # Cancel session1 + tg.cancel_scope.cancel() + + # Give some time for cancellation to propagate + await anyio.sleep(0.1) + + # Verify session1 was cancelled + assert session1_cancelled + assert session1 not in redis_dispatch._session_state + + # Verify session2 is still active and can receive messages + assert await redis_dispatch.session_exists(session2) + message2 = types.JSONRPCMessage.model_validate( + {"jsonrpc": "2.0", "method": "test2", "params": {}, "id": 2} + ) + await redis_dispatch.publish_message(session2, message2) + + # Give some time for the message to be processed + await anyio.sleep(0.1) + + # Verify session2 received the message + callback2.assert_called_once() + call_args = callback2.call_args[0][0] + assert call_args.root.method == "test2" + + +@pytest.mark.anyio +async def test_listener_task_handoff_on_cancellation(redis_dispatch): + """Test that the single listening task is properly handed off when a session is cancelled.""" + session1 = uuid4() + session2 = uuid4() + + session1_messages_received = 0 + session2_messages_received = 0 + + async def callback1(msg): + nonlocal session1_messages_received + session1_messages_received += 1 + + async def callback2(msg): + nonlocal session2_messages_received + session2_messages_received += 1 + + # Create a cancel scope for session1 + async with anyio.create_task_group() as tg: + session1_cancel_scope: anyio.CancelScope | None = None + + async def session1_runner(): + nonlocal session1_cancel_scope + with anyio.CancelScope() as cancel_scope: + session1_cancel_scope = cancel_scope + async with redis_dispatch.subscribe(session1, callback1): + # Keep session alive until cancelled + await anyio.sleep_forever() + + # Start session1 + tg.start_soon(session1_runner) + + # Wait for session1 to be established + await anyio.sleep(0.1) + assert session1 in redis_dispatch._session_state + + # Send message to session1 to verify it's working + message1 = types.JSONRPCMessage.model_validate( + {"jsonrpc": "2.0", "method": "test1", "params": {}, "id": 1} + ) + await redis_dispatch.publish_message(session1, message1) + await anyio.sleep(0.1) + assert session1_messages_received == 1 + + # Start session2 while session1 is still active + async with redis_dispatch.subscribe(session2, callback2): + # Both sessions should be active + assert session1 in redis_dispatch._session_state + assert session2 in redis_dispatch._session_state + + # Cancel session1 + assert session1_cancel_scope is not None + session1_cancel_scope.cancel() + + # Wait for cancellation to complete + await anyio.sleep(0.1) + + # Session1 should be gone, session2 should remain + assert session1 not in redis_dispatch._session_state + assert session2 in redis_dispatch._session_state + + # Send message to session2 to verify the listener was handed off + message2 = types.JSONRPCMessage.model_validate( + {"jsonrpc": "2.0", "method": "test2", "params": {}, "id": 2} + ) + await redis_dispatch.publish_message(session2, message2) + await anyio.sleep(0.1) + + # Session2 should have received the message + assert session2_messages_received == 1 + + # Session1 shouldn't receive any more messages + assert session1_messages_received == 1 + + # Send another message to verify the listener is still working + message3 = types.JSONRPCMessage.model_validate( + {"jsonrpc": "2.0", "method": "test3", "params": {}, "id": 3} + ) + await redis_dispatch.publish_message(session2, message3) + await anyio.sleep(0.1) + + assert session2_messages_received == 2 \ No newline at end of file diff --git a/tests/server/message_queue/test_redis_integration.py b/tests/server/message_queue/test_redis_integration.py new file mode 100644 index 000000000..43b4da20f --- /dev/null +++ b/tests/server/message_queue/test_redis_integration.py @@ -0,0 +1,263 @@ +import multiprocessing +import socket +import time +from collections.abc import AsyncGenerator, Generator +from unittest.mock import patch + +import anyio +import httpx +import pytest +import uvicorn +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.routing import Mount, Route + +from mcp.client.session import ClientSession +from mcp.client.sse import sse_client +from mcp.server import Server +from mcp.server.sse import SseServerTransport +from mcp.types import TextContent, Tool + +SERVER_NAME = "test_server_for_redis_integration" + +# Set up fakeredis for testing +try: + from fakeredis import aioredis as fake_redis +except ImportError: + pytest.skip( + "fakeredis is required for testing Redis functionality", allow_module_level=True + ) + + +@pytest.fixture +def server_port() -> int: + with socket.socket() as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +@pytest.fixture +def server_url(server_port: int) -> str: + return f"http://127.0.0.1:{server_port}" + + +# Test server implementation +class RedisTestServer(Server): + def __init__(self): + super().__init__(SERVER_NAME) + + @self.list_tools() + async def handle_list_tools() -> list[Tool]: + return [ + Tool( + name="test_tool", + description="A test tool", + inputSchema={"type": "object", "properties": {}}, + ) + ] + + @self.call_tool() + async def handle_call_tool(name: str, args: dict) -> list[TextContent]: + return [TextContent(type="text", text=f"Called {name}")] + + +def make_redis_server_app() -> Starlette: + """Create test Starlette app with SSE transport and Redis message dispatch""" + # Create a mock Redis instance + mock_redis = fake_redis.FakeRedis() + + # Patch the redis module within RedisMessageDispatch + with patch("mcp.server.message_queue.redis.redis", mock_redis): + from mcp.server.message_queue.redis import RedisMessageDispatch + + # Create Redis message dispatch with mock redis + message_dispatch = RedisMessageDispatch("redis://localhost:6379/0") + + # Create SSE transport with Redis message dispatch + sse = SseServerTransport("/messages/", message_dispatch=message_dispatch) + server = RedisTestServer() + + async def handle_sse(request: Request) -> None: + async with sse.connect_sse( + request.scope, request.receive, request._send + ) as streams: + await server.run( + streams[0], streams[1], server.create_initialization_options() + ) + + app = Starlette( + routes=[ + Route("/sse", endpoint=handle_sse), + Mount("/messages/", app=sse.handle_post_message), + ] + ) + + return app + + +def run_redis_server(server_port: int) -> None: + app = make_redis_server_app() + server = uvicorn.Server( + config=uvicorn.Config( + app=app, host="127.0.0.1", port=server_port, log_level="error" + ) + ) + server.run() + + # Give server time to start + while not server.started: + time.sleep(0.5) + + +@pytest.fixture() +def server(server_port: int) -> Generator[None, None, None]: + proc = multiprocessing.Process( + target=run_redis_server, kwargs={"server_port": server_port}, daemon=True + ) + proc.start() + + # Wait for server to be running + max_attempts = 20 + attempt = 0 + while attempt < max_attempts: + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect(("127.0.0.1", server_port)) + break + except ConnectionRefusedError: + time.sleep(0.1) + attempt += 1 + else: + raise RuntimeError(f"Server failed to start after {max_attempts} attempts") + + yield + + # Signal the server to stop + proc.kill() + proc.join(timeout=2) + if proc.is_alive(): + print("server process failed to terminate") + + +@pytest.fixture() +async def http_client(server, server_url) -> AsyncGenerator[httpx.AsyncClient, None]: + """Create test client""" + async with httpx.AsyncClient(base_url=server_url) as client: + yield client + + +@pytest.mark.anyio +async def test_redis_integration_basic_connection(server: None, server_url: str) -> None: + """Test that a basic SSE connection works with Redis message dispatch""" + async with sse_client(server_url + "/sse") as streams: + async with ClientSession(*streams) as session: + # Test initialization + result = await session.initialize() + assert result.serverInfo.name == SERVER_NAME + + +@pytest.mark.anyio +async def test_redis_integration_tool_call(server: None, server_url: str) -> None: + """Test that a tool call works with Redis message dispatch""" + async with sse_client(server_url + "/sse") as streams: + async with ClientSession(*streams) as session: + # Initialize session + await session.initialize() + + # Call a tool + result = await session.call_tool("test_tool", {}) + assert result.content[0].text == "Called test_tool" + + +@pytest.mark.anyio +async def test_redis_integration_session_lifecycle() -> None: + """Test that sessions are properly added to and removed from Redis using direct Redis access""" + # Create a fresh Redis instance with decode_responses=True to get str instead of bytes + mock_redis = fake_redis.FakeRedis(decode_responses=True) + active_sessions_key = "mcp:pubsub:active_sessions" + + # Mock Redis in RedisMessageDispatch + with patch("mcp.server.message_queue.redis.redis.from_url", return_value=mock_redis): + from mcp.server.message_queue.redis import RedisMessageDispatch + + # Create Redis message dispatch with our specific mock redis instance + message_dispatch = RedisMessageDispatch("redis://localhost:6379/0") + + # Create a mock callback + async def mock_callback(message): + pass + + # Test session subscription and unsubscription + from uuid import uuid4 + session_id = uuid4() + + # Subscribe to a session + async with message_dispatch.subscribe(session_id, mock_callback): + # Give a moment for the session to be added + await anyio.sleep(0.05) + + # Check that session was added to Redis + active_sessions = await mock_redis.smembers(active_sessions_key) + assert len(active_sessions) == 1 + assert list(active_sessions)[0] == session_id.hex + + # Verify session exists + assert await message_dispatch.session_exists(session_id) + + # Give a moment for cleanup + await anyio.sleep(0.05) + + # After context exit, verify the session was removed + final_sessions = await mock_redis.smembers(active_sessions_key) + assert len(final_sessions) == 0 + assert not await message_dispatch.session_exists(session_id) + + +@pytest.mark.anyio +async def test_redis_integration_message_publishing_direct() -> None: + """Test message publishing through Redis channels using direct Redis access""" + # Create a fresh Redis instance with decode_responses=True to get str instead of bytes + mock_redis = fake_redis.FakeRedis(decode_responses=True) + + # Mock Redis in RedisMessageDispatch + with patch("mcp.server.message_queue.redis.redis.from_url", return_value=mock_redis): + from mcp.server.message_queue.redis import RedisMessageDispatch + from mcp.types import JSONRPCMessage, JSONRPCRequest + + # Create Redis message dispatch with our specific mock redis instance + message_dispatch = RedisMessageDispatch("redis://localhost:6379/0") + + # Messages received through the callback + messages_received = [] + + async def message_callback(message): + messages_received.append(message) + + # Use a UUID for session ID + from uuid import uuid4 + session_id = uuid4() + + # Subscribe to the session + async with message_dispatch.subscribe(session_id, message_callback): + # Give a moment for subscription to be fully set up and start listener task + await anyio.sleep(0.05) + + # Create a test message + test_message = JSONRPCMessage( + root=JSONRPCRequest(jsonrpc="2.0", id=1, method="test_method", params={}) + ) + + # Publish the message + success = await message_dispatch.publish_message(session_id, test_message) + assert success + + # Give some time for the message to be processed + # Use a shorter sleep since we're in controlled test environment + await anyio.sleep(0.1) + + # Verify that the message was received + assert len(messages_received) > 0, "No messages were received through the callback" + received_message = messages_received[0] + assert isinstance(received_message, JSONRPCMessage) + assert received_message.root.method == "test_method" + assert received_message.root.id == 1 \ No newline at end of file diff --git a/uv.lock b/uv.lock index 12a02c820..b5dc9bf77 100644 --- a/uv.lock +++ b/uv.lock @@ -358,6 +358,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612 }, ] +[[package]] +name = "fakeredis" +version = "2.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/32/8c1c215e50cb055e24a8d5a8981edab665d131ea9068c420bf81eb0fcb63/fakeredis-2.28.1.tar.gz", hash = "sha256:5e542200b945aa0a7afdc0396efefe3cdabab61bc0f41736cc45f68960255964", size = 161179 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/77/bca49c4960c22131da3acb647978983bea07f15c255fbef0a6559a774a7a/fakeredis-2.28.1-py3-none-any.whl", hash = "sha256:38c7c17fba5d5522af9d980a8f74a4da9900a3441e8f25c0fe93ea4205d695d1", size = 113685 }, +] + [[package]] name = "ghp-import" version = "2.1.0" @@ -543,6 +557,7 @@ name = "mcp" source = { editable = "." } dependencies = [ { name = "anyio" }, + { name = "fakeredis" }, { name = "httpx" }, { name = "httpx-sse" }, { name = "pydantic" }, @@ -588,6 +603,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "anyio", specifier = ">=4.5" }, + { name = "fakeredis", specifier = "==2.28.1" }, { name = "httpx", specifier = ">=0.27" }, { name = "httpx-sse", specifier = ">=0.4" }, { name = "pydantic", specifier = ">=2.7.2,<3.0.0" }, From 564561ffeb799586cb3cc0e00cac8992520f8b40 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Thu, 1 May 2025 14:37:52 -0700 Subject: [PATCH 35/51] add a cancelscope on the finally --- src/mcp/server/message_queue/redis.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index be79cfd76..cc7303e49 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -70,11 +70,12 @@ async def subscribe(self, session_id: UUID, callback: MessageCallback): try: yield finally: - tg.cancel_scope.cancel() - await self._pubsub.unsubscribe(channel) # type: ignore - await self._redis.srem(self._active_sessions_key, session_id.hex) - del self._session_state[session_id] - logger.debug(f"Unsubscribed from Redis channel: {session_id}") + with anyio.CancelScope(shield=True): + tg.cancel_scope.cancel() + await self._pubsub.unsubscribe(channel) # type: ignore + await self._redis.srem(self._active_sessions_key, session_id.hex) + del self._session_state[session_id] + logger.debug(f"Unsubscribed from Redis channel: {session_id}") def _extract_session_id(self, channel: str) -> UUID | None: """Extract and validate session ID from channel.""" From 9419ad0cb281c1297282b62e61b2ec4b90e7174f Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Thu, 1 May 2025 14:50:56 -0700 Subject: [PATCH 36/51] Move to session heartbeat w/ TTL --- src/mcp/server/message_queue/redis.py | 38 ++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index cc7303e49..6577d863d 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -30,18 +30,20 @@ class RedisMessageDispatch: """ def __init__( - self, redis_url: str = "redis://localhost:6379/0", prefix: str = "mcp:pubsub:" + self, redis_url: str = "redis://localhost:6379/0", prefix: str = "mcp:pubsub:", + session_ttl: int = 3600 # 1 hour default TTL for sessions ) -> None: """Initialize Redis message dispatch. Args: redis_url: Redis connection string prefix: Key prefix for Redis channels to avoid collisions + session_ttl: TTL in seconds for session keys (default: 1 hour) """ self._redis = redis.from_url(redis_url, decode_responses=True) # type: ignore self._pubsub = self._redis.pubsub(ignore_subscribe_messages=True) # type: ignore self._prefix = prefix - self._active_sessions_key = f"{prefix}active_sessions" + self._session_ttl = session_ttl # Maps session IDs to the callback and task group for that SSE session. self._session_state: dict[UUID, tuple[MessageCallback, TaskGroup]] = {} # Ensures only one polling task runs at a time for message handling @@ -56,10 +58,16 @@ def _session_channel(self, session_id: UUID) -> str: """Get the Redis channel for a session.""" return f"{self._prefix}session:{session_id.hex}" + def _session_key(self, session_id: UUID) -> str: + """Get the Redis key for a session.""" + return f"{self._prefix}session_active:{session_id.hex}" + @asynccontextmanager async def subscribe(self, session_id: UUID, callback: MessageCallback): """Request-scoped context manager that subscribes to messages for a session.""" - await self._redis.sadd(self._active_sessions_key, session_id.hex) + session_key = self._session_key(session_id) + await self._redis.setex(session_key, self._session_ttl, "1") # type: ignore + channel = self._session_channel(session_id) await self._pubsub.subscribe(channel) # type: ignore @@ -67,16 +75,33 @@ async def subscribe(self, session_id: UUID, callback: MessageCallback): async with anyio.create_task_group() as tg: self._session_state[session_id] = (callback, tg) tg.start_soon(self._listen_for_messages) + # Start heartbeat for this session + tg.start_soon(self._session_heartbeat, session_id) try: yield finally: with anyio.CancelScope(shield=True): tg.cancel_scope.cancel() await self._pubsub.unsubscribe(channel) # type: ignore - await self._redis.srem(self._active_sessions_key, session_id.hex) + await self._redis.delete(session_key) # type: ignore del self._session_state[session_id] logger.debug(f"Unsubscribed from Redis channel: {session_id}") + async def _session_heartbeat(self, session_id: UUID) -> None: + """Periodically refresh the TTL for a session.""" + session_key = self._session_key(session_id) + while True: + await lowlevel.checkpoint() + try: + # Refresh TTL at half the TTL interval to avoid expiration + await anyio.sleep(self._session_ttl / 2) + with anyio.CancelScope(shield=True): + await self._redis.expire(session_key, self._session_ttl) # type: ignore + except anyio.get_cancelled_exc_class(): + break + except Exception as e: + logger.error(f"Error refreshing TTL for session {session_id}: {e}") + def _extract_session_id(self, channel: str) -> UUID | None: """Extract and validate session ID from channel.""" expected_prefix = f"{self._prefix}session:" @@ -167,6 +192,5 @@ async def publish_message( async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" - return bool( - await self._redis.sismember(self._active_sessions_key, session_id.hex) # type: ignore[attr-defined] - ) + session_key = self._session_key(session_id) + return bool(await self._redis.exists(session_key)) # type: ignore \ No newline at end of file From 046ed94fea8efebbd21b4e0ab401fa075b3ded25 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Thu, 1 May 2025 14:59:11 -0700 Subject: [PATCH 37/51] add test for TTL --- tests/server/message_queue/test_redis.py | 35 +++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/server/message_queue/test_redis.py b/tests/server/message_queue/test_redis.py index ce9a104c0..ad566e41e 100644 --- a/tests/server/message_queue/test_redis.py +++ b/tests/server/message_queue/test_redis.py @@ -23,7 +23,7 @@ async def redis_dispatch(): with patch("mcp.server.message_queue.redis.redis", fake_redis.FakeRedis): from mcp.server.message_queue.redis import RedisMessageDispatch - dispatch = RedisMessageDispatch() + dispatch = RedisMessageDispatch(session_ttl=5) # Shorter TTL for testing try: yield dispatch finally: @@ -46,6 +46,39 @@ async def test_session_exists(redis_dispatch): assert not await redis_dispatch.session_exists(session_id) +@pytest.mark.anyio +async def test_session_ttl(redis_dispatch): + """Test that session has proper TTL set.""" + session_id = uuid4() + + async with redis_dispatch.subscribe(session_id, AsyncMock()): + session_key = redis_dispatch._session_key(session_id) + ttl = await redis_dispatch._redis.ttl(session_key) # type: ignore + assert ttl > 0 + assert ttl <= redis_dispatch._session_ttl + + +@pytest.mark.anyio +async def test_session_heartbeat(redis_dispatch): + """Test that session heartbeat refreshes TTL.""" + session_id = uuid4() + + async with redis_dispatch.subscribe(session_id, AsyncMock()): + session_key = redis_dispatch._session_key(session_id) + + # Initial TTL + initial_ttl = await redis_dispatch._redis.ttl(session_key) # type: ignore + assert initial_ttl > 0 + + # Wait for heartbeat to run + await anyio.sleep(redis_dispatch._session_ttl / 2 + 0.5) + + # TTL should be refreshed + refreshed_ttl = await redis_dispatch._redis.ttl(session_key) # type: ignore + assert refreshed_ttl > 0 + assert refreshed_ttl <= redis_dispatch._session_ttl + + @pytest.mark.anyio async def test_subscribe_unsubscribe(redis_dispatch): """Test subscribing and unsubscribing from a session.""" From 56386533b02c474fd052f40f7340db3dd985a56c Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 5 May 2025 13:20:50 -0700 Subject: [PATCH 38/51] merge fixes --- src/mcp/server/fastmcp/server.py | 6 ++++-- src/mcp/server/message_queue/base.py | 10 +++++----- src/mcp/server/sse.py | 7 ++++--- src/mcp/shared/message.py | 12 +++++------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index e4f47a7cd..1e5b69eba 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -2,7 +2,6 @@ from __future__ import annotations as _annotations -from email import message import inspect import re from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence @@ -689,7 +688,10 @@ async def sse_endpoint(request: Request) -> None: # Create Starlette app with routes and middleware return Starlette( - debug=self.settings.debug, routes=routes, middleware=middleware, lifespan=lifespan + debug=self.settings.debug, + routes=routes, + middleware=middleware, + lifespan=lifespan, ) async def list_prompts(self) -> list[MCPPrompt]: diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index d54367207..60d3145a6 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -6,11 +6,11 @@ from pydantic import ValidationError -import mcp.types as types +from mcp.shared.message import SessionMessage logger = logging.getLogger(__name__) -MessageCallback = Callable[[types.JSONRPCMessage | Exception], Awaitable[None]] +MessageCallback = Callable[[SessionMessage | Exception], Awaitable[None]] @runtime_checkable @@ -22,7 +22,7 @@ class MessageDispatch(Protocol): """ async def publish_message( - self, session_id: UUID, message: types.JSONRPCMessage | str + self, session_id: UUID, message: SessionMessage | str ) -> bool: """Publish a message for the specified session. @@ -72,7 +72,7 @@ def __init__(self) -> None: self._callbacks: dict[UUID, MessageCallback] = {} async def publish_message( - self, session_id: UUID, message: types.JSONRPCMessage | str + self, session_id: UUID, message: SessionMessage | str ) -> bool: """Publish a message for the specified session.""" if session_id not in self._callbacks: @@ -82,7 +82,7 @@ async def publish_message( # Parse string messages or recreate original ValidationError if isinstance(message, str): try: - callback_argument = types.JSONRPCMessage.model_validate_json(message) + callback_argument = SessionMessage.model_validate_json(message) except ValidationError as exc: callback_argument = exc else: diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index da9c39597..e011fcc03 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -201,9 +201,10 @@ async def handle_post_message( await self._message_dispatch.publish_message(session_id, body.decode()) return - session_message = SessionMessage(message) logger.debug(f"Publishing message for session {session_id}: {message}") response = Response("Accepted", status_code=202) await response(scope, receive, send) - await self._message_dispatch.publish_message(session_id, message) - logger.debug(f"Sending session message to writer: {session_message}") + await self._message_dispatch.publish_message( + session_id, SessionMessage(message=message) + ) + logger.debug(f"Sending session message to writer: {message}") diff --git a/src/mcp/shared/message.py b/src/mcp/shared/message.py index 5583f4795..e63e65754 100644 --- a/src/mcp/shared/message.py +++ b/src/mcp/shared/message.py @@ -6,7 +6,8 @@ """ from collections.abc import Awaitable, Callable -from dataclasses import dataclass + +from pydantic import BaseModel from mcp.types import JSONRPCMessage, RequestId @@ -15,8 +16,7 @@ ResumptionTokenUpdateCallback = Callable[[ResumptionToken], Awaitable[None]] -@dataclass -class ClientMessageMetadata: +class ClientMessageMetadata(BaseModel): """Metadata specific to client messages.""" resumption_token: ResumptionToken | None = None @@ -25,8 +25,7 @@ class ClientMessageMetadata: ) -@dataclass -class ServerMessageMetadata: +class ServerMessageMetadata(BaseModel): """Metadata specific to server messages.""" related_request_id: RequestId | None = None @@ -35,8 +34,7 @@ class ServerMessageMetadata: MessageMetadata = ClientMessageMetadata | ServerMessageMetadata | None -@dataclass -class SessionMessage: +class SessionMessage(BaseModel): """A message with specific metadata for transport-specific features.""" message: JSONRPCMessage From 2437e46df535150c949e1c5accd6eefec74cefb6 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 5 May 2025 13:21:26 -0700 Subject: [PATCH 39/51] fakeredis dev dep --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 548ea005d..6ff2601e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ dependencies = [ "sse-starlette>=1.6.1", "pydantic-settings>=2.5.2", "uvicorn>=0.23.1; sys_platform != 'emscripten'", - "fakeredis==2.28.1", ] [project.optional-dependencies] @@ -57,6 +56,7 @@ dev = [ "pytest-xdist>=3.6.1", "pytest-examples>=0.0.14", "pytest-pretty>=1.2.0", + "fakeredis==2.28.1", ] docs = [ "mkdocs>=1.6.1", From 9664c8ac5d2ff9175e02851ff0f1fd6502cb3259 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 5 May 2025 13:21:40 -0700 Subject: [PATCH 40/51] fmt --- src/mcp/server/message_queue/base.py | 2 +- src/mcp/server/message_queue/redis.py | 14 ++-- tests/server/message_queue/__init__.py | 2 +- tests/server/message_queue/test_redis.py | 73 ++++++++++--------- .../message_queue/test_redis_integration.py | 70 ++++++++++-------- uv.lock | 6 +- 6 files changed, 91 insertions(+), 76 deletions(-) diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index 60d3145a6..6b210338f 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -113,4 +113,4 @@ async def session_exists(self, session_id: UUID) -> bool: async def close(self) -> None: """Close the message dispatch.""" - pass \ No newline at end of file + pass diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 6577d863d..033238d75 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -30,8 +30,10 @@ class RedisMessageDispatch: """ def __init__( - self, redis_url: str = "redis://localhost:6379/0", prefix: str = "mcp:pubsub:", - session_ttl: int = 3600 # 1 hour default TTL for sessions + self, + redis_url: str = "redis://localhost:6379/0", + prefix: str = "mcp:pubsub:", + session_ttl: int = 3600, # 1 hour default TTL for sessions ) -> None: """Initialize Redis message dispatch. @@ -51,8 +53,8 @@ def __init__( logger.debug(f"Redis message dispatch initialized: {redis_url}") async def close(self): - await self._pubsub.aclose() # type: ignore - await self._redis.aclose() # type: ignore + await self._pubsub.aclose() # type: ignore + await self._redis.aclose() # type: ignore def _session_channel(self, session_id: UUID) -> str: """Get the Redis channel for a session.""" @@ -67,7 +69,7 @@ async def subscribe(self, session_id: UUID, callback: MessageCallback): """Request-scoped context manager that subscribes to messages for a session.""" session_key = self._session_key(session_id) await self._redis.setex(session_key, self._session_ttl, "1") # type: ignore - + channel = self._session_channel(session_id) await self._pubsub.subscribe(channel) # type: ignore @@ -193,4 +195,4 @@ async def publish_message( async def session_exists(self, session_id: UUID) -> bool: """Check if a session exists.""" session_key = self._session_key(session_id) - return bool(await self._redis.exists(session_key)) # type: ignore \ No newline at end of file + return bool(await self._redis.exists(session_key)) # type: ignore diff --git a/tests/server/message_queue/__init__.py b/tests/server/message_queue/__init__.py index dd6692718..df0d26c3e 100644 --- a/tests/server/message_queue/__init__.py +++ b/tests/server/message_queue/__init__.py @@ -1 +1 @@ -# Message queue tests module \ No newline at end of file +# Message queue tests module diff --git a/tests/server/message_queue/test_redis.py b/tests/server/message_queue/test_redis.py index ad566e41e..99770a8ca 100644 --- a/tests/server/message_queue/test_redis.py +++ b/tests/server/message_queue/test_redis.py @@ -22,7 +22,7 @@ async def redis_dispatch(): # Mock the redis module entirely within RedisMessageDispatch with patch("mcp.server.message_queue.redis.redis", fake_redis.FakeRedis): from mcp.server.message_queue.redis import RedisMessageDispatch - + dispatch = RedisMessageDispatch(session_ttl=5) # Shorter TTL for testing try: yield dispatch @@ -50,7 +50,7 @@ async def test_session_exists(redis_dispatch): async def test_session_ttl(redis_dispatch): """Test that session has proper TTL set.""" session_id = uuid4() - + async with redis_dispatch.subscribe(session_id, AsyncMock()): session_key = redis_dispatch._session_key(session_id) ttl = await redis_dispatch._redis.ttl(session_key) # type: ignore @@ -62,17 +62,17 @@ async def test_session_ttl(redis_dispatch): async def test_session_heartbeat(redis_dispatch): """Test that session heartbeat refreshes TTL.""" session_id = uuid4() - + async with redis_dispatch.subscribe(session_id, AsyncMock()): session_key = redis_dispatch._session_key(session_id) - + # Initial TTL initial_ttl = await redis_dispatch._redis.ttl(session_key) # type: ignore assert initial_ttl > 0 - + # Wait for heartbeat to run await anyio.sleep(redis_dispatch._session_ttl / 2 + 0.5) - + # TTL should be refreshed refreshed_ttl = await redis_dispatch._redis.ttl(session_key) # type: ignore assert refreshed_ttl > 0 @@ -237,12 +237,12 @@ async def test_session_cancellation_isolation(redis_dispatch): """Test that cancelling one session doesn't affect other sessions.""" session1 = uuid4() session2 = uuid4() - + # Create a blocking callback for session1 to ensure it's running when cancelled session1_event = anyio.Event() session1_started = anyio.Event() session1_cancelled = False - + async def blocking_callback1(msg): session1_started.set() try: @@ -258,6 +258,7 @@ async def blocking_callback1(msg): async with redis_dispatch.subscribe(session2, callback2): # Start session1 with a blocking callback async with anyio.create_task_group() as tg: + async def session1_runner(): async with redis_dispatch.subscribe(session1, blocking_callback1): # Publish a message to trigger the blocking callback @@ -265,38 +266,38 @@ async def session1_runner(): {"jsonrpc": "2.0", "method": "test", "params": {}, "id": 1} ) await redis_dispatch.publish_message(session1, message) - + # Wait for the callback to start await session1_started.wait() - + # Keep the context alive while we test cancellation await anyio.sleep_forever() - + tg.start_soon(session1_runner) - + # Wait for session1's callback to start await session1_started.wait() - + # Cancel session1 tg.cancel_scope.cancel() - + # Give some time for cancellation to propagate await anyio.sleep(0.1) - + # Verify session1 was cancelled assert session1_cancelled assert session1 not in redis_dispatch._session_state - + # Verify session2 is still active and can receive messages assert await redis_dispatch.session_exists(session2) message2 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test2", "params": {}, "id": 2} ) await redis_dispatch.publish_message(session2, message2) - + # Give some time for the message to be processed await anyio.sleep(0.1) - + # Verify session2 received the message callback2.assert_called_once() call_args = callback2.call_args[0][0] @@ -308,22 +309,22 @@ async def test_listener_task_handoff_on_cancellation(redis_dispatch): """Test that the single listening task is properly handed off when a session is cancelled.""" session1 = uuid4() session2 = uuid4() - + session1_messages_received = 0 session2_messages_received = 0 - + async def callback1(msg): nonlocal session1_messages_received session1_messages_received += 1 - + async def callback2(msg): nonlocal session2_messages_received session2_messages_received += 1 - + # Create a cancel scope for session1 async with anyio.create_task_group() as tg: session1_cancel_scope: anyio.CancelScope | None = None - + async def session1_runner(): nonlocal session1_cancel_scope with anyio.CancelScope() as cancel_scope: @@ -331,14 +332,14 @@ async def session1_runner(): async with redis_dispatch.subscribe(session1, callback1): # Keep session alive until cancelled await anyio.sleep_forever() - + # Start session1 tg.start_soon(session1_runner) - + # Wait for session1 to be established await anyio.sleep(0.1) assert session1 in redis_dispatch._session_state - + # Send message to session1 to verify it's working message1 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test1", "params": {}, "id": 1} @@ -346,42 +347,42 @@ async def session1_runner(): await redis_dispatch.publish_message(session1, message1) await anyio.sleep(0.1) assert session1_messages_received == 1 - + # Start session2 while session1 is still active async with redis_dispatch.subscribe(session2, callback2): # Both sessions should be active assert session1 in redis_dispatch._session_state assert session2 in redis_dispatch._session_state - + # Cancel session1 assert session1_cancel_scope is not None session1_cancel_scope.cancel() - + # Wait for cancellation to complete await anyio.sleep(0.1) - + # Session1 should be gone, session2 should remain assert session1 not in redis_dispatch._session_state assert session2 in redis_dispatch._session_state - + # Send message to session2 to verify the listener was handed off message2 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test2", "params": {}, "id": 2} ) await redis_dispatch.publish_message(session2, message2) await anyio.sleep(0.1) - + # Session2 should have received the message assert session2_messages_received == 1 - + # Session1 shouldn't receive any more messages assert session1_messages_received == 1 - + # Send another message to verify the listener is still working message3 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test3", "params": {}, "id": 3} ) await redis_dispatch.publish_message(session2, message3) await anyio.sleep(0.1) - - assert session2_messages_received == 2 \ No newline at end of file + + assert session2_messages_received == 2 diff --git a/tests/server/message_queue/test_redis_integration.py b/tests/server/message_queue/test_redis_integration.py index 43b4da20f..e70343163 100644 --- a/tests/server/message_queue/test_redis_integration.py +++ b/tests/server/message_queue/test_redis_integration.py @@ -65,14 +65,14 @@ def make_redis_server_app() -> Starlette: """Create test Starlette app with SSE transport and Redis message dispatch""" # Create a mock Redis instance mock_redis = fake_redis.FakeRedis() - + # Patch the redis module within RedisMessageDispatch with patch("mcp.server.message_queue.redis.redis", mock_redis): from mcp.server.message_queue.redis import RedisMessageDispatch - + # Create Redis message dispatch with mock redis message_dispatch = RedisMessageDispatch("redis://localhost:6379/0") - + # Create SSE transport with Redis message dispatch sse = SseServerTransport("/messages/", message_dispatch=message_dispatch) server = RedisTestServer() @@ -147,7 +147,9 @@ async def http_client(server, server_url) -> AsyncGenerator[httpx.AsyncClient, N @pytest.mark.anyio -async def test_redis_integration_basic_connection(server: None, server_url: str) -> None: +async def test_redis_integration_basic_connection( + server: None, server_url: str +) -> None: """Test that a basic SSE connection works with Redis message dispatch""" async with sse_client(server_url + "/sse") as streams: async with ClientSession(*streams) as session: @@ -163,7 +165,7 @@ async def test_redis_integration_tool_call(server: None, server_url: str) -> Non async with ClientSession(*streams) as session: # Initialize session await session.initialize() - + # Call a tool result = await session.call_tool("test_tool", {}) assert result.content[0].text == "Called test_tool" @@ -175,38 +177,41 @@ async def test_redis_integration_session_lifecycle() -> None: # Create a fresh Redis instance with decode_responses=True to get str instead of bytes mock_redis = fake_redis.FakeRedis(decode_responses=True) active_sessions_key = "mcp:pubsub:active_sessions" - + # Mock Redis in RedisMessageDispatch - with patch("mcp.server.message_queue.redis.redis.from_url", return_value=mock_redis): + with patch( + "mcp.server.message_queue.redis.redis.from_url", return_value=mock_redis + ): from mcp.server.message_queue.redis import RedisMessageDispatch - + # Create Redis message dispatch with our specific mock redis instance message_dispatch = RedisMessageDispatch("redis://localhost:6379/0") - + # Create a mock callback async def mock_callback(message): pass - + # Test session subscription and unsubscription from uuid import uuid4 + session_id = uuid4() - + # Subscribe to a session async with message_dispatch.subscribe(session_id, mock_callback): # Give a moment for the session to be added await anyio.sleep(0.05) - + # Check that session was added to Redis active_sessions = await mock_redis.smembers(active_sessions_key) assert len(active_sessions) == 1 assert list(active_sessions)[0] == session_id.hex - + # Verify session exists assert await message_dispatch.session_exists(session_id) - + # Give a moment for cleanup await anyio.sleep(0.05) - + # After context exit, verify the session was removed final_sessions = await mock_redis.smembers(active_sessions_key) assert len(final_sessions) == 0 @@ -218,46 +223,53 @@ async def test_redis_integration_message_publishing_direct() -> None: """Test message publishing through Redis channels using direct Redis access""" # Create a fresh Redis instance with decode_responses=True to get str instead of bytes mock_redis = fake_redis.FakeRedis(decode_responses=True) - + # Mock Redis in RedisMessageDispatch - with patch("mcp.server.message_queue.redis.redis.from_url", return_value=mock_redis): + with patch( + "mcp.server.message_queue.redis.redis.from_url", return_value=mock_redis + ): from mcp.server.message_queue.redis import RedisMessageDispatch from mcp.types import JSONRPCMessage, JSONRPCRequest - + # Create Redis message dispatch with our specific mock redis instance message_dispatch = RedisMessageDispatch("redis://localhost:6379/0") - + # Messages received through the callback messages_received = [] - + async def message_callback(message): messages_received.append(message) - + # Use a UUID for session ID from uuid import uuid4 + session_id = uuid4() - + # Subscribe to the session async with message_dispatch.subscribe(session_id, message_callback): # Give a moment for subscription to be fully set up and start listener task await anyio.sleep(0.05) - + # Create a test message test_message = JSONRPCMessage( - root=JSONRPCRequest(jsonrpc="2.0", id=1, method="test_method", params={}) + root=JSONRPCRequest( + jsonrpc="2.0", id=1, method="test_method", params={} + ) ) - + # Publish the message success = await message_dispatch.publish_message(session_id, test_message) assert success - + # Give some time for the message to be processed # Use a shorter sleep since we're in controlled test environment await anyio.sleep(0.1) - + # Verify that the message was received - assert len(messages_received) > 0, "No messages were received through the callback" + assert ( + len(messages_received) > 0 + ), "No messages were received through the callback" received_message = messages_received[0] assert isinstance(received_message, JSONRPCMessage) assert received_message.root.method == "test_method" - assert received_message.root.id == 1 \ No newline at end of file + assert received_message.root.id == 1 diff --git a/uv.lock b/uv.lock index 7e162cf55..e819dbfe8 100644 --- a/uv.lock +++ b/uv.lock @@ -559,7 +559,6 @@ name = "mcp" source = { editable = "." } dependencies = [ { name = "anyio" }, - { name = "fakeredis" }, { name = "httpx" }, { name = "httpx-sse" }, { name = "pydantic" }, @@ -588,6 +587,7 @@ ws = [ [package.dev-dependencies] dev = [ + { name = "fakeredis" }, { name = "pyright" }, { name = "pytest" }, { name = "pytest-examples" }, @@ -607,14 +607,13 @@ docs = [ [package.metadata] requires-dist = [ { name = "anyio", specifier = ">=4.5" }, - { name = "fakeredis", specifier = "==2.28.1" }, { name = "httpx", specifier = ">=0.27" }, { name = "httpx-sse", specifier = ">=0.4" }, { name = "pydantic", specifier = ">=2.7.2,<3.0.0" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, { name = "python-dotenv", marker = "extra == 'cli'", specifier = ">=1.0.0" }, - { name = "redis", marker = "extra == 'redis'", specifier = ">=5.2.1" }, { name = "python-multipart", specifier = ">=0.0.9" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=5.2.1" }, { name = "rich", marker = "extra == 'rich'", specifier = ">=13.9.4" }, { name = "sse-starlette", specifier = ">=1.6.1" }, { name = "starlette", specifier = ">=0.27" }, @@ -626,6 +625,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "fakeredis", specifier = "==2.28.1" }, { name = "pyright", specifier = ">=1.1.391" }, { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-examples", specifier = ">=0.0.14" }, From 30b475bea93aa1094ecdafe33b5e2c958faa72c7 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 5 May 2025 13:43:02 -0700 Subject: [PATCH 41/51] convert to Pydantic models --- .../simple-prompt/mcp_simple_prompt/server.py | 3 +- src/mcp/client/sse.py | 2 +- src/mcp/client/stdio/__init__.py | 2 +- src/mcp/client/streamable_http.py | 6 +-- src/mcp/client/websocket.py | 2 +- src/mcp/server/message_queue/redis.py | 6 +-- src/mcp/server/stdio.py | 2 +- src/mcp/server/streamable_http.py | 6 +-- src/mcp/server/websocket.py | 2 +- src/mcp/shared/message.py | 2 +- tests/client/test_session.py | 6 +-- tests/client/test_stdio.py | 2 +- tests/issues/test_192_request_id.py | 6 +-- tests/server/message_queue/test_redis.py | 39 +++++++++++++------ .../message_queue/test_redis_integration.py | 18 +++++---- tests/server/test_lifespan.py | 12 +++--- tests/server/test_stdio.py | 2 +- 17 files changed, 67 insertions(+), 51 deletions(-) diff --git a/examples/servers/simple-prompt/mcp_simple_prompt/server.py b/examples/servers/simple-prompt/mcp_simple_prompt/server.py index fae749e3c..04b10ac75 100644 --- a/examples/servers/simple-prompt/mcp_simple_prompt/server.py +++ b/examples/servers/simple-prompt/mcp_simple_prompt/server.py @@ -88,13 +88,12 @@ async def get_prompt( ) if transport == "sse": + from mcp.server.message_queue.redis import RedisMessageDispatch from mcp.server.sse import SseServerTransport from starlette.applications import Starlette from starlette.responses import Response from starlette.routing import Mount, Route - from mcp.server.message_queue.redis import RedisMessageDispatch - message_dispatch = RedisMessageDispatch("redis://localhost:6379/0") sse = SseServerTransport("/messages/", message_dispatch=message_dispatch) diff --git a/src/mcp/client/sse.py b/src/mcp/client/sse.py index ff04d2f96..9795e5b52 100644 --- a/src/mcp/client/sse.py +++ b/src/mcp/client/sse.py @@ -98,7 +98,7 @@ async def sse_reader( await read_stream_writer.send(exc) continue - session_message = SessionMessage(message) + session_message = SessionMessage(message=message) await read_stream_writer.send(session_message) case _: logger.warning( diff --git a/src/mcp/client/stdio/__init__.py b/src/mcp/client/stdio/__init__.py index e8be5aff5..21c7764e7 100644 --- a/src/mcp/client/stdio/__init__.py +++ b/src/mcp/client/stdio/__init__.py @@ -144,7 +144,7 @@ async def stdout_reader(): await read_stream_writer.send(exc) continue - session_message = SessionMessage(message) + session_message = SessionMessage(message=message) await read_stream_writer.send(session_message) except anyio.ClosedResourceError: await anyio.lowlevel.checkpoint() diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index ef424e3b3..ca26046b9 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -153,7 +153,7 @@ async def _handle_sse_event( ): message.root.id = original_request_id - session_message = SessionMessage(message) + session_message = SessionMessage(message=message) await read_stream_writer.send(session_message) # Call resumption token callback if we have an ID @@ -286,7 +286,7 @@ async def _handle_json_response( try: content = await response.aread() message = JSONRPCMessage.model_validate_json(content) - session_message = SessionMessage(message) + session_message = SessionMessage(message=message) await read_stream_writer.send(session_message) except Exception as exc: logger.error(f"Error parsing JSON response: {exc}") @@ -333,7 +333,7 @@ async def _send_session_terminated_error( id=request_id, error=ErrorData(code=32600, message="Session terminated"), ) - session_message = SessionMessage(JSONRPCMessage(jsonrpc_error)) + session_message = SessionMessage(message=JSONRPCMessage(jsonrpc_error)) await read_stream_writer.send(session_message) async def post_writer( diff --git a/src/mcp/client/websocket.py b/src/mcp/client/websocket.py index ac542fb3f..598fdaf25 100644 --- a/src/mcp/client/websocket.py +++ b/src/mcp/client/websocket.py @@ -60,7 +60,7 @@ async def ws_reader(): async for raw_text in ws: try: message = types.JSONRPCMessage.model_validate_json(raw_text) - session_message = SessionMessage(message) + session_message = SessionMessage(message=message) await read_stream_writer.send(session_message) except ValidationError as exc: # If JSON parse or model validation fails, send the exception diff --git a/src/mcp/server/message_queue/redis.py b/src/mcp/server/message_queue/redis.py index 033238d75..628ce026c 100644 --- a/src/mcp/server/message_queue/redis.py +++ b/src/mcp/server/message_queue/redis.py @@ -8,8 +8,8 @@ from anyio.abc import TaskGroup from pydantic import ValidationError -import mcp.types as types from mcp.server.message_queue.base import MessageCallback +from mcp.shared.message import SessionMessage try: import redis.asyncio as redis @@ -165,7 +165,7 @@ async def _handle_message(self, session_id: UUID, data: str) -> None: # Parse message or pass validation error to callback msg_or_error = None try: - msg_or_error = types.JSONRPCMessage.model_validate_json(data) + msg_or_error = SessionMessage.model_validate_json(data) except ValidationError as exc: msg_or_error = exc @@ -174,7 +174,7 @@ async def _handle_message(self, session_id: UUID, data: str) -> None: logger.error(f"Error in message handler for {session_id}: {e}") async def publish_message( - self, session_id: UUID, message: types.JSONRPCMessage | str + self, session_id: UUID, message: SessionMessage | str ) -> bool: """Publish a message for the specified session.""" if not await self.session_exists(session_id): diff --git a/src/mcp/server/stdio.py b/src/mcp/server/stdio.py index f0bbe5a31..11c8f7ee4 100644 --- a/src/mcp/server/stdio.py +++ b/src/mcp/server/stdio.py @@ -67,7 +67,7 @@ async def stdin_reader(): await read_stream_writer.send(exc) continue - session_message = SessionMessage(message) + session_message = SessionMessage(message=message) await read_stream_writer.send(session_message) except anyio.ClosedResourceError: await anyio.lowlevel.checkpoint() diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index ace74b33b..79c8a8913 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -398,7 +398,7 @@ async def _handle_post_request( await response(scope, receive, send) # Process the message after sending the response - session_message = SessionMessage(message) + session_message = SessionMessage(message=message) await writer.send(session_message) return @@ -413,7 +413,7 @@ async def _handle_post_request( if self.is_json_response_enabled: # Process the message - session_message = SessionMessage(message) + session_message = SessionMessage(message=message) await writer.send(session_message) try: # Process messages from the request-specific stream @@ -512,7 +512,7 @@ async def sse_writer(): async with anyio.create_task_group() as tg: tg.start_soon(response, scope, receive, send) # Then send the message to be processed by the server - session_message = SessionMessage(message) + session_message = SessionMessage(message=message) await writer.send(session_message) except Exception: logger.exception("SSE response error") diff --git a/src/mcp/server/websocket.py b/src/mcp/server/websocket.py index 9dc3f2a25..bb0b1ca6e 100644 --- a/src/mcp/server/websocket.py +++ b/src/mcp/server/websocket.py @@ -42,7 +42,7 @@ async def ws_reader(): await read_stream_writer.send(exc) continue - session_message = SessionMessage(client_message) + session_message = SessionMessage(message=client_message) await read_stream_writer.send(session_message) except anyio.ClosedResourceError: await websocket.close() diff --git a/src/mcp/shared/message.py b/src/mcp/shared/message.py index e63e65754..c96a0a1e6 100644 --- a/src/mcp/shared/message.py +++ b/src/mcp/shared/message.py @@ -38,4 +38,4 @@ class SessionMessage(BaseModel): """A message with specific metadata for transport-specific features.""" message: JSONRPCMessage - metadata: MessageMetadata = None + metadata: MessageMetadata | None = None diff --git a/tests/client/test_session.py b/tests/client/test_session.py index 6abcf70cb..cd3dae293 100644 --- a/tests/client/test_session.py +++ b/tests/client/test_session.py @@ -62,7 +62,7 @@ async def mock_server(): async with server_to_client_send: await server_to_client_send.send( SessionMessage( - JSONRPCMessage( + message=JSONRPCMessage( JSONRPCResponse( jsonrpc="2.0", id=jsonrpc_request.root.id, @@ -153,7 +153,7 @@ async def mock_server(): async with server_to_client_send: await server_to_client_send.send( SessionMessage( - JSONRPCMessage( + message=JSONRPCMessage( JSONRPCResponse( jsonrpc="2.0", id=jsonrpc_request.root.id, @@ -220,7 +220,7 @@ async def mock_server(): async with server_to_client_send: await server_to_client_send.send( SessionMessage( - JSONRPCMessage( + message=JSONRPCMessage( JSONRPCResponse( jsonrpc="2.0", id=jsonrpc_request.root.id, diff --git a/tests/client/test_stdio.py b/tests/client/test_stdio.py index 523ba199a..d93c63aef 100644 --- a/tests/client/test_stdio.py +++ b/tests/client/test_stdio.py @@ -23,7 +23,7 @@ async def test_stdio_client(): async with write_stream: for message in messages: - session_message = SessionMessage(message) + session_message = SessionMessage(message=message) await write_stream.send(session_message) read_messages = [] diff --git a/tests/issues/test_192_request_id.py b/tests/issues/test_192_request_id.py index cf5eb6083..4c6094494 100644 --- a/tests/issues/test_192_request_id.py +++ b/tests/issues/test_192_request_id.py @@ -65,7 +65,7 @@ async def run_server(): jsonrpc="2.0", ) - await client_writer.send(SessionMessage(JSONRPCMessage(root=init_req))) + await client_writer.send(SessionMessage(message=JSONRPCMessage(root=init_req))) response = ( await server_reader.receive() ) # Get init response but don't need to check it @@ -77,7 +77,7 @@ async def run_server(): jsonrpc="2.0", ) await client_writer.send( - SessionMessage(JSONRPCMessage(root=initialized_notification)) + SessionMessage(message=JSONRPCMessage(root=initialized_notification)) ) # Send ping request with custom ID @@ -85,7 +85,7 @@ async def run_server(): id=custom_request_id, method="ping", params={}, jsonrpc="2.0" ) - await client_writer.send(SessionMessage(JSONRPCMessage(root=ping_request))) + await client_writer.send(SessionMessage(message=JSONRPCMessage(root=ping_request))) # Read response response = await server_reader.receive() diff --git a/tests/server/message_queue/test_redis.py b/tests/server/message_queue/test_redis.py index 99770a8ca..bacdae4ef 100644 --- a/tests/server/message_queue/test_redis.py +++ b/tests/server/message_queue/test_redis.py @@ -2,10 +2,12 @@ from uuid import uuid4 import anyio -from pydantic import ValidationError import pytest +from pydantic import ValidationError import mcp.types as types +from mcp.server.message_queue.redis import RedisMessageDispatch +from mcp.shared.message import SessionMessage # Set up fakeredis for testing try: @@ -97,7 +99,7 @@ async def test_subscribe_unsubscribe(redis_dispatch): @pytest.mark.anyio -async def test_publish_message_valid_json(redis_dispatch): +async def test_publish_message_valid_json(redis_dispatch: RedisMessageDispatch): """Test publishing a valid JSON-RPC message.""" session_id = uuid4() callback = AsyncMock() @@ -108,7 +110,9 @@ async def test_publish_message_valid_json(redis_dispatch): # Subscribe to messages async with redis_dispatch.subscribe(session_id, callback): # Publish message - published = await redis_dispatch.publish_message(session_id, message) + published = await redis_dispatch.publish_message( + session_id, SessionMessage(message=message) + ) assert published # Give some time for the message to be processed @@ -117,9 +121,11 @@ async def test_publish_message_valid_json(redis_dispatch): # Callback should have been called with the message callback.assert_called_once() call_args = callback.call_args[0][0] - assert isinstance(call_args, types.JSONRPCMessage) - assert isinstance(call_args.root, types.JSONRPCRequest) - assert call_args.root.method == "test" # Access method through root attribute + assert isinstance(call_args, SessionMessage) + assert isinstance(call_args.message.root, types.JSONRPCRequest) + assert ( + call_args.message.root.method == "test" + ) # Access method through root attribute @pytest.mark.anyio @@ -177,7 +183,7 @@ async def test_extract_session_id(redis_dispatch): @pytest.mark.anyio -async def test_multiple_sessions(redis_dispatch): +async def test_multiple_sessions(redis_dispatch: RedisMessageDispatch): """Test handling multiple concurrent sessions.""" session1 = uuid4() session2 = uuid4() @@ -194,13 +200,17 @@ async def test_multiple_sessions(redis_dispatch): message1 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test1", "params": {}, "id": 1} ) - await redis_dispatch.publish_message(session1, message1) + await redis_dispatch.publish_message( + session1, SessionMessage(message=message1) + ) # Publish to session2 message2 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test2", "params": {}, "id": 2} ) - await redis_dispatch.publish_message(session2, message2) + await redis_dispatch.publish_message( + session2, SessionMessage(message=message2) + ) # Give some time for messages to be processed await anyio.sleep(0.1) @@ -210,10 +220,12 @@ async def test_multiple_sessions(redis_dispatch): callback2.assert_called_once() call1_args = callback1.call_args[0][0] - assert call1_args.root.method == "test1" + assert isinstance(call1_args, SessionMessage) + assert call1_args.message.root.method == "test1" # type: ignore call2_args = callback2.call_args[0][0] - assert call2_args.root.method == "test2" + assert isinstance(call2_args, SessionMessage) + assert call2_args.message.root.method == "test2" # type: ignore @pytest.mark.anyio @@ -306,7 +318,10 @@ async def session1_runner(): @pytest.mark.anyio async def test_listener_task_handoff_on_cancellation(redis_dispatch): - """Test that the single listening task is properly handed off when a session is cancelled.""" + """ + Test that the single listening task is properly + handed off when a session is cancelled. + """ session1 = uuid4() session2 = uuid4() diff --git a/tests/server/message_queue/test_redis_integration.py b/tests/server/message_queue/test_redis_integration.py index e70343163..cff621bcd 100644 --- a/tests/server/message_queue/test_redis_integration.py +++ b/tests/server/message_queue/test_redis_integration.py @@ -168,13 +168,13 @@ async def test_redis_integration_tool_call(server: None, server_url: str) -> Non # Call a tool result = await session.call_tool("test_tool", {}) - assert result.content[0].text == "Called test_tool" + assert result.content[0].text == "Called test_tool" # type: ignore @pytest.mark.anyio async def test_redis_integration_session_lifecycle() -> None: - """Test that sessions are properly added to and removed from Redis using direct Redis access""" - # Create a fresh Redis instance with decode_responses=True to get str instead of bytes + """Test that sessions are properly added to and + removed from Redis using direct Redis access""" mock_redis = fake_redis.FakeRedis(decode_responses=True) active_sessions_key = "mcp:pubsub:active_sessions" @@ -221,7 +221,6 @@ async def mock_callback(message): @pytest.mark.anyio async def test_redis_integration_message_publishing_direct() -> None: """Test message publishing through Redis channels using direct Redis access""" - # Create a fresh Redis instance with decode_responses=True to get str instead of bytes mock_redis = fake_redis.FakeRedis(decode_responses=True) # Mock Redis in RedisMessageDispatch @@ -229,6 +228,7 @@ async def test_redis_integration_message_publishing_direct() -> None: "mcp.server.message_queue.redis.redis.from_url", return_value=mock_redis ): from mcp.server.message_queue.redis import RedisMessageDispatch + from mcp.shared.message import SessionMessage from mcp.types import JSONRPCMessage, JSONRPCRequest # Create Redis message dispatch with our specific mock redis instance @@ -258,7 +258,9 @@ async def message_callback(message): ) # Publish the message - success = await message_dispatch.publish_message(session_id, test_message) + success = await message_dispatch.publish_message( + session_id, SessionMessage(message=test_message) + ) assert success # Give some time for the message to be processed @@ -270,6 +272,6 @@ async def message_callback(message): len(messages_received) > 0 ), "No messages were received through the callback" received_message = messages_received[0] - assert isinstance(received_message, JSONRPCMessage) - assert received_message.root.method == "test_method" - assert received_message.root.id == 1 + assert isinstance(received_message, SessionMessage) + assert received_message.message.root.method == "test_method" + assert received_message.message.root.id == 1 diff --git a/tests/server/test_lifespan.py b/tests/server/test_lifespan.py index a3ff59bc1..d8e76de1a 100644 --- a/tests/server/test_lifespan.py +++ b/tests/server/test_lifespan.py @@ -84,7 +84,7 @@ async def run_server(): ) await send_stream1.send( SessionMessage( - JSONRPCMessage( + message=JSONRPCMessage( root=JSONRPCRequest( jsonrpc="2.0", id=1, @@ -100,7 +100,7 @@ async def run_server(): # Send initialized notification await send_stream1.send( SessionMessage( - JSONRPCMessage( + message=JSONRPCMessage( root=JSONRPCNotification( jsonrpc="2.0", method="notifications/initialized", @@ -112,7 +112,7 @@ async def run_server(): # Call the tool to verify lifespan context await send_stream1.send( SessionMessage( - JSONRPCMessage( + message=JSONRPCMessage( root=JSONRPCRequest( jsonrpc="2.0", id=2, @@ -188,7 +188,7 @@ async def run_server(): ) await send_stream1.send( SessionMessage( - JSONRPCMessage( + message=JSONRPCMessage( root=JSONRPCRequest( jsonrpc="2.0", id=1, @@ -204,7 +204,7 @@ async def run_server(): # Send initialized notification await send_stream1.send( SessionMessage( - JSONRPCMessage( + message=JSONRPCMessage( root=JSONRPCNotification( jsonrpc="2.0", method="notifications/initialized", @@ -216,7 +216,7 @@ async def run_server(): # Call the tool to verify lifespan context await send_stream1.send( SessionMessage( - JSONRPCMessage( + message=JSONRPCMessage( root=JSONRPCRequest( jsonrpc="2.0", id=2, diff --git a/tests/server/test_stdio.py b/tests/server/test_stdio.py index c546a7167..570e4c199 100644 --- a/tests/server/test_stdio.py +++ b/tests/server/test_stdio.py @@ -51,7 +51,7 @@ async def test_stdio_server(): async with write_stream: for response in responses: - session_message = SessionMessage(response) + session_message = SessionMessage(message=response) await write_stream.send(session_message) stdout.seek(0) From 01141897a24709a93c6f792dcbbb5ce8944e9ea1 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 5 May 2025 13:43:34 -0700 Subject: [PATCH 42/51] fmt --- src/mcp/client/sse.py | 4 +++- tests/issues/test_192_request_id.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/mcp/client/sse.py b/src/mcp/client/sse.py index 9795e5b52..f0425e7af 100644 --- a/src/mcp/client/sse.py +++ b/src/mcp/client/sse.py @@ -98,7 +98,9 @@ async def sse_reader( await read_stream_writer.send(exc) continue - session_message = SessionMessage(message=message) + session_message = SessionMessage( + message=message + ) await read_stream_writer.send(session_message) case _: logger.warning( diff --git a/tests/issues/test_192_request_id.py b/tests/issues/test_192_request_id.py index 4c6094494..c05f08f8c 100644 --- a/tests/issues/test_192_request_id.py +++ b/tests/issues/test_192_request_id.py @@ -85,7 +85,9 @@ async def run_server(): id=custom_request_id, method="ping", params={}, jsonrpc="2.0" ) - await client_writer.send(SessionMessage(message=JSONRPCMessage(root=ping_request))) + await client_writer.send( + SessionMessage(message=JSONRPCMessage(root=ping_request)) + ) # Read response response = await server_reader.receive() From 7081a409fd5063b2093813f696ac5d2121aab5e8 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 5 May 2025 13:44:56 -0700 Subject: [PATCH 43/51] more type fixes --- tests/server/message_queue/test_redis_integration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/server/message_queue/test_redis_integration.py b/tests/server/message_queue/test_redis_integration.py index cff621bcd..a4334d12b 100644 --- a/tests/server/message_queue/test_redis_integration.py +++ b/tests/server/message_queue/test_redis_integration.py @@ -273,5 +273,5 @@ async def message_callback(message): ), "No messages were received through the callback" received_message = messages_received[0] assert isinstance(received_message, SessionMessage) - assert received_message.message.root.method == "test_method" - assert received_message.message.root.id == 1 + assert received_message.message.root.method == "test_method" # type: ignore + assert received_message.message.root.id == 1 # type: ignore From 5ae3cc6913449480dcd802ff687f8cc7e6e00079 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 5 May 2025 16:16:29 -0700 Subject: [PATCH 44/51] test cleanup --- tests/server/message_queue/conftest.py | 28 +++ tests/server/message_queue/test_redis.py | 156 +++++++--------- .../message_queue/test_redis_integration.py | 168 +++++++----------- 3 files changed, 163 insertions(+), 189 deletions(-) create mode 100644 tests/server/message_queue/conftest.py diff --git a/tests/server/message_queue/conftest.py b/tests/server/message_queue/conftest.py new file mode 100644 index 000000000..3422da2aa --- /dev/null +++ b/tests/server/message_queue/conftest.py @@ -0,0 +1,28 @@ +"""Shared fixtures for message queue tests.""" + +from collections.abc import AsyncGenerator +from unittest.mock import patch + +import pytest + +from mcp.server.message_queue.redis import RedisMessageDispatch + +# Set up fakeredis for testing +try: + from fakeredis import aioredis as fake_redis +except ImportError: + pytest.skip( + "fakeredis is required for testing Redis functionality", allow_module_level=True + ) + + +@pytest.fixture +async def message_dispatch() -> AsyncGenerator[RedisMessageDispatch, None]: + """Create a shared Redis message dispatch with a fake Redis client.""" + with patch("mcp.server.message_queue.redis.redis", fake_redis.FakeRedis): + # Shorter TTL for testing + message_dispatch = RedisMessageDispatch(session_ttl=5) + try: + yield message_dispatch + finally: + await message_dispatch.close() diff --git a/tests/server/message_queue/test_redis.py b/tests/server/message_queue/test_redis.py index bacdae4ef..da4d1c442 100644 --- a/tests/server/message_queue/test_redis.py +++ b/tests/server/message_queue/test_redis.py @@ -1,4 +1,4 @@ -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock from uuid import uuid4 import anyio @@ -9,97 +9,75 @@ from mcp.server.message_queue.redis import RedisMessageDispatch from mcp.shared.message import SessionMessage -# Set up fakeredis for testing -try: - from fakeredis import aioredis as fake_redis -except ImportError: - pytest.skip( - "fakeredis is required for testing Redis functionality", allow_module_level=True - ) - - -@pytest.fixture -async def redis_dispatch(): - """Create a Redis message dispatch with a fake Redis client.""" - # Mock the redis module entirely within RedisMessageDispatch - with patch("mcp.server.message_queue.redis.redis", fake_redis.FakeRedis): - from mcp.server.message_queue.redis import RedisMessageDispatch - - dispatch = RedisMessageDispatch(session_ttl=5) # Shorter TTL for testing - try: - yield dispatch - finally: - await dispatch.close() - @pytest.mark.anyio -async def test_session_exists(redis_dispatch): +async def test_session_exists(message_dispatch): """Test session existence check.""" session_id = uuid4() # Initially session should not exist - assert not await redis_dispatch.session_exists(session_id) + assert not await message_dispatch.session_exists(session_id) # After subscribing, session should exist - async with redis_dispatch.subscribe(session_id, AsyncMock()): - assert await redis_dispatch.session_exists(session_id) + async with message_dispatch.subscribe(session_id, AsyncMock()): + assert await message_dispatch.session_exists(session_id) # After unsubscribing, session should not exist - assert not await redis_dispatch.session_exists(session_id) + assert not await message_dispatch.session_exists(session_id) @pytest.mark.anyio -async def test_session_ttl(redis_dispatch): +async def test_session_ttl(message_dispatch): """Test that session has proper TTL set.""" session_id = uuid4() - async with redis_dispatch.subscribe(session_id, AsyncMock()): - session_key = redis_dispatch._session_key(session_id) - ttl = await redis_dispatch._redis.ttl(session_key) # type: ignore + async with message_dispatch.subscribe(session_id, AsyncMock()): + session_key = message_dispatch._session_key(session_id) + ttl = await message_dispatch._redis.ttl(session_key) # type: ignore assert ttl > 0 - assert ttl <= redis_dispatch._session_ttl + assert ttl <= message_dispatch._session_ttl @pytest.mark.anyio -async def test_session_heartbeat(redis_dispatch): +async def test_session_heartbeat(message_dispatch): """Test that session heartbeat refreshes TTL.""" session_id = uuid4() - async with redis_dispatch.subscribe(session_id, AsyncMock()): - session_key = redis_dispatch._session_key(session_id) + async with message_dispatch.subscribe(session_id, AsyncMock()): + session_key = message_dispatch._session_key(session_id) # Initial TTL - initial_ttl = await redis_dispatch._redis.ttl(session_key) # type: ignore + initial_ttl = await message_dispatch._redis.ttl(session_key) # type: ignore assert initial_ttl > 0 # Wait for heartbeat to run - await anyio.sleep(redis_dispatch._session_ttl / 2 + 0.5) + await anyio.sleep(message_dispatch._session_ttl / 2 + 0.5) # TTL should be refreshed - refreshed_ttl = await redis_dispatch._redis.ttl(session_key) # type: ignore + refreshed_ttl = await message_dispatch._redis.ttl(session_key) # type: ignore assert refreshed_ttl > 0 - assert refreshed_ttl <= redis_dispatch._session_ttl + assert refreshed_ttl <= message_dispatch._session_ttl @pytest.mark.anyio -async def test_subscribe_unsubscribe(redis_dispatch): +async def test_subscribe_unsubscribe(message_dispatch): """Test subscribing and unsubscribing from a session.""" session_id = uuid4() callback = AsyncMock() # Subscribe - async with redis_dispatch.subscribe(session_id, callback): + async with message_dispatch.subscribe(session_id, callback): # Check that session is tracked - assert session_id in redis_dispatch._session_state - assert await redis_dispatch.session_exists(session_id) + assert session_id in message_dispatch._session_state + assert await message_dispatch.session_exists(session_id) # After context exit, session should be cleaned up - assert session_id not in redis_dispatch._session_state - assert not await redis_dispatch.session_exists(session_id) + assert session_id not in message_dispatch._session_state + assert not await message_dispatch.session_exists(session_id) @pytest.mark.anyio -async def test_publish_message_valid_json(redis_dispatch: RedisMessageDispatch): +async def test_publish_message_valid_json(message_dispatch: RedisMessageDispatch): """Test publishing a valid JSON-RPC message.""" session_id = uuid4() callback = AsyncMock() @@ -108,9 +86,9 @@ async def test_publish_message_valid_json(redis_dispatch: RedisMessageDispatch): ) # Subscribe to messages - async with redis_dispatch.subscribe(session_id, callback): + async with message_dispatch.subscribe(session_id, callback): # Publish message - published = await redis_dispatch.publish_message( + published = await message_dispatch.publish_message( session_id, SessionMessage(message=message) ) assert published @@ -129,16 +107,16 @@ async def test_publish_message_valid_json(redis_dispatch: RedisMessageDispatch): @pytest.mark.anyio -async def test_publish_message_invalid_json(redis_dispatch): +async def test_publish_message_invalid_json(message_dispatch): """Test publishing an invalid JSON string.""" session_id = uuid4() callback = AsyncMock() invalid_json = '{"invalid": "json",,}' # Invalid JSON # Subscribe to messages - async with redis_dispatch.subscribe(session_id, callback): + async with message_dispatch.subscribe(session_id, callback): # Publish invalid message - published = await redis_dispatch.publish_message(session_id, invalid_json) + published = await message_dispatch.publish_message(session_id, invalid_json) assert published # Give some time for the message to be processed @@ -151,56 +129,56 @@ async def test_publish_message_invalid_json(redis_dispatch): @pytest.mark.anyio -async def test_publish_to_nonexistent_session(redis_dispatch): +async def test_publish_to_nonexistent_session(message_dispatch): """Test publishing to a session that doesn't exist.""" session_id = uuid4() message = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test", "params": {}, "id": 1} ) - published = await redis_dispatch.publish_message(session_id, message) + published = await message_dispatch.publish_message(session_id, message) assert not published @pytest.mark.anyio -async def test_extract_session_id(redis_dispatch): +async def test_extract_session_id(message_dispatch): """Test extracting session ID from channel name.""" session_id = uuid4() - channel = redis_dispatch._session_channel(session_id) + channel = message_dispatch._session_channel(session_id) # Valid channel - extracted_id = redis_dispatch._extract_session_id(channel) + extracted_id = message_dispatch._extract_session_id(channel) assert extracted_id == session_id # Invalid channel format - extracted_id = redis_dispatch._extract_session_id("invalid_channel_name") + extracted_id = message_dispatch._extract_session_id("invalid_channel_name") assert extracted_id is None # Invalid UUID in channel - invalid_channel = f"{redis_dispatch._prefix}session:invalid_uuid" - extracted_id = redis_dispatch._extract_session_id(invalid_channel) + invalid_channel = f"{message_dispatch._prefix}session:invalid_uuid" + extracted_id = message_dispatch._extract_session_id(invalid_channel) assert extracted_id is None @pytest.mark.anyio -async def test_multiple_sessions(redis_dispatch: RedisMessageDispatch): +async def test_multiple_sessions(message_dispatch: RedisMessageDispatch): """Test handling multiple concurrent sessions.""" session1 = uuid4() session2 = uuid4() callback1 = AsyncMock() callback2 = AsyncMock() - async with redis_dispatch.subscribe(session1, callback1): - async with redis_dispatch.subscribe(session2, callback2): + async with message_dispatch.subscribe(session1, callback1): + async with message_dispatch.subscribe(session2, callback2): # Both sessions should exist - assert await redis_dispatch.session_exists(session1) - assert await redis_dispatch.session_exists(session2) + assert await message_dispatch.session_exists(session1) + assert await message_dispatch.session_exists(session2) # Publish to session1 message1 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test1", "params": {}, "id": 1} ) - await redis_dispatch.publish_message( + await message_dispatch.publish_message( session1, SessionMessage(message=message1) ) @@ -208,7 +186,7 @@ async def test_multiple_sessions(redis_dispatch: RedisMessageDispatch): message2 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test2", "params": {}, "id": 2} ) - await redis_dispatch.publish_message( + await message_dispatch.publish_message( session2, SessionMessage(message=message2) ) @@ -229,23 +207,23 @@ async def test_multiple_sessions(redis_dispatch: RedisMessageDispatch): @pytest.mark.anyio -async def test_task_group_cancellation(redis_dispatch): +async def test_task_group_cancellation(message_dispatch): """Test that task group is properly cancelled when context exits.""" session_id = uuid4() callback = AsyncMock() - async with redis_dispatch.subscribe(session_id, callback): + async with message_dispatch.subscribe(session_id, callback): # Check that task group is active - _, task_group = redis_dispatch._session_state[session_id] + _, task_group = message_dispatch._session_state[session_id] assert task_group.cancel_scope.cancel_called is False # After context exit, task group should be cancelled # And session state should be cleaned up - assert session_id not in redis_dispatch._session_state + assert session_id not in message_dispatch._session_state @pytest.mark.anyio -async def test_session_cancellation_isolation(redis_dispatch): +async def test_session_cancellation_isolation(message_dispatch): """Test that cancelling one session doesn't affect other sessions.""" session1 = uuid4() session2 = uuid4() @@ -267,17 +245,17 @@ async def blocking_callback1(msg): callback2 = AsyncMock() # Start session2 first - async with redis_dispatch.subscribe(session2, callback2): + async with message_dispatch.subscribe(session2, callback2): # Start session1 with a blocking callback async with anyio.create_task_group() as tg: async def session1_runner(): - async with redis_dispatch.subscribe(session1, blocking_callback1): + async with message_dispatch.subscribe(session1, blocking_callback1): # Publish a message to trigger the blocking callback message = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test", "params": {}, "id": 1} ) - await redis_dispatch.publish_message(session1, message) + await message_dispatch.publish_message(session1, message) # Wait for the callback to start await session1_started.wait() @@ -298,14 +276,14 @@ async def session1_runner(): # Verify session1 was cancelled assert session1_cancelled - assert session1 not in redis_dispatch._session_state + assert session1 not in message_dispatch._session_state # Verify session2 is still active and can receive messages - assert await redis_dispatch.session_exists(session2) + assert await message_dispatch.session_exists(session2) message2 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test2", "params": {}, "id": 2} ) - await redis_dispatch.publish_message(session2, message2) + await message_dispatch.publish_message(session2, message2) # Give some time for the message to be processed await anyio.sleep(0.1) @@ -317,7 +295,7 @@ async def session1_runner(): @pytest.mark.anyio -async def test_listener_task_handoff_on_cancellation(redis_dispatch): +async def test_listener_task_handoff_on_cancellation(message_dispatch): """ Test that the single listening task is properly handed off when a session is cancelled. @@ -344,7 +322,7 @@ async def session1_runner(): nonlocal session1_cancel_scope with anyio.CancelScope() as cancel_scope: session1_cancel_scope = cancel_scope - async with redis_dispatch.subscribe(session1, callback1): + async with message_dispatch.subscribe(session1, callback1): # Keep session alive until cancelled await anyio.sleep_forever() @@ -353,21 +331,21 @@ async def session1_runner(): # Wait for session1 to be established await anyio.sleep(0.1) - assert session1 in redis_dispatch._session_state + assert session1 in message_dispatch._session_state # Send message to session1 to verify it's working message1 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test1", "params": {}, "id": 1} ) - await redis_dispatch.publish_message(session1, message1) + await message_dispatch.publish_message(session1, message1) await anyio.sleep(0.1) assert session1_messages_received == 1 # Start session2 while session1 is still active - async with redis_dispatch.subscribe(session2, callback2): + async with message_dispatch.subscribe(session2, callback2): # Both sessions should be active - assert session1 in redis_dispatch._session_state - assert session2 in redis_dispatch._session_state + assert session1 in message_dispatch._session_state + assert session2 in message_dispatch._session_state # Cancel session1 assert session1_cancel_scope is not None @@ -377,14 +355,14 @@ async def session1_runner(): await anyio.sleep(0.1) # Session1 should be gone, session2 should remain - assert session1 not in redis_dispatch._session_state - assert session2 in redis_dispatch._session_state + assert session1 not in message_dispatch._session_state + assert session2 in message_dispatch._session_state # Send message to session2 to verify the listener was handed off message2 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test2", "params": {}, "id": 2} ) - await redis_dispatch.publish_message(session2, message2) + await message_dispatch.publish_message(session2, message2) await anyio.sleep(0.1) # Session2 should have received the message @@ -397,7 +375,7 @@ async def session1_runner(): message3 = types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test3", "params": {}, "id": 3} ) - await redis_dispatch.publish_message(session2, message3) + await message_dispatch.publish_message(session2, message3) await anyio.sleep(0.1) assert session2_messages_received == 2 diff --git a/tests/server/message_queue/test_redis_integration.py b/tests/server/message_queue/test_redis_integration.py index a4334d12b..52077c208 100644 --- a/tests/server/message_queue/test_redis_integration.py +++ b/tests/server/message_queue/test_redis_integration.py @@ -2,10 +2,11 @@ import socket import time from collections.abc import AsyncGenerator, Generator +from contextlib import asynccontextmanager from unittest.mock import patch +from uuid import uuid4 import anyio -import httpx import pytest import uvicorn from starlette.applications import Starlette @@ -15,6 +16,7 @@ from mcp.client.session import ClientSession from mcp.client.sse import sse_client from mcp.server import Server +from mcp.server.message_queue.redis import RedisMessageDispatch from mcp.server.sse import SseServerTransport from mcp.types import TextContent, Tool @@ -64,7 +66,7 @@ async def handle_call_tool(name: str, args: dict) -> list[TextContent]: def make_redis_server_app() -> Starlette: """Create test Starlette app with SSE transport and Redis message dispatch""" # Create a mock Redis instance - mock_redis = fake_redis.FakeRedis() + mock_redis = fake_redis.FakeRedis(decode_responses=True) # Patch the redis module within RedisMessageDispatch with patch("mcp.server.message_queue.redis.redis", mock_redis): @@ -85,11 +87,20 @@ async def handle_sse(request: Request) -> None: streams[0], streams[1], server.create_initialization_options() ) + @asynccontextmanager + async def close_redis(app: Starlette) -> AsyncGenerator[None, None]: + try: + yield + finally: + await message_dispatch.close() + await mock_redis.aclose() # type: ignore + app = Starlette( routes=[ Route("/sse", endpoint=handle_sse), Mount("/messages/", app=sse.handle_post_message), - ] + ], + lifespan=close_redis, ) return app @@ -133,19 +144,12 @@ def server(server_port: int) -> Generator[None, None, None]: yield # Signal the server to stop - proc.kill() + proc.terminate() proc.join(timeout=2) if proc.is_alive(): print("server process failed to terminate") -@pytest.fixture() -async def http_client(server, server_url) -> AsyncGenerator[httpx.AsyncClient, None]: - """Create test client""" - async with httpx.AsyncClient(base_url=server_url) as client: - yield client - - @pytest.mark.anyio async def test_redis_integration_basic_connection( server: None, server_url: str @@ -172,106 +176,70 @@ async def test_redis_integration_tool_call(server: None, server_url: str) -> Non @pytest.mark.anyio -async def test_redis_integration_session_lifecycle() -> None: - """Test that sessions are properly added to and - removed from Redis using direct Redis access""" - mock_redis = fake_redis.FakeRedis(decode_responses=True) - active_sessions_key = "mcp:pubsub:active_sessions" - - # Mock Redis in RedisMessageDispatch - with patch( - "mcp.server.message_queue.redis.redis.from_url", return_value=mock_redis - ): - from mcp.server.message_queue.redis import RedisMessageDispatch - - # Create Redis message dispatch with our specific mock redis instance - message_dispatch = RedisMessageDispatch("redis://localhost:6379/0") - - # Create a mock callback - async def mock_callback(message): - pass - - # Test session subscription and unsubscription - from uuid import uuid4 - - session_id = uuid4() - - # Subscribe to a session - async with message_dispatch.subscribe(session_id, mock_callback): - # Give a moment for the session to be added - await anyio.sleep(0.05) - - # Check that session was added to Redis - active_sessions = await mock_redis.smembers(active_sessions_key) - assert len(active_sessions) == 1 - assert list(active_sessions)[0] == session_id.hex +async def test_redis_integration_session_lifecycle( + message_dispatch: RedisMessageDispatch, +) -> None: + # Create a mock callback + async def mock_callback(message): + pass - # Verify session exists - assert await message_dispatch.session_exists(session_id) + # Test session subscription and unsubscription + session_id = uuid4() - # Give a moment for cleanup - await anyio.sleep(0.05) + # Subscribe to a session + async with message_dispatch.subscribe(session_id, mock_callback): + session_key = message_dispatch._session_key(session_id) + assert await message_dispatch._redis.exists(session_key) == 1 # type: ignore + assert await message_dispatch.session_exists(session_id) - # After context exit, verify the session was removed - final_sessions = await mock_redis.smembers(active_sessions_key) - assert len(final_sessions) == 0 - assert not await message_dispatch.session_exists(session_id) + assert await message_dispatch._redis.exists(session_key) == 0 # type: ignore + assert not await message_dispatch.session_exists(session_id) @pytest.mark.anyio -async def test_redis_integration_message_publishing_direct() -> None: +async def test_redis_integration_message_publishing_direct( + message_dispatch: RedisMessageDispatch, +) -> None: """Test message publishing through Redis channels using direct Redis access""" - mock_redis = fake_redis.FakeRedis(decode_responses=True) + from mcp.shared.message import SessionMessage + from mcp.types import JSONRPCMessage, JSONRPCRequest - # Mock Redis in RedisMessageDispatch - with patch( - "mcp.server.message_queue.redis.redis.from_url", return_value=mock_redis - ): - from mcp.server.message_queue.redis import RedisMessageDispatch - from mcp.shared.message import SessionMessage - from mcp.types import JSONRPCMessage, JSONRPCRequest + # Messages received through the callback + messages_received = [] - # Create Redis message dispatch with our specific mock redis instance - message_dispatch = RedisMessageDispatch("redis://localhost:6379/0") - - # Messages received through the callback - messages_received = [] + async def message_callback(message): + messages_received.append(message) - async def message_callback(message): - messages_received.append(message) + # Use a UUID for session ID + from uuid import uuid4 - # Use a UUID for session ID - from uuid import uuid4 + session_id = uuid4() - session_id = uuid4() + # Subscribe to the session + async with message_dispatch.subscribe(session_id, message_callback): + # Give a moment for subscription to be fully set up and start listener task + await anyio.sleep(0.05) - # Subscribe to the session - async with message_dispatch.subscribe(session_id, message_callback): - # Give a moment for subscription to be fully set up and start listener task - await anyio.sleep(0.05) + # Create a test message + test_message = JSONRPCMessage( + root=JSONRPCRequest(jsonrpc="2.0", id=1, method="test_method", params={}) + ) - # Create a test message - test_message = JSONRPCMessage( - root=JSONRPCRequest( - jsonrpc="2.0", id=1, method="test_method", params={} - ) - ) - - # Publish the message - success = await message_dispatch.publish_message( - session_id, SessionMessage(message=test_message) - ) - assert success - - # Give some time for the message to be processed - # Use a shorter sleep since we're in controlled test environment - await anyio.sleep(0.1) - - # Verify that the message was received - assert ( - len(messages_received) > 0 - ), "No messages were received through the callback" - received_message = messages_received[0] - assert isinstance(received_message, SessionMessage) - assert received_message.message.root.method == "test_method" # type: ignore - assert received_message.message.root.id == 1 # type: ignore + # Publish the message + success = await message_dispatch.publish_message( + session_id, SessionMessage(message=test_message) + ) + assert success + + # Give some time for the message to be processed + # Use a shorter sleep since we're in controlled test environment + await anyio.sleep(0.1) + + # Verify that the message was received + assert ( + len(messages_received) > 0 + ), "No messages were received through the callback" + received_message = messages_received[0] + assert isinstance(received_message, SessionMessage) + assert received_message.message.root.method == "test_method" # type: ignore + assert received_message.message.root.id == 1 # type: ignore From 46b78f227fd8030e0ac32e206e5be2e97fc5dcc5 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 5 May 2025 16:16:49 -0700 Subject: [PATCH 45/51] rename to message dispatch --- tests/server/{message_queue => message_dispatch}/__init__.py | 0 tests/server/{message_queue => message_dispatch}/conftest.py | 0 tests/server/{message_queue => message_dispatch}/test_redis.py | 0 .../{message_queue => message_dispatch}/test_redis_integration.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename tests/server/{message_queue => message_dispatch}/__init__.py (100%) rename tests/server/{message_queue => message_dispatch}/conftest.py (100%) rename tests/server/{message_queue => message_dispatch}/test_redis.py (100%) rename tests/server/{message_queue => message_dispatch}/test_redis_integration.py (100%) diff --git a/tests/server/message_queue/__init__.py b/tests/server/message_dispatch/__init__.py similarity index 100% rename from tests/server/message_queue/__init__.py rename to tests/server/message_dispatch/__init__.py diff --git a/tests/server/message_queue/conftest.py b/tests/server/message_dispatch/conftest.py similarity index 100% rename from tests/server/message_queue/conftest.py rename to tests/server/message_dispatch/conftest.py diff --git a/tests/server/message_queue/test_redis.py b/tests/server/message_dispatch/test_redis.py similarity index 100% rename from tests/server/message_queue/test_redis.py rename to tests/server/message_dispatch/test_redis.py diff --git a/tests/server/message_queue/test_redis_integration.py b/tests/server/message_dispatch/test_redis_integration.py similarity index 100% rename from tests/server/message_queue/test_redis_integration.py rename to tests/server/message_dispatch/test_redis_integration.py From e21d514b5bdd8eb12a45fce0ddccd51e77ab86a1 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 5 May 2025 17:57:09 -0700 Subject: [PATCH 46/51] make int tests better --- src/mcp/client/sse.py | 7 +- src/mcp/server/message_queue/base.py | 2 +- src/mcp/server/sse.py | 2 + tests/server/message_dispatch/test_redis.py | 33 +- .../test_redis_integration.py | 404 +++++++++++------- 5 files changed, 260 insertions(+), 188 deletions(-) diff --git a/src/mcp/client/sse.py b/src/mcp/client/sse.py index f0425e7af..1c03fc58c 100644 --- a/src/mcp/client/sse.py +++ b/src/mcp/client/sse.py @@ -98,9 +98,7 @@ async def sse_reader( await read_stream_writer.send(exc) continue - session_message = SessionMessage( - message=message - ) + session_message = SessionMessage(message=message) await read_stream_writer.send(session_message) case _: logger.warning( @@ -150,3 +148,6 @@ async def post_writer(endpoint_url: str): finally: await read_stream_writer.aclose() await write_stream.aclose() + await read_stream.aclose() + await write_stream_reader.aclose() + diff --git a/src/mcp/server/message_queue/base.py b/src/mcp/server/message_queue/base.py index 6b210338f..20c714550 100644 --- a/src/mcp/server/message_queue/base.py +++ b/src/mcp/server/message_queue/base.py @@ -28,7 +28,7 @@ async def publish_message( Args: session_id: The UUID of the session this message is for - message: The message to publish (JSONRPCMessage or str for invalid JSON) + message: The message to publish (SessionMessage or str for invalid JSON) Returns: bool: True if message was published, False if session not found diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index e011fcc03..98f32629e 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -152,6 +152,8 @@ async def response_wrapper(scope: Scope, receive: Receive, send: Send): )(scope, receive, send) await read_stream_writer.aclose() await write_stream_reader.aclose() + await sse_stream_writer.aclose() + await sse_stream_reader.aclose() logging.debug(f"Client session disconnected {session_id}") logger.debug("Starting SSE response task") diff --git a/tests/server/message_dispatch/test_redis.py b/tests/server/message_dispatch/test_redis.py index da4d1c442..46936f259 100644 --- a/tests/server/message_dispatch/test_redis.py +++ b/tests/server/message_dispatch/test_redis.py @@ -10,33 +10,6 @@ from mcp.shared.message import SessionMessage -@pytest.mark.anyio -async def test_session_exists(message_dispatch): - """Test session existence check.""" - session_id = uuid4() - - # Initially session should not exist - assert not await message_dispatch.session_exists(session_id) - - # After subscribing, session should exist - async with message_dispatch.subscribe(session_id, AsyncMock()): - assert await message_dispatch.session_exists(session_id) - - # After unsubscribing, session should not exist - assert not await message_dispatch.session_exists(session_id) - - -@pytest.mark.anyio -async def test_session_ttl(message_dispatch): - """Test that session has proper TTL set.""" - session_id = uuid4() - - async with message_dispatch.subscribe(session_id, AsyncMock()): - session_key = message_dispatch._session_key(session_id) - ttl = await message_dispatch._redis.ttl(session_key) # type: ignore - assert ttl > 0 - assert ttl <= message_dispatch._session_ttl - @pytest.mark.anyio async def test_session_heartbeat(message_dispatch): @@ -129,12 +102,12 @@ async def test_publish_message_invalid_json(message_dispatch): @pytest.mark.anyio -async def test_publish_to_nonexistent_session(message_dispatch): +async def test_publish_to_nonexistent_session(message_dispatch: RedisMessageDispatch): """Test publishing to a session that doesn't exist.""" session_id = uuid4() - message = types.JSONRPCMessage.model_validate( + message = SessionMessage(message=types.JSONRPCMessage.model_validate( {"jsonrpc": "2.0", "method": "test", "params": {}, "id": 1} - ) + )) published = await message_dispatch.publish_message(session_id, message) assert not published diff --git a/tests/server/message_dispatch/test_redis_integration.py b/tests/server/message_dispatch/test_redis_integration.py index 52077c208..73a39389d 100644 --- a/tests/server/message_dispatch/test_redis_integration.py +++ b/tests/server/message_dispatch/test_redis_integration.py @@ -1,16 +1,25 @@ -import multiprocessing +""" +Integration tests for Redis message dispatch functionality. + +These tests validate Redis message dispatch by making actual HTTP calls and testing +that messages flow correctly through the Redis backend. + +This version runs the server in a task instead of a separate process to allow +access to the fakeredis instance for verification of Redis keys. +""" + +import asyncio import socket -import time -from collections.abc import AsyncGenerator, Generator +from collections.abc import AsyncGenerator from contextlib import asynccontextmanager -from unittest.mock import patch -from uuid import uuid4 import anyio import pytest import uvicorn +from sse_starlette.sse import AppStatus from starlette.applications import Starlette from starlette.requests import Request +from starlette.responses import Response from starlette.routing import Mount, Route from mcp.client.session import ClientSession @@ -20,16 +29,15 @@ from mcp.server.sse import SseServerTransport from mcp.types import TextContent, Tool -SERVER_NAME = "test_server_for_redis_integration" - -# Set up fakeredis for testing try: - from fakeredis import aioredis as fake_redis + from fakeredis import aioredis as fake_redis # noqa: F401 except ImportError: pytest.skip( "fakeredis is required for testing Redis functionality", allow_module_level=True ) +SERVER_NAME = "test_server_for_redis_integration_v3" + @pytest.fixture def server_port() -> int: @@ -43,8 +51,9 @@ def server_url(server_port: int) -> str: return f"http://127.0.0.1:{server_port}" -# Test server implementation class RedisTestServer(Server): + """Test server with basic tool functionality.""" + def __init__(self): super().__init__(SERVER_NAME) @@ -55,191 +64,278 @@ async def handle_list_tools() -> list[Tool]: name="test_tool", description="A test tool", inputSchema={"type": "object", "properties": {}}, - ) + ), + Tool( + name="echo_message", + description="Echo a message back", + inputSchema={ + "type": "object", + "properties": {"message": {"type": "string"}}, + "required": ["message"], + }, + ), ] @self.call_tool() async def handle_call_tool(name: str, args: dict) -> list[TextContent]: + if name == "echo_message": + message = args.get("message", "") + return [TextContent(type="text", text=f"Echo: {message}")] return [TextContent(type="text", text=f"Called {name}")] -def make_redis_server_app() -> Starlette: - """Create test Starlette app with SSE transport and Redis message dispatch""" - # Create a mock Redis instance - mock_redis = fake_redis.FakeRedis(decode_responses=True) - - # Patch the redis module within RedisMessageDispatch - with patch("mcp.server.message_queue.redis.redis", mock_redis): - from mcp.server.message_queue.redis import RedisMessageDispatch - - # Create Redis message dispatch with mock redis - message_dispatch = RedisMessageDispatch("redis://localhost:6379/0") - - # Create SSE transport with Redis message dispatch - sse = SseServerTransport("/messages/", message_dispatch=message_dispatch) - server = RedisTestServer() - - async def handle_sse(request: Request) -> None: - async with sse.connect_sse( - request.scope, request.receive, request._send - ) as streams: - await server.run( - streams[0], streams[1], server.create_initialization_options() - ) - - @asynccontextmanager - async def close_redis(app: Starlette) -> AsyncGenerator[None, None]: - try: - yield - finally: - await message_dispatch.close() - await mock_redis.aclose() # type: ignore - - app = Starlette( - routes=[ - Route("/sse", endpoint=handle_sse), - Mount("/messages/", app=sse.handle_post_message), - ], - lifespan=close_redis, - ) - - return app - - -def run_redis_server(server_port: int) -> None: - app = make_redis_server_app() - server = uvicorn.Server( - config=uvicorn.Config( - app=app, host="127.0.0.1", port=server_port, log_level="error" - ) +@pytest.fixture() +async def redis_server_and_app(message_dispatch: RedisMessageDispatch): + """Create a mock Redis instance and Starlette app for testing.""" + + # Create SSE transport with Redis message dispatch + sse = SseServerTransport("/messages/", message_dispatch=message_dispatch) + server = RedisTestServer() + + async def handle_sse(request: Request): + async with sse.connect_sse( + request.scope, request.receive, request._send + ) as streams: + await server.run( + streams[0], streams[1], server.create_initialization_options() + ) + return Response() + + @asynccontextmanager + async def lifespan(app: Starlette) -> AsyncGenerator[None, None]: + """Manage the lifecycle of the application.""" + try: + yield + finally: + await message_dispatch.close() + + app = Starlette( + routes=[ + Route("/sse", endpoint=handle_sse), + Mount("/messages/", app=sse.handle_post_message), + ], + lifespan=lifespan, ) - server.run() - # Give server time to start - while not server.started: - time.sleep(0.5) + # Return the app, message_dispatch, and mock_redis for testing + return app, message_dispatch, message_dispatch._redis @pytest.fixture() -def server(server_port: int) -> Generator[None, None, None]: - proc = multiprocessing.Process( - target=run_redis_server, kwargs={"server_port": server_port}, daemon=True - ) - proc.start() +async def server_and_redis(redis_server_and_app, server_port: int): + """Run the server in a task and return the Redis instance for inspection.""" + app, message_dispatch, mock_redis = redis_server_and_app - # Wait for server to be running - max_attempts = 20 - attempt = 0 - while attempt < max_attempts: + # Create a server config + config = uvicorn.Config( + app=app, host="127.0.0.1", port=server_port, log_level="error" + ) + server = uvicorn.Server(config=config) + AppStatus.should_exit = False + AppStatus.should_exit_event = None + # Run server in a task group + async with anyio.create_task_group() as tg: + # Start server in background + tg.start_soon(server.serve) + + # Wait for server to be ready + max_attempts = 20 + attempt = 0 + while attempt < max_attempts: + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect(("127.0.0.1", server_port)) + break + except ConnectionRefusedError: + await anyio.sleep(0.1) + attempt += 1 + else: + raise RuntimeError(f"Server failed to start after {max_attempts} attempts") + + # Yield Redis for tests try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.connect(("127.0.0.1", server_port)) - break - except ConnectionRefusedError: - time.sleep(0.1) - attempt += 1 - else: - raise RuntimeError(f"Server failed to start after {max_attempts} attempts") - - yield + yield mock_redis, message_dispatch + finally: + server.should_exit = True - # Signal the server to stop - proc.terminate() - proc.join(timeout=2) - if proc.is_alive(): - print("server process failed to terminate") - -@pytest.mark.anyio -async def test_redis_integration_basic_connection( - server: None, server_url: str -) -> None: - """Test that a basic SSE connection works with Redis message dispatch""" +@pytest.fixture() +async def client_session(server_and_redis, server_url: str): + """Create a client session for testing.""" async with sse_client(server_url + "/sse") as streams: async with ClientSession(*streams) as session: - # Test initialization + # Initialize the session result = await session.initialize() assert result.serverInfo.name == SERVER_NAME + yield session @pytest.mark.anyio -async def test_redis_integration_tool_call(server: None, server_url: str) -> None: - """Test that a tool call works with Redis message dispatch""" - async with sse_client(server_url + "/sse") as streams: - async with ClientSession(*streams) as session: - # Initialize session - await session.initialize() +async def test_redis_integration_key_verification( + server_and_redis, client_session +) -> None: + """Test that Redis keys are created correctly for sessions.""" + mock_redis, message_dispatch = server_and_redis + # client_session argument is used by this test (fixture yields session) - # Call a tool - result = await session.call_tool("test_tool", {}) - assert result.content[0].text == "Called test_tool" # type: ignore + # Check that session keys exist in Redis + # The keys should follow pattern: mcp:pubsub:session_active: + all_keys = await mock_redis.keys("*") # type: ignore + # Should have session active key and potentially channel subscription + assert len(all_keys) > 0 -@pytest.mark.anyio -async def test_redis_integration_session_lifecycle( - message_dispatch: RedisMessageDispatch, -) -> None: - # Create a mock callback - async def mock_callback(message): - pass + # Find session active key + session_key = None + for key in all_keys: + if key.startswith("mcp:pubsub:session_active:"): + session_key = key + break + + assert session_key is not None, f"No session key found. Keys: {all_keys}" - # Test session subscription and unsubscription - session_id = uuid4() + # Verify session key has a TTL + ttl = await mock_redis.ttl(session_key) # type: ignore + assert ttl > 0, f"Session key should have TTL, got: {ttl}" - # Subscribe to a session - async with message_dispatch.subscribe(session_id, mock_callback): - session_key = message_dispatch._session_key(session_id) - assert await message_dispatch._redis.exists(session_key) == 1 # type: ignore - assert await message_dispatch.session_exists(session_id) - assert await message_dispatch._redis.exists(session_key) == 0 # type: ignore - assert not await message_dispatch.session_exists(session_id) +# Note: This test doesn't check cleanup since the session is managed by fixture @pytest.mark.anyio -async def test_redis_integration_message_publishing_direct( - message_dispatch: RedisMessageDispatch, +async def test_redis_integration_pubsub_channels( + server_and_redis, client_session ) -> None: - """Test message publishing through Redis channels using direct Redis access""" - from mcp.shared.message import SessionMessage - from mcp.types import JSONRPCMessage, JSONRPCRequest + """Test that Redis pubsub channels are used correctly.""" + mock_redis, message_dispatch = server_and_redis - # Messages received through the callback - messages_received = [] + # Make a tool call to ensure the pubsub channel is active + tool_result = await client_session.call_tool("test_tool", {}) + assert tool_result.content[0].text == "Called test_tool" # type: ignore - async def message_callback(message): - messages_received.append(message) + # Check pubsub channels (fakeredis might not expose all pubsub info) + # But we can verify keys related to pubsub + all_keys = await mock_redis.keys("*") # type: ignore - # Use a UUID for session ID - from uuid import uuid4 + # Should have session keys + assert any(key.startswith("mcp:pubsub:session_active:") for key in all_keys) - session_id = uuid4() - # Subscribe to the session - async with message_dispatch.subscribe(session_id, message_callback): - # Give a moment for subscription to be fully set up and start listener task - await anyio.sleep(0.05) +@pytest.mark.anyio +async def test_redis_integration_message_publishing( + server_and_redis, client_session +) -> None: + """Test that messages are properly published through Redis.""" + mock_redis, message_dispatch = server_and_redis - # Create a test message - test_message = JSONRPCMessage( - root=JSONRPCRequest(jsonrpc="2.0", id=1, method="test_method", params={}) + # Make multiple tool calls to test message publishing + for i in range(3): + tool_result = await client_session.call_tool( + "echo_message", {"message": f"Test {i}"} ) + assert tool_result.content[0].text == f"Echo: Test {i}" # type: ignore - # Publish the message - success = await message_dispatch.publish_message( - session_id, SessionMessage(message=test_message) - ) - assert success + # If we can successfully make these calls, it means the Redis + # pubsub message publishing is working correctly - # Give some time for the message to be processed - # Use a shorter sleep since we're in controlled test environment - await anyio.sleep(0.1) - # Verify that the message was received +@pytest.mark.anyio +async def test_redis_integration_session_cleanup_detailed( + server_and_redis, server_url: str +) -> None: + """Test Redis key cleanup when sessions end.""" + mock_redis, message_dispatch = server_and_redis + + # Create and terminate multiple sessions to verify cleanup + session_keys_seen = set() + + for i in range(3): + async with sse_client(server_url + "/sse") as streams: + async with ClientSession(*streams) as session: + # Initialize session + result = await session.initialize() + assert result.serverInfo.name == SERVER_NAME + + # Check Redis keys + all_keys = await mock_redis.keys("*") # type: ignore + + # Find session active key + for key in all_keys: + if key.startswith("mcp:pubsub:session_active:"): + session_keys_seen.add(key) + # Verify session has value "1" + value = await mock_redis.get(key) # type: ignore + assert value == "1" + + # After each session ends, verify cleanup + await anyio.sleep(0.1) # Give time for cleanup + all_keys = await mock_redis.keys("*") # type: ignore assert ( - len(messages_received) > 0 - ), "No messages were received through the callback" - received_message = messages_received[0] - assert isinstance(received_message, SessionMessage) - assert received_message.message.root.method == "test_method" # type: ignore - assert received_message.message.root.id == 1 # type: ignore + len(all_keys) == 0 + ), f"Session keys should be cleaned up, found: {all_keys}" + + # Verify we saw different session keys for each session + assert len(session_keys_seen) == 3, "Should have seen 3 unique session keys" + + +# Include the other tests from the previous version +@pytest.mark.anyio +async def test_redis_integration_basic_tool_call( + server_and_redis, client_session +) -> None: + """Test that a basic tool call works with Redis message dispatch.""" + mock_redis, message_dispatch = server_and_redis + + # Call a tool + result = await client_session.call_tool("test_tool", {}) + assert result.content[0].text == "Called test_tool" # type: ignore + + +@pytest.mark.anyio +async def test_redis_integration_multiple_clients_with_keys( + server_and_redis, server_url: str +) -> None: + """Test multiple clients and verify Redis key management.""" + mock_redis, message_dispatch = server_and_redis + + # Track session keys for multiple concurrent clients + session_keys = [] + + async def client_task(client_id: int) -> str: + async with sse_client(server_url + "/sse") as streams: + async with ClientSession(*streams) as session: + await session.initialize() + + # Check session keys + all_keys = await mock_redis.keys("*") # type: ignore + for key in all_keys: + if ( + key.startswith("mcp:pubsub:session_active:") + and key not in session_keys + ): + session_keys.append(key) + + result = await session.call_tool( + "echo_message", + {"message": f"Message from client {client_id}"}, + ) + return result.content[0].text # type: ignore + + # Run multiple clients concurrently + client_tasks = [client_task(i) for i in range(3)] + results = await asyncio.gather(*client_tasks) + + # Verify all clients received their respective messages + assert len(results) == 3 + for i, result in enumerate(results): + assert result == f"Echo: Message from client {i}" + + # At peak we should have seen multiple session keys + assert ( + len(session_keys) >= 2 + ), f"Should have seen multiple session keys, got: {session_keys}" + + # After all clients disconnect, keys should be cleaned up + await anyio.sleep(0.1) # Give time for cleanup + all_keys = await mock_redis.keys("*") # type: ignore + assert len(all_keys) == 0, f"Session keys should be cleaned up, found: {all_keys}" From ee9f4deeb0f94b682f2ad05281e575c03f969281 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 5 May 2025 17:59:13 -0700 Subject: [PATCH 47/51] lint --- src/mcp/client/sse.py | 4 +++- tests/server/message_dispatch/test_redis.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mcp/client/sse.py b/src/mcp/client/sse.py index 1c03fc58c..6258a7d15 100644 --- a/src/mcp/client/sse.py +++ b/src/mcp/client/sse.py @@ -98,7 +98,9 @@ async def sse_reader( await read_stream_writer.send(exc) continue - session_message = SessionMessage(message=message) + session_message = SessionMessage( + message=message + ) await read_stream_writer.send(session_message) case _: logger.warning( diff --git a/tests/server/message_dispatch/test_redis.py b/tests/server/message_dispatch/test_redis.py index 46936f259..b1d4dd3a4 100644 --- a/tests/server/message_dispatch/test_redis.py +++ b/tests/server/message_dispatch/test_redis.py @@ -10,7 +10,6 @@ from mcp.shared.message import SessionMessage - @pytest.mark.anyio async def test_session_heartbeat(message_dispatch): """Test that session heartbeat refreshes TTL.""" From 206a98a33562c14d9f2ec26911d3999bab00713c Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Mon, 5 May 2025 18:08:54 -0700 Subject: [PATCH 48/51] tests hanging From bb59e5d2b748c09208e3faa802d1a6425e8efc31 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 6 May 2025 03:17:12 +0000 Subject: [PATCH 49/51] do cleanup after test --- .../test_redis_integration.py | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/tests/server/message_dispatch/test_redis_integration.py b/tests/server/message_dispatch/test_redis_integration.py index 73a39389d..2585d291b 100644 --- a/tests/server/message_dispatch/test_redis_integration.py +++ b/tests/server/message_dispatch/test_redis_integration.py @@ -131,32 +131,36 @@ async def server_and_redis(redis_server_and_app, server_port: int): app=app, host="127.0.0.1", port=server_port, log_level="error" ) server = uvicorn.Server(config=config) - AppStatus.should_exit = False - AppStatus.should_exit_event = None - # Run server in a task group - async with anyio.create_task_group() as tg: - # Start server in background - tg.start_soon(server.serve) - - # Wait for server to be ready - max_attempts = 20 - attempt = 0 - while attempt < max_attempts: + try: + async with anyio.create_task_group() as tg: + # Start server in background + tg.start_soon(server.serve) + + # Wait for server to be ready + max_attempts = 20 + attempt = 0 + while attempt < max_attempts: + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect(("127.0.0.1", server_port)) + break + except ConnectionRefusedError: + await anyio.sleep(0.1) + attempt += 1 + else: + raise RuntimeError(f"Server failed to start after {max_attempts} attempts") + + # Yield Redis for tests try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.connect(("127.0.0.1", server_port)) - break - except ConnectionRefusedError: - await anyio.sleep(0.1) - attempt += 1 - else: - raise RuntimeError(f"Server failed to start after {max_attempts} attempts") - - # Yield Redis for tests - try: - yield mock_redis, message_dispatch - finally: - server.should_exit = True + yield mock_redis, message_dispatch + finally: + server.should_exit = True + finally: + # These class variables are set top-level in starlette-sse + # It isn't designed to be run multiple times in a single + # Python process so we need to manually reset them. + AppStatus.should_exit = False + AppStatus.should_exit_event = None @pytest.fixture() From ca9a54a3f1c9cc907c037022900f4d47566813d2 Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 6 May 2025 03:18:13 +0000 Subject: [PATCH 50/51] fmt --- src/mcp/client/sse.py | 1 - tests/server/message_dispatch/test_redis.py | 8 +++++--- tests/server/message_dispatch/test_redis_integration.py | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/mcp/client/sse.py b/src/mcp/client/sse.py index 6258a7d15..7df251f79 100644 --- a/src/mcp/client/sse.py +++ b/src/mcp/client/sse.py @@ -152,4 +152,3 @@ async def post_writer(endpoint_url: str): await write_stream.aclose() await read_stream.aclose() await write_stream_reader.aclose() - diff --git a/tests/server/message_dispatch/test_redis.py b/tests/server/message_dispatch/test_redis.py index b1d4dd3a4..d355f9e68 100644 --- a/tests/server/message_dispatch/test_redis.py +++ b/tests/server/message_dispatch/test_redis.py @@ -104,9 +104,11 @@ async def test_publish_message_invalid_json(message_dispatch): async def test_publish_to_nonexistent_session(message_dispatch: RedisMessageDispatch): """Test publishing to a session that doesn't exist.""" session_id = uuid4() - message = SessionMessage(message=types.JSONRPCMessage.model_validate( - {"jsonrpc": "2.0", "method": "test", "params": {}, "id": 1} - )) + message = SessionMessage( + message=types.JSONRPCMessage.model_validate( + {"jsonrpc": "2.0", "method": "test", "params": {}, "id": 1} + ) + ) published = await message_dispatch.publish_message(session_id, message) assert not published diff --git a/tests/server/message_dispatch/test_redis_integration.py b/tests/server/message_dispatch/test_redis_integration.py index 2585d291b..2eb21b1cf 100644 --- a/tests/server/message_dispatch/test_redis_integration.py +++ b/tests/server/message_dispatch/test_redis_integration.py @@ -148,7 +148,9 @@ async def server_and_redis(redis_server_and_app, server_port: int): await anyio.sleep(0.1) attempt += 1 else: - raise RuntimeError(f"Server failed to start after {max_attempts} attempts") + raise RuntimeError( + f"Server failed to start after {max_attempts} attempts" + ) # Yield Redis for tests try: From 9832c3471d05cc899502c4aca968205debe6469f Mon Sep 17 00:00:00 2001 From: Akash DSouza Date: Tue, 6 May 2025 12:52:08 -0700 Subject: [PATCH 51/51] clean up int test --- .../test_redis_integration.py | 103 ++---------------- 1 file changed, 8 insertions(+), 95 deletions(-) diff --git a/tests/server/message_dispatch/test_redis_integration.py b/tests/server/message_dispatch/test_redis_integration.py index 2eb21b1cf..f01113872 100644 --- a/tests/server/message_dispatch/test_redis_integration.py +++ b/tests/server/message_dispatch/test_redis_integration.py @@ -29,13 +29,6 @@ from mcp.server.sse import SseServerTransport from mcp.types import TextContent, Tool -try: - from fakeredis import aioredis as fake_redis # noqa: F401 -except ImportError: - pytest.skip( - "fakeredis is required for testing Redis functionality", allow_module_level=True - ) - SERVER_NAME = "test_server_for_redis_integration_v3" @@ -117,7 +110,6 @@ async def lifespan(app: Starlette) -> AsyncGenerator[None, None]: lifespan=lifespan, ) - # Return the app, message_dispatch, and mock_redis for testing return app, message_dispatch, message_dispatch._redis @@ -152,7 +144,6 @@ async def server_and_redis(redis_server_and_app, server_port: int): f"Server failed to start after {max_attempts} attempts" ) - # Yield Redis for tests try: yield mock_redis, message_dispatch finally: @@ -170,7 +161,6 @@ async def client_session(server_and_redis, server_url: str): """Create a client session for testing.""" async with sse_client(server_url + "/sse") as streams: async with ClientSession(*streams) as session: - # Initialize the session result = await session.initialize() assert result.serverInfo.name == SERVER_NAME yield session @@ -181,17 +171,12 @@ async def test_redis_integration_key_verification( server_and_redis, client_session ) -> None: """Test that Redis keys are created correctly for sessions.""" - mock_redis, message_dispatch = server_and_redis - # client_session argument is used by this test (fixture yields session) + mock_redis, _ = server_and_redis - # Check that session keys exist in Redis - # The keys should follow pattern: mcp:pubsub:session_active: all_keys = await mock_redis.keys("*") # type: ignore - # Should have session active key and potentially channel subscription assert len(all_keys) > 0 - # Find session active key session_key = None for key in all_keys: if key.startswith("mcp:pubsub:session_active:"): @@ -200,80 +185,40 @@ async def test_redis_integration_key_verification( assert session_key is not None, f"No session key found. Keys: {all_keys}" - # Verify session key has a TTL ttl = await mock_redis.ttl(session_key) # type: ignore assert ttl > 0, f"Session key should have TTL, got: {ttl}" -# Note: This test doesn't check cleanup since the session is managed by fixture - - -@pytest.mark.anyio -async def test_redis_integration_pubsub_channels( - server_and_redis, client_session -) -> None: - """Test that Redis pubsub channels are used correctly.""" - mock_redis, message_dispatch = server_and_redis - - # Make a tool call to ensure the pubsub channel is active - tool_result = await client_session.call_tool("test_tool", {}) - assert tool_result.content[0].text == "Called test_tool" # type: ignore - - # Check pubsub channels (fakeredis might not expose all pubsub info) - # But we can verify keys related to pubsub - all_keys = await mock_redis.keys("*") # type: ignore - - # Should have session keys - assert any(key.startswith("mcp:pubsub:session_active:") for key in all_keys) - - @pytest.mark.anyio -async def test_redis_integration_message_publishing( - server_and_redis, client_session -) -> None: +async def test_tool_calls(server_and_redis, client_session) -> None: """Test that messages are properly published through Redis.""" - mock_redis, message_dispatch = server_and_redis + mock_redis, _ = server_and_redis - # Make multiple tool calls to test message publishing for i in range(3): tool_result = await client_session.call_tool( "echo_message", {"message": f"Test {i}"} ) assert tool_result.content[0].text == f"Echo: Test {i}" # type: ignore - # If we can successfully make these calls, it means the Redis - # pubsub message publishing is working correctly - @pytest.mark.anyio -async def test_redis_integration_session_cleanup_detailed( - server_and_redis, server_url: str -) -> None: +async def test_session_cleanup(server_and_redis, server_url: str) -> None: """Test Redis key cleanup when sessions end.""" - mock_redis, message_dispatch = server_and_redis - - # Create and terminate multiple sessions to verify cleanup + mock_redis, _ = server_and_redis session_keys_seen = set() for i in range(3): async with sse_client(server_url + "/sse") as streams: async with ClientSession(*streams) as session: - # Initialize session - result = await session.initialize() - assert result.serverInfo.name == SERVER_NAME + await session.initialize() - # Check Redis keys all_keys = await mock_redis.keys("*") # type: ignore - - # Find session active key for key in all_keys: if key.startswith("mcp:pubsub:session_active:"): session_keys_seen.add(key) - # Verify session has value "1" value = await mock_redis.get(key) # type: ignore assert value == "1" - # After each session ends, verify cleanup await anyio.sleep(0.1) # Give time for cleanup all_keys = await mock_redis.keys("*") # type: ignore assert ( @@ -284,43 +229,16 @@ async def test_redis_integration_session_cleanup_detailed( assert len(session_keys_seen) == 3, "Should have seen 3 unique session keys" -# Include the other tests from the previous version -@pytest.mark.anyio -async def test_redis_integration_basic_tool_call( - server_and_redis, client_session -) -> None: - """Test that a basic tool call works with Redis message dispatch.""" - mock_redis, message_dispatch = server_and_redis - - # Call a tool - result = await client_session.call_tool("test_tool", {}) - assert result.content[0].text == "Called test_tool" # type: ignore - - @pytest.mark.anyio -async def test_redis_integration_multiple_clients_with_keys( - server_and_redis, server_url: str -) -> None: +async def concurrent_tool_call(server_and_redis, server_url: str) -> None: """Test multiple clients and verify Redis key management.""" - mock_redis, message_dispatch = server_and_redis - - # Track session keys for multiple concurrent clients - session_keys = [] + mock_redis, _ = server_and_redis async def client_task(client_id: int) -> str: async with sse_client(server_url + "/sse") as streams: async with ClientSession(*streams) as session: await session.initialize() - # Check session keys - all_keys = await mock_redis.keys("*") # type: ignore - for key in all_keys: - if ( - key.startswith("mcp:pubsub:session_active:") - and key not in session_keys - ): - session_keys.append(key) - result = await session.call_tool( "echo_message", {"message": f"Message from client {client_id}"}, @@ -336,11 +254,6 @@ async def client_task(client_id: int) -> str: for i, result in enumerate(results): assert result == f"Echo: Message from client {i}" - # At peak we should have seen multiple session keys - assert ( - len(session_keys) >= 2 - ), f"Should have seen multiple session keys, got: {session_keys}" - # After all clients disconnect, keys should be cleaned up await anyio.sleep(0.1) # Give time for cleanup all_keys = await mock_redis.keys("*") # type: ignore