From 3ca27e47d57e35200316cbf18a07fca1bbdd0b68 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 12:50:33 +0200 Subject: [PATCH 01/28] Fix tests to correctly use gitignore for LS setup --- .../test_retrieval_with_ignored_dirs.py | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index 5150d53a..5bbb3721 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -6,23 +6,43 @@ import pytest from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger +from serena.util.file_system import GitignoreParser from test.conftest import get_repo_path # This mark will be applied to all tests in this module pytestmark = pytest.mark.python +def create_ls_with_ignored_dirs( + ignored_paths: list[str] | None = None, + repo_path: Path | None = None, + language: Language = Language.PYTHON, +) -> SyncLanguageServer: + """Helper function to create a SyncLanguageServer with ignored directories.""" + if ignored_paths is None: + ignored_paths = [] + + if repo_path is None: + repo_path = get_repo_path(language) + + gitignore_parser = GitignoreParser(str(repo_path)) + for spec in gitignore_parser.get_ignore_specs(): + ignored_paths.extend(spec.patterns) + + config = MultilspyConfig( + code_language=language, + trace_lsp_communication=False, + ignored_paths=ignored_paths, # Configure the relative path to be ignored + ) + logger = MultilspyLogger() + return SyncLanguageServer.create(config, logger, str(repo_path)) + + @pytest.fixture(scope="module") def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" - config = MultilspyConfig( - code_language=Language.PYTHON, - trace_lsp_communication=False, - ignored_paths=["scripts", "custom_test"], # Configure the relative path to be ignored - ) - logger = MultilspyLogger() - repo_path = get_repo_path(Language.PYTHON) - ls = SyncLanguageServer.create(config, logger, str(repo_path)) + ignored_paths = ["scripts", "custom_test_repo"] + ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, language=Language.PYTHON) ls.start() try: yield ls @@ -56,13 +76,8 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): @pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True) def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: """Tests that refs and symbols with glob patterns are ignored.""" - config = MultilspyConfig( - code_language=Language.PYTHON, - trace_lsp_communication=False, - ignored_paths=["*ipts", "custom_t*"], - ) - logger = MultilspyLogger() - ls = SyncLanguageServer.create(config, logger, str(repo_path)) + ignored_paths = ["*ipts", "custom_t*"] + ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, repo_path=repo_path, language=Language.PYTHON) ls.start() # same as in the above tests root = ls.request_full_symbol_tree()[0] From 47a133a7c0dffd2e8fef1097ccb7a18bea1c60cb Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:23:04 +0200 Subject: [PATCH 02/28] Typo --- test/multilspy/python/test_retrieval_with_ignored_dirs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index 5bbb3721..70f0e37d 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -41,7 +41,7 @@ def create_ls_with_ignored_dirs( @pytest.fixture(scope="module") def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" - ignored_paths = ["scripts", "custom_test_repo"] + ignored_paths = ["scripts", "custom_test"] ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, language=Language.PYTHON) ls.start() try: From 40e9bb87c1aa0379989df6f0fd142282e7c7a4e1 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:55:31 +0200 Subject: [PATCH 03/28] Tests: use gitignore parser in LS setup, remove duplication --- test/conftest.py | 11 ++++-- .../test_retrieval_with_ignored_dirs.py | 35 +++---------------- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index 92b34302..c0bbe0a4 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -7,6 +7,7 @@ from sensai.util.logging import LOG_DEFAULT_FORMAT, configure from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig from multilspy.multilspy_logger import MultilspyLogger +from serena.util.file_system import GitignoreParser configure(level=logging.DEBUG) @@ -26,8 +27,14 @@ def get_repo_path(language: Language) -> Path: return Path(__file__).parent / "resources" / "repos" / language / "test_repo" -def create_ls(language: Language, repo_path: str): - config = MultilspyConfig(code_language=language) +def create_ls(language: Language, repo_path: str | None = None, ignored_paths: list[str] | None = None) -> SyncLanguageServer: + ignored_paths = ignored_paths or [] + if repo_path is None: + repo_path = str(get_repo_path(language)) + gitignore_parser = GitignoreParser(str(repo_path)) + for spec in gitignore_parser.get_ignore_specs(): + ignored_paths.extend(spec.patterns) + config = MultilspyConfig(code_language=language, ignored_paths=ignored_paths) logger = MultilspyLogger(log_level=logging.DEBUG) # Simple way: Use basicConfig to set format for all handlers diff --git a/test/multilspy/python/test_retrieval_with_ignored_dirs.py b/test/multilspy/python/test_retrieval_with_ignored_dirs.py index 70f0e37d..0db668ef 100644 --- a/test/multilspy/python/test_retrieval_with_ignored_dirs.py +++ b/test/multilspy/python/test_retrieval_with_ignored_dirs.py @@ -4,45 +4,18 @@ from pathlib import Path import pytest from multilspy.language_server import SyncLanguageServer -from multilspy.multilspy_config import Language, MultilspyConfig -from multilspy.multilspy_logger import MultilspyLogger -from serena.util.file_system import GitignoreParser -from test.conftest import get_repo_path +from multilspy.multilspy_config import Language +from test.conftest import create_ls # This mark will be applied to all tests in this module pytestmark = pytest.mark.python -def create_ls_with_ignored_dirs( - ignored_paths: list[str] | None = None, - repo_path: Path | None = None, - language: Language = Language.PYTHON, -) -> SyncLanguageServer: - """Helper function to create a SyncLanguageServer with ignored directories.""" - if ignored_paths is None: - ignored_paths = [] - - if repo_path is None: - repo_path = get_repo_path(language) - - gitignore_parser = GitignoreParser(str(repo_path)) - for spec in gitignore_parser.get_ignore_specs(): - ignored_paths.extend(spec.patterns) - - config = MultilspyConfig( - code_language=language, - trace_lsp_communication=False, - ignored_paths=ignored_paths, # Configure the relative path to be ignored - ) - logger = MultilspyLogger() - return SyncLanguageServer.create(config, logger, str(repo_path)) - - @pytest.fixture(scope="module") def ls_with_ignored_dirs() -> Generator[SyncLanguageServer, None, None]: """Fixture to set up an LS for the python test repo with the 'scripts' directory ignored.""" ignored_paths = ["scripts", "custom_test"] - ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, language=Language.PYTHON) + ls = create_ls(ignored_paths=ignored_paths, language=Language.PYTHON) ls.start() try: yield ls @@ -77,7 +50,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SyncLanguageServer): def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None: """Tests that refs and symbols with glob patterns are ignored.""" ignored_paths = ["*ipts", "custom_t*"] - ls = create_ls_with_ignored_dirs(ignored_paths=ignored_paths, repo_path=repo_path, language=Language.PYTHON) + ls = create_ls(ignored_paths=ignored_paths, repo_path=str(repo_path), language=Language.PYTHON) ls.start() # same as in the above tests root = ls.request_full_symbol_tree()[0] From 71ab18fddfa28b8552b3b34f1b85718c12b44884 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:56:19 +0200 Subject: [PATCH 04/28] Agent: force log format through basicConfig Needed to make multilspy logs respect our log format --- src/serena/agent.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 2093365d..1d8acd4c 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -28,7 +28,7 @@ from overrides import override from pathspec import PathSpec from ruamel.yaml.comments import CommentedMap from sensai.util import logging -from sensai.util.logging import FallbackHandler +from sensai.util.logging import LOG_DEFAULT_FORMAT, FallbackHandler from sensai.util.string import ToStringMixin, dict_string from multilspy import SyncLanguageServer @@ -543,6 +543,8 @@ class SerenaAgent: if Logger.root.level > serena_log_level: log.info(f"Changing the root logger level to {serena_log_level}") Logger.root.setLevel(serena_log_level) + # Override existing configuration (needed to configure multilspy logger as desired) + logging.basicConfig(format=LOG_DEFAULT_FORMAT, level=serena_log_level, force=True) # open GUI log window if enabled self._gui_log_handler: Union["GuiLogViewerHandler", None] = None # noqa From b1e14d82678e5ed7c997c4b461bfb3394344f051 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 13:58:12 +0200 Subject: [PATCH 05/28] Force-added ingored file in test resources Needed for tests involving ignored patterns --- .../ignored_module.py | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py diff --git a/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py b/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py new file mode 100644 index 00000000..1e6da936 --- /dev/null +++ b/test/resources/repos/python/test_repo/ignore_this_dir_with_postfix/ignored_module.py @@ -0,0 +1,141 @@ +""" +Example demonstrating user management with the test_repo module. + +This example showcases: +- Creating and managing users +- Using various object types and relationships +- Type annotations and complex Python patterns +""" + +import logging +from dataclasses import dataclass +from typing import Any + +from test_repo.models import User, create_user_object +from test_repo.services import UserService + +# Set up logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@dataclass +class UserStats: + """Statistics about user activity.""" + + user_id: str + login_count: int = 0 + last_active_days: int = 0 + engagement_score: float = 0.0 + + def is_active(self) -> bool: + """Check if the user is considered active.""" + return self.last_active_days < 30 + + +class UserManager: + """Example class demonstrating complex user management.""" + + def __init__(self, service: UserService): + self.service = service + self.active_users: dict[str, User] = {} + self.user_stats: dict[str, UserStats] = {} + + def register_user(self, name: str, email: str, roles: list[str] | None = None) -> User: + """Register a new user.""" + logger.info(f"Registering new user: {name} ({email})") + user = self.service.create_user(name=name, email=email, roles=roles) + self.active_users[user.id] = user + self.user_stats[user.id] = UserStats(user_id=user.id) + return user + + def get_user(self, user_id: str) -> User | None: + """Get a user by ID.""" + if user_id in self.active_users: + return self.active_users[user_id] + + # Try to fetch from service + user = self.service.get_user(user_id) + if user: + self.active_users[user.id] = user + return user + + def update_user_stats(self, user_id: str, login_count: int, days_since_active: int) -> None: + """Update statistics for a user.""" + if user_id not in self.user_stats: + self.user_stats[user_id] = UserStats(user_id=user_id) + + stats = self.user_stats[user_id] + stats.login_count = login_count + stats.last_active_days = days_since_active + + # Calculate engagement score based on activity + engagement = (100 - min(days_since_active, 100)) * 0.8 + engagement += min(login_count, 20) * 0.2 + stats.engagement_score = engagement + + def get_active_users(self) -> list[User]: + """Get all active users.""" + active_user_ids = [user_id for user_id, stats in self.user_stats.items() if stats.is_active()] + return [self.active_users[user_id] for user_id in active_user_ids if user_id in self.active_users] + + def get_user_by_email(self, email: str) -> User | None: + """Find a user by their email address.""" + for user in self.active_users.values(): + if user.email == email: + return user + return None + + +# Example function demonstrating type annotations +def process_user_data(users: list[User], include_inactive: bool = False, transform_func: callable | None = None) -> dict[str, Any]: + """Process user data with optional transformations.""" + result: dict[str, Any] = {"users": [], "total": 0, "admin_count": 0} + + for user in users: + if transform_func: + user_data = transform_func(user.to_dict()) + else: + user_data = user.to_dict() + + result["users"].append(user_data) + result["total"] += 1 + + if "admin" in user.roles: + result["admin_count"] += 1 + + return result + + +def main(): + """Main function demonstrating the usage of UserManager.""" + # Initialize service and manager + service = UserService() + manager = UserManager(service) + + # Register some users + admin = manager.register_user("Admin User", "admin@example.com", ["admin"]) + user1 = manager.register_user("Regular User", "user@example.com", ["user"]) + user2 = manager.register_user("Another User", "another@example.com", ["user"]) + + # Update some stats + manager.update_user_stats(admin.id, 100, 5) + manager.update_user_stats(user1.id, 50, 10) + manager.update_user_stats(user2.id, 10, 45) # Inactive user + + # Get active users + active_users = manager.get_active_users() + logger.info(f"Active users: {len(active_users)}") + + # Process user data + user_data = process_user_data(active_users, transform_func=lambda u: {**u, "full_name": u.get("name", "")}) + + logger.info(f"Processed {user_data['total']} users, {user_data['admin_count']} admins") + + # Example of calling create_user directly + external_user = create_user_object(id="ext123", name="External User", email="external@example.org", roles=["external"]) + logger.info(f"Created external user: {external_user.name}") + + +if __name__ == "__main__": + main() From 386a8df54e7ac48a5eaa3ca1502115c3dd97909d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 17:04:28 +0200 Subject: [PATCH 06/28] Timeout fixup --- src/serena/agent.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/serena/agent.py b/src/serena/agent.py index 1d8acd4c..864efed0 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -9,6 +9,8 @@ import platform import re import shutil import sys +import threading +from time import time import traceback import webbrowser from abc import ABC, abstractmethod @@ -846,6 +848,15 @@ class SerenaAgent: """ Starts/resets the language server for the current project """ + + tool_timeout = self.serena_config.tool_timeout + if tool_timeout is None or tool_timeout < 0: + ls_timeout = None + else: + if tool_timeout < 10: + raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds") + ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout + # stop the language server if it is running if self.is_language_server_running(): assert self.language_server is not None @@ -878,6 +889,7 @@ class SerenaAgent: multilspy_config, ls_logger, self._active_project.project_root, + timeout=ls_timeout, ) self.language_server.start() if not self.language_server.is_running(): From 8c465b53f73d608c8ae98afd584b2d778104582f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 20:39:49 +0200 Subject: [PATCH 07/28] LS: minor deduplication, logging --- src/multilspy/language_server.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 00374aa4..ffc1613f 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -317,7 +317,7 @@ class LanguageServer: return False - async def _shutdown(self, timeout: float = 10.0): + async def _shutdown(self, timeout: float = 5.0): """ A robust shutdown process designed to terminate cleanly on all platforms, including Windows, by explicitly closing all I/O pipes. @@ -946,7 +946,7 @@ class LanguageServer: self._document_symbols_cache[cache_key] = (file_data.content_hash, result) self._cache_has_changed = True return result - + async def request_full_symbol_tree(self, within_relative_path: str | None = None, include_body: bool = False) -> List[multilspy_types.UnifiedSymbolInformation]: """ Will go through all files in the project or within a relative path and build a tree of symbols. @@ -1750,12 +1750,9 @@ class SyncLanguageServer: :return: None """ - self.loop = asyncio.new_event_loop() - self.loop_thread = threading.Thread(target=self.loop.run_forever, daemon=True) - self.loop_thread.start() - ctx = self.language_server.start_server() - asyncio.run_coroutine_threadsafe(ctx.__aenter__(), loop=self.loop).result() + self.start() yield self + self.language_server.logger.log("SyncLS Startup: exiting LS context", logging.DEBUG) self.stop() def request_definition(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]: From 5c530fa642ecdd6d2734089f4353bdbb64a52564 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 20:40:15 +0200 Subject: [PATCH 08/28] Multilspy logger: default stacklevel to 2 --- src/multilspy/multilspy_logger.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/multilspy/multilspy_logger.py b/src/multilspy/multilspy_logger.py index e604e1a7..495c8a70 100644 --- a/src/multilspy/multilspy_logger.py +++ b/src/multilspy/multilspy_logger.py @@ -30,7 +30,7 @@ class MultilspyLogger: self.logger.setLevel(log_level) self.json_format = json_format - def log(self, debug_message: str, level: int, sanitized_error_message: str = "") -> None: + def log(self, debug_message: str, level: int, sanitized_error_message: str = "", stacklevel: int = 2) -> None: """ Log the debug and santized messages using the logger """ @@ -59,6 +59,7 @@ class MultilspyLogger: self.logger.log( level=level, msg=debug_log_line.json(), + stacklevel=stacklevel, ) else: - self.logger.log(level, debug_message) + self.logger.log(level, debug_message, stacklevel=stacklevel) From 2fba67f2310aa3d41bae8569175ae8540bdd2eab Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 20:41:52 +0200 Subject: [PATCH 09/28] LS: hacky solution for waiting till pyright and ts servers are ready --- .../pyright_language_server/pyright_server.py | 64 ++++++++++++++++--- .../typescript_language_server.py | 22 +++++-- 2 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/multilspy/language_servers/pyright_language_server/pyright_server.py b/src/multilspy/language_servers/pyright_language_server/pyright_server.py index 2a8ee1ba..48855831 100644 --- a/src/multilspy/language_servers/pyright_language_server/pyright_server.py +++ b/src/multilspy/language_servers/pyright_language_server/pyright_server.py @@ -2,10 +2,12 @@ Provides Python specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Python. """ +import asyncio import json import logging import os import pathlib +import re from contextlib import asynccontextmanager from typing import AsyncIterator, Tuple @@ -38,6 +40,10 @@ class PyrightServer(LanguageServer): "python", ) + # Event to signal when initial workspace analysis is complete + self.analysis_complete = asyncio.Event() + self.found_source_files = False + @override def is_ignored_dirname(self, dirname: str) -> bool: return super().is_ignored_dirname(dirname) or dirname in ["venv", "__pycache__"] @@ -145,16 +151,19 @@ class PyrightServer(LanguageServer): @asynccontextmanager async def start_server(self) -> AsyncIterator["PyrightServer"]: """ - Starts the Pyright Language Server, waits for the server to be ready and yields the LanguageServer instance. + Starts the Pyright Language Server and waits for initial workspace analysis to complete. + + This prevents zombie processes by ensuring Pyright has finished its initial background + tasks before we consider the server ready. Usage: ``` async with lsp.start_server(): - # LanguageServer has been initialized and ready to serve requests + # LanguageServer has been initialized and workspace analysis is complete await lsp.request_definition(...) await lsp.request_references(...) # Shutdown the LanguageServer on exit from scope - # LanguageServer has been shutdown + # LanguageServer has been shutdown cleanly ``` """ @@ -164,13 +173,33 @@ class PyrightServer(LanguageServer): async def do_nothing(params): return - async def check_experimental_status(params): - if params["quiescent"] == True: + async def window_log_message(msg): + """ + Monitor Pyright's log messages to detect when initial analysis is complete. + Pyright logs "Found X source files" when it finishes scanning the workspace. + """ + message_text = msg.get("message", "") + self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO) + + # Look for "Found X source files" which indicates workspace scanning is complete + # Unfortunately, pyright is unreliable and there seems to be no better way + if re.search(r"Found \d+ source files?", message_text): + self.logger.log("Pyright workspace scanning complete", logging.INFO) + self.found_source_files = True + self.analysis_complete.set() self.completions_available.set() - async def window_log_message(msg): - self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + async def check_experimental_status(params): + """ + Also listen for experimental/serverStatus as a backup signal + """ + if params.get("quiescent") == True: + self.logger.log("Received experimental/serverStatus with quiescent=true", logging.INFO) + if not self.found_source_files: + self.analysis_complete.set() + self.completions_available.set() + # Set up notification handlers self.server.on_request("client/registerCapability", do_nothing) self.server.on_notification("language/status", do_nothing) self.server.on_notification("window/logMessage", window_log_message) @@ -195,6 +224,23 @@ class PyrightServer(LanguageServer): self.logger.log(f"Received initialize response from pyright server: {init_response}", logging.INFO) # Verify that the server supports our required features - self.server.notify.initialized({}) + assert "textDocumentSync" in init_response["capabilities"] + assert "completionProvider" in init_response["capabilities"] + assert "definitionProvider" in init_response["capabilities"] - yield self + # Complete the initialization handshake + self.server.notify.initialized({}) + + # Wait for Pyright to complete its initial workspace analysis + # This prevents zombie processes by ensuring background tasks finish + self.logger.log("Waiting for Pyright to complete initial workspace analysis...", logging.INFO) + try: + await asyncio.wait_for(self.analysis_complete.wait(), timeout=1.0) + self.logger.log("Pyright initial analysis complete, server ready", logging.INFO) + except asyncio.TimeoutError: + self.logger.log("Timeout waiting for Pyright analysis completion, proceeding anyway", logging.WARNING) + # Fallback: assume analysis is complete after timeout + self.analysis_complete.set() + self.completions_available.set() + + yield self \ No newline at end of file diff --git a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py index 122de61f..479936c9 100644 --- a/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py +++ b/src/multilspy/language_servers/typescript_language_server/typescript_language_server.py @@ -186,12 +186,22 @@ class TypeScriptLanguageServer(LanguageServer): async def window_log_message(msg): self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + + async def check_experimental_status(params): + """ + Also listen for experimental/serverStatus as a backup signal + """ + if params.get("quiescent") == True: + self.server_ready.set() + self.completions_available.set() self.server.on_request("client/registerCapability", register_capability_handler) self.server.on_notification("window/logMessage", window_log_message) self.server.on_request("workspace/executeClientCommand", execute_client_command_handler) self.server.on_notification("$/progress", do_nothing) self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + self.server.on_notification("experimental/serverStatus", check_experimental_status) async with super().start_server(): self.logger.log("Starting TypeScript server process", logging.INFO) @@ -213,11 +223,13 @@ class TypeScriptLanguageServer(LanguageServer): } self.server.notify.initialized({}) - self.completions_available.set() - - # TypeScript server is typically ready immediately after initialization - self.server_ready.set() - await self.server_ready.wait() + try: + await asyncio.wait_for(self.server_ready.wait(), timeout=1.0) + except asyncio.TimeoutError: + self.logger.log("Timeout waiting for TypeScript server to become ready, proceeding anyway", logging.INFO) + # Fallback: assume server is ready after timeout + self.server_ready.set() + self.completions_available.set() yield self From 3f5b4da532c2363ba1d59ba0079e07f4cf95f6e7 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 20:43:10 +0200 Subject: [PATCH 10/28] More config options for logging in tests, formating --- src/serena/agent.py | 2 +- test/conftest.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 864efed0..ded70f78 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -10,7 +10,6 @@ import re import shutil import sys import threading -from time import time import traceback import webbrowser from abc import ABC, abstractmethod @@ -22,6 +21,7 @@ from fnmatch import fnmatch from functools import cached_property from logging import Logger from pathlib import Path +from time import time from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast diff --git a/test/conftest.py b/test/conftest.py index c0bbe0a4..994034e2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -2,7 +2,7 @@ import logging from pathlib import Path import pytest -from sensai.util.logging import LOG_DEFAULT_FORMAT, configure +from sensai.util.logging import configure from multilspy.language_server import SyncLanguageServer from multilspy.multilspy_config import Language, MultilspyConfig @@ -27,18 +27,21 @@ def get_repo_path(language: Language) -> Path: return Path(__file__).parent / "resources" / "repos" / language / "test_repo" -def create_ls(language: Language, repo_path: str | None = None, ignored_paths: list[str] | None = None) -> SyncLanguageServer: +def create_ls( + language: Language, + repo_path: str | None = None, + ignored_paths: list[str] | None = None, + trace_lsp_communication: bool = False, + log_level: int = logging.INFO, +) -> SyncLanguageServer: ignored_paths = ignored_paths or [] if repo_path is None: repo_path = str(get_repo_path(language)) gitignore_parser = GitignoreParser(str(repo_path)) for spec in gitignore_parser.get_ignore_specs(): ignored_paths.extend(spec.patterns) - config = MultilspyConfig(code_language=language, ignored_paths=ignored_paths) - logger = MultilspyLogger(log_level=logging.DEBUG) - - # Simple way: Use basicConfig to set format for all handlers - logging.basicConfig(format=LOG_DEFAULT_FORMAT, level=logging.DEBUG, force=True) # Override existing configuration + config = MultilspyConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication) + logger = MultilspyLogger(log_level=log_level) return SyncLanguageServer.create(config, logger, repo_path) From cd10d2f056edf6ba49756f9bc1d90fdb5ade0d94 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 13 Jun 2025 23:33:15 +0200 Subject: [PATCH 11/28] Minor --- src/multilspy/language_server.py | 2 -- src/serena/agent.py | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index ffc1613f..c7f921d6 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -2252,8 +2252,6 @@ class SyncLanguageServer: shutdown_type = "Nuclear" if is_windows else "Graceful" self.language_server.logger.log(f"{shutdown_type} shutdown complete - all references cleared", logging.INFO) - - def save_cache(self): """ Save the cache to a file. diff --git a/src/serena/agent.py b/src/serena/agent.py index ded70f78..0584ec47 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -522,7 +522,7 @@ class SerenaAgent: :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. If not specified, will take the value from the serena configuration. - :param gui_log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. + :param log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. """ # obtain serena configuration @@ -804,6 +804,8 @@ class SerenaAgent: :return: a string overview of the current configuration, including the active and available configuration options """ result_str = "Current configuration:\n" + result_str += f"Serena version: {serena_version()}\n" + result_str += f"Loglevel: {self.serena_config.log_level}, trace_lsp_communication={self.serena_config.trace_lsp_communication}\n" if self._active_project is not None: result_str += f"Active project: {self._active_project.project_name}\n" else: From f3901dfec24c97a27ef38693f792d2b1d9689ba6 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 14 Jun 2025 00:01:10 +0200 Subject: [PATCH 12/28] Fix: trace_lsp_communication in yaml was ignored --- src/serena/agent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/serena/agent.py b/src/serena/agent.py index 0584ec47..db300c5d 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -377,6 +377,7 @@ class SerenaConfig(SerenaConfigBase): instance.log_level = loaded_commented_yaml.get("log_level", loaded_commented_yaml.get("gui_log_level", logging.INFO)) instance.web_dashboard = loaded_commented_yaml.get("web_dashboard", True) instance.tool_timeout = loaded_commented_yaml.get("tool_timeout", DEFAULT_TOOL_TIMEOUT) + instance.trace_lsp_communication = loaded_commented_yaml.get("trace_lsp_communication", False) # re-save the configuration file if any migrations were performed if num_project_migrations > 0: From 4514d04c90631dd5496fdcd0daf7299065732d55 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 14 Jun 2025 00:04:54 +0200 Subject: [PATCH 13/28] Minor (docstrings, validation) --- src/multilspy/language_server.py | 33 ++++++++++++------ src/serena/agent.py | 58 +++++++++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index c7f921d6..5bd0ae5e 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -1760,12 +1760,15 @@ class SyncLanguageServer: Raise a [textDocument/definition](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition) request to the Language Server for the symbol at the given line and column in the given file. Wait for the response and return the result. - :param relative_file_path: The relative path of the file that has the symbol for which definition should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol + :param file_path: The path of the file relative to the repository root. + :param line: The line number (zero-indexed). + :param column: The column number (zero-indexed). - :return List[multilspy_types.Location]: A list of locations where the symbol is defined + :return: A list of Locations defining the symbol. """ + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + result = asyncio.run_coroutine_threadsafe( self.language_server.request_definition(file_path, line, column), self.loop ).result(timeout=self.timeout) @@ -1773,15 +1776,18 @@ class SyncLanguageServer: def request_references(self, file_path: str, line: int, column: int) -> List[multilspy_types.Location]: """ - Raise a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server - to find references to the symbol at the given line and column in the given file. Wait for the response and return the result. + Raises a [textDocument/references](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references) request to the Language Server + for the symbol at the given line and column in the given file. Wait for the response and return the result. - :param relative_file_path: The relative path of the file that has the symbol for which references should be looked up - :param line: The line number of the symbol - :param column: The column number of the symbol + :param file_path: The path of the file relative to the repository root. + :param line: The line number (zero-indexed). + :param column: The column number (zero-indexed). - :return List[multilspy_types.Location]: A list of locations where the symbol is referenced + :return: A list of Locations referencing the symbol. """ + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + try: result = asyncio.run_coroutine_threadsafe( self.language_server.request_references(file_path, line, column), self.loop @@ -1867,6 +1873,9 @@ class SyncLanguageServer: :return: A list of root symbols representing the top-level packages/modules in the project. """ + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + result = asyncio.run_coroutine_threadsafe( self.language_server.request_full_symbol_tree(within_relative_path, include_body), self.loop ).result(timeout=self.timeout) @@ -1879,7 +1888,9 @@ class SyncLanguageServer: Maps relative paths of all contained files to info about top-level symbols in the file (name, kind, line, column). """ - assert self.loop + if not self.is_running(): + raise RuntimeError("Language server is not running. Cannot make requests to a stopped language server.") + result = asyncio.run_coroutine_threadsafe( self.language_server.request_dir_overview(relative_dir_path), self.loop ).result(timeout=self.timeout) diff --git a/src/serena/agent.py b/src/serena/agent.py index db300c5d..b8c177f3 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -9,7 +9,6 @@ import platform import re import shutil import sys -import threading import traceback import webbrowser from abc import ABC, abstractmethod @@ -21,7 +20,6 @@ from fnmatch import fnmatch from functools import cached_property from logging import Logger from pathlib import Path -from time import time from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast @@ -306,6 +304,59 @@ class SerenaConfigBase(ABC): else: raise ValueError(f"Project '{project_name}' not found in Serena configuration; valid project names: {self.project_names}") + def to_dict(self) -> dict: + """Convert configuration to dictionary for serialization.""" + return { + "projects": [ + { + "project_root": project.project_root, + "project_config": { + "project_name": project.project_config.project_name, + "language": project.project_config.language, + "ignored_paths": project.project_config.ignored_paths, + "ignore_all_files_in_gitignore": project.project_config.ignore_all_files_in_gitignore, + }, + } + for project in self.projects + ], + "gui_log_window_enabled": self.gui_log_window_enabled, + "log_level": self.log_level, + "trace_lsp_communication": self.trace_lsp_communication, + "web_dashboard": self.web_dashboard, + "tool_timeout": self.tool_timeout, + } + + @classmethod + def from_dict(cls, data: dict) -> "SerenaConfigBase": + """Create configuration from dictionary.""" + projects = [] + for project_data in data.get("projects", []): + project_config = ProjectConfig( + project_name=project_data["project_config"]["project_name"], + language=project_data["project_config"]["language"], + ignored_paths=project_data["project_config"]["ignored_paths"], + ignore_all_files_in_gitignore=project_data["project_config"]["ignore_all_files_in_gitignore"], + ) + project = Project(project_root=project_data["project_root"], project_config=project_config) + projects.append(project) + + # Create a basic config class that can be instantiated + from dataclasses import dataclass + + @dataclass(kw_only=True) + class DeserializedConfig(SerenaConfigBase): + def _add_new_project(self, project: Project) -> None: + self.projects.append(project) + + return DeserializedConfig( + projects=projects, + gui_log_window_enabled=data.get("gui_log_window_enabled", False), + log_level=data.get("log_level", logging.INFO), + trace_lsp_communication=data.get("trace_lsp_communication", False), + web_dashboard=data.get("web_dashboard", True), + tool_timeout=data.get("tool_timeout", DEFAULT_TOOL_TIMEOUT), + ) + @dataclass(kw_only=True) class SerenaConfig(SerenaConfigBase): @@ -851,14 +902,13 @@ class SerenaAgent: """ Starts/resets the language server for the current project """ - tool_timeout = self.serena_config.tool_timeout if tool_timeout is None or tool_timeout < 0: ls_timeout = None else: if tool_timeout < 10: raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds") - ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout + ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout # stop the language server if it is running if self.is_language_server_running(): From 9c28f901398446244079418222e631a7f1ac4caf Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 14 Jun 2025 00:31:47 +0200 Subject: [PATCH 14/28] Major: Implement process isolation for MCP server to prevent asyncio deadlocks ROOT CAUSE: Asyncio event loop contamination between MCP server and SerenaAgent was causing intermittent deadlocks. MCP server runs in its own asyncio loop, but SerenaAgent creates coroutines that leak into the MCP context, leading to unawaited coroutines and eventual event loop blocking. SOLUTION: Complete process isolation with clean architecture Process Isolation Changes: - Add ProcessIsolatedSerenaAgent wrapper that runs SerenaAgent in separate process - Implement JSON-RPC based IPC for communication between MCP server and agent - Add SerenaAgentWorker class to handle requests in the isolated process - Centralize error handling in _make_request_with_result() helper method - Use serena_config.tool_timeout instead of hardcoded timeout parameters Architecture Improvements: - Define ToolProtocol interface for clean tool contracts - Implement ProcessIsolatedTool with complete Tool interface (apply + apply_ex) - Remove hacky make_process_isolated_tool function - use single make_tool path - Eliminate inheritance abuse - ProcessIsolatedTool implements protocol cleanly - Preserve full docstring parsing functionality through process boundary MCP Server Updates: - Update start_mcp_server() to use process isolation by default - Maintain backward compatibility with create_mcp_server_and_agent() - Retrieve tool metadata from isolated process for proper MCP tool creation - Ensure proper process lifecycle management in server lifespan handlers Key Benefits: - Prevents asyncio contamination deadlocks completely - Maintains full docstring parsing and parameter schema generation - Clean protocol-based architecture with no inheritance hacks - Centralized configuration-driven timeout handling - All tools implement consistent apply_ex interface - Process isolation is transparent to MCP clients This fix ensures that MCP symbolic tools no longer create deadlocks --- src/serena/agent.py | 18 +- src/serena/mcp.py | 122 +++++++-- src/serena/process_isolated_agent.py | 361 +++++++++++++++++++++++++++ 3 files changed, 474 insertions(+), 27 deletions(-) create mode 100644 src/serena/process_isolated_agent.py diff --git a/src/serena/agent.py b/src/serena/agent.py index b8c177f3..75d0e77c 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -21,7 +21,7 @@ from functools import cached_property from logging import Logger from pathlib import Path from types import TracebackType -from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Literal, Protocol, Self, TypeVar, Union, cast import yaml from overrides import override @@ -1026,6 +1026,22 @@ class ToolMarkerDoesNotRequireActiveProject: pass +class ToolProtocol(Protocol): + """Protocol defining the complete interface that make_tool() expects from a tool.""" + + def get_name(self) -> str: + """Get the tool name.""" + ... + + def apply(self, **kwargs: Any) -> str: + """The application logic of the tool, docstring will be set here and used for MCP tool description.""" + ... + + def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str: + """Apply the tool with logging and exception handling.""" + ... + + class Tool(Component): # NOTE: each tool should implement the apply method, which is then used in # the central method of the Tool class `apply_ex`. diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 73c04fc8..f499b3d5 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -19,9 +19,10 @@ from mcp.server.fastmcp.utilities.func_metadata import func_metadata from sensai.util import logging from sensai.util.helper import mark_used -from serena.agent import SerenaAgent, Tool, show_fatal_exception_safe +from serena.agent import SerenaAgent, SerenaConfig, ToolProtocol, show_fatal_exception_safe from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES +from serena.process_isolated_agent import ProcessIsolatedSerenaAgent, ProcessIsolatedTool log = logging.getLogger(__name__) LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" @@ -46,7 +47,7 @@ class SerenaMCPRequestContext: def make_tool( - tool: Tool, + tool: ToolProtocol, ) -> MCPTool: func_name = tool.get_name() @@ -68,17 +69,17 @@ def make_tool( func_doc = f"{docstring.description.strip().strip('.')}." else: func_doc = "" - if (docstring.returns) and (docstring_returns := docstring.returns.description): + if docstring.returns and (docstring_returns_descr := docstring.returns.description): # Only add a space before "Returns" if func_doc is not empty prefix = " " if func_doc else "" - func_doc = f"{func_doc}{prefix}Returns {docstring_returns.strip().strip('.')}." + func_doc = f"{func_doc}{prefix}Returns {docstring_returns_descr.strip().strip('.')}." # Parse the parameter descriptions from the docstring and add pass its description # to the parameters schema. docstring_params = {param.arg_name: param for param in docstring.params} parameters_properties: dict[str, dict[str, Any]] = parameters["properties"] for parameter, properties in parameters_properties.items(): - if (param_doc := docstring_params.get(parameter)) and (param_doc.description): + if (param_doc := docstring_params.get(parameter)) and param_doc.description: param_desc = f"{param_doc.description.strip().strip('.') + '.'}" properties["description"] = param_desc[0].upper() + param_desc[1:] @@ -109,7 +110,54 @@ def create_mcp_server_and_agent( tool_timeout: float | None = None, ) -> tuple[FastMCP, SerenaAgent]: """ - Create an MCP server. + Create an MCP server (legacy - now uses process isolation). + + :param project: "Either an absolute path to the project directory or a name of an already registered project. " + "If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name " + "afterwards. + :param host: The host to bind to + :param port: The port to bind to + :param context: The context name or path to context file + :param modes: List of mode names or paths to mode files + :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. + :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. + If not specified, will take the value from the serena configuration. + :param log_level: Log level. If not specified, will take the value from the serena configuration. + :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. + This is useful for debugging language server issues. + :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. + """ + # Delegate to process-isolated version for safety + mcp, process_agent = create_mcp_server_and_process_isolated_agent( + project=project, + host=host, + port=port, + context=context, + modes=modes, + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) + # Return a dummy agent for compatibility + return mcp, process_agent # type: ignore + + +def create_mcp_server_and_process_isolated_agent( + project: str | None, + host: str = "0.0.0.0", + port: int = 8000, + context: str = DEFAULT_CONTEXT, + modes: Sequence[str] = DEFAULT_MODES, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, +) -> tuple[FastMCP, ProcessIsolatedSerenaAgent]: + """ + Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. :param project: "Either an absolute path to the project directory or a name of an already registered project. " "If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name " @@ -131,10 +179,9 @@ def create_mcp_server_and_agent( modes_instances = [SerenaAgentMode.load(mode) for mode in modes] try: - agent = SerenaAgent( + # First create a regular agent to get the config + temp_agent = SerenaAgent( project=project, - # Callback disabled for the time being (see above) - # project_activation_callback=update_tools context=context_instance, modes=modes_instances, enable_web_dashboard=enable_web_dashboard, @@ -143,39 +190,61 @@ def create_mcp_server_and_agent( trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, ) + + # Create process-isolated agent with the same config + if not isinstance(temp_agent.serena_config, SerenaConfig): + raise TypeError("Expected SerenaConfig instance") + process_agent = ProcessIsolatedSerenaAgent(temp_agent.serena_config) + + # Clean up temp agent + if temp_agent.language_server is not None: + temp_agent.language_server.stop() + del temp_agent + except Exception as e: show_fatal_exception_safe(e) raise def update_tools() -> None: - """Update the tools in the MCP server.""" - # Tools may change as a result of project activation. - # NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not, - # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, - # so we need to register all tools at startup, unfortunately. - nonlocal mcp, agent - tools = agent.get_exposed_tool_instances() + """Update the tools in the MCP server - adapted for process isolation.""" + nonlocal mcp, process_agent + + # Get tool instances data from process-isolated agent + try: + tool_instances_data = process_agent.get_tool_instances() + except Exception as e: + log.error(f"Failed to get tool instances from process agent: {e}") + return + if mcp is not None: mcp._tool_manager._tools = {} - for tool in tools: - # noinspection PyProtectedMember - mcp._tool_manager._tools[tool.get_name()] = make_tool(tool) + for tool_data in tool_instances_data: + # Create ProcessIsolatedTool instance that can be used with make_tool + tool_instance = ProcessIsolatedTool( + process_agent=process_agent, tool_name=tool_data["name"], tool_doc=tool_data["apply_method_doc"] + ) + # Use the original make_tool function to get full docstring parsing + mcp_tool = make_tool(tool_instance) + mcp._tool_manager._tools[tool_data["name"]] = mcp_tool @asynccontextmanager async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" - nonlocal agent + nonlocal process_agent mark_used(mcp_server) - yield - if agent.language_server is not None: - agent.language_server.stop() + + try: + process_agent.start() + yield + finally: + process_agent.stop() mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) mcp = FastMCP(**mcp_settings.model_dump()) update_tools() - return mcp, agent + return mcp, process_agent class ProjectType(click.ParamType): @@ -313,7 +382,8 @@ def start_mcp_server( # This is for backward compatibility with the old CLI, should be removed in the future! project_file = project_file_arg if project_file_arg is not None else project_file_opt - mcp_server, agent = create_mcp_server_and_agent( + # Use process isolation by default to prevent asyncio event loop contamination + mcp_server, agent = create_mcp_server_and_process_isolated_agent( project=project_file, host=host, port=port, @@ -335,7 +405,7 @@ def start_mcp_server( ) log.info( - f"Starting serena agent in MCP server with config:\n{agent.get_current_config_overview()}." + f"Starting process-isolated serena agent in MCP server with config:\n{agent.serena_config}." f"\n Log level: {agent.serena_config.log_level}" ) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py new file mode 100644 index 00000000..de562a11 --- /dev/null +++ b/src/serena/process_isolated_agent.py @@ -0,0 +1,361 @@ +""" +Process-isolated SerenaAgent to prevent asyncio event loop contamination between MCP server and language server. + +This module provides: +1. ProcessIsolatedSerenaAgent - A wrapper that runs SerenaAgent in a separate process +2. SerenaAgentWorker - The worker process that hosts the actual SerenaAgent +3. JSON-RPC based IPC for communication between processes +""" + +import logging +import multiprocessing +import queue +import threading +import time +import traceback +from typing import Any, Self + +from serena.agent import SerenaAgent, SerenaConfig, ToolProtocol + +log = logging.getLogger(__name__) + + +class SerenaAgentWorker: + """Worker process that hosts the actual SerenaAgent.""" + + def __init__(self, request_queue: multiprocessing.Queue, response_queue: multiprocessing.Queue): + self.request_queue = request_queue + self.response_queue = response_queue + self.agent: SerenaAgent | None = None + self.running = True + + def run(self) -> None: + """Main worker loop - runs in separate process.""" + try: + log.info("SerenaAgent worker process started") + + while self.running: + try: + # Wait for requests with timeout to allow clean shutdown + request = self.request_queue.get(timeout=1.0) + if request is None: # Shutdown signal + break + + response = self._handle_request(request) + self.response_queue.put(response) + + except queue.Empty: + continue + except Exception as e: + log.error(f"Error in worker process: {e}") + response = { + "id": getattr(request, "id", None), + "error": f"Worker process error: {e!s}", + "traceback": traceback.format_exc(), + } + self.response_queue.put(response) + + except Exception as e: + log.error(f"Fatal error in worker process: {e}") + finally: + self._cleanup() + log.info("SerenaAgent worker process stopped") + + def _handle_request(self, request: dict[str, Any]) -> dict[str, Any]: + """Handle a single request.""" + try: + method = request["method"] + params = request.get("params", {}) + request_id = request["id"] + + if method == "initialize": + return self._initialize(request_id, params) + elif method == "tool_call": + return self._tool_call(request_id, params) + elif method == "get_active_tool_names": + return self._get_active_tool_names(request_id) + elif method == "is_language_server_running": + return self._is_language_server_running(request_id) + elif method == "reset_language_server": + return self._reset_language_server(request_id) + elif method == "get_tool_instances": + return self._get_tool_instances(request_id) + else: + return {"id": request_id, "error": f"Unknown method: {method}"} + + except Exception as e: + return {"id": request.get("id"), "error": str(e), "traceback": traceback.format_exc()} + + def _initialize(self, request_id: str, params: dict[str, Any]) -> dict[str, Any]: + """Initialize the SerenaAgent.""" + try: + config_dict = params["config"] + serena_config = SerenaConfig.from_dict(config_dict) + + self.agent = SerenaAgent(serena_config=serena_config) + + return {"id": request_id, "result": "SerenaAgent initialized successfully"} + except Exception as e: + return {"id": request_id, "error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} + + def _tool_call(self, request_id: str, params: dict[str, Any]) -> dict[str, Any]: + """Execute a tool call.""" + if self.agent is None: + return {"id": request_id, "error": "SerenaAgent not initialized"} + + try: + tool_name = params["tool_name"] + tool_params = params["tool_params"] + + # Get the tool by name + tool = None + for tool_instance in self.agent._active_tools.values(): + if tool_instance.get_name() == tool_name: + tool = tool_instance + break + + if tool is None: + return {"id": request_id, "error": f"Tool '{tool_name}' not found or not active"} + + # Execute the tool + result = tool.apply_ex(**tool_params) + + return {"id": request_id, "result": result} + + except Exception as e: + return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + + def _get_active_tool_names(self, request_id: str) -> dict[str, Any]: + """Get list of active tool names.""" + if self.agent is None: + return {"id": request_id, "error": "SerenaAgent not initialized"} + + try: + tool_names = self.agent.get_active_tool_names() + return {"id": request_id, "result": tool_names} + except Exception as e: + return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + + def _is_language_server_running(self, request_id: str) -> dict[str, Any]: + """Check if language server is running.""" + if self.agent is None: + return {"id": request_id, "error": "SerenaAgent not initialized"} + + try: + is_running = self.agent.is_language_server_running() + return {"id": request_id, "result": is_running} + except Exception as e: + return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + + def _reset_language_server(self, request_id: str) -> dict[str, Any]: + """Reset the language server.""" + if self.agent is None: + return {"id": request_id, "error": "SerenaAgent not initialized"} + + try: + self.agent.reset_language_server() + return {"id": request_id, "result": "Language server reset successfully"} + except Exception as e: + return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + + def _get_tool_instances(self, request_id: str) -> dict[str, Any]: + """Get exposed tool instances for MCP tool creation.""" + if self.agent is None: + return {"id": request_id, "error": "SerenaAgent not initialized"} + + try: + tool_instances = self.agent.get_exposed_tool_instances() + # Serialize tool information that can be used to recreate tools on the MCP side + tool_data = [] + for tool in tool_instances: + tool_data.append( + { + "name": tool.get_name(), + "class_name": tool.__class__.__name__, + "apply_method_doc": getattr(tool, "apply").__doc__, + } + ) + return {"id": request_id, "result": tool_data} + except Exception as e: + return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + + def _cleanup(self) -> None: + """Clean up resources.""" + if self.agent is not None: + try: + if self.agent.is_language_server_running() and self.agent.language_server is not None: + self.agent.language_server.stop() + except Exception as e: + log.error(f"Error stopping language server: {e}") + self.agent = None + + +class ProcessIsolatedSerenaAgent: + """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" + + def __init__(self, serena_config: SerenaConfig): + self.serena_config = serena_config + self.process: multiprocessing.Process | None = None + self.request_queue: multiprocessing.Queue | None = None + self.response_queue: multiprocessing.Queue | None = None + self.request_id_counter = 0 + self._lock = threading.Lock() + + def start(self) -> None: + """Start the worker process.""" + if self.process is not None: + raise RuntimeError("ProcessIsolatedSerenaAgent already started") + + log.info("Starting process-isolated SerenaAgent") + + # Create communication queues + self.request_queue = multiprocessing.Queue() + self.response_queue = multiprocessing.Queue() + + # Create and start worker process + worker = SerenaAgentWorker(self.request_queue, self.response_queue) + self.process = multiprocessing.Process(target=worker.run) + self.process.start() + + # Initialize the agent in the worker process + try: + self._make_request_with_result("initialize", {"config": self.serena_config.to_dict()}) + except Exception as e: + self.stop() + raise RuntimeError(f"Failed to initialize SerenaAgent: {e}") from e + + log.info("Process-isolated SerenaAgent started successfully") + + def stop(self) -> None: + """Stop the worker process.""" + if self.process is None: + return + + log.info("Stopping process-isolated SerenaAgent") + + try: + # Send shutdown signal + if self.request_queue is not None: + self.request_queue.put(None) + + # Wait for process to terminate + if self.process.is_alive(): + self.process.join(timeout=10.0) + + if self.process.is_alive(): + log.warning("Worker process did not terminate gracefully, forcing termination") + self.process.terminate() + self.process.join(timeout=5.0) + + if self.process.is_alive(): + log.error("Worker process could not be terminated, killing it") + self.process.kill() + self.process.join() + + except Exception as e: + log.error(f"Error stopping worker process: {e}") + + finally: + self.process = None + self.request_queue = None + self.response_queue = None + + log.info("Process-isolated SerenaAgent stopped") + + def _make_request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]: + """Make a request to the worker process.""" + if self.process is None or not self.process.is_alive(): + raise RuntimeError("Worker process is not running") + + with self._lock: + self.request_id_counter += 1 + request_id = str(self.request_id_counter) + + request = {"id": request_id, "method": method, "params": params or {}} + + # Send request + if self.request_queue is not None: + self.request_queue.put(request) + + # Wait for response + timeout = self.serena_config.tool_timeout + start_time = time.time() + while time.time() - start_time < timeout: + try: + if self.response_queue is not None: + response = self.response_queue.get(timeout=1.0) + else: + raise RuntimeError("Response queue is not initialized") + if response["id"] == request_id: + return response + else: + # Put back response for different request + if self.response_queue is not None: + self.response_queue.put(response) + except queue.Empty: + continue + + raise TimeoutError(f"Request {request_id} timed out after {timeout} seconds") + + def _make_request_with_result(self, method: str, params: dict[str, Any] | None = None) -> Any: + """Make a request and return the result, raising an exception if there's an error.""" + response = self._make_request(method, params) + if "error" in response: + raise RuntimeError(f"Request {method} failed: {response['error']}") + return response["result"] + + def tool_call(self, tool_name: str, **tool_params: Any) -> str: + """Call a tool in the worker process.""" + return self._make_request_with_result("tool_call", {"tool_name": tool_name, "tool_params": tool_params}) + + def get_active_tool_names(self) -> list[str]: + """Get list of active tool names.""" + return self._make_request_with_result("get_active_tool_names") + + def is_language_server_running(self) -> bool: + """Check if language server is running.""" + return self._make_request_with_result("is_language_server_running") + + def reset_language_server(self) -> None: + """Reset the language server.""" + self._make_request_with_result("reset_language_server") + + def get_tool_instances(self) -> list[dict[str, Any]]: + """Get tool instances data for MCP tool creation.""" + return self._make_request_with_result("get_tool_instances") + + def __enter__(self) -> Self: + self.start() + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.stop() + + +class ProcessIsolatedTool(ToolProtocol): + """A clean tool wrapper that delegates to ProcessIsolatedSerenaAgent.""" + + def __init__(self, process_agent: ProcessIsolatedSerenaAgent, tool_name: str, tool_doc: str | None = None): + self.process_agent = process_agent + self.tool_name = tool_name + self._tool_doc = tool_doc + # Set the docstring for the apply method if provided + if tool_doc: + self.apply.__doc__ = tool_doc + + def apply(self, **kwargs: Any) -> str: + """Apply the tool through the process-isolated agent.""" + return self.process_agent.tool_call(self.tool_name, **kwargs) + + def get_name(self) -> str: + """Get the tool name for this process-isolated tool.""" + return self.tool_name + + def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str: + """Apply the tool with logging and exception handling.""" + try: + return self.apply(**kwargs) + except Exception as e: + if catch_exceptions: + return f"Error executing tool {self.tool_name}: {e!s}" + raise From 4a4b665d60fd0670e4d45b0d080ddf260e88b519 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 14 Jun 2025 01:24:14 +0200 Subject: [PATCH 15/28] Testing: allow setting PYTEST_MARKERS to control which tests to run by default --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3ce1ed4c..ec13ddda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,7 +107,9 @@ exclude = "^build/|^docs/" PYDEVD_DISABLE_FILE_VALIDATION = "1" [tool.poe.tasks] -test = "pytest test --color=yes" +# Uses PYTEST_MARKERS env var for default markers (defaults to "not java and not rust") +# Set PYTEST_MARKERS="" to run all tests +test = "pytest test --color=yes -m \"${PYTEST_MARKERS:-not java and not rust}\"" _black_check = "black --check --exclude src/multilspy/ src scripts test" _ruff_check = "ruff check --exclude .venv/ --exclude src/multilspy/ src scripts test" _black_format = "black --exclude .venv/|src/multilspy/ src scripts test" From 37d100446057d8c9a3a568c87bfa880c0af9fd74 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sat, 14 Jun 2025 01:59:29 +0200 Subject: [PATCH 16/28] Fixes in SerenaAgent isolation in process creation make_tool previously depended on tool instances in the main process, but the tool instances are no longer available there To solve this, the tool interface was adjusted to provide static information which is used in the ProcessIsolatedTool wrapper. I also tried another approach before, where ProcessIsolatedTool would accept tool metadata at init, but this lead to serialization issues that couldn't be easily solved (mcp's FuncMetadata is not serializable, despite being a BaseModel...). Tests for checking that the wrapped tool instance has the same metadata as the wrapper have been added. --- src/serena/agent.py | 189 ++++++++++++++---- src/serena/agno.py | 4 +- src/serena/mcp.py | 68 +++---- src/serena/process_isolated_agent.py | 60 +++--- .../test_make_tool_process_isolation.py | 160 +++++++++++++++ test/serena/test_mcp.py | 4 +- test/serena/test_serena_agent.py | 37 +++- 7 files changed, 414 insertions(+), 108 deletions(-) create mode 100644 test/serena/test_make_tool_process_isolation.py diff --git a/src/serena/agent.py b/src/serena/agent.py index 75d0e77c..ebb8a36a 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -21,9 +21,10 @@ from functools import cached_property from logging import Logger from pathlib import Path from types import TracebackType -from typing import TYPE_CHECKING, Any, Literal, Protocol, Self, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar, Union, cast import yaml +from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata from overrides import override from pathspec import PathSpec from ruamel.yaml.comments import CommentedMap @@ -548,6 +549,77 @@ class MemoriesManagerMDFilesInProject(MemoriesManager): return f"Memory {name} deleted." +def create_serena_config( + project: str | None = None, + serena_config: SerenaConfigBase | None = None, + context: SerenaAgentContext | None = None, + modes: list[SerenaAgentMode] | None = None, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, +) -> SerenaConfig: + """ + Create a SerenaConfig instance without instantiating a full SerenaAgent. + + This function extracts the configuration creation logic from SerenaAgent.__init__ + to allow creating configurations independently for process isolation and other use cases. + + :param project: the project to register/activate or None + :param serena_config: the base Serena configuration or None to read from default location + :param context: the context in which the agent will operate + :param modes: list of modes in which the agent will operate + :param enable_web_dashboard: Whether to enable the web dashboard + :param enable_gui_log_window: Whether to enable the GUI log window + :param log_level: Log level + :param trace_lsp_communication: Whether to trace LSP communication + :param tool_timeout: Timeout in seconds for tool execution + :return: A fully configured SerenaConfig instance + """ + # obtain serena configuration + if serena_config is not None: + # If a complete SerenaConfig is provided, use it directly + if isinstance(serena_config, SerenaConfig): + config = serena_config + else: + # For SerenaConfigBase instances (like test configs), create an in-memory SerenaConfig + # that preserves the base config attributes without loading from file + from ruamel.yaml.comments import CommentedMap + + config = SerenaConfig.__new__(SerenaConfig) # Create without calling __init__ + # Initialize basic attributes from base config + config.projects = getattr(serena_config, "projects", []) + config.gui_log_window_enabled = serena_config.gui_log_window_enabled + config.log_level = serena_config.log_level + config.trace_lsp_communication = serena_config.trace_lsp_communication + config.web_dashboard = serena_config.web_dashboard + config.tool_timeout = serena_config.tool_timeout + # Set empty yaml for in-memory config + config.loaded_commented_yaml = CommentedMap() + else: + config = SerenaConfig.from_config_file() + + # Apply parameter overrides + if enable_web_dashboard is not None: + config.web_dashboard = enable_web_dashboard + if enable_gui_log_window is not None: + config.gui_log_window_enabled = enable_gui_log_window + if log_level is not None: + log_level = cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], log_level.upper()) + # transform to int + config.log_level = logging.getLevelNamesMapping()[log_level] + if trace_lsp_communication is not None: + config.trace_lsp_communication = trace_lsp_communication + if tool_timeout is not None: + config.tool_timeout = tool_timeout + + # Note: Project registration/activation is handled separately by the caller + # since it involves complex logic that may require the full agent context + + return config + + class SerenaAgent: def __init__( self, @@ -577,20 +649,18 @@ class SerenaAgent: :param log_level: Log level for the GUI log window. If not specified, will take the value from the serena configuration. :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. """ - # obtain serena configuration - self.serena_config = serena_config or SerenaConfig.from_config_file() - if enable_web_dashboard is not None: - self.serena_config.web_dashboard = enable_web_dashboard - if enable_gui_log_window is not None: - self.serena_config.gui_log_window_enabled = enable_gui_log_window - if log_level is not None: - log_level = cast(Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], log_level.upper()) - # transform to int - self.serena_config.log_level = logging.getLevelNamesMapping()[log_level] - if trace_lsp_communication is not None: - self.serena_config.trace_lsp_communication = trace_lsp_communication - if tool_timeout is not None: - self.serena_config.tool_timeout = tool_timeout + # obtain serena configuration using the decoupled factory function + self.serena_config = create_serena_config( + project=None, # Don't pass project here, handle activation separately + serena_config=serena_config, + context=context, + modes=modes, + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) # adjust log level serena_log_level = self.serena_config.log_level @@ -617,7 +687,7 @@ class SerenaAgent: # instantiate all tool classes self._all_tools: dict[type[Tool], Tool] = {tool_class: tool_class(self) for tool_class in ToolRegistry.get_all_tool_classes()} - tool_names = [tool.get_name() for tool in self._all_tools.values()] + tool_names = [tool.get_name_from_cls() for tool in self._all_tools.values()] # If GUI log window is enabled, set the tool names for highlighting if self._gui_log_handler is not None: @@ -654,7 +724,7 @@ class SerenaAgent: modes = SerenaAgentMode.load_default_modes() self._context = context self._modes = modes - log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name() for tool in self._all_tools.values()])}") + log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name_from_cls() for tool in self._all_tools.values()])}") self._active_tools: dict[type[Tool], Tool] = {} self._update_active_tools() @@ -839,7 +909,7 @@ class SerenaAgent: """ :return: the list of names of the active tools for the current project """ - return sorted([tool.get_name() for tool in self.get_active_tool_classes()]) + return sorted([tool.get_name_from_cls() for tool in self.get_active_tool_classes()]) def tool_is_active(self, tool_class: type["Tool"] | str) -> bool: """ @@ -885,7 +955,7 @@ class SerenaAgent: result_str += " " + ", ".join(chunk) + "\n" # Available but not active tools - all_tool_names = sorted([tool.get_name() for tool in self._all_tools.values()]) + all_tool_names = sorted([tool.get_name_from_cls() for tool in self._all_tools.values()]) inactive_tool_names = [tool for tool in all_tool_names if tool not in active_tool_names] if inactive_tool_names: result_str += "Available but not active tools:\n" @@ -1026,23 +1096,31 @@ class ToolMarkerDoesNotRequireActiveProject: pass -class ToolProtocol(Protocol): +class ToolInterface(ABC): """Protocol defining the complete interface that make_tool() expects from a tool.""" + @abstractmethod def get_name(self) -> str: """Get the tool name.""" ... - def apply(self, **kwargs: Any) -> str: - """The application logic of the tool, docstring will be set here and used for MCP tool description.""" + @abstractmethod + def get_apply_docstring(self) -> str: + """Get the docstring for the tool application, used by the MCP server.""" ... + @abstractmethod + def get_apply_fn_metadata(self) -> FuncMetadata: + """Get the metadata for the tool application function, used by the MCP server.""" + ... + + @abstractmethod def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str: """Apply the tool with logging and exception handling.""" ... -class Tool(Component): +class Tool(Component, ToolInterface): # NOTE: each tool should implement the apply method, which is then used in # the central method of the Tool class `apply_ex`. # Failure to do so will result in a RuntimeError at tool execution time. @@ -1054,7 +1132,7 @@ class Tool(Component): # and to validate the tool call arguments. @classmethod - def get_name(cls) -> str: + def get_name_from_cls(cls) -> str: name = cls.__name__ if name.endswith("Tool"): name = name[:-4] @@ -1062,6 +1140,9 @@ class Tool(Component): name = "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_") return name + def get_name(self) -> str: + return self.get_name_from_cls() + def get_apply_fn(self) -> Callable: apply_fn = getattr(self, "apply") if apply_fn is None: @@ -1084,12 +1165,48 @@ class Tool(Component): return "" return docstring.strip() - def get_function_description(self) -> str: - apply_fn = self.get_apply_fn() + @classmethod + def get_apply_docstring_from_cls(cls) -> str: + """Get the docstring for the apply method from the class (static metadata). + Needed for creating MCP tools in a separate process without running into serialization issues. + """ + # First try to get from __dict__ to handle dynamic docstring changes + if "apply" in cls.__dict__: + apply_fn = cls.__dict__["apply"] + else: + # Fall back to getattr for inherited methods + apply_fn = getattr(cls, "apply", None) + if apply_fn is None: + raise AttributeError(f"apply method not defined in {cls}. Did you forget to implement it?") + docstring = apply_fn.__doc__ - if docstring is None: - raise Exception(f"Missing docstring for {self}") - return docstring + if not docstring: + raise AttributeError(f"apply method has no (or empty) docstring in {cls}. Did you forget to implement it?") + return docstring.strip() + + def get_apply_docstring(self) -> str: + """Get the docstring for the apply method (instance method implementing ToolProtocol).""" + return self.get_apply_docstring_from_cls() + + def get_apply_fn_metadata(self) -> FuncMetadata: + """Get the metadata for the apply method (instance method implementing ToolProtocol).""" + return self.get_apply_fn_metadata_from_cls() + + @classmethod + def get_apply_fn_metadata_from_cls(cls) -> FuncMetadata: + """Get the metadata for the apply method from the class (static metadata). + Needed for creating MCP tools in a separate process without running into serialization issues. + """ + # First try to get from __dict__ to handle dynamic docstring changes + if "apply" in cls.__dict__: + apply_fn = cls.__dict__["apply"] + else: + # Fall back to getattr for inherited methods + apply_fn = getattr(cls, "apply", None) + if apply_fn is None: + raise AttributeError(f"apply method not defined in {cls}. Did you forget to implement it?") + + return func_metadata(apply_fn, skip_names=["self", "cls"]) def _log_tool_application(self, frame: Any) -> None: params = {} @@ -1101,7 +1218,7 @@ class Tool(Component): params.update(value) else: params[param] = value - log.info(f"{self.get_name()}: {dict_string(params)}") + log.info(f"{self.get_name_from_cls()}: {dict_string(params)}") @staticmethod def _limit_length(result: str, max_answer_chars: int) -> str: @@ -1123,9 +1240,9 @@ class Tool(Component): try: if not self.is_active(): - return f"Error: Tool '{self.get_name()}' is not active. Active tools: {self.agent.get_active_tool_names()}" + return f"Error: Tool '{self.get_name_from_cls()}' is not active. Active tools: {self.agent.get_active_tool_names()}" except Exception as e: - return f"RuntimeError while checking if tool {self.get_name()} is active: {e}" + return f"RuntimeError while checking if tool {self.get_name_from_cls()} is active: {e}" if log_call: self._log_tool_application(inspect.currentframe()) @@ -1143,7 +1260,7 @@ class Tool(Component): # apply the actual tool with a timeout execution_fn = lambda: apply_fn(**kwargs) - execution_result = execute_with_timeout(execution_fn, self.agent.serena_config.tool_timeout, self.get_name()) + execution_result = execute_with_timeout(execution_fn, self.agent.serena_config.tool_timeout, self.get_name_from_cls()) if execution_result.status == ExecutionResult.Status.SUCCESS: result = cast(str, execution_result.result_value) else: @@ -1696,7 +1813,7 @@ class DeleteLinesTool(Tool, ToolMarkerCanEdit): """ if not self.lines_read.were_lines_read(relative_path, (start_line, end_line)): read_lines_tool = self.agent.get_tool(ReadFileTool) - return f"Error: Must call `{read_lines_tool.get_name()}` first to read exactly the affected lines." + return f"Error: Must call `{read_lines_tool.get_name_from_cls()}` first to read exactly the affected lines." self.symbol_manager.delete_lines(relative_path, start_line, end_line) return SUCCESS_RESULT @@ -2158,7 +2275,7 @@ def _iter_tool_classes(same_module_only: bool = True) -> Generator[type[Tool], N yield tool_class -_TOOL_REGISTRY_DICT: dict[str, type[Tool]] = {tool_class.get_name(): tool_class for tool_class in _iter_tool_classes()} +_TOOL_REGISTRY_DICT: dict[str, type[Tool]] = {tool_class.get_name_from_cls(): tool_class for tool_class in _iter_tool_classes()} """maps tool name to the corresponding tool class""" @@ -2194,7 +2311,7 @@ class ToolRegistry: tool_dict: dict[str, type[Tool] | Tool] = {} for tool_class in tools: - tool_dict[tool_class.get_name()] = tool_class + tool_dict[tool_class.get_name_from_cls()] = tool_class for tool_name in sorted(tool_dict.keys()): tool_class = tool_dict[tool_name] print(f" * `{tool_name}`: {tool_class.get_tool_description().strip()}") diff --git a/src/serena/agno.py b/src/serena/agno.py index c9150693..0bb0e70f 100644 --- a/src/serena/agno.py +++ b/src/serena/agno.py @@ -25,7 +25,7 @@ class SerenaAgnoToolkit(Toolkit): def __init__(self, serena_agent: SerenaAgent): super().__init__("Serena") for tool in serena_agent.get_exposed_tool_instances(): - self.functions[tool.get_name()] = self._create_agno_function(tool) + self.functions[tool.get_name_from_cls()] = self._create_agno_function(tool) log.info("Agno agent functions: %s", list(self.functions.keys())) @staticmethod @@ -39,7 +39,7 @@ class SerenaAgnoToolkit(Toolkit): return tool.apply_ex(log_call=True, catch_exceptions=True, **kwargs) function = Function.from_callable(tool.get_apply_fn()) - function.name = tool.get_name() + function.name = tool.get_name_from_cls() function.entrypoint = entrypoint function.skip_entrypoint_processing = True return function diff --git a/src/serena/mcp.py b/src/serena/mcp.py index f499b3d5..db188a2f 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -8,21 +8,23 @@ from contextlib import asynccontextmanager from dataclasses import dataclass from logging import Formatter, Logger, StreamHandler from pathlib import Path -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal import click # Add click import import docstring_parser from mcp.server.fastmcp import server from mcp.server.fastmcp.server import FastMCP, Settings from mcp.server.fastmcp.tools.base import Tool as MCPTool -from mcp.server.fastmcp.utilities.func_metadata import func_metadata from sensai.util import logging from sensai.util.helper import mark_used -from serena.agent import SerenaAgent, SerenaConfig, ToolProtocol, show_fatal_exception_safe +from serena.agent import SerenaAgent, ToolInterface, create_serena_config, show_fatal_exception_safe from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES -from serena.process_isolated_agent import ProcessIsolatedSerenaAgent, ProcessIsolatedTool + +# Import for type annotations +if TYPE_CHECKING: + from serena.process_isolated_agent import ProcessIsolatedSerenaAgent log = logging.getLogger(__name__) LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" @@ -47,18 +49,12 @@ class SerenaMCPRequestContext: def make_tool( - tool: ToolProtocol, + tool: ToolInterface, ) -> MCPTool: func_name = tool.get_name() - - apply_fn = getattr(tool, "apply") - if apply_fn is None: - raise ValueError(f"Tool does not have an apply method: {tool}") - - func_doc = apply_fn.__doc__ or "" + func_doc = tool.get_apply_docstring() or "" + func_arg_metadata = tool.get_apply_fn_metadata() is_async = False - - func_arg_metadata = func_metadata(apply_fn) parameters = func_arg_metadata.arg_model.model_json_schema() docstring = docstring_parser.parse(func_doc) @@ -94,6 +90,7 @@ def make_tool( fn_metadata=func_arg_metadata, is_async=is_async, context_kwarg=None, + annotations=None, ) @@ -108,7 +105,7 @@ def create_mcp_server_and_agent( log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, tool_timeout: float | None = None, -) -> tuple[FastMCP, SerenaAgent]: +) -> tuple[FastMCP, "ProcessIsolatedSerenaAgent"]: """ Create an MCP server (legacy - now uses process isolation). @@ -141,7 +138,7 @@ def create_mcp_server_and_agent( tool_timeout=tool_timeout, ) # Return a dummy agent for compatibility - return mcp, process_agent # type: ignore + return mcp, process_agent def create_mcp_server_and_process_isolated_agent( @@ -155,7 +152,7 @@ def create_mcp_server_and_process_isolated_agent( log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, tool_timeout: float | None = None, -) -> tuple[FastMCP, ProcessIsolatedSerenaAgent]: +) -> tuple[FastMCP, "ProcessIsolatedSerenaAgent"]: """ Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. @@ -179,8 +176,8 @@ def create_mcp_server_and_process_isolated_agent( modes_instances = [SerenaAgentMode.load(mode) for mode in modes] try: - # First create a regular agent to get the config - temp_agent = SerenaAgent( + # Create configuration without instantiating a full SerenaAgent + serena_config = create_serena_config( project=project, context=context_instance, modes=modes_instances, @@ -191,15 +188,10 @@ def create_mcp_server_and_process_isolated_agent( tool_timeout=tool_timeout, ) - # Create process-isolated agent with the same config - if not isinstance(temp_agent.serena_config, SerenaConfig): - raise TypeError("Expected SerenaConfig instance") - process_agent = ProcessIsolatedSerenaAgent(temp_agent.serena_config) + # Create process-isolated agent with the configuration + from serena.process_isolated_agent import ProcessIsolatedSerenaAgent - # Clean up temp agent - if temp_agent.language_server is not None: - temp_agent.language_server.stop() - del temp_agent + process_agent = ProcessIsolatedSerenaAgent(serena_config) except Exception as e: show_fatal_exception_safe(e) @@ -209,23 +201,29 @@ def create_mcp_server_and_process_isolated_agent( """Update the tools in the MCP server - adapted for process isolation.""" nonlocal mcp, process_agent - # Get tool instances data from process-isolated agent + # Check if process agent is running + if process_agent.process is None or not process_agent.process.is_alive(): + log.debug("Process agent not running yet, skipping tool update") + return + + # Get tool names from process-isolated agent try: - tool_instances_data = process_agent.get_tool_instances() + tool_names = process_agent.get_tool_instances() except Exception as e: - log.error(f"Failed to get tool instances from process agent: {e}") + log.error(f"Failed to get tool names from process agent: {e}") return if mcp is not None: + from serena.process_isolated_agent import ProcessIsolatedTool + mcp._tool_manager._tools = {} - for tool_data in tool_instances_data: + for tool_name in tool_names: # Create ProcessIsolatedTool instance that can be used with make_tool - tool_instance = ProcessIsolatedTool( - process_agent=process_agent, tool_name=tool_data["name"], tool_doc=tool_data["apply_method_doc"] - ) + # Metadata is extracted from ToolRegistry using classmethods + tool_instance = ProcessIsolatedTool(process_agent=process_agent, tool_name=tool_name) # Use the original make_tool function to get full docstring parsing mcp_tool = make_tool(tool_instance) - mcp._tool_manager._tools[tool_data["name"]] = mcp_tool + mcp._tool_manager._tools[tool_name] = mcp_tool @asynccontextmanager async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: @@ -235,6 +233,8 @@ def create_mcp_server_and_process_isolated_agent( try: process_agent.start() + # Update tools now that the process agent is running + update_tools() yield finally: process_agent.stop() diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index de562a11..543b5ec2 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -15,7 +15,9 @@ import time import traceback from typing import Any, Self -from serena.agent import SerenaAgent, SerenaConfig, ToolProtocol +from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata + +from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry log = logging.getLogger(__name__) @@ -110,7 +112,7 @@ class SerenaAgentWorker: # Get the tool by name tool = None for tool_instance in self.agent._active_tools.values(): - if tool_instance.get_name() == tool_name: + if tool_instance.get_name_from_cls() == tool_name: tool = tool_instance break @@ -159,23 +161,15 @@ class SerenaAgentWorker: return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} def _get_tool_instances(self, request_id: str) -> dict[str, Any]: - """Get exposed tool instances for MCP tool creation.""" + """Get exposed tool names for MCP tool creation.""" if self.agent is None: return {"id": request_id, "error": "SerenaAgent not initialized"} try: tool_instances = self.agent.get_exposed_tool_instances() - # Serialize tool information that can be used to recreate tools on the MCP side - tool_data = [] - for tool in tool_instances: - tool_data.append( - { - "name": tool.get_name(), - "class_name": tool.__class__.__name__, - "apply_method_doc": getattr(tool, "apply").__doc__, - } - ) - return {"id": request_id, "result": tool_data} + # Return only tool names - metadata will be reconstructed from ToolRegistry + tool_names = [tool.get_name_from_cls() for tool in tool_instances] + return {"id": request_id, "result": tool_names} except Exception as e: return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} @@ -193,7 +187,7 @@ class SerenaAgentWorker: class ProcessIsolatedSerenaAgent: """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" - def __init__(self, serena_config: SerenaConfig): + def __init__(self, serena_config: SerenaConfigBase): self.serena_config = serena_config self.process: multiprocessing.Process | None = None self.request_queue: multiprocessing.Queue | None = None @@ -320,8 +314,8 @@ class ProcessIsolatedSerenaAgent: """Reset the language server.""" self._make_request_with_result("reset_language_server") - def get_tool_instances(self) -> list[dict[str, Any]]: - """Get tool instances data for MCP tool creation.""" + def get_tool_instances(self) -> list[str]: + """Get tool names for MCP tool creation.""" return self._make_request_with_result("get_tool_instances") def __enter__(self) -> Self: @@ -332,30 +326,36 @@ class ProcessIsolatedSerenaAgent: self.stop() -class ProcessIsolatedTool(ToolProtocol): +class ProcessIsolatedTool(ToolInterface): """A clean tool wrapper that delegates to ProcessIsolatedSerenaAgent.""" - def __init__(self, process_agent: ProcessIsolatedSerenaAgent, tool_name: str, tool_doc: str | None = None): + def __init__(self, process_agent: ProcessIsolatedSerenaAgent, tool_name: str): self.process_agent = process_agent - self.tool_name = tool_name - self._tool_doc = tool_doc - # Set the docstring for the apply method if provided - if tool_doc: - self.apply.__doc__ = tool_doc + self._tool_name = tool_name - def apply(self, **kwargs: Any) -> str: - """Apply the tool through the process-isolated agent.""" - return self.process_agent.tool_call(self.tool_name, **kwargs) + @property + def _tool_class(self) -> type[Tool]: + return ToolRegistry.get_tool_class_by_name(self._tool_name) def get_name(self) -> str: """Get the tool name for this process-isolated tool.""" - return self.tool_name + return self._tool_name + + def get_apply_docstring(self) -> str: + """Get the docstring for the apply method.""" + # in the actual tool, this is a classmethod + return self._tool_class.get_apply_docstring_from_cls() + + def get_apply_fn_metadata(self) -> FuncMetadata: + """Get the metadata for the apply method.""" + # in the actual tool, this is a classmethod + return self._tool_class.get_apply_fn_metadata_from_cls() def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, **kwargs: Any) -> str: """Apply the tool with logging and exception handling.""" try: - return self.apply(**kwargs) + return self.process_agent.tool_call(self._tool_name, **kwargs) except Exception as e: if catch_exceptions: - return f"Error executing tool {self.tool_name}: {e!s}" + return f"Error executing tool {self._tool_name}: {e!s}" raise diff --git a/test/serena/test_make_tool_process_isolation.py b/test/serena/test_make_tool_process_isolation.py new file mode 100644 index 00000000..f0fdda26 --- /dev/null +++ b/test/serena/test_make_tool_process_isolation.py @@ -0,0 +1,160 @@ +"""Tests for make_tool consistency between regular tools and ProcessIsolatedTool.""" + +import pytest +from mcp.server.fastmcp.tools.base import Tool as MCPTool + +from serena.agent import SerenaAgent, ToolRegistry +from serena.mcp import make_tool +from serena.process_isolated_agent import ProcessIsolatedSerenaAgent, ProcessIsolatedTool +from test.serena.test_serena_agent import SerenaConfigForTests + + +@pytest.fixture +def in_memory_config(): + """Create an in-memory configuration for tests.""" + return SerenaConfigForTests() + + +@pytest.fixture +def regular_agent(in_memory_config): + """Create a regular SerenaAgent for comparison.""" + return SerenaAgent(serena_config=in_memory_config) + + +@pytest.fixture +def process_isolated_agent(in_memory_config): + """Create a ProcessIsolatedSerenaAgent for comparison.""" + agent = ProcessIsolatedSerenaAgent(in_memory_config) + agent.start() + yield agent + agent.stop() + + +class TestMakeToolProcessIsolation: + """Test that make_tool produces identical metadata for regular and process-isolated tools.""" + + @pytest.mark.parametrize("tool_name", ToolRegistry.get_tool_names()) + def test_make_tool_metadata_consistency( + self, tool_name: str, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent + ): + """Test that make_tool produces identical metadata for regular and process-isolated tools.""" + # Get regular tool instance + tool_class = ToolRegistry.get_tool_class_by_name(tool_name) + regular_tool = regular_agent.get_tool(tool_class) + + # Get ProcessIsolatedTool instance + isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) + + # Create MCP tools from both + regular_mcp_tool = make_tool(regular_tool) + isolated_mcp_tool = make_tool(isolated_tool) + + # Verify both are MCPTool instances + assert isinstance(regular_mcp_tool, MCPTool) + assert isinstance(isolated_mcp_tool, MCPTool) + + # Test name consistency + assert regular_mcp_tool.name == isolated_mcp_tool.name + assert regular_mcp_tool.name == tool_name + + # Test description consistency + assert regular_mcp_tool.description == isolated_mcp_tool.description, ( + f"Tool {tool_name}: descriptions differ\n" + f"Regular: {regular_mcp_tool.description}\n" + f"Isolated: {isolated_mcp_tool.description}" + ) + + # Test parameters schema consistency + assert regular_mcp_tool.parameters == isolated_mcp_tool.parameters, ( + f"Tool {tool_name}: parameter schemas differ\n" + f"Regular: {regular_mcp_tool.parameters}\n" + f"Isolated: {isolated_mcp_tool.parameters}" + ) + + # Test function metadata consistency (compare schemas, not class objects) + regular_schema = regular_mcp_tool.fn_metadata.arg_model.model_json_schema() + isolated_schema = isolated_mcp_tool.fn_metadata.arg_model.model_json_schema() + assert ( + regular_schema == isolated_schema + ), f"Tool {tool_name}: function metadata schemas differ\nRegular: {regular_schema}\nIsolated: {isolated_schema}" + + # Test async flag consistency + assert regular_mcp_tool.is_async == isolated_mcp_tool.is_async + + # Test context kwarg consistency + assert regular_mcp_tool.context_kwarg == isolated_mcp_tool.context_kwarg + + @pytest.mark.parametrize("tool_name", ToolRegistry.get_tool_names()[:5]) # Test first 5 tools for faster execution + def test_tool_protocol_methods_consistency( + self, tool_name: str, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent + ): + """Test that Tool methods return identical results for regular and process-isolated tools.""" + # Get regular tool instance + tool_class = ToolRegistry.get_tool_class_by_name(tool_name) + regular_tool = regular_agent.get_tool(tool_class) + + # Get ProcessIsolatedTool instance + isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) + + # Test get_name() + assert regular_tool.get_name_from_cls() == isolated_tool.get_name() + assert regular_tool.get_name_from_cls() == tool_name + + # Test get_apply_docstring() + regular_docstring = regular_tool.get_apply_docstring() + isolated_docstring = isolated_tool.get_apply_docstring() + assert ( + regular_docstring == isolated_docstring + ), f"Tool {tool_name}: docstrings differ\nRegular: {regular_docstring}\nIsolated: {isolated_docstring}" + + # Test get_apply_fn_metadata() + regular_metadata = regular_tool.get_apply_fn_metadata() + isolated_metadata = isolated_tool.get_apply_fn_metadata() + + # Compare metadata properties (compare schemas, not class objects) + regular_schema = regular_metadata.arg_model.model_json_schema() + isolated_schema = isolated_metadata.arg_model.model_json_schema() + assert ( + regular_schema == isolated_schema + ), f"Tool {tool_name}: metadata schemas differ\nRegular: {regular_schema}\nIsolated: {isolated_schema}" + + def test_process_isolated_tool_uses_tool_registry(self, process_isolated_agent: ProcessIsolatedSerenaAgent): + """Test that ProcessIsolatedTool correctly uses ToolRegistry for metadata.""" + tool_name = ToolRegistry.get_tool_names()[0] # Use first available tool + isolated_tool = ProcessIsolatedTool(process_isolated_agent, tool_name) + + # Verify that the tool uses ToolRegistry + assert isolated_tool._tool_class == ToolRegistry.get_tool_class_by_name(tool_name) + + # Verify that metadata comes from the tool class + expected_docstring = isolated_tool._tool_class.get_apply_docstring_from_cls() + expected_metadata = isolated_tool._tool_class.get_apply_fn_metadata_from_cls() + + assert isolated_tool.get_apply_docstring() == expected_docstring + # Compare schemas, not class objects + isolated_schema = isolated_tool.get_apply_fn_metadata().arg_model.model_json_schema() + expected_schema = expected_metadata.arg_model.model_json_schema() + assert isolated_schema == expected_schema + + def test_tool_registry_completeness(self, regular_agent: SerenaAgent, process_isolated_agent: ProcessIsolatedSerenaAgent): + """Test that all tools are available in both agents and the registry.""" + # Get tool names from both agents + regular_active_tools = set(regular_agent.get_active_tool_names()) + regular_all_tools = set(tool.get_name_from_cls() for tool in regular_agent.get_exposed_tool_instances()) + isolated_tool_names = set(process_isolated_agent.get_tool_instances()) + + # The process isolated agent should have all tools (exposed, not just active) + assert regular_all_tools == isolated_tool_names, ( + f"Tool sets differ:\n" + f"Regular exposed only: {regular_all_tools - isolated_tool_names}\n" + f"Isolated only: {isolated_tool_names - regular_all_tools}" + ) + + # Active tools should be a subset of all tools + assert regular_active_tools.issubset( + regular_all_tools + ), f"Some active tools not in exposed tools: {regular_active_tools - regular_all_tools}" + + # All tools should be in the registry + registry_tool_names = set(ToolRegistry.get_tool_names()) + assert regular_all_tools.issubset(registry_tool_names), f"Some tools not in registry: {regular_all_tools - registry_tool_names}" diff --git a/test/serena/test_mcp.py b/test/serena/test_mcp.py index f179b857..eed27a7f 100644 --- a/test/serena/test_mcp.py +++ b/test/serena/test_mcp.py @@ -224,7 +224,7 @@ def test_make_tool_missing_apply() -> None: """, "", ), - ("", ""), + ("Description without params.", "Description without params."), ], ) def test_make_tool_descriptions(docstring, expected_description) -> None: @@ -287,7 +287,7 @@ def test_make_tool_all_tools(tool_class) -> None: # Basic validation assert isinstance(mcp_tool, MCPTool) - assert mcp_tool.name == tool_class.get_name() + assert mcp_tool.name == tool_class.get_name_from_cls() # The description should be a string (either from docstring or default) assert isinstance(mcp_tool.description, str) diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index f1208dd3..49a47d88 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -6,7 +6,7 @@ from dataclasses import dataclass import pytest from multilspy.multilspy_config import Language -from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, SerenaAgent, SerenaConfigBase +from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfigBase from test.conftest import LanguageParamRequest, get_repo_path @@ -19,17 +19,46 @@ class SerenaConfigForTests(SerenaConfigBase): gui_log_window_enabled: bool = False web_dashboard: bool = False + def __post_init__(self): + # Initialize with empty projects list if not already set + if not hasattr(self, "projects") or self.projects is None: + self.projects = [] + @pytest.fixture def serena_config(): - return SerenaConfigForTests() + """Create an in-memory configuration for tests with test repositories pre-registered.""" + # Create test projects for all supported languages + test_projects = [] + for language in [Language.PYTHON, Language.GO, Language.JAVA, Language.RUST, Language.TYPESCRIPT, Language.PHP]: + repo_path = get_repo_path(language) + if repo_path.exists(): + project_name = f"test_repo_{language}" + project = Project( + project_root=str(repo_path), + project_config=ProjectConfig( + project_name=project_name, + language=language, + ignored_paths=[], + excluded_tools=set(), + read_only=False, + ignore_all_files_in_gitignore=True, + initial_prompt="", + encoding="utf-8", + ), + ) + test_projects.append(project) + + config = SerenaConfigForTests() + config.projects = test_projects + return config @pytest.fixture def serena_agent(request: LanguageParamRequest, serena_config) -> SerenaAgent: language = Language(request.param) - repo = get_repo_path(language) - return SerenaAgent(project=str(repo), serena_config=serena_config) + project_name = f"test_repo_{language}" + return SerenaAgent(project=project_name, serena_config=serena_config) class TestSerenaAgent: From 8c76eb0adf0fe575684e9a32dccfd6cc937ea56e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 15:14:50 +0200 Subject: [PATCH 17/28] Update mcp dependency, remove not needed fastmcp --- pyproject.toml | 1 - uv.lock | 760 ++++++++++++++++++++++++------------------------- 2 files changed, 372 insertions(+), 389 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ec13ddda..4efcecab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ dependencies = [ "python-dotenv>=1.0.0, <2", "mcp>=1.5.0", "fastapi>=0.115.12", - "fastmcp>=0.4.1", "sensai-utils>=1.4.0", "pydantic>=2.10.6", "types-pyyaml>=6.0.12.20241230", diff --git a/uv.lock b/uv.lock index 8be9884e..540c1267 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,10 @@ version = 1 -revision = 2 +revision = 1 requires-python = "==3.11.*" [[package]] name = "agno" -version = "1.2.15" +version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, @@ -20,23 +20,23 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2d/e6/5f747db61e35500750b9f595e8836649bd0d7a22ed0c2ec41167e5dec8bc/agno-1.2.15.tar.gz", hash = "sha256:b589d1277c756e5d8ed5c5ad2052d7910e1c2ad007fa3c830a4e8332809f2099", size = 467123, upload-time = "2025-04-09T00:13:42.792Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/8b/20a6d88f0740d7a17c45016048cb4d278eb5c78a2f75c443a566159420fd/agno-1.6.2.tar.gz", hash = "sha256:f78492021e84ef3ec730d502a89963d11f42cbefd872dfe1f353975489dd9cc9", size = 635136 } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/4b/05c42f236c18d11b29af368910df398a8a67e4f5c5681ead30ebde07a548/agno-1.2.15-py3-none-any.whl", hash = "sha256:e82b57f2980bff1ce7574a16ed963485f2efa3519e9322e17fa6ba07719bd5e2", size = 616626, upload-time = "2025-04-09T00:13:40.805Z" }, + { url = "https://files.pythonhosted.org/packages/72/b9/45456c06f034553327ddcd2e8db6fd224144d0fd2e62fd1e7b07acc18353/agno-1.6.2-py3-none-any.whl", hash = "sha256:7f831893f7b70a931438390650843096da92f42961b5ce63660b35a79a94d0a7", size = 851217 }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, ] [[package]] name = "anthropic" -version = "0.49.0" +version = "0.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -47,9 +47,9 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/e3/a88c8494ce4d1a88252b9e053607e885f9b14d0a32273d47b727cbee4228/anthropic-0.49.0.tar.gz", hash = "sha256:c09e885b0f674b9119b4f296d8508907f6cff0009bc20d5cf6b35936c40b4398", size = 210016, upload-time = "2025-02-28T19:35:47.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/28/80cb9bb6e7ce77d404145b51da4257455805c17f0a6be528ff3286e3882f/anthropic-0.54.0.tar.gz", hash = "sha256:5e6f997d97ce8e70eac603c3ec2e7f23addeff953fbbb76b19430562bb6ba815", size = 312376 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/74/5d90ad14d55fbe3f9c474fdcb6e34b4bed99e3be8efac98734a5ddce88c1/anthropic-0.49.0-py3-none-any.whl", hash = "sha256:bbc17ad4e7094988d2fa86b87753ded8dce12498f4b85fe5810f208f454a8375", size = 243368, upload-time = "2025-02-28T19:35:44.963Z" }, + { url = "https://files.pythonhosted.org/packages/de/b9/6ffb48e82c5e97b03cecee872d134a6b6666c2767b2d32ed709f3a60a8fe/anthropic-0.54.0-py3-none-any.whl", hash = "sha256:c1062a0a905daeec17ca9c06c401e4b3f24cb0495841d29d752568a1d4018d56", size = 288774 }, ] [[package]] @@ -61,18 +61,18 @@ dependencies = [ { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +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, upload-time = "2025-03-17T00:02:52.713Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, ] [[package]] name = "asttokens" version = "3.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, ] [[package]] @@ -86,13 +86,13 @@ dependencies = [ { name = "pathspec" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, - { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372 }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865 }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699 }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028 }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, ] [package.optional-dependencies] @@ -105,88 +105,88 @@ jupyter = [ name = "cachetools" version = "5.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, + { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, ] [[package]] name = "certifi" -version = "2025.1.31" +version = "2025.6.15" 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, upload-time = "2025-01-31T02:16:47.166Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753 } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" }, + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650 }, ] [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995, upload-time = "2024-12-24T18:10:12.838Z" }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471, upload-time = "2024-12-24T18:10:14.101Z" }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831, upload-time = "2024-12-24T18:10:15.512Z" }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335, upload-time = "2024-12-24T18:10:18.369Z" }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862, upload-time = "2024-12-24T18:10:19.743Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673, upload-time = "2024-12-24T18:10:21.139Z" }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211, upload-time = "2024-12-24T18:10:22.382Z" }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039, upload-time = "2024-12-24T18:10:24.802Z" }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939, upload-time = "2024-12-24T18:10:26.124Z" }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075, upload-time = "2024-12-24T18:10:30.027Z" }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340, upload-time = "2024-12-24T18:10:32.679Z" }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205, upload-time = "2024-12-24T18:10:34.724Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441, upload-time = "2024-12-24T18:10:37.574Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 }, ] [[package]] name = "click" -version = "8.1.8" +version = "8.2.1" 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, upload-time = "2024-12-21T18:38:44.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, + { 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 = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, ] [[package]] name = "distro" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, ] [[package]] name = "docstring-parser" version = "0.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565, upload-time = "2024-03-15T10:39:44.419Z" } +sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533, upload-time = "2024-03-15T10:39:41.527Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 }, ] [[package]] @@ -197,16 +197,16 @@ dependencies = [ { name = "python-dotenv" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892 }, ] [[package]] name = "executing" version = "2.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702 }, ] [[package]] @@ -218,26 +218,9 @@ dependencies = [ { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, -] - -[[package]] -name = "fastmcp" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "mcp" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "python-dotenv" }, - { name = "typer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/84/17b549133263d7ee77141970769bbc401525526bf1af043ea6842bce1a55/fastmcp-0.4.1.tar.gz", hash = "sha256:713ad3b8e4e04841c9e2f3ca022b053adb89a286ceffad0d69ae7b56f31cbe64", size = 785575, upload-time = "2024-12-09T13:33:11.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0b/008a340435fe8f0879e9d608f48af2737ad48440e09bd33b83b3fd03798b/fastmcp-0.4.1-py3-none-any.whl", hash = "sha256:664b42c376fb89ec90a50c9433f5a1f4d24f36696d6c41b024b427ae545f9619", size = 35282, upload-time = "2024-12-09T13:33:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, ] [[package]] @@ -247,9 +230,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, ] [[package]] @@ -259,28 +242,28 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196, upload-time = "2025-01-02T07:32:43.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, ] [[package]] name = "google-auth" -version = "2.38.0" +version = "2.40.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866, upload-time = "2025-01-23T01:05:29.119Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770, upload-time = "2025-01-23T01:05:26.572Z" }, + { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137 }, ] [[package]] name = "google-genai" -version = "1.10.0" +version = "1.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -291,48 +274,48 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/7a/224e2f70c835202042969685ee3da00a6475508d1b64f0f1e90144f96beb/google_genai-1.10.0.tar.gz", hash = "sha256:f59423e0f155dc66b7792c8a0e6724c75c72dc699d1eb7907d4d0006d4f6186f", size = 156355, upload-time = "2025-04-09T03:48:21.961Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/12/ad9f08be2ca85122ca50ac69ae70454f18a3c7d840bcc4ed43f517ab47be/google_genai-1.20.0.tar.gz", hash = "sha256:dccca78f765233844b1bd4f1f7a2237d9a76fe6038cf9aa72c0cd991e3c107b5", size = 201550 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/a0/56839a2e202d79c773edd1c1db124da8eb2a7b657267a888080b678d0369/google_genai-1.10.0-py3-none-any.whl", hash = "sha256:41b105a2fcf8a027fc45cc16694cd559b8cd1272eab7345ad58cfa2c353bf34f", size = 154705, upload-time = "2025-04-09T03:48:20.49Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b4/08f3ea414060a7e7d4436c08bb22d03dabef74cc05ef13ef8cd846156d5b/google_genai-1.20.0-py3-none-any.whl", hash = "sha256:ccd61d6ebcb14f5c778b817b8010e3955ae4f6ddfeaabf65f42f6d5e3e5a8125", size = 203039 }, ] [[package]] name = "greenlet" -version = "3.1.1" +version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752 } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" }, - { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" }, - { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517, upload-time = "2024-09-20T17:44:24.101Z" }, - { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload-time = "2024-09-20T17:08:40.577Z" }, - { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413, upload-time = "2024-09-20T17:08:31.728Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload-time = "2024-09-20T17:44:14.222Z" }, - { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198, upload-time = "2024-09-20T17:09:23.903Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930, upload-time = "2024-09-20T17:25:18.656Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219 }, + { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383 }, + { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422 }, + { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375 }, + { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627 }, + { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502 }, + { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498 }, + { url = "https://files.pythonhosted.org/packages/89/5f/b16dec0cbfd3070658e0d744487919740c6d45eb90946f6787689a7efbce/greenlet-3.2.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:751261fc5ad7b6705f5f76726567375bb2104a059454e0226e1eef6c756748ba", size = 1139977 }, + { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017 }, ] [[package]] name = "h11" -version = "0.14.0" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418, upload-time = "2022-09-25T15:40:01.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259, upload-time = "2022-09-25T15:39:59.68Z" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, ] [[package]] name = "httpcore" -version = "1.0.7" +version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196, upload-time = "2024-11-15T12:30:47.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551, upload-time = "2024-11-15T12:30:45.782Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, ] [[package]] @@ -345,41 +328,41 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +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/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] [[package]] name = "httpx-sse" version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624, upload-time = "2023-12-22T08:01:21.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819, upload-time = "2023-12-22T08:01:19.89Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] [[package]] name = "iniconfig" version = "2.1.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, upload-time = "2025-03-19T20:09:59.721Z" } +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/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] [[package]] name = "ipython" -version = "9.1.0" +version = "9.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -394,9 +377,9 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/9a/6b8984bedc990f3a4aa40ba8436dea27e23d26a64527de7c2e5e12e76841/ipython-9.1.0.tar.gz", hash = "sha256:a47e13a5e05e02f3b8e1e7a0f9db372199fe8c3763532fe7a1e0379e4e135f16", size = 4373688, upload-time = "2025-04-07T10:18:28.704Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/09/4c7e06b96fbd203e06567b60fb41b06db606b6a82db6db7b2c85bb72a15c/ipython-9.3.0.tar.gz", hash = "sha256:79eb896f9f23f50ad16c3bc205f686f6e030ad246cc309c6279a242b14afe9d8", size = 4426460 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/9d/4ff2adf55d1b6e3777b0303fdbe5b723f76e46cba4a53a32fe82260d2077/ipython-9.1.0-py3-none-any.whl", hash = "sha256:2df07257ec2f84a6b346b8d83100bcf8fa501c6e01ab75cd3799b0bb253b3d2a", size = 604053, upload-time = "2025-04-07T10:18:24.869Z" }, + { url = "https://files.pythonhosted.org/packages/3c/99/9ed3d52d00f1846679e3aa12e2326ac7044b5e7f90dc822b60115fa533ca/ipython-9.3.0-py3-none-any.whl", hash = "sha256:1a0b6dd9221a1f5dddf725b57ac0cb6fddc7b5f470576231ae9162b9b3455a04", size = 605320 }, ] [[package]] @@ -406,9 +389,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074 }, ] [[package]] @@ -418,9 +401,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "parso" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, ] [[package]] @@ -430,38 +413,38 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, ] [[package]] name = "jiter" -version = "0.9.0" +version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/c2/e4562507f52f0af7036da125bb699602ead37a2332af0788f8e0a3417f36/jiter-0.9.0.tar.gz", hash = "sha256:aadba0964deb424daa24492abc3d229c60c4a31bfee205aedbf1acc7639d7893", size = 162604, upload-time = "2025-03-10T21:37:03.278Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759 } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/44/e241a043f114299254e44d7e777ead311da400517f179665e59611ab0ee4/jiter-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6c4d99c71508912a7e556d631768dcdef43648a93660670986916b297f1c54af", size = 314654, upload-time = "2025-03-10T21:35:23.939Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1b/a7e5e42db9fa262baaa9489d8d14ca93f8663e7f164ed5e9acc9f467fc00/jiter-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f60fb8ce7df529812bf6c625635a19d27f30806885139e367af93f6e734ef58", size = 320909, upload-time = "2025-03-10T21:35:26.127Z" }, - { url = "https://files.pythonhosted.org/packages/60/bf/8ebdfce77bc04b81abf2ea316e9c03b4a866a7d739cf355eae4d6fd9f6fe/jiter-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51c4e1a4f8ea84d98b7b98912aa4290ac3d1eabfde8e3c34541fae30e9d1f08b", size = 341733, upload-time = "2025-03-10T21:35:27.94Z" }, - { url = "https://files.pythonhosted.org/packages/a8/4e/754ebce77cff9ab34d1d0fa0fe98f5d42590fd33622509a3ba6ec37ff466/jiter-0.9.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f4c677c424dc76684fea3e7285a7a2a7493424bea89ac441045e6a1fb1d7b3b", size = 365097, upload-time = "2025-03-10T21:35:29.605Z" }, - { url = "https://files.pythonhosted.org/packages/32/2c/6019587e6f5844c612ae18ca892f4cd7b3d8bbf49461ed29e384a0f13d98/jiter-0.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2221176dfec87f3470b21e6abca056e6b04ce9bff72315cb0b243ca9e835a4b5", size = 406603, upload-time = "2025-03-10T21:35:31.696Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/c9e6546c817ab75a1a7dab6dcc698e62e375e1017113e8e983fccbd56115/jiter-0.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c7adb66f899ffa25e3c92bfcb593391ee1947dbdd6a9a970e0d7e713237d572", size = 396625, upload-time = "2025-03-10T21:35:33.182Z" }, - { url = "https://files.pythonhosted.org/packages/be/bd/976b458add04271ebb5a255e992bd008546ea04bb4dcadc042a16279b4b4/jiter-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98d27330fdfb77913c1097a7aab07f38ff2259048949f499c9901700789ac15", size = 351832, upload-time = "2025-03-10T21:35:35.394Z" }, - { url = "https://files.pythonhosted.org/packages/07/51/fe59e307aaebec9265dbad44d9d4381d030947e47b0f23531579b9a7c2df/jiter-0.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eda3f8cc74df66892b1d06b5d41a71670c22d95a1ca2cbab73654745ce9d0419", size = 384590, upload-time = "2025-03-10T21:35:37.171Z" }, - { url = "https://files.pythonhosted.org/packages/db/55/5dcd2693794d8e6f4889389ff66ef3be557a77f8aeeca8973a97a7c00557/jiter-0.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dd5ab5ddc11418dce28343123644a100f487eaccf1de27a459ab36d6cca31043", size = 520690, upload-time = "2025-03-10T21:35:38.717Z" }, - { url = "https://files.pythonhosted.org/packages/54/d5/9f51dc90985e9eb251fbbb747ab2b13b26601f16c595a7b8baba964043bd/jiter-0.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:42f8a68a69f047b310319ef8e2f52fdb2e7976fb3313ef27df495cf77bcad965", size = 512649, upload-time = "2025-03-10T21:35:40.157Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e5/4e385945179bcf128fa10ad8dca9053d717cbe09e258110e39045c881fe5/jiter-0.9.0-cp311-cp311-win32.whl", hash = "sha256:a25519efb78a42254d59326ee417d6f5161b06f5da827d94cf521fed961b1ff2", size = 206920, upload-time = "2025-03-10T21:35:41.72Z" }, - { url = "https://files.pythonhosted.org/packages/4c/47/5e0b94c603d8e54dd1faab439b40b832c277d3b90743e7835879ab663757/jiter-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:923b54afdd697dfd00d368b7ccad008cccfeb1efb4e621f32860c75e9f25edbd", size = 210119, upload-time = "2025-03-10T21:35:43.46Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dd/6cefc6bd68b1c3c979cecfa7029ab582b57690a31cd2f346c4d0ce7951b6/jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978", size = 317473 }, + { url = "https://files.pythonhosted.org/packages/be/cf/fc33f5159ce132be1d8dd57251a1ec7a631c7df4bd11e1cd198308c6ae32/jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc", size = 321971 }, + { url = "https://files.pythonhosted.org/packages/68/a4/da3f150cf1d51f6c472616fb7650429c7ce053e0c962b41b68557fdf6379/jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d", size = 345574 }, + { url = "https://files.pythonhosted.org/packages/84/34/6e8d412e60ff06b186040e77da5f83bc158e9735759fcae65b37d681f28b/jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2", size = 371028 }, + { url = "https://files.pythonhosted.org/packages/fb/d9/9ee86173aae4576c35a2f50ae930d2ccb4c4c236f6cb9353267aa1d626b7/jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61", size = 491083 }, + { url = "https://files.pythonhosted.org/packages/d9/2c/f955de55e74771493ac9e188b0f731524c6a995dffdcb8c255b89c6fb74b/jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db", size = 388821 }, + { url = "https://files.pythonhosted.org/packages/81/5a/0e73541b6edd3f4aada586c24e50626c7815c561a7ba337d6a7eb0a915b4/jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5", size = 352174 }, + { url = "https://files.pythonhosted.org/packages/1c/c0/61eeec33b8c75b31cae42be14d44f9e6fe3ac15a4e58010256ac3abf3638/jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606", size = 391869 }, + { url = "https://files.pythonhosted.org/packages/41/22/5beb5ee4ad4ef7d86f5ea5b4509f680a20706c4a7659e74344777efb7739/jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605", size = 523741 }, + { url = "https://files.pythonhosted.org/packages/ea/10/768e8818538e5817c637b0df52e54366ec4cebc3346108a4457ea7a98f32/jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5", size = 514527 }, + { url = "https://files.pythonhosted.org/packages/73/6d/29b7c2dc76ce93cbedabfd842fc9096d01a0550c52692dfc33d3cc889815/jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7", size = 210765 }, + { url = "https://files.pythonhosted.org/packages/c2/c9/d394706deb4c660137caf13e33d05a031d734eb99c051142e039d8ceb794/jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812", size = 209234 }, ] [[package]] name = "joblib" version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746 }, ] [[package]] @@ -471,27 +454,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, ] [[package]] @@ -501,14 +484,14 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899 }, ] [[package]] name = "mcp" -version = "1.6.0" +version = "1.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -516,104 +499,106 @@ dependencies = [ { name = "httpx-sse" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "python-multipart" }, { name = "sse-starlette" }, { name = "starlette" }, - { name = "uvicorn" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/d2/f587cb965a56e992634bebc8611c5b579af912b74e04eb9164bd49527d21/mcp-1.6.0.tar.gz", hash = "sha256:d9324876de2c5637369f43161cd71eebfd803df5a95e46225cab8d280e366723", size = 200031, upload-time = "2025-03-27T16:46:32.336Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f2/dc2450e566eeccf92d89a00c3e813234ad58e2ba1e31d11467a09ac4f3b9/mcp-1.9.4.tar.gz", hash = "sha256:cfb0bcd1a9535b42edaef89947b9e18a8feb49362e1cc059d6e7fc636f2cb09f", size = 333294 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/30/20a7f33b0b884a9d14dd3aa94ff1ac9da1479fe2ad66dd9e2736075d2506/mcp-1.6.0-py3-none-any.whl", hash = "sha256:7bd24c6ea042dbec44c754f100984d186620d8b841ec30f1b19eda9b93a634d0", size = 76077, upload-time = "2025-03-27T16:46:29.919Z" }, + { url = "https://files.pythonhosted.org/packages/97/fc/80e655c955137393c443842ffcc4feccab5b12fa7cb8de9ced90f90e6998/mcp-1.9.4-py3-none-any.whl", hash = "sha256:7fcf36b62936adb8e63f89346bccca1268eeca9bf6dfb562ee10b1dfbda9dac0", size = 130232 }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] [[package]] name = "mypy" -version = "1.15.0" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, + { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139 } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload-time = "2025-02-05T03:50:17.287Z" }, - { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload-time = "2025-02-05T03:49:51.21Z" }, - { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload-time = "2025-02-05T03:50:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload-time = "2025-02-05T03:49:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload-time = "2025-02-05T03:49:07.707Z" }, - { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload-time = "2025-02-05T03:49:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" }, + { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498 }, + { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755 }, + { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138 }, + { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156 }, + { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426 }, + { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319 }, + { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773 }, ] [[package]] name = "mypy-extensions" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, ] [[package]] name = "nodeenv" version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, ] [[package]] name = "overrides" version = "7.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832 }, ] [[package]] name = "packaging" -version = "24.2" +version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, ] [[package]] name = "parso" version = "0.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, ] [[package]] name = "pastel" version = "0.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555, upload-time = "2020-09-16T19:21:12.43Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/f1/4594f5e0fcddb6953e5b8fe00da8c317b8b41b547e2b3ae2da7512943c62/pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d", size = 7555 } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955 }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, ] [[package]] @@ -623,94 +608,94 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ptyprocess" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, ] [[package]] name = "platformdirs" -version = "4.3.7" +version = "4.3.8" 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, upload-time = "2025-03-19T20:36:10.989Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567 }, ] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] [[package]] name = "poethepoet" -version = "0.33.1" +version = "0.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pastel" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/1d/ec87271390cc5fafdd5996137331ad3a7ce99b715e4ee68db554d202817f/poethepoet-0.33.1.tar.gz", hash = "sha256:8775e09b64f773278b5483659ff238a708723491efadeedd1c2cbf773558cb4c", size = 62536, upload-time = "2025-03-15T20:38:56.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/b1/d4f4361b278fae10f6074675385ce3acf53c647f8e6eeba22c652f8ba985/poethepoet-0.35.0.tar.gz", hash = "sha256:b396ae862d7626e680bbd0985b423acf71634ce93a32d8b5f38340f44f5fbc3e", size = 66006 } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/ea/c476bfec360eb6831ce46df2719f76d1132b9a87da11c302081a9def5fce/poethepoet-0.33.1-py3-none-any.whl", hash = "sha256:b86d80a81b2ca4e4ce8e8f716cc6004a1a1cdead027778bc07d1c26cb3664770", size = 83512, upload-time = "2025-03-15T20:38:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/38/08/abc2d7e2400dd8906e3208f9b88ac610f097d7ee0c7a1fa4a157b49a9e86/poethepoet-0.35.0-py3-none-any.whl", hash = "sha256:bed5ae1fd63f179dfa67aabb93fa253d79695c69667c927d8b24ff378799ea75", size = 87164 }, ] [[package]] name = "prompt-toolkit" -version = "3.0.50" +version = "3.0.51" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087, upload-time = "2025-01-20T15:55:35.072Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816, upload-time = "2025-01-20T15:55:29.98Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810 }, ] [[package]] name = "psutil" version = "7.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, - { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, - { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, - { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, ] [[package]] name = "ptyprocess" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, ] [[package]] name = "pure-eval" version = "0.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842 }, ] [[package]] name = "pyasn1" version = "0.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, ] [[package]] @@ -720,14 +705,14 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, ] [[package]] name = "pydantic" -version = "2.11.3" +version = "2.11.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -735,133 +720,135 @@ dependencies = [ { 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, upload-time = "2025-04-08T13:27:06.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591, upload-time = "2025-04-08T13:27:03.789Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782 }, ] [[package]] name = "pydantic-core" -version = "2.33.1" +version = "2.33.2" 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, upload-time = "2025-04-02T09:49:41.8Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } wheels = [ - { 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, upload-time = "2025-04-02T09:47:04.199Z" }, - { 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, upload-time = "2025-04-02T09:47:05.686Z" }, - { 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, upload-time = "2025-04-02T09:47:07.042Z" }, - { 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, upload-time = "2025-04-02T09:47:08.63Z" }, - { 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, upload-time = "2025-04-02T09:47:10.267Z" }, - { 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, upload-time = "2025-04-02T09:47:11.724Z" }, - { 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, upload-time = "2025-04-02T09:47:13.099Z" }, - { 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, upload-time = "2025-04-02T09:47:14.355Z" }, - { 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, upload-time = "2025-04-02T09:47:15.676Z" }, - { 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, upload-time = "2025-04-02T09:47:17Z" }, - { 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, upload-time = "2025-04-02T09:47:18.631Z" }, - { url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765, upload-time = "2025-04-02T09:47:20.34Z" }, - { url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688, upload-time = "2025-04-02T09:47:22.029Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185, upload-time = "2025-04-02T09:47:23.385Z" }, - { 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, upload-time = "2025-04-02T09:49:03.419Z" }, - { 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, upload-time = "2025-04-02T09:49:05.391Z" }, - { 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, upload-time = "2025-04-02T09:49:07.352Z" }, - { 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, upload-time = "2025-04-02T09:49:09.304Z" }, - { 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, upload-time = "2025-04-02T09:49:11.25Z" }, - { 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, upload-time = "2025-04-02T09:49:13.292Z" }, - { 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, upload-time = "2025-04-02T09:49:15.597Z" }, - { 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, upload-time = "2025-04-02T09:49:17.61Z" }, - { 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, upload-time = "2025-04-02T09:49:19.559Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 }, ] [[package]] name = "pydantic-settings" -version = "2.8.1" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550, upload-time = "2025-02-27T10:10:32.338Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839, upload-time = "2025-02-27T10:10:30.711Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 }, ] [[package]] name = "pygments" version = "2.19.1" 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, upload-time = "2025-01-06T17:26:30.443Z" } +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/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] [[package]] name = "pyright" -version = "1.1.399" +version = "1.1.402" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954, upload-time = "2025-04-10T04:40:25.703Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/04/ce0c132d00e20f2d2fb3b3e7c125264ca8b909e693841210534b1ea1752f/pyright-1.1.402.tar.gz", hash = "sha256:85a33c2d40cd4439c66aa946fd4ce71ab2f3f5b8c22ce36a623f59ac22937683", size = 3888207 } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584, upload-time = "2025-04-10T04:40:23.502Z" }, + { url = "https://files.pythonhosted.org/packages/fe/37/1a1c62d955e82adae588be8e374c7f77b165b6cb4203f7d581269959abbc/pyright-1.1.402-py3-none-any.whl", hash = "sha256:2c721f11869baac1884e846232800fe021c33f1b4acb3929cff321f7ea4e2982", size = 5624004 }, ] [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232 } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797 }, ] [[package]] name = "python-dotenv" version = "1.1.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, upload-time = "2025-03-25T10:14:56.835Z" } +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/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, ] [[package]] name = "python-multipart" version = "0.0.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, ] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -869,9 +856,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, ] [[package]] @@ -882,75 +869,75 @@ dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +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/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229 }, ] [[package]] name = "rsa" -version = "4.9" +version = "4.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711, upload-time = "2022-07-20T10:28:36.115Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315, upload-time = "2022-07-20T10:28:34.978Z" }, + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, ] [[package]] name = "ruamel-yaml" -version = "0.18.10" +version = "0.18.14" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447, upload-time = "2025-01-06T14:08:51.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/87/6da0df742a4684263261c253f00edd5829e6aca970fff69e75028cccc547/ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7", size = 145511 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729, upload-time = "2025-01-06T14:08:47.471Z" }, + { url = "https://files.pythonhosted.org/packages/af/6d/6fe4805235e193aad4aaf979160dd1f3c487c57d48b810c816e6e842171b/ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2", size = 118570 }, ] [[package]] name = "ruamel-yaml-clib" version = "0.2.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload-time = "2024-10-20T10:10:56.22Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224, upload-time = "2024-10-20T10:12:45.162Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480, upload-time = "2024-10-20T10:12:46.758Z" }, - { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068, upload-time = "2024-10-20T10:12:48.605Z" }, - { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012, upload-time = "2024-10-20T10:12:51.124Z" }, - { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352, upload-time = "2024-10-21T11:26:41.438Z" }, - { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344, upload-time = "2024-10-21T11:26:43.62Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498, upload-time = "2024-12-11T19:58:15.592Z" }, - { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205, upload-time = "2024-10-20T10:12:52.865Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185, upload-time = "2024-10-20T10:12:54.652Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224 }, + { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480 }, + { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068 }, + { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012 }, + { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352 }, + { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344 }, + { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498 }, + { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205 }, + { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185 }, ] [[package]] name = "ruff" -version = "0.11.5" +version = "0.11.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488, upload-time = "2025-04-10T17:13:29.369Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054 } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150, upload-time = "2025-04-10T17:12:37.886Z" }, - { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637, upload-time = "2025-04-10T17:12:41.602Z" }, - { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012, upload-time = "2025-04-10T17:12:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338, upload-time = "2025-04-10T17:12:47.172Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277, upload-time = "2025-04-10T17:12:50.628Z" }, - { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614, upload-time = "2025-04-10T17:12:53.783Z" }, - { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873, upload-time = "2025-04-10T17:12:56.956Z" }, - { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190, upload-time = "2025-04-10T17:13:00.194Z" }, - { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301, upload-time = "2025-04-10T17:13:03.246Z" }, - { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132, upload-time = "2025-04-10T17:13:06.209Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937, upload-time = "2025-04-10T17:13:08.855Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683, upload-time = "2025-04-10T17:13:11.378Z" }, - { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217, upload-time = "2025-04-10T17:13:14.565Z" }, - { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521, upload-time = "2025-04-10T17:13:17.8Z" }, - { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697, upload-time = "2025-04-10T17:13:20.582Z" }, - { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665, upload-time = "2025-04-10T17:13:23.349Z" }, - { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287, upload-time = "2025-04-10T17:13:26.538Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516 }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083 }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024 }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324 }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416 }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197 }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615 }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080 }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315 }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640 }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364 }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462 }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028 }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992 }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944 }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669 }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928 }, ] [[package]] @@ -960,9 +947,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/b1/9dba08e2d8ea739b58d1ac393c066169f1ca6506c86f0322d6e56e600ae2/sensai_utils-1.4.0.tar.gz", hash = "sha256:2d32bdcc91fd1428c5cae0181e98623142d2d5f7e115e23d585a842dd9dc59ba", size = 56317, upload-time = "2025-01-27T22:47:51.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/b1/9dba08e2d8ea739b58d1ac393c066169f1ca6506c86f0322d6e56e600ae2/sensai_utils-1.4.0.tar.gz", hash = "sha256:2d32bdcc91fd1428c5cae0181e98623142d2d5f7e115e23d585a842dd9dc59ba", size = 56317 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/df/49cc942b653427df0bbd3b9a2fb1e85652df3d4c1d9f6dcbfce1b6155716/sensai_utils-1.4.0-py3-none-any.whl", hash = "sha256:ed6fc57552620e43b33cf364ea0bc0fd7df39391069dd7b621b113ef55547507", size = 63214, upload-time = "2025-01-27T22:47:49.606Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/49cc942b653427df0bbd3b9a2fb1e85652df3d4c1d9f6dcbfce1b6155716/sensai_utils-1.4.0-py3-none-any.whl", hash = "sha256:ed6fc57552620e43b33cf364ea0bc0fd7df39391069dd7b621b113ef55547507", size = 63214 }, ] [[package]] @@ -973,7 +960,6 @@ dependencies = [ { name = "docstring-parser" }, { name = "dotenv" }, { name = "fastapi" }, - { name = "fastmcp" }, { name = "jinja2" }, { name = "joblib" }, { name = "mcp" }, @@ -1021,7 +1007,6 @@ requires-dist = [ { name = "docstring-parser", specifier = ">=0.16" }, { name = "dotenv", specifier = ">=0.9.9" }, { name = "fastapi", specifier = ">=0.115.12" }, - { name = "fastmcp", specifier = ">=0.4.1" }, { name = "google-genai", marker = "extra == 'google'", specifier = ">=1.8.0" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", marker = "extra == 'dev'" }, @@ -1053,61 +1038,60 @@ provides-extras = ["dev", "agno", "anthropic", "google"] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, ] [[package]] name = "smmap" version = "5.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, ] [[package]] name = "sqlalchemy" -version = "2.0.40" +version = "2.0.41" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/c3/3f2bfa5e4dcd9938405fe2fab5b6ab94a9248a4f9536ea2fd497da20525f/sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00", size = 9664299, upload-time = "2025-03-27T17:52:31.876Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424 } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/7e/55044a9ec48c3249bb38d5faae93f09579c35e862bb318ebd1ed7a1994a5/sqlalchemy-2.0.40-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6bacab7514de6146a1976bc56e1545bee247242fab030b89e5f70336fc0003e", size = 2114025, upload-time = "2025-03-27T18:49:29.456Z" }, - { url = "https://files.pythonhosted.org/packages/77/0f/dcf7bba95f847aec72f638750747b12d37914f71c8cc7c133cf326ab945c/sqlalchemy-2.0.40-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5654d1ac34e922b6c5711631f2da497d3a7bffd6f9f87ac23b35feea56098011", size = 2104419, upload-time = "2025-03-27T18:49:30.75Z" }, - { url = "https://files.pythonhosted.org/packages/75/70/c86a5c20715e4fe903dde4c2fd44fc7e7a0d5fb52c1b954d98526f65a3ea/sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35904d63412db21088739510216e9349e335f142ce4a04b69e2528020ee19ed4", size = 3222720, upload-time = "2025-03-27T18:44:29.871Z" }, - { url = "https://files.pythonhosted.org/packages/12/cf/b891a8c1d0c27ce9163361664c2128c7a57de3f35000ea5202eb3a2917b7/sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7a80ed86d6aaacb8160a1caef6680d4ddd03c944d985aecee940d168c411d1", size = 3222682, upload-time = "2025-03-27T18:55:20.097Z" }, - { url = "https://files.pythonhosted.org/packages/15/3f/7709d8c8266953d945435a96b7f425ae4172a336963756b58e996fbef7f3/sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:519624685a51525ddaa7d8ba8265a1540442a2ec71476f0e75241eb8263d6f51", size = 3159542, upload-time = "2025-03-27T18:44:31.333Z" }, - { url = "https://files.pythonhosted.org/packages/85/7e/717eaabaf0f80a0132dc2032ea8f745b7a0914451c984821a7c8737fb75a/sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ee5f9999a5b0e9689bed96e60ee53c3384f1a05c2dd8068cc2e8361b0df5b7a", size = 3179864, upload-time = "2025-03-27T18:55:21.784Z" }, - { url = "https://files.pythonhosted.org/packages/e4/cc/03eb5dfcdb575cbecd2bd82487b9848f250a4b6ecfb4707e834b4ce4ec07/sqlalchemy-2.0.40-cp311-cp311-win32.whl", hash = "sha256:c0cae71e20e3c02c52f6b9e9722bca70e4a90a466d59477822739dc31ac18b4b", size = 2084675, upload-time = "2025-03-27T18:48:55.915Z" }, - { url = "https://files.pythonhosted.org/packages/9a/48/440946bf9dc4dc231f4f31ef0d316f7135bf41d4b86aaba0c0655150d370/sqlalchemy-2.0.40-cp311-cp311-win_amd64.whl", hash = "sha256:574aea2c54d8f1dd1699449f332c7d9b71c339e04ae50163a3eb5ce4c4325ee4", size = 2110099, upload-time = "2025-03-27T18:48:57.45Z" }, - { url = "https://files.pythonhosted.org/packages/d1/7c/5fc8e802e7506fe8b55a03a2e1dab156eae205c91bee46305755e086d2e2/sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a", size = 1903894, upload-time = "2025-03-27T18:40:43.796Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/b00e3ffae32b74b5180e15d2ab4040531ee1bef4c19755fe7926622dc958/sqlalchemy-2.0.41-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6375cd674fe82d7aa9816d1cb96ec592bac1726c11e0cafbf40eeee9a4516b5f", size = 2121232 }, + { url = "https://files.pythonhosted.org/packages/ef/30/6547ebb10875302074a37e1970a5dce7985240665778cfdee2323709f749/sqlalchemy-2.0.41-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f8c9fdd15a55d9465e590a402f42082705d66b05afc3ffd2d2eb3c6ba919560", size = 2110897 }, + { url = "https://files.pythonhosted.org/packages/9e/21/59df2b41b0f6c62da55cd64798232d7349a9378befa7f1bb18cf1dfd510a/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f9dc8c44acdee06c8fc6440db9eae8b4af8b01e4b1aee7bdd7241c22edff4f", size = 3273313 }, + { url = "https://files.pythonhosted.org/packages/62/e4/b9a7a0e5c6f79d49bcd6efb6e90d7536dc604dab64582a9dec220dab54b6/sqlalchemy-2.0.41-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c11ceb9a1f482c752a71f203a81858625d8df5746d787a4786bca4ffdf71c6", size = 3273807 }, + { url = "https://files.pythonhosted.org/packages/39/d8/79f2427251b44ddee18676c04eab038d043cff0e764d2d8bb08261d6135d/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:911cc493ebd60de5f285bcae0491a60b4f2a9f0f5c270edd1c4dbaef7a38fc04", size = 3209632 }, + { url = "https://files.pythonhosted.org/packages/d4/16/730a82dda30765f63e0454918c982fb7193f6b398b31d63c7c3bd3652ae5/sqlalchemy-2.0.41-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03968a349db483936c249f4d9cd14ff2c296adfa1290b660ba6516f973139582", size = 3233642 }, + { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475 }, + { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903 }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224 }, ] [[package]] name = "sse-starlette" -version = "2.2.1" +version = "2.3.6" 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, upload-time = "2024-12-25T09:09:30.616Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/f4/989bc70cb8091eda43a9034ef969b25145291f3601703b82766e5172dfed/sse_starlette-2.3.6.tar.gz", hash = "sha256:0382336f7d4ec30160cf9ca0518962905e1b69b72d6c1c995131e0a703b436e3", size = 18284 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120, upload-time = "2024-12-25T09:09:26.761Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/78850ac6e79af5b9508f8841b0f26aa9fd329a1ba00bf65453c2d312bcc8/sse_starlette-2.3.6-py3-none-any.whl", hash = "sha256:d49a8285b182f6e2228e2609c350398b2ca2c36216c2675d875f81e93548f760", size = 10606 }, ] [[package]] @@ -1119,21 +1103,21 @@ dependencies = [ { name = "executing" }, { name = "pure-eval" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521 }, ] [[package]] name = "starlette" -version = "0.46.1" +version = "0.46.2" 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, upload-time = "2025-03-08T10:55:34.504Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995, upload-time = "2025-03-08T10:55:32.662Z" }, + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, ] [[package]] @@ -1143,18 +1127,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/f8/022d8704a3314f3e96dbd6bbd16ebe119ce30e35f41aabfa92345652fceb/syrupy-4.9.1.tar.gz", hash = "sha256:b7d0fcadad80a7d2f6c4c71917918e8ebe2483e8c703dfc8d49cdbb01081f9a4", size = 52492, upload-time = "2025-03-24T01:36:37.225Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/f8/022d8704a3314f3e96dbd6bbd16ebe119ce30e35f41aabfa92345652fceb/syrupy-4.9.1.tar.gz", hash = "sha256:b7d0fcadad80a7d2f6c4c71917918e8ebe2483e8c703dfc8d49cdbb01081f9a4", size = 52492 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/9d/aef9ec5fd5a4ee2f6a96032c4eda5888c5c7cec65cef6b28c4fc37671d88/syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda", size = 52214, upload-time = "2025-03-24T01:36:35.278Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9d/aef9ec5fd5a4ee2f6a96032c4eda5888c5c7cec65cef6b28c4fc37671d88/syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda", size = 52214 }, ] [[package]] name = "tokenize-rt" -version = "6.1.0" +version = "6.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/0a/5854d8ced8c1e00193d1353d13db82d7f813f99bd5dcb776ce3e2a4c0d19/tokenize_rt-6.1.0.tar.gz", hash = "sha256:e8ee836616c0877ab7c7b54776d2fefcc3bde714449a206762425ae114b53c86", size = 5506, upload-time = "2024-10-22T00:14:59.189Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/ed/8f07e893132d5051d86a553e749d5c89b2a4776eb3a579b72ed61f8559ca/tokenize_rt-6.2.0.tar.gz", hash = "sha256:8439c042b330c553fdbe1758e4a05c0ed460dbbbb24a606f11f0dee75da4cad6", size = 5476 } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/ba/576aac29b10dfa49a6ce650001d1bb31f81e734660555eaf144bfe5b8995/tokenize_rt-6.1.0-py2.py3-none-any.whl", hash = "sha256:d706141cdec4aa5f358945abe36b911b8cbdc844545da99e811250c0cee9b6fc", size = 6015, upload-time = "2024-10-22T00:14:57.469Z" }, + { url = "https://files.pythonhosted.org/packages/33/f0/3fe8c6e69135a845f4106f2ff8b6805638d4e85c264e70114e8126689587/tokenize_rt-6.2.0-py2.py3-none-any.whl", hash = "sha256:a152bf4f249c847a66497a4a95f63376ed68ac6abf092a2f7cfb29d044ecff44", size = 6004 }, ] [[package]] @@ -1164,51 +1148,51 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/55/b128ee446606e9f2e49582bb1e462a255317401fd7adb0b2114e50006d01/toml_sort-0.24.2.tar.gz", hash = "sha256:20cb7c5e9de9c871990f1594f028aaf8bd0f78d7ce37995a22289dc157a45b79", size = 17973, upload-time = "2024-11-19T14:03:05.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/55/b128ee446606e9f2e49582bb1e462a255317401fd7adb0b2114e50006d01/toml_sort-0.24.2.tar.gz", hash = "sha256:20cb7c5e9de9c871990f1594f028aaf8bd0f78d7ce37995a22289dc157a45b79", size = 17973 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/55/7a7c02de848eef670bd018b62122ce32a6e227203e5a626c96d4935958a6/toml_sort-0.24.2-py3-none-any.whl", hash = "sha256:d81d299789a1fd9dd306a4021951eab5fc0c5486599e277fcf8142c7735f3308", size = 20162, upload-time = "2024-11-19T14:03:03.338Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/7a7c02de848eef670bd018b62122ce32a6e227203e5a626c96d4935958a6/toml_sort-0.24.2-py3-none-any.whl", hash = "sha256:d81d299789a1fd9dd306a4021951eab5fc0c5486599e277fcf8142c7735f3308", size = 20162 }, ] [[package]] name = "tomli" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] [[package]] name = "tomlkit" -version = "0.13.2" +version = "0.13.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885, upload-time = "2024-08-14T08:19:41.488Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955, upload-time = "2024-08-14T08:19:40.05Z" }, + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901 }, ] [[package]] name = "traitlets" version = "5.14.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621 } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 }, ] [[package]] name = "typer" -version = "0.15.2" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1216,88 +1200,88 @@ 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, upload-time = "2025-02-27T19:17:34.807Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625 } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061, upload-time = "2025-02-27T19:17:32.111Z" }, + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317 }, ] [[package]] name = "types-pyyaml" -version = "6.0.12.20250402" +version = "6.0.12.20250516" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/68/609eed7402f87c9874af39d35942744e39646d1ea9011765ec87b01b2a3c/types_pyyaml-6.0.12.20250402.tar.gz", hash = "sha256:d7c13c3e6d335b6af4b0122a01ff1d270aba84ab96d1a1a1063ecba3e13ec075", size = 17282, upload-time = "2025-04-02T02:56:00.235Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/22/59e2aeb48ceeee1f7cd4537db9568df80d62bdb44a7f9e743502ea8aab9c/types_pyyaml-6.0.12.20250516.tar.gz", hash = "sha256:9f21a70216fc0fa1b216a8176db5f9e0af6eb35d2f2932acb87689d03a5bf6ba", size = 17378 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/56/1fe61db05685fbb512c07ea9323f06ea727125951f1eb4dff110b3311da3/types_pyyaml-6.0.12.20250402-py3-none-any.whl", hash = "sha256:652348fa9e7a203d4b0d21066dfb00760d3cbd5a15ebb7cf8d33c88a49546681", size = 20329, upload-time = "2025-04-02T02:55:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/e0af6f7f6a260d9af67e1db4f54d732abad514252a7a378a6c4d17dd1036/types_pyyaml-6.0.12.20250516-py3-none-any.whl", hash = "sha256:8478208feaeb53a34cb5d970c56a7cd76b72659442e733e268a94dc72b2d0530", size = 20312 }, ] [[package]] name = "typing-extensions" -version = "4.13.2" +version = "4.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 }, ] [[package]] name = "typing-inspection" -version = "0.4.0" +version = "0.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222, upload-time = "2025-02-25T17:27:59.638Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } wheels = [ - { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125, upload-time = "2025-02-25T17:27:57.754Z" }, + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, ] [[package]] name = "urllib3" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, ] [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.34.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568, upload-time = "2024-12-15T13:33:30.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631 } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431 }, ] [[package]] name = "wcwidth" version = "0.2.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, ] [[package]] name = "websockets" version = "15.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, ] From a0c820cc33d9b0048bacfdc4cf602d0a86f730ec Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 15:51:41 +0200 Subject: [PATCH 18/28] Removed unused method --- src/serena/agent.py | 6 ++--- src/serena/mcp.py | 56 +++------------------------------------------ 2 files changed, 6 insertions(+), 56 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index 98b5fc20..a169e2fc 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -107,12 +107,12 @@ def get_serena_managed_dir(project_root: str | Path) -> str: def is_running_in_docker() -> bool: """Check if we're running inside a Docker container.""" # Check for Docker-specific files - if os.path.exists('/.dockerenv'): + if os.path.exists("/.dockerenv"): return True # Check cgroup for docker references try: - with open('/proc/self/cgroup', 'r') as f: - return 'docker' in f.read() + with open("/proc/self/cgroup") as f: + return "docker" in f.read() except FileNotFoundError: return False diff --git a/src/serena/mcp.py b/src/serena/mcp.py index db188a2f..e307301e 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -105,53 +105,6 @@ def create_mcp_server_and_agent( log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, tool_timeout: float | None = None, -) -> tuple[FastMCP, "ProcessIsolatedSerenaAgent"]: - """ - Create an MCP server (legacy - now uses process isolation). - - :param project: "Either an absolute path to the project directory or a name of an already registered project. " - "If the project passed here hasn't been registered yet, it will be registered automatically and can be activated by its name " - "afterwards. - :param host: The host to bind to - :param port: The port to bind to - :param context: The context name or path to context file - :param modes: List of mode names or paths to mode files - :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. - :param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then. - If not specified, will take the value from the serena configuration. - :param log_level: Log level. If not specified, will take the value from the serena configuration. - :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. - This is useful for debugging language server issues. - :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. - """ - # Delegate to process-isolated version for safety - mcp, process_agent = create_mcp_server_and_process_isolated_agent( - project=project, - host=host, - port=port, - context=context, - modes=modes, - enable_web_dashboard=enable_web_dashboard, - enable_gui_log_window=enable_gui_log_window, - log_level=log_level, - trace_lsp_communication=trace_lsp_communication, - tool_timeout=tool_timeout, - ) - # Return a dummy agent for compatibility - return mcp, process_agent - - -def create_mcp_server_and_process_isolated_agent( - project: str | None, - host: str = "0.0.0.0", - port: int = 8000, - context: str = DEFAULT_CONTEXT, - modes: Sequence[str] = DEFAULT_MODES, - enable_web_dashboard: bool | None = None, - enable_gui_log_window: bool | None = None, - log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, - trace_lsp_communication: bool | None = None, - tool_timeout: float | None = None, ) -> tuple[FastMCP, "ProcessIsolatedSerenaAgent"]: """ Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. @@ -218,11 +171,8 @@ def create_mcp_server_and_process_isolated_agent( mcp._tool_manager._tools = {} for tool_name in tool_names: - # Create ProcessIsolatedTool instance that can be used with make_tool - # Metadata is extracted from ToolRegistry using classmethods - tool_instance = ProcessIsolatedTool(process_agent=process_agent, tool_name=tool_name) - # Use the original make_tool function to get full docstring parsing - mcp_tool = make_tool(tool_instance) + process_isolated_tool = ProcessIsolatedTool(process_agent=process_agent, tool_name=tool_name) + mcp_tool = make_tool(process_isolated_tool) mcp._tool_manager._tools[tool_name] = mcp_tool @asynccontextmanager @@ -383,7 +333,7 @@ def start_mcp_server( project_file = project_file_arg if project_file_arg is not None else project_file_opt # Use process isolation by default to prevent asyncio event loop contamination - mcp_server, agent = create_mcp_server_and_process_isolated_agent( + mcp_server, agent = create_mcp_server_and_agent( project=project_file, host=host, port=port, From ad96a7b25567f28a8e1bc689d4d56f54bf74c81d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 16:14:40 +0200 Subject: [PATCH 19/28] Minor renaming --- src/serena/mcp.py | 6 +++++- src/serena/process_isolated_agent.py | 15 ++++++++++----- test/serena/test_make_tool_process_isolation.py | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index e307301e..a7f9ea2a 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -160,8 +160,12 @@ def create_mcp_server_and_agent( return # Get tool names from process-isolated agent + # Tools may change as a result of project activation. + # NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not, + # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, + # so we need to register all tools at startup, unfortunately. try: - tool_names = process_agent.get_tool_instances() + tool_names = process_agent.get_exposed_tool_names() except Exception as e: log.error(f"Failed to get tool names from process agent: {e}") return diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 543b5ec2..741e848c 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -80,8 +80,8 @@ class SerenaAgentWorker: return self._is_language_server_running(request_id) elif method == "reset_language_server": return self._reset_language_server(request_id) - elif method == "get_tool_instances": - return self._get_tool_instances(request_id) + elif method == "get_exposed_tool_names": + return self._get_exposed_tool_names(request_id) else: return {"id": request_id, "error": f"Unknown method: {method}"} @@ -160,7 +160,7 @@ class SerenaAgentWorker: except Exception as e: return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} - def _get_tool_instances(self, request_id: str) -> dict[str, Any]: + def _get_exposed_tool_names(self, request_id: str) -> dict[str, Any]: """Get exposed tool names for MCP tool creation.""" if self.agent is None: return {"id": request_id, "error": "SerenaAgent not initialized"} @@ -314,9 +314,14 @@ class ProcessIsolatedSerenaAgent: """Reset the language server.""" self._make_request_with_result("reset_language_server") - def get_tool_instances(self) -> list[str]: + def get_exposed_tool_names(self) -> list[str]: """Get tool names for MCP tool creation.""" - return self._make_request_with_result("get_tool_instances") + return self._make_request_with_result("get_exposed_tool_names") + + def get_exposed_tool_instances(self) -> list[ToolInterface]: + """Get exposed tool instances for MCP tool creation.""" + tool_names = self.get_exposed_tool_names() + return [ProcessIsolatedTool(self, tool_name) for tool_name in tool_names] def __enter__(self) -> Self: self.start() diff --git a/test/serena/test_make_tool_process_isolation.py b/test/serena/test_make_tool_process_isolation.py index f0fdda26..3404aa1a 100644 --- a/test/serena/test_make_tool_process_isolation.py +++ b/test/serena/test_make_tool_process_isolation.py @@ -141,7 +141,7 @@ class TestMakeToolProcessIsolation: # Get tool names from both agents regular_active_tools = set(regular_agent.get_active_tool_names()) regular_all_tools = set(tool.get_name_from_cls() for tool in regular_agent.get_exposed_tool_instances()) - isolated_tool_names = set(process_isolated_agent.get_tool_instances()) + isolated_tool_names = set(process_isolated_agent.get_exposed_tool_names()) # The process isolated agent should have all tools (exposed, not just active) assert regular_all_tools == isolated_tool_names, ( From b284eed69c8fe24f07240ffd449508bfb6253ac4 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 16:36:31 +0200 Subject: [PATCH 20/28] Replaced string interfaces by enum --- src/serena/process_isolated_agent.py | 63 ++++++++++++++++------------ 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 741e848c..12a80714 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -13,6 +13,7 @@ import queue import threading import time import traceback +from enum import StrEnum from typing import Any, Self from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata @@ -70,20 +71,21 @@ class SerenaAgentWorker: params = request.get("params", {}) request_id = request["id"] - if method == "initialize": - return self._initialize(request_id, params) - elif method == "tool_call": - return self._tool_call(request_id, params) - elif method == "get_active_tool_names": - return self._get_active_tool_names(request_id) - elif method == "is_language_server_running": - return self._is_language_server_running(request_id) - elif method == "reset_language_server": - return self._reset_language_server(request_id) - elif method == "get_exposed_tool_names": - return self._get_exposed_tool_names(request_id) - else: - return {"id": request_id, "error": f"Unknown method: {method}"} + match method: + case self.RequestMethod.INITIALIZE: + return self._initialize(request_id, params) + case self.RequestMethod.TOOL_CALL: + return self._tool_call(request_id, params) + case self.RequestMethod.GET_ACTIVE_TOOL_NAMES: + return self._get_active_tool_names(request_id) + case self.RequestMethod.IS_LANGUAGE_SERVER_RUNNING: + return self._is_language_server_running(request_id) + case self.RequestMethod.RESET_LANGUAGE_SERVER: + return self._reset_language_server(request_id) + case self.RequestMethod.GET_EXPOSED_TOOL_NAMES: + return self._get_exposed_tool_names(request_id) + case _: + return {"id": request_id, "error": f"Unknown method: {method}"} except Exception as e: return {"id": request.get("id"), "error": str(e), "traceback": traceback.format_exc()} @@ -183,6 +185,16 @@ class SerenaAgentWorker: log.error(f"Error stopping language server: {e}") self.agent = None + class RequestMethod(StrEnum): + """Enumeration of available request methods for type-safe IPC.""" + + INITIALIZE = "initialize" + TOOL_CALL = "tool_call" + GET_ACTIVE_TOOL_NAMES = "get_active_tool_names" + IS_LANGUAGE_SERVER_RUNNING = "is_language_server_running" + RESET_LANGUAGE_SERVER = "reset_language_server" + GET_EXPOSED_TOOL_NAMES = "get_exposed_tool_names" + class ProcessIsolatedSerenaAgent: """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" @@ -213,7 +225,7 @@ class ProcessIsolatedSerenaAgent: # Initialize the agent in the worker process try: - self._make_request_with_result("initialize", {"config": self.serena_config.to_dict()}) + self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, {"config": self.serena_config.to_dict()}) except Exception as e: self.stop() raise RuntimeError(f"Failed to initialize SerenaAgent: {e}") from e @@ -256,7 +268,7 @@ class ProcessIsolatedSerenaAgent: log.info("Process-isolated SerenaAgent stopped") - def _make_request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]: + def _make_request(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> dict[str, Any]: """Make a request to the worker process.""" if self.process is None or not self.process.is_alive(): raise RuntimeError("Worker process is not running") @@ -291,7 +303,7 @@ class ProcessIsolatedSerenaAgent: raise TimeoutError(f"Request {request_id} timed out after {timeout} seconds") - def _make_request_with_result(self, method: str, params: dict[str, Any] | None = None) -> Any: + def _make_request_with_result(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> Any: """Make a request and return the result, raising an exception if there's an error.""" response = self._make_request(method, params) if "error" in response: @@ -300,28 +312,25 @@ class ProcessIsolatedSerenaAgent: def tool_call(self, tool_name: str, **tool_params: Any) -> str: """Call a tool in the worker process.""" - return self._make_request_with_result("tool_call", {"tool_name": tool_name, "tool_params": tool_params}) + return self._make_request_with_result( + SerenaAgentWorker.RequestMethod.TOOL_CALL, {"tool_name": tool_name, "tool_params": tool_params} + ) def get_active_tool_names(self) -> list[str]: """Get list of active tool names.""" - return self._make_request_with_result("get_active_tool_names") + return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_ACTIVE_TOOL_NAMES) def is_language_server_running(self) -> bool: """Check if language server is running.""" - return self._make_request_with_result("is_language_server_running") + return self._make_request_with_result(SerenaAgentWorker.RequestMethod.IS_LANGUAGE_SERVER_RUNNING) def reset_language_server(self) -> None: """Reset the language server.""" - self._make_request_with_result("reset_language_server") + self._make_request_with_result(SerenaAgentWorker.RequestMethod.RESET_LANGUAGE_SERVER) def get_exposed_tool_names(self) -> list[str]: """Get tool names for MCP tool creation.""" - return self._make_request_with_result("get_exposed_tool_names") - - def get_exposed_tool_instances(self) -> list[ToolInterface]: - """Get exposed tool instances for MCP tool creation.""" - tool_names = self.get_exposed_tool_names() - return [ProcessIsolatedTool(self, tool_name) for tool_name in tool_names] + return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_EXPOSED_TOOL_NAMES) def __enter__(self) -> Self: self.start() From fd90ec471df1a4ab306c4160fcf6441d7f3d265d Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 17:47:36 +0200 Subject: [PATCH 21/28] Brought normal and isolated agent interfaces closer together --- src/serena/mcp.py | 2 +- src/serena/process_isolated_agent.py | 92 +++++++++++++++++++++++++--- 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index a7f9ea2a..e8f4ec66 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -144,7 +144,7 @@ def create_mcp_server_and_agent( # Create process-isolated agent with the configuration from serena.process_isolated_agent import ProcessIsolatedSerenaAgent - process_agent = ProcessIsolatedSerenaAgent(serena_config) + process_agent = ProcessIsolatedSerenaAgent(serena_config=serena_config) except Exception as e: show_fatal_exception_safe(e) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 12a80714..d8e3ec5b 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -13,12 +13,14 @@ import queue import threading import time import traceback +from collections.abc import Callable from enum import StrEnum -from typing import Any, Self +from typing import Any, Literal, Self from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry +from serena.config import SerenaAgentContext, SerenaAgentMode log = logging.getLogger(__name__) @@ -93,10 +95,37 @@ class SerenaAgentWorker: def _initialize(self, request_id: str, params: dict[str, Any]) -> dict[str, Any]: """Initialize the SerenaAgent.""" try: - config_dict = params["config"] - serena_config = SerenaConfig.from_dict(config_dict) + # Extract all possible initialization parameters + project = params.get("project") + project_activation_callback = params.get("project_activation_callback") + serena_config = params.get("serena_config") + context = params.get("context") + modes = params.get("modes") + enable_web_dashboard = params.get("enable_web_dashboard") + enable_gui_log_window = params.get("enable_gui_log_window") + log_level = params.get("log_level") + trace_lsp_communication = params.get("trace_lsp_communication") + tool_timeout = params.get("tool_timeout") - self.agent = SerenaAgent(serena_config=serena_config) + # Handle legacy config_dict parameter for backward compatibility + if "config" in params and serena_config is None: + config_dict = params["config"] + serena_config = SerenaConfig.from_dict(config_dict) + elif serena_config is not None and isinstance(serena_config, dict): + serena_config = SerenaConfig.from_dict(serena_config) + + self.agent = SerenaAgent( + project=project, + project_activation_callback=project_activation_callback, + serena_config=serena_config, + context=context, + modes=modes, + enable_web_dashboard=enable_web_dashboard, + enable_gui_log_window=enable_gui_log_window, + log_level=log_level, + trace_lsp_communication=trace_lsp_communication, + tool_timeout=tool_timeout, + ) return {"id": request_id, "result": "SerenaAgent initialized successfully"} except Exception as e: @@ -199,8 +228,39 @@ class SerenaAgentWorker: class ProcessIsolatedSerenaAgent: """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" - def __init__(self, serena_config: SerenaConfigBase): - self.serena_config = serena_config + def __init__( + self, + project: str | None = None, + project_activation_callback: Callable[[], None] | None = None, + serena_config: SerenaConfigBase | None = None, + context: SerenaAgentContext | None = None, + modes: list[SerenaAgentMode] | None = None, + enable_web_dashboard: bool | None = None, + enable_gui_log_window: bool | None = None, + log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, + trace_lsp_communication: bool | None = None, + tool_timeout: float | None = None, + ): + # Store all initialization parameters to pass to worker + self._init_params = { + "project": project, + "project_activation_callback": project_activation_callback, + "serena_config": serena_config, + "context": context, + "modes": modes, + "enable_web_dashboard": enable_web_dashboard, + "enable_gui_log_window": enable_gui_log_window, + "log_level": log_level, + "trace_lsp_communication": trace_lsp_communication, + "tool_timeout": tool_timeout, + } + + # Keep serena_config for compatibility + if serena_config is not None: + self.serena_config = serena_config + else: + # Use the default config loader + self.serena_config = SerenaConfig.from_config_file() self.process: multiprocessing.Process | None = None self.request_queue: multiprocessing.Queue | None = None self.response_queue: multiprocessing.Queue | None = None @@ -225,7 +285,20 @@ class ProcessIsolatedSerenaAgent: # Initialize the agent in the worker process try: - self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, {"config": self.serena_config.to_dict()}) + # Prepare initialization parameters, converting complex objects to dict if present + init_params = self._init_params.copy() + if init_params["serena_config"] is not None and hasattr(init_params["serena_config"], "to_dict"): + # Convert SerenaConfigBase to dict for serialization + serena_config_obj = init_params["serena_config"] + init_params["serena_config"] = serena_config_obj.to_dict() # type: ignore + # Note: context and modes are not serializable across processes, + # so they will be None and SerenaAgent will use defaults + init_params["context"] = None + init_params["modes"] = None + # project_activation_callback cannot be serialized across processes + init_params["project_activation_callback"] = None + + self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, init_params) except Exception as e: self.stop() raise RuntimeError(f"Failed to initialize SerenaAgent: {e}") from e @@ -316,6 +389,11 @@ class ProcessIsolatedSerenaAgent: SerenaAgentWorker.RequestMethod.TOOL_CALL, {"tool_name": tool_name, "tool_params": tool_params} ) + def get_tool(self, tool_cls: type[Tool]) -> "ProcessIsolatedTool": + """Get a process-isolated tool that delegates to this agent.""" + tool_name = tool_cls.get_name_from_cls() + return ProcessIsolatedTool(self, tool_name) + def get_active_tool_names(self) -> list[str]: """Get list of active tool names.""" return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_ACTIVE_TOOL_NAMES) From 1327c7a118d91952959200331808dd62af3e3ce3 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 17:47:46 +0200 Subject: [PATCH 22/28] Tests for isolated agent --- pyproject.toml | 1 + test/serena/test_serena_agent.py | 75 ++++++++++++++++++++++++-------- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4efcecab..4132dd7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -260,4 +260,5 @@ markers = [ "typescript: language server running for TypeScript", "php: language server running for PHP", "snapshot: snapshot tests", + "isolated_process: test runs with process isolated agent", ] diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 49a47d88..853d86a1 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -7,7 +7,8 @@ import pytest from multilspy.multilspy_config import Language from serena.agent import FindReferencingSymbolsTool, FindSymbolTool, Project, ProjectConfig, SerenaAgent, SerenaConfigBase -from test.conftest import LanguageParamRequest, get_repo_path +from serena.process_isolated_agent import ProcessIsolatedSerenaAgent +from test.conftest import get_repo_path @dataclass @@ -55,13 +56,35 @@ def serena_config(): @pytest.fixture -def serena_agent(request: LanguageParamRequest, serena_config) -> SerenaAgent: +def serena_agent(request: pytest.FixtureRequest, serena_config): language = Language(request.param) project_name = f"test_repo_{language}" - return SerenaAgent(project=project_name, serena_config=serena_config) + + # Check if this test should use process isolation by looking at the test parameters + isolated_process = False + if hasattr(request, "node") and hasattr(request.node, "callspec"): + # Get the isolated_process parameter value from the test + params = request.node.callspec.params + isolated_process = params.get("isolated_process", False) + + if isolated_process: + agent = ProcessIsolatedSerenaAgent(project=project_name, serena_config=serena_config) + agent.start() + + # Add cleanup to stop the process + def cleanup(): + agent.stop() + + request.addfinalizer(cleanup) + return agent + else: + return SerenaAgent(project=project_name, serena_config=serena_config) class TestSerenaAgent: + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,symbol_name,expected_kind,expected_file", [ @@ -74,16 +97,20 @@ class TestSerenaAgent: ], indirect=["serena_agent"], ) - def test_find_symbol(self, serena_agent: SerenaAgent, symbol_name: str, expected_kind: str, expected_file: str): + def test_find_symbol(self, serena_agent, symbol_name: str, expected_kind: str, expected_file: str, isolated_process: bool): agent = serena_agent find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply(symbol_name) + result = find_symbol_tool.apply_ex(name_path=symbol_name) + symbols = json.loads(result) assert any( symbol_name in s["name_path"] and expected_kind.lower() in s["kind"].lower() and expected_file in s["relative_path"] for s in symbols - ), f"Expected to find {symbol_name} ({expected_kind}) in {expected_file} for {agent.get_active_project().language.name}" + ), f"Expected to find {symbol_name} ({expected_kind}) in {expected_file}" + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,symbol_name,def_file,ref_file", [ @@ -108,23 +135,30 @@ class TestSerenaAgent: ], indirect=["serena_agent"], ) - def test_find_symbol_references(self, serena_agent: SerenaAgent, symbol_name: str, def_file: str, ref_file: str) -> None: + def test_find_symbol_references(self, serena_agent, symbol_name: str, def_file: str, ref_file: str, isolated_process: bool) -> None: agent = serena_agent + # Find the symbol location first find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply(symbol_name, relative_path=def_file) + result = find_symbol_tool.apply_ex(name_path=symbol_name, relative_path=def_file) + time.sleep(1) symbols = json.loads(result) # Find the definition def_symbol = symbols[0] + # Now find references find_refs_tool = agent.get_tool(FindReferencingSymbolsTool) - result = find_refs_tool.apply(name_path=def_symbol["name_path"], relative_path=def_symbol["relative_path"]) + result = find_refs_tool.apply_ex(name_path=def_symbol["name_path"], relative_path=def_symbol["relative_path"]) + refs = json.loads(result) assert any( ref["relative_path"] == ref_file for ref in refs - ), f"Expected to find reference to {symbol_name} in {ref_file} for {agent._active_project.language.name}. refs={refs}" + ), f"Expected to find reference to {symbol_name} in {ref_file}. refs={refs}" + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,name_path,substring_matching,expected_symbol_name,expected_kind,expected_file", [ @@ -193,16 +227,18 @@ class TestSerenaAgent: ) def test_find_symbol_name_path( self, - serena_agent: SerenaAgent, + serena_agent, name_path: str, substring_matching: bool, expected_symbol_name: str, expected_kind: str, expected_file: str, + isolated_process: bool, ): agent = serena_agent + find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply( + result = find_symbol_tool.apply_ex( name_path=name_path, depth=0, relative_path=None, @@ -211,6 +247,7 @@ class TestSerenaAgent: exclude_kinds=None, substring_matching=substring_matching, ) + symbols = json.loads(result) assert any( expected_symbol_name == s["name_path"].split("/")[-1] @@ -219,6 +256,9 @@ class TestSerenaAgent: for s in symbols ), f"Expected to find {name_path} ({expected_kind}) in {expected_file} for {agent._active_project.language.name}. Symbols: {symbols}" + @pytest.mark.parametrize( + "isolated_process", [pytest.param(False, id="direct"), pytest.param(True, id="isolated", marks=pytest.mark.isolated_process)] + ) @pytest.mark.parametrize( "serena_agent,name_path", [ @@ -239,17 +279,18 @@ class TestSerenaAgent: ) def test_find_symbol_name_path_no_match( self, - serena_agent: SerenaAgent, + serena_agent, name_path: str, + isolated_process: bool, ): agent = serena_agent + find_symbol_tool = agent.get_tool(FindSymbolTool) - result = find_symbol_tool.apply( + result = find_symbol_tool.apply_ex( name_path=name_path, depth=0, substring_matching=True, ) + symbols = json.loads(result) - assert ( - not symbols - ), f"Expected to find no symbols for {name_path} for {agent._active_project.language.name}. Symbols found: {symbols}" + assert not symbols, f"Expected to find no symbols for {name_path}. Symbols found: {symbols}" From bc6b0f2ebaffe6b5b0866a5d5be1dbba67d9ffff Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 17:57:36 +0200 Subject: [PATCH 23/28] Replaced queue-based approach to worker with pipe-based one --- src/serena/process_isolated_agent.py | 166 +++++++++++++-------------- 1 file changed, 77 insertions(+), 89 deletions(-) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index d8e3ec5b..be6234b3 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -9,12 +9,10 @@ This module provides: import logging import multiprocessing -import queue -import threading -import time import traceback from collections.abc import Callable from enum import StrEnum +from multiprocessing.connection import Connection from typing import Any, Literal, Self from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata @@ -28,37 +26,40 @@ log = logging.getLogger(__name__) class SerenaAgentWorker: """Worker process that hosts the actual SerenaAgent.""" - def __init__(self, request_queue: multiprocessing.Queue, response_queue: multiprocessing.Queue): - self.request_queue = request_queue - self.response_queue = response_queue + def __init__(self, conn: Connection): + self.conn = conn self.agent: SerenaAgent | None = None - self.running = True def run(self) -> None: """Main worker loop - runs in separate process.""" try: log.info("SerenaAgent worker process started") - while self.running: + while True: try: - # Wait for requests with timeout to allow clean shutdown - request = self.request_queue.get(timeout=1.0) + # Block until request received or connection closed + request = self.conn.recv() if request is None: # Shutdown signal break response = self._handle_request(request) - self.response_queue.put(response) + self.conn.send(response) - except queue.Empty: - continue + except EOFError: + # Connection closed - parent process terminated + log.info("Connection closed, worker shutting down") + break except Exception as e: log.error(f"Error in worker process: {e}") - response = { - "id": getattr(request, "id", None), - "error": f"Worker process error: {e!s}", - "traceback": traceback.format_exc(), - } - self.response_queue.put(response) + try: + response = { + "error": f"Worker process error: {e!s}", + "traceback": traceback.format_exc(), + } + self.conn.send(response) + except (EOFError, BrokenPipeError): + # Connection is broken, can't send error response + break except Exception as e: log.error(f"Fatal error in worker process: {e}") @@ -71,28 +72,27 @@ class SerenaAgentWorker: try: method = request["method"] params = request.get("params", {}) - request_id = request["id"] match method: case self.RequestMethod.INITIALIZE: - return self._initialize(request_id, params) + return self._initialize(params) case self.RequestMethod.TOOL_CALL: - return self._tool_call(request_id, params) + return self._tool_call(params) case self.RequestMethod.GET_ACTIVE_TOOL_NAMES: - return self._get_active_tool_names(request_id) + return self._get_active_tool_names() case self.RequestMethod.IS_LANGUAGE_SERVER_RUNNING: - return self._is_language_server_running(request_id) + return self._is_language_server_running() case self.RequestMethod.RESET_LANGUAGE_SERVER: - return self._reset_language_server(request_id) + return self._reset_language_server() case self.RequestMethod.GET_EXPOSED_TOOL_NAMES: - return self._get_exposed_tool_names(request_id) + return self._get_exposed_tool_names() case _: - return {"id": request_id, "error": f"Unknown method: {method}"} + return {"error": f"Unknown method: {method}"} except Exception as e: - return {"id": request.get("id"), "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} - def _initialize(self, request_id: str, params: dict[str, Any]) -> dict[str, Any]: + def _initialize(self, params: dict[str, Any]) -> dict[str, Any]: """Initialize the SerenaAgent.""" try: # Extract all possible initialization parameters @@ -127,14 +127,14 @@ class SerenaAgentWorker: tool_timeout=tool_timeout, ) - return {"id": request_id, "result": "SerenaAgent initialized successfully"} + return {"result": "SerenaAgent initialized successfully"} except Exception as e: - return {"id": request_id, "error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} + return {"error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} - def _tool_call(self, request_id: str, params: dict[str, Any]) -> dict[str, Any]: + def _tool_call(self, params: dict[str, Any]) -> dict[str, Any]: """Execute a tool call.""" if self.agent is None: - return {"id": request_id, "error": "SerenaAgent not initialized"} + return {"error": "SerenaAgent not initialized"} try: tool_name = params["tool_name"] @@ -148,61 +148,61 @@ class SerenaAgentWorker: break if tool is None: - return {"id": request_id, "error": f"Tool '{tool_name}' not found or not active"} + return {"error": f"Tool '{tool_name}' not found or not active"} # Execute the tool result = tool.apply_ex(**tool_params) - return {"id": request_id, "result": result} + return {"result": result} except Exception as e: - return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} - def _get_active_tool_names(self, request_id: str) -> dict[str, Any]: + def _get_active_tool_names(self) -> dict[str, Any]: """Get list of active tool names.""" if self.agent is None: - return {"id": request_id, "error": "SerenaAgent not initialized"} + return {"error": "SerenaAgent not initialized"} try: tool_names = self.agent.get_active_tool_names() - return {"id": request_id, "result": tool_names} + return {"result": tool_names} except Exception as e: - return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} - def _is_language_server_running(self, request_id: str) -> dict[str, Any]: + def _is_language_server_running(self) -> dict[str, Any]: """Check if language server is running.""" if self.agent is None: - return {"id": request_id, "error": "SerenaAgent not initialized"} + return {"error": "SerenaAgent not initialized"} try: is_running = self.agent.is_language_server_running() - return {"id": request_id, "result": is_running} + return {"result": is_running} except Exception as e: - return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} - def _reset_language_server(self, request_id: str) -> dict[str, Any]: + def _reset_language_server(self) -> dict[str, Any]: """Reset the language server.""" if self.agent is None: - return {"id": request_id, "error": "SerenaAgent not initialized"} + return {"error": "SerenaAgent not initialized"} try: self.agent.reset_language_server() - return {"id": request_id, "result": "Language server reset successfully"} + return {"result": "Language server reset successfully"} except Exception as e: - return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} - def _get_exposed_tool_names(self, request_id: str) -> dict[str, Any]: + def _get_exposed_tool_names(self) -> dict[str, Any]: """Get exposed tool names for MCP tool creation.""" if self.agent is None: - return {"id": request_id, "error": "SerenaAgent not initialized"} + return {"error": "SerenaAgent not initialized"} try: tool_instances = self.agent.get_exposed_tool_instances() # Return only tool names - metadata will be reconstructed from ToolRegistry tool_names = [tool.get_name_from_cls() for tool in tool_instances] - return {"id": request_id, "result": tool_names} + return {"result": tool_names} except Exception as e: - return {"id": request_id, "error": str(e), "traceback": traceback.format_exc()} + return {"error": str(e), "traceback": traceback.format_exc()} def _cleanup(self) -> None: """Clean up resources.""" @@ -215,7 +215,7 @@ class SerenaAgentWorker: self.agent = None class RequestMethod(StrEnum): - """Enumeration of available request methods for type-safe IPC.""" + """Enumeration of available request methods.""" INITIALIZE = "initialize" TOOL_CALL = "tool_call" @@ -262,10 +262,7 @@ class ProcessIsolatedSerenaAgent: # Use the default config loader self.serena_config = SerenaConfig.from_config_file() self.process: multiprocessing.Process | None = None - self.request_queue: multiprocessing.Queue | None = None - self.response_queue: multiprocessing.Queue | None = None - self.request_id_counter = 0 - self._lock = threading.Lock() + self.conn: Connection | None = None def start(self) -> None: """Start the worker process.""" @@ -274,12 +271,12 @@ class ProcessIsolatedSerenaAgent: log.info("Starting process-isolated SerenaAgent") - # Create communication queues - self.request_queue = multiprocessing.Queue() - self.response_queue = multiprocessing.Queue() + # Create communication pipe + parent_conn, child_conn = multiprocessing.Pipe() + self.conn = parent_conn # Create and start worker process - worker = SerenaAgentWorker(self.request_queue, self.response_queue) + worker = SerenaAgentWorker(child_conn) self.process = multiprocessing.Process(target=worker.run) self.process.start() @@ -313,9 +310,9 @@ class ProcessIsolatedSerenaAgent: log.info("Stopping process-isolated SerenaAgent") try: - # Send shutdown signal - if self.request_queue is not None: - self.request_queue.put(None) + # Close connection to signal worker to shutdown + if self.conn is not None: + self.conn.close() # Wait for process to terminate if self.process.is_alive(): @@ -336,8 +333,7 @@ class ProcessIsolatedSerenaAgent: finally: self.process = None - self.request_queue = None - self.response_queue = None + self.conn = None log.info("Process-isolated SerenaAgent stopped") @@ -346,35 +342,27 @@ class ProcessIsolatedSerenaAgent: if self.process is None or not self.process.is_alive(): raise RuntimeError("Worker process is not running") - with self._lock: - self.request_id_counter += 1 - request_id = str(self.request_id_counter) + if self.conn is None: + raise RuntimeError("Connection is not initialized") - request = {"id": request_id, "method": method, "params": params or {}} + request = {"method": method, "params": params or {}} # Send request - if self.request_queue is not None: - self.request_queue.put(request) + try: + self.conn.send(request) + except (EOFError, BrokenPipeError) as e: + raise RuntimeError("Failed to send request: worker process may have crashed") from e - # Wait for response + # Wait for response with timeout timeout = self.serena_config.tool_timeout - start_time = time.time() - while time.time() - start_time < timeout: + if self.conn.poll(timeout): try: - if self.response_queue is not None: - response = self.response_queue.get(timeout=1.0) - else: - raise RuntimeError("Response queue is not initialized") - if response["id"] == request_id: - return response - else: - # Put back response for different request - if self.response_queue is not None: - self.response_queue.put(response) - except queue.Empty: - continue - - raise TimeoutError(f"Request {request_id} timed out after {timeout} seconds") + response = self.conn.recv() + return response + except (EOFError, BrokenPipeError) as e: + raise RuntimeError("Failed to receive response: worker process may have crashed") from e + else: + raise TimeoutError(f"Request {method} timed out after {timeout} seconds") def _make_request_with_result(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> Any: """Make a request and return the result, raising an exception if there's an error.""" From 343c00f6052a228c970e69d8b0c7b4d9ec2ffd2e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 18:31:17 +0200 Subject: [PATCH 24/28] Fix process termination issues and allow terminating through dashboard --- src/serena/dashboard.py | 22 +++++-- src/serena/mcp.py | 21 +++++++ src/serena/process_isolated_agent.py | 61 +++++++++++++++++++ .../test_make_tool_process_isolation.py | 2 +- 4 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 622bef11..549c003c 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -3,6 +3,7 @@ import queue import socket import sys import threading +from collections.abc import Callable import uvicorn from fastapi import FastAPI @@ -68,9 +69,12 @@ class ResponseToolNames(BaseModel): class SerenaDashboardAPI: log = logging.getLogger(__qualname__) - def __init__(self, memory_log_handler: MemoryLogHandler, tool_names: list[str]) -> None: + def __init__( + self, memory_log_handler: MemoryLogHandler, tool_names: list[str], shutdown_callback: Callable[[], None] | None = None + ) -> None: self._memory_log_handler = memory_log_handler self._tool_names = tool_names + self._shutdown_callback = shutdown_callback self._app = FastAPI(title="Serena Dashboard") self._setup_routes() @@ -92,9 +96,19 @@ class SerenaDashboardAPI: async def _shutdown(self) -> None: print("Shutdown initiated by dashbaord ...", file=sys.stderr) log.info("Shutting down Serena") - # noinspection PyUnresolvedReferences - # noinspection PyProtectedMember - os._exit(0) + if self._shutdown_callback: + self._shutdown_callback() + else: + # Try to use the global shutdown function from process_isolated_agent + try: + from serena.process_isolated_agent import call_global_shutdown + + call_global_shutdown() + except ImportError: + # Fallback to the old behavior if not in process-isolated mode + # noinspection PyUnresolvedReferences + # noinspection PyProtectedMember + os._exit(0) @staticmethod def _find_first_free_port(start_port: int) -> int: diff --git a/src/serena/mcp.py b/src/serena/mcp.py index e8f4ec66..a5ade13c 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -183,14 +183,35 @@ def create_mcp_server_and_agent( async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" nonlocal process_agent + import asyncio + mark_used(mcp_server) + async def monitor_worker_process() -> None: + """Monitor the worker process and shutdown server if it exits.""" + while True: + if process_agent.process is None or not process_agent.process.is_alive(): + log.info("Worker process has exited, shutting down MCP server") + # Trigger server shutdown + import os + + os._exit(0) + await asyncio.sleep(1) + try: process_agent.start() # Update tools now that the process agent is running update_tools() + # Start monitoring task + monitor_task = asyncio.create_task(monitor_worker_process()) yield finally: + if "monitor_task" in locals(): + monitor_task.cancel() + import contextlib + + with contextlib.suppress(asyncio.CancelledError): + await monitor_task process_agent.stop() mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index be6234b3..6ff44683 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -23,6 +23,36 @@ from serena.config import SerenaAgentContext, SerenaAgentMode log = logging.getLogger(__name__) +# Global shutdown function that can be called by the dashboard +class _GlobalShutdownRegistry: + """Registry for global shutdown function.""" + + def __init__(self) -> None: + self.shutdown_func: Callable[[], None] | None = None + + def set_shutdown_func(self, func: Callable[[], None]) -> None: + """Set the global shutdown function.""" + self.shutdown_func = func + + def call_shutdown(self) -> None: + """Call the global shutdown function if it exists.""" + if self.shutdown_func: + self.shutdown_func() + + +_shutdown_registry = _GlobalShutdownRegistry() + + +def set_global_shutdown_func(func: Callable[[], None]) -> None: + """Set the global shutdown function.""" + _shutdown_registry.set_shutdown_func(func) + + +def call_global_shutdown() -> None: + """Call the global shutdown function if it exists.""" + _shutdown_registry.call_shutdown() + + class SerenaAgentWorker: """Worker process that hosts the actual SerenaAgent.""" @@ -86,6 +116,8 @@ class SerenaAgentWorker: return self._reset_language_server() case self.RequestMethod.GET_EXPOSED_TOOL_NAMES: return self._get_exposed_tool_names() + case self.RequestMethod.SHUTDOWN: + return self._shutdown() case _: return {"error": f"Unknown method: {method}"} @@ -127,6 +159,16 @@ class SerenaAgentWorker: tool_timeout=tool_timeout, ) + # Set up global shutdown function for dashboard + def shutdown_worker() -> None: + log.info("Global shutdown function called") + self._cleanup() + import sys + + sys.exit(0) + + set_global_shutdown_func(shutdown_worker) + return {"result": "SerenaAgent initialized successfully"} except Exception as e: return {"error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} @@ -204,6 +246,20 @@ class SerenaAgentWorker: except Exception as e: return {"error": str(e), "traceback": traceback.format_exc()} + def _shutdown(self) -> dict[str, Any]: + """Handle shutdown request from dashboard.""" + try: + log.info("Shutdown requested from dashboard") + # Clean up resources before exiting + self._cleanup() + # Exit the worker process - this will cause the main process to detect the termination + # and shut down gracefully through the server lifespan manager + import sys + + sys.exit(0) + except Exception as e: + return {"error": str(e), "traceback": traceback.format_exc()} + def _cleanup(self) -> None: """Clean up resources.""" if self.agent is not None: @@ -223,6 +279,7 @@ class SerenaAgentWorker: IS_LANGUAGE_SERVER_RUNNING = "is_language_server_running" RESET_LANGUAGE_SERVER = "reset_language_server" GET_EXPOSED_TOOL_NAMES = "get_exposed_tool_names" + SHUTDOWN = "shutdown" class ProcessIsolatedSerenaAgent: @@ -394,6 +451,10 @@ class ProcessIsolatedSerenaAgent: """Reset the language server.""" self._make_request_with_result(SerenaAgentWorker.RequestMethod.RESET_LANGUAGE_SERVER) + def shutdown_from_dashboard(self) -> None: + """Request shutdown from dashboard.""" + self._make_request_with_result(SerenaAgentWorker.RequestMethod.SHUTDOWN) + def get_exposed_tool_names(self) -> list[str]: """Get tool names for MCP tool creation.""" return self._make_request_with_result(SerenaAgentWorker.RequestMethod.GET_EXPOSED_TOOL_NAMES) diff --git a/test/serena/test_make_tool_process_isolation.py b/test/serena/test_make_tool_process_isolation.py index 3404aa1a..fb13f866 100644 --- a/test/serena/test_make_tool_process_isolation.py +++ b/test/serena/test_make_tool_process_isolation.py @@ -24,7 +24,7 @@ def regular_agent(in_memory_config): @pytest.fixture def process_isolated_agent(in_memory_config): """Create a ProcessIsolatedSerenaAgent for comparison.""" - agent = ProcessIsolatedSerenaAgent(in_memory_config) + agent = ProcessIsolatedSerenaAgent(serena_config=in_memory_config) agent.start() yield agent agent.stop() From 7b35b77bc920e5450cf1f5df1e9907075d70119f Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 18:56:45 +0200 Subject: [PATCH 25/28] Isolating dashboard process, solving shutdown issues --- src/serena/mcp.py | 160 ++++++++++-- src/serena/process_isolated_agent.py | 355 +++++++++++++++++++++++++-- 2 files changed, 470 insertions(+), 45 deletions(-) diff --git a/src/serena/mcp.py b/src/serena/mcp.py index a5ade13c..6c91d0bd 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -142,7 +142,7 @@ def create_mcp_server_and_agent( ) # Create process-isolated agent with the configuration - from serena.process_isolated_agent import ProcessIsolatedSerenaAgent + from serena.process_isolated_agent import ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent process_agent = ProcessIsolatedSerenaAgent(serena_config=serena_config) @@ -184,35 +184,159 @@ def create_mcp_server_and_agent( """Manage server startup and shutdown lifecycle.""" nonlocal process_agent import asyncio + import webbrowser mark_used(mcp_server) + dashboard: ProcessIsolatedDashboard | None = None + shutdown_event = asyncio.Event() + async def monitor_worker_process() -> None: """Monitor the worker process and shutdown server if it exits.""" - while True: - if process_agent.process is None or not process_agent.process.is_alive(): - log.info("Worker process has exited, shutting down MCP server") - # Trigger server shutdown - import os + while not shutdown_event.is_set(): + try: + if process_agent.process is None or not process_agent.process.is_alive(): + log.info("Worker process has exited, shutting down MCP server") + shutdown_event.set() + break + await asyncio.sleep(1) + except asyncio.CancelledError: + break + except Exception as e: + log.error(f"Error monitoring worker process: {e}") + await asyncio.sleep(1) - os._exit(0) - await asyncio.sleep(1) + async def monitor_dashboard_shutdown() -> None: + """Monitor the dashboard shutdown queue.""" + if dashboard is None or dashboard.shutdown_queue is None: + return + + while not shutdown_event.is_set(): + try: + # Check if dashboard sent shutdown signal + if not dashboard.shutdown_queue.empty(): + dashboard.shutdown_queue.get_nowait() + log.info("Dashboard shutdown signal received via queue") + shutdown_event.set() + # Force immediate exit since MCP server doesn't respond properly + import os + import sys + + log.info("Forcing immediate process exit from dashboard signal") + sys.stdout.flush() + sys.stderr.flush() + os._exit(0) + await asyncio.sleep(0.1) # Check more frequently + except asyncio.CancelledError: + break + except Exception as e: + log.error(f"Error monitoring dashboard shutdown: {e}") + await asyncio.sleep(1) + + def shutdown_server() -> None: + """Shutdown the server.""" + log.info("Shutdown initiated by dashboard") + shutdown_event.set() + # Force immediate exit since the MCP server doesn't respond to shutdown events + import os + import sys + + log.info("Dashboard shutdown - forcing process exit immediately") + sys.stdout.flush() + sys.stderr.flush() + os._exit(0) try: process_agent.start() + + # Set up global shutdown function for dashboard + from serena.process_isolated_agent import set_global_shutdown_func + + set_global_shutdown_func(shutdown_server) + # Update tools now that the process agent is running update_tools() - # Start monitoring task - monitor_task = asyncio.create_task(monitor_worker_process()) - yield - finally: - if "monitor_task" in locals(): - monitor_task.cancel() - import contextlib - with contextlib.suppress(asyncio.CancelledError): - await monitor_task - process_agent.stop() + # Start process-isolated dashboard if enabled + if serena_config.web_dashboard: + try: + tool_names = process_agent.get_exposed_tool_names() + dashboard = ProcessIsolatedDashboard(tool_names) + port = dashboard.start() + webbrowser.open(f"http://localhost:{port}/dashboard/index.html") + log.info(f"Dashboard started on port {port}") + except Exception as e: + log.error(f"Failed to start dashboard: {e}") + dashboard = None + + # Start monitoring tasks + tasks = [] + monitor_task = asyncio.create_task(monitor_worker_process()) + tasks.append(monitor_task) + + # Start dashboard shutdown monitor if dashboard is running + if dashboard is not None: + dashboard_monitor_task = asyncio.create_task(monitor_dashboard_shutdown()) + tasks.append(dashboard_monitor_task) + + # Set up signal handlers in main process + def signal_handler(signum: int, frame: Any) -> None: + log.info(f"Received signal {signum} in main process") + shutdown_event.set() + # Give a short delay for graceful shutdown, then force exit + import os + import threading + + def force_exit() -> None: + import time + + time.sleep(2.0) # Wait 2 seconds for graceful shutdown + log.warning("Forcing exit after timeout") + os._exit(1) + + threading.Thread(target=force_exit, daemon=True).start() + + import signal + + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + try: + # Start the server and wait for shutdown + yield + except (KeyboardInterrupt, SystemExit): + log.info("Received shutdown signal") + shutdown_event.set() + + except Exception as e: + log.error(f"Error in server lifespan: {e}") + shutdown_event.set() + finally: + log.info("Starting server shutdown cleanup") + + # Cancel monitoring tasks + import contextlib + + if "tasks" in locals(): + for task in tasks: + task.cancel() + with contextlib.suppress(TimeoutError, asyncio.CancelledError): + await asyncio.wait_for(task, timeout=1.0) + + # Stop dashboard first + if dashboard is not None: + try: + dashboard.stop() + except Exception as e: + log.error(f"Error stopping dashboard: {e}") + + # Stop process agent + try: + process_agent.stop() + except Exception as e: + log.error(f"Error stopping process agent: {e}") + + log.info("Server shutdown cleanup completed") mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) mcp = FastMCP(**mcp_settings.model_dump()) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 6ff44683..a9b59a7b 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -7,8 +7,10 @@ This module provides: 3. JSON-RPC based IPC for communication between processes """ +import asyncio import logging import multiprocessing +import signal import traceback from collections.abc import Callable from enum import StrEnum @@ -19,6 +21,7 @@ from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry from serena.config import SerenaAgentContext, SerenaAgentMode +from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI log = logging.getLogger(__name__) @@ -53,43 +56,326 @@ def call_global_shutdown() -> None: _shutdown_registry.call_shutdown() +class ProcessIsolatedDashboard: + """Process-isolated dashboard to prevent asyncio contamination with MCP server and worker.""" + + def __init__(self, tool_names: list[str]): + self.tool_names = tool_names + self.process: multiprocessing.Process | None = None + self.log_queue: multiprocessing.Queue[str] | None = None + self.shutdown_queue: multiprocessing.Queue[bool] | None = None + self.port_queue: multiprocessing.Queue[int] | None = None + self._log_handler: ProcessDashboardLogHandler | None = None + + def start(self) -> int: + """Start the dashboard process and return the port number.""" + if self.process is not None: + raise RuntimeError("Dashboard process already started") + + log.info("Starting process-isolated dashboard") + + # Create communication queues + self.log_queue = multiprocessing.Queue() + self.shutdown_queue = multiprocessing.Queue() + self.port_queue = multiprocessing.Queue() + + # Create and start dashboard process + dashboard_worker = DashboardWorker( + log_queue=self.log_queue, shutdown_queue=self.shutdown_queue, port_queue=self.port_queue, tool_names=self.tool_names + ) + self.process = multiprocessing.Process(target=dashboard_worker.run) + self.process.start() + + # Set up log handler to send logs to dashboard process + self._log_handler = ProcessDashboardLogHandler(self.log_queue) + logging.getLogger().addHandler(self._log_handler) + + # Wait for dashboard to start and return port + try: + port = self.port_queue.get(timeout=10) # Wait up to 10 seconds for startup + log.info(f"Dashboard started on port {port}") + return port + except Exception as e: + self.stop() + raise RuntimeError(f"Failed to start dashboard: {e}") from e + + def stop(self) -> None: + """Stop the dashboard process.""" + if self.process is None: + return + + log.info("Stopping process-isolated dashboard") + + try: + # Remove log handler + if self._log_handler is not None: + logging.getLogger().removeHandler(self._log_handler) + self._log_handler = None + + # Signal shutdown + if self.shutdown_queue is not None: + import contextlib + + with contextlib.suppress(Exception): + self.shutdown_queue.put_nowait(True) + + # Wait for process to terminate + if self.process.is_alive(): + self.process.join(timeout=5.0) + + if self.process.is_alive(): + log.warning("Dashboard process did not terminate gracefully, forcing termination") + self.process.terminate() + self.process.join(timeout=3.0) + + if self.process.is_alive(): + log.error("Dashboard process could not be terminated, killing it") + self.process.kill() + self.process.join() + + except Exception as e: + log.error(f"Error stopping dashboard process: {e}") + + finally: + self.process = None + self.log_queue = None + self.shutdown_queue = None + self.port_queue = None + + log.info("Process-isolated dashboard stopped") + + def update_tool_names(self, tool_names: list[str]) -> None: + """Update tool names (for future enhancement).""" + self.tool_names = tool_names + # TODO: Could send update to dashboard process if needed + + +class ProcessDashboardLogHandler(logging.Handler): + """Log handler that sends log messages to dashboard process via queue.""" + + def __init__(self, log_queue: "multiprocessing.Queue[str]", level: int = logging.NOTSET): + super().__init__(level=level) + self.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s")) + self.log_queue = log_queue + + def emit(self, record: logging.LogRecord) -> None: + try: + msg = self.format(record) + # Non-blocking put to avoid deadlocks + self.log_queue.put_nowait(msg) + except Exception: + # Silently ignore queue errors to avoid recursive logging + pass + + +class DashboardWorker: + """Worker process that hosts the dashboard with its own asyncio loop.""" + + def __init__( + self, + log_queue: "multiprocessing.Queue[str]", + shutdown_queue: "multiprocessing.Queue[bool]", + port_queue: "multiprocessing.Queue[int]", + tool_names: list[str], + ): + self.log_queue = log_queue + self.shutdown_queue = shutdown_queue + self.port_queue = port_queue + self.tool_names = tool_names + self.memory_log_handler: MemoryLogHandler | None = None + self.dashboard_api: SerenaDashboardAPI | None = None + self.log_processor_task: asyncio.Task | None = None + + def run(self) -> None: + """Main dashboard worker loop - runs in separate process.""" + try: + log.info("Dashboard worker process started") + asyncio.run(self._async_main()) + except Exception as e: + log.error(f"Fatal error in dashboard worker process: {e}") + finally: + log.info("Dashboard worker process stopped") + + async def _async_main(self) -> None: + """Main async loop for dashboard process.""" + try: + # Set up memory log handler + self.memory_log_handler = MemoryLogHandler() + + # Set up dashboard API with shutdown callback + self.dashboard_api = SerenaDashboardAPI( + memory_log_handler=self.memory_log_handler, tool_names=self.tool_names, shutdown_callback=self._handle_shutdown + ) + + # Start log processor task + self.log_processor_task = asyncio.create_task(self._process_log_queue()) + + # Find free port and signal to parent + port = self.dashboard_api._find_first_free_port(0x5EDA) + self.port_queue.put(port) + + # Start dashboard server + import uvicorn + + config = uvicorn.Config( + app=self.dashboard_api._app, host="0.0.0.0", port=port, workers=1, log_config=None, log_level="critical" + ) + server = uvicorn.Server(config) + + # Start server and shutdown monitor concurrently + shutdown_task = asyncio.create_task(self._monitor_shutdown()) + server_task = asyncio.create_task(server.serve()) + + # Wait for either server to complete or shutdown signal + done, pending = await asyncio.wait([server_task, shutdown_task], return_when=asyncio.FIRST_COMPLETED) + + # Cancel pending tasks + for task in pending: + task.cancel() + import contextlib + + with contextlib.suppress(asyncio.CancelledError): + await task + + except Exception as e: + log.error(f"Error in dashboard worker: {e}") + finally: + # Clean up + if self.log_processor_task and not self.log_processor_task.done(): + self.log_processor_task.cancel() + import contextlib + + with contextlib.suppress(asyncio.CancelledError): + await self.log_processor_task + + async def _process_log_queue(self) -> None: + """Process log messages from the queue and forward to memory handler.""" + while True: + try: + # Check for log messages + while not self.log_queue.empty(): + try: + log_msg = self.log_queue.get_nowait() + if self.memory_log_handler: + # Create a dummy log record and emit it + record = logging.LogRecord( + name="forwarded", level=logging.INFO, pathname="", lineno=0, msg=log_msg, args=(), exc_info=None + ) + self.memory_log_handler.emit(record) + except Exception: + break + + # Small delay to avoid busy waiting + await asyncio.sleep(0.1) + + except asyncio.CancelledError: + break + except Exception as e: + log.error(f"Error processing log queue: {e}") + await asyncio.sleep(1) + + async def _monitor_shutdown(self) -> None: + """Monitor for shutdown signals.""" + while True: + try: + # Check for shutdown signal + if not self.shutdown_queue.empty(): + self.shutdown_queue.get_nowait() + log.info("Dashboard shutdown signal received") + break + + await asyncio.sleep(0.5) + + except asyncio.CancelledError: + break + except Exception as e: + log.error(f"Error monitoring shutdown: {e}") + await asyncio.sleep(1) + + def _handle_shutdown(self) -> None: + """Handle shutdown request from dashboard UI.""" + log.info("Dashboard UI shutdown requested") + # Signal shutdown to the main process through the parent dashboard + if self.shutdown_queue is not None: + try: + self.shutdown_queue.put_nowait(True) + log.info("Sent shutdown signal to main process via queue") + except Exception as e: + log.error(f"Failed to send shutdown signal: {e}") + + # Also try the global shutdown as fallback + try: + call_global_shutdown() + except Exception as e: + log.error(f"Global shutdown failed: {e}") + + # Force exit this dashboard process + import os + import threading + + def delayed_exit() -> None: + import time + + time.sleep(0.5) # Give time for shutdown signal to be sent + log.info("Dashboard process forcing exit") + os._exit(0) + + threading.Thread(target=delayed_exit, daemon=True).start() + + class SerenaAgentWorker: """Worker process that hosts the actual SerenaAgent.""" def __init__(self, conn: Connection): self.conn = conn self.agent: SerenaAgent | None = None + self._shutdown_requested = False def run(self) -> None: """Main worker loop - runs in separate process.""" try: + # Set up signal handler for clean shutdown + signal.signal(signal.SIGTERM, self._signal_handler) + signal.signal(signal.SIGINT, self._signal_handler) + log.info("SerenaAgent worker process started") - while True: + while not self._shutdown_requested: try: - # Block until request received or connection closed - request = self.conn.recv() - if request is None: # Shutdown signal - break + # Use polling to avoid blocking indefinitely + if self.conn.poll(timeout=0.5): # Poll every 500ms + try: + request = self.conn.recv() + if request is None: # Explicit shutdown signal + break - response = self._handle_request(request) - self.conn.send(response) + response = self._handle_request(request) + self.conn.send(response) + except EOFError: + # Connection closed - parent process terminated + log.info("Connection closed, worker shutting down") + break + except (BrokenPipeError, ConnectionResetError): + # Connection broken, can't communicate + log.info("Connection broken, worker shutting down") + break + except Exception as e: + log.error(f"Error processing request: {e}") + try: + response = { + "error": f"Worker process error: {e!s}", + "traceback": traceback.format_exc(), + } + self.conn.send(response) + except (EOFError, BrokenPipeError, ConnectionResetError): + # Connection is broken, can't send error response + log.info("Connection broken during error response, shutting down") + break + # Continue polling if no data available - except EOFError: - # Connection closed - parent process terminated - log.info("Connection closed, worker shutting down") - break except Exception as e: - log.error(f"Error in worker process: {e}") - try: - response = { - "error": f"Worker process error: {e!s}", - "traceback": traceback.format_exc(), - } - self.conn.send(response) - except (EOFError, BrokenPipeError): - # Connection is broken, can't send error response - break + log.error(f"Error in worker main loop: {e}") + break except Exception as e: log.error(f"Fatal error in worker process: {e}") @@ -97,6 +383,11 @@ class SerenaAgentWorker: self._cleanup() log.info("SerenaAgent worker process stopped") + def _signal_handler(self, signum: int, frame: Any) -> None: + """Handle shutdown signals gracefully.""" + log.info(f"Received signal {signum}, initiating shutdown") + self._shutdown_requested = True + def _handle_request(self, request: dict[str, Any]) -> dict[str, Any]: """Handle a single request.""" try: @@ -351,6 +642,8 @@ class ProcessIsolatedSerenaAgent: init_params["modes"] = None # project_activation_callback cannot be serialized across processes init_params["project_activation_callback"] = None + # Disable web dashboard in worker process - it will run in MCP process instead + init_params["enable_web_dashboard"] = False self._make_request_with_result(SerenaAgentWorker.RequestMethod.INITIALIZE, init_params) except Exception as e: @@ -371,20 +664,28 @@ class ProcessIsolatedSerenaAgent: if self.conn is not None: self.conn.close() - # Wait for process to terminate + # Wait for process to terminate with shorter timeout if self.process.is_alive(): - self.process.join(timeout=10.0) + self.process.join(timeout=3.0) # Reduced from 10s to 3s if self.process.is_alive(): - log.warning("Worker process did not terminate gracefully, forcing termination") + log.warning("Worker process did not terminate gracefully, sending SIGTERM") self.process.terminate() - self.process.join(timeout=5.0) + self.process.join(timeout=2.0) # Reduced from 5s to 2s if self.process.is_alive(): - log.error("Worker process could not be terminated, killing it") + log.error("Worker process did not respond to SIGTERM, sending SIGKILL") self.process.kill() - self.process.join() + self.process.join(timeout=1.0) # Add timeout for kill as well + if self.process.is_alive(): + log.error("Worker process could not be killed - forcing cleanup") + + except KeyboardInterrupt: + log.warning("Keyboard interrupt during shutdown - forcing termination") + if self.process and self.process.is_alive(): + self.process.kill() + self.process.join(timeout=1.0) except Exception as e: log.error(f"Error stopping worker process: {e}") From cbf4933c04959f423f1d037199c514d50d3cd775 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Sun, 15 Jun 2025 20:47:36 +0200 Subject: [PATCH 26/28] Forwarding logs from agent process, WIP --- src/serena/dashboard.py | 4 + src/serena/mcp.py | 61 +++++++-------- src/serena/process_isolated_agent.py | 106 +++++++++++---------------- 3 files changed, 72 insertions(+), 99 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 549c003c..63bf79ab 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -78,6 +78,10 @@ class SerenaDashboardAPI: self._app = FastAPI(title="Serena Dashboard") self._setup_routes() + @property + def memory_log_handler(self) -> MemoryLogHandler: + return self._memory_log_handler + def _setup_routes(self) -> None: self._app.mount("/dashboard", StaticFiles(directory=SERENA_DASHBOARD_DIR), name="dashboard") diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 6c91d0bd..ff4d4c8e 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -2,13 +2,14 @@ The Serena Model Context Protocol (MCP) Server """ +import contextlib import sys from collections.abc import AsyncIterator, Sequence from contextlib import asynccontextmanager from dataclasses import dataclass from logging import Formatter, Logger, StreamHandler from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal +from typing import Any, Literal import click # Add click import import docstring_parser @@ -21,10 +22,7 @@ from sensai.util.helper import mark_used from serena.agent import SerenaAgent, ToolInterface, create_serena_config, show_fatal_exception_safe from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES - -# Import for type annotations -if TYPE_CHECKING: - from serena.process_isolated_agent import ProcessIsolatedSerenaAgent +from serena.process_isolated_agent import ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent log = logging.getLogger(__name__) LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" @@ -141,10 +139,7 @@ def create_mcp_server_and_agent( tool_timeout=tool_timeout, ) - # Create process-isolated agent with the configuration - from serena.process_isolated_agent import ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent - - process_agent = ProcessIsolatedSerenaAgent(serena_config=serena_config) + serena_agent_process = ProcessIsolatedSerenaAgent(serena_config=serena_config) except Exception as e: show_fatal_exception_safe(e) @@ -152,10 +147,10 @@ def create_mcp_server_and_agent( def update_tools() -> None: """Update the tools in the MCP server - adapted for process isolation.""" - nonlocal mcp, process_agent + nonlocal mcp, serena_agent_process # Check if process agent is running - if process_agent.process is None or not process_agent.process.is_alive(): + if serena_agent_process.process is None or not serena_agent_process.process.is_alive(): log.debug("Process agent not running yet, skipping tool update") return @@ -165,7 +160,7 @@ def create_mcp_server_and_agent( # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, # so we need to register all tools at startup, unfortunately. try: - tool_names = process_agent.get_exposed_tool_names() + tool_names = serena_agent_process.get_exposed_tool_names() except Exception as e: log.error(f"Failed to get tool names from process agent: {e}") return @@ -175,27 +170,27 @@ def create_mcp_server_and_agent( mcp._tool_manager._tools = {} for tool_name in tool_names: - process_isolated_tool = ProcessIsolatedTool(process_agent=process_agent, tool_name=tool_name) + process_isolated_tool = ProcessIsolatedTool(process_agent=serena_agent_process, tool_name=tool_name) mcp_tool = make_tool(process_isolated_tool) mcp._tool_manager._tools[tool_name] = mcp_tool @asynccontextmanager async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" - nonlocal process_agent + nonlocal serena_agent_process import asyncio import webbrowser mark_used(mcp_server) - dashboard: ProcessIsolatedDashboard | None = None + serena_dashboard_process: ProcessIsolatedDashboard | None = None shutdown_event = asyncio.Event() async def monitor_worker_process() -> None: """Monitor the worker process and shutdown server if it exits.""" while not shutdown_event.is_set(): try: - if process_agent.process is None or not process_agent.process.is_alive(): + if serena_agent_process.process is None or not serena_agent_process.process.is_alive(): log.info("Worker process has exited, shutting down MCP server") shutdown_event.set() break @@ -208,14 +203,14 @@ def create_mcp_server_and_agent( async def monitor_dashboard_shutdown() -> None: """Monitor the dashboard shutdown queue.""" - if dashboard is None or dashboard.shutdown_queue is None: + if serena_dashboard_process is None or serena_dashboard_process.shutdown_queue is None: return while not shutdown_event.is_set(): try: # Check if dashboard sent shutdown signal - if not dashboard.shutdown_queue.empty(): - dashboard.shutdown_queue.get_nowait() + if not serena_dashboard_process.shutdown_queue.empty(): + serena_dashboard_process.shutdown_queue.get_nowait() log.info("Dashboard shutdown signal received via queue") shutdown_event.set() # Force immediate exit since MCP server doesn't respond properly @@ -247,27 +242,26 @@ def create_mcp_server_and_agent( os._exit(0) try: - process_agent.start() # Set up global shutdown function for dashboard from serena.process_isolated_agent import set_global_shutdown_func set_global_shutdown_func(shutdown_server) - # Update tools now that the process agent is running - update_tools() - # Start process-isolated dashboard if enabled if serena_config.web_dashboard: try: - tool_names = process_agent.get_exposed_tool_names() - dashboard = ProcessIsolatedDashboard(tool_names) - port = dashboard.start() + serena_dashboard_process = ProcessIsolatedDashboard(tool_names=[]) + port = serena_dashboard_process.start() webbrowser.open(f"http://localhost:{port}/dashboard/index.html") log.info(f"Dashboard started on port {port}") except Exception as e: log.error(f"Failed to start dashboard: {e}") - dashboard = None + serena_dashboard_process = None + + serena_agent_process.start() + # Update tools now that the process agent is running + update_tools() # Start monitoring tasks tasks = [] @@ -275,7 +269,7 @@ def create_mcp_server_and_agent( tasks.append(monitor_task) # Start dashboard shutdown monitor if dashboard is running - if dashboard is not None: + if serena_dashboard_process is not None: dashboard_monitor_task = asyncio.create_task(monitor_dashboard_shutdown()) tasks.append(dashboard_monitor_task) @@ -314,9 +308,6 @@ def create_mcp_server_and_agent( finally: log.info("Starting server shutdown cleanup") - # Cancel monitoring tasks - import contextlib - if "tasks" in locals(): for task in tasks: task.cancel() @@ -324,15 +315,15 @@ def create_mcp_server_and_agent( await asyncio.wait_for(task, timeout=1.0) # Stop dashboard first - if dashboard is not None: + if serena_dashboard_process is not None: try: - dashboard.stop() + serena_dashboard_process.stop() except Exception as e: log.error(f"Error stopping dashboard: {e}") # Stop process agent try: - process_agent.stop() + serena_agent_process.stop() except Exception as e: log.error(f"Error stopping process agent: {e}") @@ -343,7 +334,7 @@ def create_mcp_server_and_agent( update_tools() - return mcp, process_agent + return mcp, serena_agent_process class ProjectType(click.ParamType): diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index a9b59a7b..7b16767b 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -8,16 +8,20 @@ This module provides: """ import asyncio +import contextlib import logging import multiprocessing import signal import traceback from collections.abc import Callable from enum import StrEnum +from logging.handlers import QueueHandler from multiprocessing.connection import Connection from typing import Any, Literal, Self +import uvicorn from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata +from sensai.util.logging import LOG_DEFAULT_FORMAT from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry from serena.config import SerenaAgentContext, SerenaAgentMode @@ -25,6 +29,8 @@ from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI log = logging.getLogger(__name__) +_global_log_q: "multiprocessing.Queue[str]" = multiprocessing.Queue() + # Global shutdown function that can be called by the dashboard class _GlobalShutdownRegistry: @@ -62,7 +68,6 @@ class ProcessIsolatedDashboard: def __init__(self, tool_names: list[str]): self.tool_names = tool_names self.process: multiprocessing.Process | None = None - self.log_queue: multiprocessing.Queue[str] | None = None self.shutdown_queue: multiprocessing.Queue[bool] | None = None self.port_queue: multiprocessing.Queue[int] | None = None self._log_handler: ProcessDashboardLogHandler | None = None @@ -75,25 +80,21 @@ class ProcessIsolatedDashboard: log.info("Starting process-isolated dashboard") # Create communication queues - self.log_queue = multiprocessing.Queue() self.shutdown_queue = multiprocessing.Queue() self.port_queue = multiprocessing.Queue() # Create and start dashboard process - dashboard_worker = DashboardWorker( - log_queue=self.log_queue, shutdown_queue=self.shutdown_queue, port_queue=self.port_queue, tool_names=self.tool_names - ) + dashboard_worker = DashboardWorker(shutdown_queue=self.shutdown_queue, port_queue=self.port_queue, tool_names=self.tool_names) self.process = multiprocessing.Process(target=dashboard_worker.run) self.process.start() # Set up log handler to send logs to dashboard process - self._log_handler = ProcessDashboardLogHandler(self.log_queue) - logging.getLogger().addHandler(self._log_handler) + logging.Logger.root.addHandler(ProcessDashboardLogHandler()) # Wait for dashboard to start and return port try: port = self.port_queue.get(timeout=10) # Wait up to 10 seconds for startup - log.info(f"Dashboard started on port {port}") + log.debug(f"Dashboard started on port {port}") return port except Exception as e: self.stop() @@ -107,11 +108,6 @@ class ProcessIsolatedDashboard: log.info("Stopping process-isolated dashboard") try: - # Remove log handler - if self._log_handler is not None: - logging.getLogger().removeHandler(self._log_handler) - self._log_handler = None - # Signal shutdown if self.shutdown_queue is not None: import contextlib @@ -138,7 +134,6 @@ class ProcessIsolatedDashboard: finally: self.process = None - self.log_queue = None self.shutdown_queue = None self.port_queue = None @@ -153,19 +148,14 @@ class ProcessIsolatedDashboard: class ProcessDashboardLogHandler(logging.Handler): """Log handler that sends log messages to dashboard process via queue.""" - def __init__(self, log_queue: "multiprocessing.Queue[str]", level: int = logging.NOTSET): + def __init__(self, level: int = logging.NOTSET): super().__init__(level=level) - self.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s")) - self.log_queue = log_queue + self.setFormatter(logging.Formatter(LOG_DEFAULT_FORMAT)) def emit(self, record: logging.LogRecord) -> None: - try: - msg = self.format(record) - # Non-blocking put to avoid deadlocks - self.log_queue.put_nowait(msg) - except Exception: - # Silently ignore queue errors to avoid recursive logging - pass + msg = self.format(record) + # Non-blocking put to avoid deadlocks + _global_log_q.put_nowait(msg) class DashboardWorker: @@ -173,18 +163,13 @@ class DashboardWorker: def __init__( self, - log_queue: "multiprocessing.Queue[str]", shutdown_queue: "multiprocessing.Queue[bool]", port_queue: "multiprocessing.Queue[int]", tool_names: list[str], ): - self.log_queue = log_queue self.shutdown_queue = shutdown_queue self.port_queue = port_queue self.tool_names = tool_names - self.memory_log_handler: MemoryLogHandler | None = None - self.dashboard_api: SerenaDashboardAPI | None = None - self.log_processor_task: asyncio.Task | None = None def run(self) -> None: """Main dashboard worker loop - runs in separate process.""" @@ -198,28 +183,21 @@ class DashboardWorker: async def _async_main(self) -> None: """Main async loop for dashboard process.""" + log_processor_task = None try: - # Set up memory log handler - self.memory_log_handler = MemoryLogHandler() - # Set up dashboard API with shutdown callback - self.dashboard_api = SerenaDashboardAPI( - memory_log_handler=self.memory_log_handler, tool_names=self.tool_names, shutdown_callback=self._handle_shutdown + dashboard_api = SerenaDashboardAPI( + memory_log_handler=MemoryLogHandler(), tool_names=self.tool_names, shutdown_callback=self._handle_shutdown ) # Start log processor task - self.log_processor_task = asyncio.create_task(self._process_log_queue()) + log_processor_task = asyncio.create_task(self._process_log_queue(dashboard_api)) # Find free port and signal to parent - port = self.dashboard_api._find_first_free_port(0x5EDA) + port = dashboard_api._find_first_free_port(0x5EDA) self.port_queue.put(port) - # Start dashboard server - import uvicorn - - config = uvicorn.Config( - app=self.dashboard_api._app, host="0.0.0.0", port=port, workers=1, log_config=None, log_level="critical" - ) + config = uvicorn.Config(app=dashboard_api._app, host="0.0.0.0", port=port, workers=1, log_config=None, log_level="critical") server = uvicorn.Server(config) # Start server and shutdown monitor concurrently @@ -228,12 +206,9 @@ class DashboardWorker: # Wait for either server to complete or shutdown signal done, pending = await asyncio.wait([server_task, shutdown_task], return_when=asyncio.FIRST_COMPLETED) - # Cancel pending tasks for task in pending: task.cancel() - import contextlib - with contextlib.suppress(asyncio.CancelledError): await task @@ -241,33 +216,31 @@ class DashboardWorker: log.error(f"Error in dashboard worker: {e}") finally: # Clean up - if self.log_processor_task and not self.log_processor_task.done(): - self.log_processor_task.cancel() - import contextlib + if log_processor_task and not log_processor_task.done(): + log_processor_task.cancel() with contextlib.suppress(asyncio.CancelledError): - await self.log_processor_task + await log_processor_task - async def _process_log_queue(self) -> None: + @staticmethod + async def _process_log_queue(dashboard_api: SerenaDashboardAPI) -> None: """Process log messages from the queue and forward to memory handler.""" while True: try: # Check for log messages - while not self.log_queue.empty(): + while not _global_log_q.empty(): try: - log_msg = self.log_queue.get_nowait() - if self.memory_log_handler: - # Create a dummy log record and emit it - record = logging.LogRecord( - name="forwarded", level=logging.INFO, pathname="", lineno=0, msg=log_msg, args=(), exc_info=None - ) - self.memory_log_handler.emit(record) - except Exception: - break + log_msg = _global_log_q.get_nowait() + record = logging.LogRecord( + name="forwarded", level=logging.INFO, pathname="", lineno=0, msg=log_msg, args=(), exc_info=None + ) + dashboard_api.memory_log_handler.emit(record) + except Exception as e: + log.error(f"Error processing log message: {e}") + await asyncio.sleep(1) # Small delay to avoid busy waiting await asyncio.sleep(0.1) - except asyncio.CancelledError: break except Exception as e: @@ -331,8 +304,13 @@ class SerenaAgentWorker: self.agent: SerenaAgent | None = None self._shutdown_requested = False - def run(self) -> None: + def run(self, q: "multiprocessing.Queue[str]") -> None: """Main worker loop - runs in separate process.""" + qh = QueueHandler(q) + root = logging.getLogger() + root.setLevel(logging.DEBUG) + root.addHandler(qh) + try: # Set up signal handler for clean shutdown signal.signal(signal.SIGTERM, self._signal_handler) @@ -623,9 +601,9 @@ class ProcessIsolatedSerenaAgent: parent_conn, child_conn = multiprocessing.Pipe() self.conn = parent_conn - # Create and start worker process + # Create and start worker process, passing along the dashboard's queue if available worker = SerenaAgentWorker(child_conn) - self.process = multiprocessing.Process(target=worker.run) + self.process = multiprocessing.Process(target=worker.run, args=[_global_log_q]) self.process.start() # Initialize the agent in the worker process From beb6b4e67de4a2e6c4c06ca19e66390ed6afe221 Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 00:52:00 +0200 Subject: [PATCH 27/28] Simplify dashboard process management --- src/serena/dashboard.py | 14 +- src/serena/mcp.py | 224 ++++--------- src/serena/process_isolated_agent.py | 484 +++++++++------------------ 3 files changed, 224 insertions(+), 498 deletions(-) diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 63bf79ab..22e34956 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -1,7 +1,6 @@ import os import queue import socket -import sys import threading from collections.abc import Callable @@ -98,21 +97,16 @@ class SerenaDashboardAPI: return ResponseToolNames(tool_names=self._tool_names) async def _shutdown(self) -> None: - print("Shutdown initiated by dashbaord ...", file=sys.stderr) log.info("Shutting down Serena") if self._shutdown_callback: self._shutdown_callback() else: # Try to use the global shutdown function from process_isolated_agent - try: - from serena.process_isolated_agent import call_global_shutdown + from serena.process_isolated_agent import request_global_shutdown - call_global_shutdown() - except ImportError: - # Fallback to the old behavior if not in process-isolated mode - # noinspection PyUnresolvedReferences - # noinspection PyProtectedMember - os._exit(0) + request_global_shutdown() + # noinspection PyProtectedMember + os._exit(0) @staticmethod def _find_first_free_port(start_port: int) -> int: diff --git a/src/serena/mcp.py b/src/serena/mcp.py index ff4d4c8e..3551ec11 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -2,8 +2,13 @@ The Serena Model Context Protocol (MCP) Server """ +import asyncio import contextlib +import os +import signal import sys +import threading +import time from collections.abc import AsyncIterator, Sequence from contextlib import asynccontextmanager from dataclasses import dataclass @@ -11,7 +16,7 @@ from logging import Formatter, Logger, StreamHandler from pathlib import Path from typing import Any, Literal -import click # Add click import +import click import docstring_parser from mcp.server.fastmcp import server from mcp.server.fastmcp.server import FastMCP, Settings @@ -22,7 +27,13 @@ from sensai.util.helper import mark_used from serena.agent import SerenaAgent, ToolInterface, create_serena_config, show_fatal_exception_safe from serena.config import SerenaAgentContext, SerenaAgentMode from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES -from serena.process_isolated_agent import ProcessIsolatedDashboard, ProcessIsolatedSerenaAgent +from serena.process_isolated_agent import ( + ProcessIsolatedDashboard, + ProcessIsolatedSerenaAgent, + ProcessIsolatedTool, + global_shutdown_event, + request_global_shutdown, +) log = logging.getLogger(__name__) LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" @@ -103,7 +114,7 @@ def create_mcp_server_and_agent( log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, tool_timeout: float | None = None, -) -> tuple[FastMCP, "ProcessIsolatedSerenaAgent"]: +) -> tuple[FastMCP, ProcessIsolatedSerenaAgent]: """ Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. @@ -127,7 +138,6 @@ def create_mcp_server_and_agent( modes_instances = [SerenaAgentMode.load(mode) for mode in modes] try: - # Create configuration without instantiating a full SerenaAgent serena_config = create_serena_config( project=project, context=context_instance, @@ -138,9 +148,12 @@ def create_mcp_server_and_agent( trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, ) - serena_agent_process = ProcessIsolatedSerenaAgent(serena_config=serena_config) + # Start process-isolated dashboard if enabled + serena_dashboard_process = None + if serena_config.web_dashboard: + serena_dashboard_process = ProcessIsolatedDashboard(tool_names=[]) except Exception as e: show_fatal_exception_safe(e) raise @@ -149,25 +162,13 @@ def create_mcp_server_and_agent( """Update the tools in the MCP server - adapted for process isolation.""" nonlocal mcp, serena_agent_process - # Check if process agent is running - if serena_agent_process.process is None or not serena_agent_process.process.is_alive(): - log.debug("Process agent not running yet, skipping tool update") - return - # Get tool names from process-isolated agent # Tools may change as a result of project activation. # NOTE: While we could pass updated tool information on to the MCP server via the callback, Claude Desktop does not, # unfortunately, query for changed tools. It only queries for changed resources and prompts regularly, # so we need to register all tools at startup, unfortunately. - try: - tool_names = serena_agent_process.get_exposed_tool_names() - except Exception as e: - log.error(f"Failed to get tool names from process agent: {e}") - return - + tool_names = serena_agent_process.get_exposed_tool_names() if mcp is not None: - from serena.process_isolated_agent import ProcessIsolatedTool - mcp._tool_manager._tools = {} for tool_name in tool_names: process_isolated_tool = ProcessIsolatedTool(process_agent=serena_agent_process, tool_name=tool_name) @@ -177,163 +178,69 @@ def create_mcp_server_and_agent( @asynccontextmanager async def server_lifespan(mcp_server: FastMCP) -> AsyncIterator[None]: """Manage server startup and shutdown lifecycle.""" - nonlocal serena_agent_process - import asyncio - import webbrowser - mark_used(mcp_server) - serena_dashboard_process: ProcessIsolatedDashboard | None = None - shutdown_event = asyncio.Event() + def signal_handler(signum: int, frame: Any) -> None: + log.info(f"Received signal {signum} in main process") + request_global_shutdown() - async def monitor_worker_process() -> None: - """Monitor the worker process and shutdown server if it exits.""" - while not shutdown_event.is_set(): - try: - if serena_agent_process.process is None or not serena_agent_process.process.is_alive(): - log.info("Worker process has exited, shutting down MCP server") - shutdown_event.set() - break - await asyncio.sleep(1) - except asyncio.CancelledError: - break - except Exception as e: - log.error(f"Error monitoring worker process: {e}") - await asyncio.sleep(1) + def force_exit() -> None: + time.sleep(2.0) # Wait 2 seconds for graceful shutdown + log.warning("Forcing exit after timeout") + os._exit(1) - async def monitor_dashboard_shutdown() -> None: - """Monitor the dashboard shutdown queue.""" - if serena_dashboard_process is None or serena_dashboard_process.shutdown_queue is None: - return + threading.Thread(target=force_exit, daemon=True).start() - while not shutdown_event.is_set(): - try: - # Check if dashboard sent shutdown signal - if not serena_dashboard_process.shutdown_queue.empty(): - serena_dashboard_process.shutdown_queue.get_nowait() - log.info("Dashboard shutdown signal received via queue") - shutdown_event.set() - # Force immediate exit since MCP server doesn't respond properly - import os - import sys + # Install signal handlers + sigint_singal = signal.signal(signal.SIGINT, signal_handler) + sigterm_signal = signal.signal(signal.SIGTERM, signal_handler) - log.info("Forcing immediate process exit from dashboard signal") - sys.stdout.flush() - sys.stderr.flush() - os._exit(0) - await asyncio.sleep(0.1) # Check more frequently - except asyncio.CancelledError: - break - except Exception as e: - log.error(f"Error monitoring dashboard shutdown: {e}") - await asyncio.sleep(1) + if serena_dashboard_process is not None: + log.info("Starting dashboard process") + serena_dashboard_process.start() + log.info("Starting serena agent process") + serena_agent_process.start() + update_tools() - def shutdown_server() -> None: - """Shutdown the server.""" - log.info("Shutdown initiated by dashboard") - shutdown_event.set() - # Force immediate exit since the MCP server doesn't respond to shutdown events - import os - import sys + async def monitor_global_shutdown() -> None: + """Monitor the global shutdown event and trigger local shutdown.""" + while not global_shutdown_event.is_set(): + # Poll the multiprocessing Event in async context + await asyncio.sleep(0.1) + continue + log.info("Global shutdown event detected, initiating server shutdown") + request_global_shutdown() + # Send SIGTERM to self to trigger graceful shutdown + os.kill(os.getpid(), signal.SIGTERM) - log.info("Dashboard shutdown - forcing process exit immediately") - sys.stdout.flush() - sys.stderr.flush() - os._exit(0) + # Start monitoring task + monitor_task = asyncio.create_task(monitor_global_shutdown()) try: - - # Set up global shutdown function for dashboard - from serena.process_isolated_agent import set_global_shutdown_func - - set_global_shutdown_func(shutdown_server) - - # Start process-isolated dashboard if enabled - if serena_config.web_dashboard: - try: - serena_dashboard_process = ProcessIsolatedDashboard(tool_names=[]) - port = serena_dashboard_process.start() - webbrowser.open(f"http://localhost:{port}/dashboard/index.html") - log.info(f"Dashboard started on port {port}") - except Exception as e: - log.error(f"Failed to start dashboard: {e}") - serena_dashboard_process = None - - serena_agent_process.start() - # Update tools now that the process agent is running - update_tools() - - # Start monitoring tasks - tasks = [] - monitor_task = asyncio.create_task(monitor_worker_process()) - tasks.append(monitor_task) - - # Start dashboard shutdown monitor if dashboard is running - if serena_dashboard_process is not None: - dashboard_monitor_task = asyncio.create_task(monitor_dashboard_shutdown()) - tasks.append(dashboard_monitor_task) - - # Set up signal handlers in main process - def signal_handler(signum: int, frame: Any) -> None: - log.info(f"Received signal {signum} in main process") - shutdown_event.set() - # Give a short delay for graceful shutdown, then force exit - import os - import threading - - def force_exit() -> None: - import time - - time.sleep(2.0) # Wait 2 seconds for graceful shutdown - log.warning("Forcing exit after timeout") - os._exit(1) - - threading.Thread(target=force_exit, daemon=True).start() - - import signal - - signal.signal(signal.SIGTERM, signal_handler) - signal.signal(signal.SIGINT, signal_handler) - - try: - # Start the server and wait for shutdown - yield - except (KeyboardInterrupt, SystemExit): - log.info("Received shutdown signal") - shutdown_event.set() - + yield + except (KeyboardInterrupt, SystemExit): + log.info("Received shutdown signal") + request_global_shutdown() except Exception as e: log.error(f"Error in server lifespan: {e}") - shutdown_event.set() + request_global_shutdown() finally: - log.info("Starting server shutdown cleanup") + # Cancel monitor task + monitor_task.cancel() - if "tasks" in locals(): - for task in tasks: - task.cancel() - with contextlib.suppress(TimeoutError, asyncio.CancelledError): - await asyncio.wait_for(task, timeout=1.0) + with contextlib.suppress(asyncio.CancelledError): + await monitor_task - # Stop dashboard first + serena_agent_process.stop() if serena_dashboard_process is not None: - try: - serena_dashboard_process.stop() - except Exception as e: - log.error(f"Error stopping dashboard: {e}") - - # Stop process agent - try: - serena_agent_process.stop() - except Exception as e: - log.error(f"Error stopping process agent: {e}") - - log.info("Server shutdown cleanup completed") + serena_dashboard_process.stop() + request_global_shutdown() + log.info("Shutting down all processes") + signal.signal(signal.SIGINT, sigint_singal) + signal.signal(signal.SIGTERM, sigterm_signal) mcp_settings = Settings(lifespan=server_lifespan, host=host, port=port) mcp = FastMCP(**mcp_settings.model_dump()) - - update_tools() - return mcp, serena_agent_process @@ -494,9 +401,6 @@ def start_mcp_server( f"Used path: {project_file}" ) - log.info( - f"Starting process-isolated serena agent in MCP server with config:\n{agent.serena_config}." - f"\n Log level: {agent.serena_config.log_level}" - ) + log.info(f"Starting MCP server with config:\n{agent.serena_config}.\n Log level: {agent.serena_config.log_level}") mcp_server.run(transport=transport) diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 7b16767b..768e45f6 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -1,27 +1,20 @@ -""" -Process-isolated SerenaAgent to prevent asyncio event loop contamination between MCP server and language server. - -This module provides: -1. ProcessIsolatedSerenaAgent - A wrapper that runs SerenaAgent in a separate process -2. SerenaAgentWorker - The worker process that hosts the actual SerenaAgent -3. JSON-RPC based IPC for communication between processes -""" - import asyncio import contextlib import logging import multiprocessing -import signal +import os import traceback +import webbrowser from collections.abc import Callable from enum import StrEnum from logging.handlers import QueueHandler from multiprocessing.connection import Connection +from multiprocessing.sharedctypes import Synchronized +from multiprocessing.synchronize import Event as EventClass from typing import Any, Literal, Self import uvicorn from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata -from sensai.util.logging import LOG_DEFAULT_FORMAT from serena.agent import SerenaAgent, SerenaConfig, SerenaConfigBase, Tool, ToolInterface, ToolRegistry from serena.config import SerenaAgentContext, SerenaAgentMode @@ -29,296 +22,182 @@ from serena.dashboard import MemoryLogHandler, SerenaDashboardAPI log = logging.getLogger(__name__) -_global_log_q: "multiprocessing.Queue[str]" = multiprocessing.Queue() +# Global synchronization primitives +_global_log_queue: multiprocessing.Queue = multiprocessing.Queue() +_dashboard_ready_event = multiprocessing.Event() +_dashboard_port_value = multiprocessing.Value("i", 0) +global_shutdown_event = multiprocessing.Event() -# Global shutdown function that can be called by the dashboard -class _GlobalShutdownRegistry: - """Registry for global shutdown function.""" - - def __init__(self) -> None: - self.shutdown_func: Callable[[], None] | None = None - - def set_shutdown_func(self, func: Callable[[], None]) -> None: - """Set the global shutdown function.""" - self.shutdown_func = func - - def call_shutdown(self) -> None: - """Call the global shutdown function if it exists.""" - if self.shutdown_func: - self.shutdown_func() +def request_global_shutdown() -> None: + """Signal the global shutdown event.""" + global_shutdown_event.set() + log.info("Global shutdown event set") -_shutdown_registry = _GlobalShutdownRegistry() +def _dashboard_worker( + tool_names: list[str], + log_q: "multiprocessing.Queue[Any]", + dashboard_ready_event: EventClass, + port_value: "Synchronized[int]", + shutdown_evt: EventClass, +) -> None: + """Entry point for the dashboard process.""" + # Route all logging to the shared queue + root = logging.getLogger() + root.handlers.clear() + root.setLevel(logging.DEBUG) + root.addHandler(QueueHandler(log_q)) + + async def _process_logs(api: SerenaDashboardAPI) -> None: + while not shutdown_evt.is_set(): + while not log_q.empty(): + record = log_q.get_nowait() + if record is None: + break + api.memory_log_handler.emit(record) + # Small delay to avoid busy waiting + await asyncio.sleep(0.1) + + async def _monitor_shutdown() -> None: + # Poll the multiprocessing Event in async context + # Check every 100ms until shutdown is requested + loop = asyncio.get_event_loop() + while True: + # Check in executor to avoid blocking + result = await loop.run_in_executor(None, shutdown_evt.is_set) + if result: + break + await asyncio.sleep(0.1) + + async def _async_main() -> None: + api = SerenaDashboardAPI( + memory_log_handler=MemoryLogHandler(), + tool_names=tool_names, + shutdown_callback=shutdown_evt.set, + ) + # Pick a free port and signal readiness + port = api._find_first_free_port(0x5EDA) + port_value.value = port + + # Start server first, then log processing + config = uvicorn.Config( + app=api._app, + host="0.0.0.0", + port=port, + workers=1, + log_config=None, + log_level="critical", + ) + server = uvicorn.Server(config) + + # Serve and await shutdown signal + server_task = asyncio.create_task(server.serve()) + shutdown_task = asyncio.create_task(_monitor_shutdown()) + logging_loop_task = asyncio.create_task(_process_logs(api)) + dashboard_ready_event.set() + + done, pending = await asyncio.wait( + [server_task, shutdown_task, logging_loop_task], + return_when=asyncio.FIRST_COMPLETED, + ) + if shutdown_task in done: + server.should_exit = True + await server_task + + # Cancel remaining tasks + for task in pending: + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task + + try: + asyncio.run(_async_main()) + except BaseException: + logging.exception("Dashboard worker crashed") + finally: + logging.info("Dashboard worker exiting") -def set_global_shutdown_func(func: Callable[[], None]) -> None: - """Set the global shutdown function.""" - _shutdown_registry.set_shutdown_func(func) - - -def call_global_shutdown() -> None: - """Call the global shutdown function if it exists.""" - _shutdown_registry.call_shutdown() +def _shutdown_process(proc: multiprocessing.Process, timeout: float = 1.0) -> None: + """Helper to shutdown a process gracefully.""" + if proc.is_alive(): + proc.terminate() + proc.join(timeout=timeout) + if proc.is_alive(): + log.error("Process did not terminate gracefully, forcing kill") + proc.kill() + proc.join(timeout=timeout) + if proc.is_alive(): + log.error("Process did not respond to kill") class ProcessIsolatedDashboard: - """Process-isolated dashboard to prevent asyncio contamination with MCP server and worker.""" + """Dashboard running in a separate process to avoid asyncio contamination.""" def __init__(self, tool_names: list[str]): self.tool_names = tool_names self.process: multiprocessing.Process | None = None - self.shutdown_queue: multiprocessing.Queue[bool] | None = None - self.port_queue: multiprocessing.Queue[int] | None = None - self._log_handler: ProcessDashboardLogHandler | None = None - def start(self) -> int: - """Start the dashboard process and return the port number.""" + def start(self, timeout: float = 10.0) -> None: + """Start the dashboard process.""" if self.process is not None: - raise RuntimeError("Dashboard process already started") + raise RuntimeError("Dashboard already started") - log.info("Starting process-isolated dashboard") - - # Create communication queues - self.shutdown_queue = multiprocessing.Queue() - self.port_queue = multiprocessing.Queue() - - # Create and start dashboard process - dashboard_worker = DashboardWorker(shutdown_queue=self.shutdown_queue, port_queue=self.port_queue, tool_names=self.tool_names) - self.process = multiprocessing.Process(target=dashboard_worker.run) + self.process = multiprocessing.Process( + target=_dashboard_worker, + args=(self.tool_names, _global_log_queue, _dashboard_ready_event, _dashboard_port_value, global_shutdown_event), + daemon=True, + ) self.process.start() - # Set up log handler to send logs to dashboard process - logging.Logger.root.addHandler(ProcessDashboardLogHandler()) + if not _dashboard_ready_event.wait(timeout): + self.process.terminate() + self.process.join(timeout=1.0) + self.process = None + raise RuntimeError("Dashboard failed to start within timeout") - # Wait for dashboard to start and return port - try: - port = self.port_queue.get(timeout=10) # Wait up to 10 seconds for startup - log.debug(f"Dashboard started on port {port}") - return port - except Exception as e: - self.stop() - raise RuntimeError(f"Failed to start dashboard: {e}") from e + port = _dashboard_port_value.value + log.info(f"Dashboard started on port {port}") + webbrowser.open(f"http://localhost:{port}/dashboard/index.html") - def stop(self) -> None: - """Stop the dashboard process.""" + def stop(self, timeout: float = 1.0) -> None: + """Signal shutdown and wait for the dashboard process to exit.""" if self.process is None: return - log.info("Stopping process-isolated dashboard") - - try: - # Signal shutdown - if self.shutdown_queue is not None: - import contextlib - - with contextlib.suppress(Exception): - self.shutdown_queue.put_nowait(True) - - # Wait for process to terminate - if self.process.is_alive(): - self.process.join(timeout=5.0) - - if self.process.is_alive(): - log.warning("Dashboard process did not terminate gracefully, forcing termination") - self.process.terminate() - self.process.join(timeout=3.0) - - if self.process.is_alive(): - log.error("Dashboard process could not be terminated, killing it") - self.process.kill() - self.process.join() - - except Exception as e: - log.error(f"Error stopping dashboard process: {e}") - - finally: - self.process = None - self.shutdown_queue = None - self.port_queue = None - - log.info("Process-isolated dashboard stopped") - - def update_tool_names(self, tool_names: list[str]) -> None: - """Update tool names (for future enhancement).""" - self.tool_names = tool_names - # TODO: Could send update to dashboard process if needed - - -class ProcessDashboardLogHandler(logging.Handler): - """Log handler that sends log messages to dashboard process via queue.""" - - def __init__(self, level: int = logging.NOTSET): - super().__init__(level=level) - self.setFormatter(logging.Formatter(LOG_DEFAULT_FORMAT)) - - def emit(self, record: logging.LogRecord) -> None: - msg = self.format(record) - # Non-blocking put to avoid deadlocks - _global_log_q.put_nowait(msg) - - -class DashboardWorker: - """Worker process that hosts the dashboard with its own asyncio loop.""" - - def __init__( - self, - shutdown_queue: "multiprocessing.Queue[bool]", - port_queue: "multiprocessing.Queue[int]", - tool_names: list[str], - ): - self.shutdown_queue = shutdown_queue - self.port_queue = port_queue - self.tool_names = tool_names - - def run(self) -> None: - """Main dashboard worker loop - runs in separate process.""" - try: - log.info("Dashboard worker process started") - asyncio.run(self._async_main()) - except Exception as e: - log.error(f"Fatal error in dashboard worker process: {e}") - finally: - log.info("Dashboard worker process stopped") - - async def _async_main(self) -> None: - """Main async loop for dashboard process.""" - log_processor_task = None - try: - # Set up dashboard API with shutdown callback - dashboard_api = SerenaDashboardAPI( - memory_log_handler=MemoryLogHandler(), tool_names=self.tool_names, shutdown_callback=self._handle_shutdown - ) - - # Start log processor task - log_processor_task = asyncio.create_task(self._process_log_queue(dashboard_api)) - - # Find free port and signal to parent - port = dashboard_api._find_first_free_port(0x5EDA) - self.port_queue.put(port) - - config = uvicorn.Config(app=dashboard_api._app, host="0.0.0.0", port=port, workers=1, log_config=None, log_level="critical") - server = uvicorn.Server(config) - - # Start server and shutdown monitor concurrently - shutdown_task = asyncio.create_task(self._monitor_shutdown()) - server_task = asyncio.create_task(server.serve()) - - # Wait for either server to complete or shutdown signal - done, pending = await asyncio.wait([server_task, shutdown_task], return_when=asyncio.FIRST_COMPLETED) - # Cancel pending tasks - for task in pending: - task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await task - - except Exception as e: - log.error(f"Error in dashboard worker: {e}") - finally: - # Clean up - if log_processor_task and not log_processor_task.done(): - log_processor_task.cancel() - - with contextlib.suppress(asyncio.CancelledError): - await log_processor_task - - @staticmethod - async def _process_log_queue(dashboard_api: SerenaDashboardAPI) -> None: - """Process log messages from the queue and forward to memory handler.""" - while True: - try: - # Check for log messages - while not _global_log_q.empty(): - try: - log_msg = _global_log_q.get_nowait() - record = logging.LogRecord( - name="forwarded", level=logging.INFO, pathname="", lineno=0, msg=log_msg, args=(), exc_info=None - ) - dashboard_api.memory_log_handler.emit(record) - except Exception as e: - log.error(f"Error processing log message: {e}") - await asyncio.sleep(1) - - # Small delay to avoid busy waiting - await asyncio.sleep(0.1) - except asyncio.CancelledError: - break - except Exception as e: - log.error(f"Error processing log queue: {e}") - await asyncio.sleep(1) - - async def _monitor_shutdown(self) -> None: - """Monitor for shutdown signals.""" - while True: - try: - # Check for shutdown signal - if not self.shutdown_queue.empty(): - self.shutdown_queue.get_nowait() - log.info("Dashboard shutdown signal received") - break - - await asyncio.sleep(0.5) - - except asyncio.CancelledError: - break - except Exception as e: - log.error(f"Error monitoring shutdown: {e}") - await asyncio.sleep(1) - - def _handle_shutdown(self) -> None: - """Handle shutdown request from dashboard UI.""" - log.info("Dashboard UI shutdown requested") - # Signal shutdown to the main process through the parent dashboard - if self.shutdown_queue is not None: - try: - self.shutdown_queue.put_nowait(True) - log.info("Sent shutdown signal to main process via queue") - except Exception as e: - log.error(f"Failed to send shutdown signal: {e}") - - # Also try the global shutdown as fallback - try: - call_global_shutdown() - except Exception as e: - log.error(f"Global shutdown failed: {e}") - - # Force exit this dashboard process - import os - import threading - - def delayed_exit() -> None: - import time - - time.sleep(0.5) # Give time for shutdown signal to be sent - log.info("Dashboard process forcing exit") - os._exit(0) - - threading.Thread(target=delayed_exit, daemon=True).start() + log.info("Stopping dashboard process") + request_global_shutdown() + _shutdown_process(self.process, timeout=timeout) + self.process = None class SerenaAgentWorker: """Worker process that hosts the actual SerenaAgent.""" + class RequestMethod(StrEnum): + INITIALIZE = "initialize" + TOOL_CALL = "tool_call" + GET_ACTIVE_TOOL_NAMES = "get_active_tool_names" + IS_LANGUAGE_SERVER_RUNNING = "is_language_server_running" + RESET_LANGUAGE_SERVER = "reset_language_server" + GET_EXPOSED_TOOL_NAMES = "get_exposed_tool_names" + SHUTDOWN = "shutdown" + def __init__(self, conn: Connection): self.conn = conn self.agent: SerenaAgent | None = None - self._shutdown_requested = False - def run(self, q: "multiprocessing.Queue[str]") -> None: + def run(self, log_queue: "multiprocessing.Queue[str]") -> None: """Main worker loop - runs in separate process.""" - qh = QueueHandler(q) + qh = QueueHandler(log_queue) root = logging.getLogger() root.setLevel(logging.DEBUG) root.addHandler(qh) + log.info("SerenaAgent worker process started") try: - # Set up signal handler for clean shutdown - signal.signal(signal.SIGTERM, self._signal_handler) - signal.signal(signal.SIGINT, self._signal_handler) - - log.info("SerenaAgent worker process started") - - while not self._shutdown_requested: + while not global_shutdown_event.is_set(): try: # Use polling to avoid blocking indefinitely if self.conn.poll(timeout=0.5): # Poll every 500ms @@ -354,17 +233,12 @@ class SerenaAgentWorker: except Exception as e: log.error(f"Error in worker main loop: {e}") break - except Exception as e: log.error(f"Fatal error in worker process: {e}") finally: self._cleanup() log.info("SerenaAgent worker process stopped") - - def _signal_handler(self, signum: int, frame: Any) -> None: - """Handle shutdown signals gracefully.""" - log.info(f"Received signal {signum}, initiating shutdown") - self._shutdown_requested = True + os._exit(0) # Exit without raising any further exceptions def _handle_request(self, request: dict[str, Any]) -> dict[str, Any]: """Handle a single request.""" @@ -386,10 +260,9 @@ class SerenaAgentWorker: case self.RequestMethod.GET_EXPOSED_TOOL_NAMES: return self._get_exposed_tool_names() case self.RequestMethod.SHUTDOWN: - return self._shutdown() + return self.shutdown() case _: return {"error": f"Unknown method: {method}"} - except Exception as e: return {"error": str(e), "traceback": traceback.format_exc()} @@ -427,17 +300,6 @@ class SerenaAgentWorker: trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, ) - - # Set up global shutdown function for dashboard - def shutdown_worker() -> None: - log.info("Global shutdown function called") - self._cleanup() - import sys - - sys.exit(0) - - set_global_shutdown_func(shutdown_worker) - return {"result": "SerenaAgent initialized successfully"} except Exception as e: return {"error": f"Failed to initialize SerenaAgent: {e!s}", "traceback": traceback.format_exc()} @@ -515,18 +377,15 @@ class SerenaAgentWorker: except Exception as e: return {"error": str(e), "traceback": traceback.format_exc()} - def _shutdown(self) -> dict[str, Any]: - """Handle shutdown request from dashboard.""" + def shutdown(self) -> dict[str, Any]: try: - log.info("Shutdown requested from dashboard") - # Clean up resources before exiting + log.info("Shutting down SerenaAgent worker process on request") self._cleanup() - # Exit the worker process - this will cause the main process to detect the termination - # and shut down gracefully through the server lifespan manager - import sys - - sys.exit(0) + request_global_shutdown() + # Return successful response before exiting + return {"result": "Shutdown initiated"} except Exception as e: + log.error(f"Error during shutdown: {e}") return {"error": str(e), "traceback": traceback.format_exc()} def _cleanup(self) -> None: @@ -539,17 +398,6 @@ class SerenaAgentWorker: log.error(f"Error stopping language server: {e}") self.agent = None - class RequestMethod(StrEnum): - """Enumeration of available request methods.""" - - INITIALIZE = "initialize" - TOOL_CALL = "tool_call" - GET_ACTIVE_TOOL_NAMES = "get_active_tool_names" - IS_LANGUAGE_SERVER_RUNNING = "is_language_server_running" - RESET_LANGUAGE_SERVER = "reset_language_server" - GET_EXPOSED_TOOL_NAMES = "get_exposed_tool_names" - SHUTDOWN = "shutdown" - class ProcessIsolatedSerenaAgent: """Process-isolated wrapper for SerenaAgent that prevents asyncio contamination.""" @@ -603,7 +451,7 @@ class ProcessIsolatedSerenaAgent: # Create and start worker process, passing along the dashboard's queue if available worker = SerenaAgentWorker(child_conn) - self.process = multiprocessing.Process(target=worker.run, args=[_global_log_q]) + self.process = multiprocessing.Process(target=worker.run, args=[_global_log_queue]) self.process.start() # Initialize the agent in the worker process @@ -634,31 +482,13 @@ class ProcessIsolatedSerenaAgent: """Stop the worker process.""" if self.process is None: return - - log.info("Stopping process-isolated SerenaAgent") - + log.info("Stopping SerenaAgent process") try: # Close connection to signal worker to shutdown if self.conn is not None: self.conn.close() - - # Wait for process to terminate with shorter timeout - if self.process.is_alive(): - self.process.join(timeout=3.0) # Reduced from 10s to 3s - - if self.process.is_alive(): - log.warning("Worker process did not terminate gracefully, sending SIGTERM") - self.process.terminate() - self.process.join(timeout=2.0) # Reduced from 5s to 2s - - if self.process.is_alive(): - log.error("Worker process did not respond to SIGTERM, sending SIGKILL") - self.process.kill() - self.process.join(timeout=1.0) # Add timeout for kill as well - - if self.process.is_alive(): - log.error("Worker process could not be killed - forcing cleanup") - + _shutdown_process(self.process, timeout=2.0) + self.process = None except KeyboardInterrupt: log.warning("Keyboard interrupt during shutdown - forcing termination") if self.process and self.process.is_alive(): @@ -666,12 +496,11 @@ class ProcessIsolatedSerenaAgent: self.process.join(timeout=1.0) except Exception as e: log.error(f"Error stopping worker process: {e}") - finally: self.process = None self.conn = None - log.info("Process-isolated SerenaAgent stopped") + log.info("SerenaAgent stopped") def _make_request(self, method: SerenaAgentWorker.RequestMethod, params: dict[str, Any] | None = None) -> dict[str, Any]: """Make a request to the worker process.""" @@ -693,8 +522,7 @@ class ProcessIsolatedSerenaAgent: timeout = self.serena_config.tool_timeout if self.conn.poll(timeout): try: - response = self.conn.recv() - return response + return self.conn.recv() except (EOFError, BrokenPipeError) as e: raise RuntimeError("Failed to receive response: worker process may have crashed") from e else: From 3d4892394a75fb01f14745ed401f7e753defde7c Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Mon, 16 Jun 2025 02:40:04 +0200 Subject: [PATCH 28/28] Fixed project activation in process-isolated agent --- src/serena/agent.py | 3 --- src/serena/mcp.py | 6 ++++-- src/serena/process_isolated_agent.py | 3 +++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/serena/agent.py b/src/serena/agent.py index a169e2fc..1933d8e1 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -569,7 +569,6 @@ class MemoriesManagerMDFilesInProject(MemoriesManager): def create_serena_config( - project: str | None = None, serena_config: SerenaConfigBase | None = None, context: SerenaAgentContext | None = None, modes: list[SerenaAgentMode] | None = None, @@ -585,7 +584,6 @@ def create_serena_config( This function extracts the configuration creation logic from SerenaAgent.__init__ to allow creating configurations independently for process isolation and other use cases. - :param project: the project to register/activate or None :param serena_config: the base Serena configuration or None to read from default location :param context: the context in which the agent will operate :param modes: list of modes in which the agent will operate @@ -670,7 +668,6 @@ class SerenaAgent: """ # obtain serena configuration using the decoupled factory function self.serena_config = create_serena_config( - project=None, # Don't pass project here, handle activation separately serena_config=serena_config, context=context, modes=modes, diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 3551ec11..3eab295f 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -137,9 +137,11 @@ def create_mcp_server_and_agent( context_instance = SerenaAgentContext.load(context) modes_instances = [SerenaAgentMode.load(mode) for mode in modes] + if project is not None: + log.info(f"Will use project at mcp server startup: {project}") + try: serena_config = create_serena_config( - project=project, context=context_instance, modes=modes_instances, enable_web_dashboard=enable_web_dashboard, @@ -148,7 +150,7 @@ def create_mcp_server_and_agent( trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, ) - serena_agent_process = ProcessIsolatedSerenaAgent(serena_config=serena_config) + serena_agent_process = ProcessIsolatedSerenaAgent(project=project, serena_config=serena_config) # Start process-isolated dashboard if enabled serena_dashboard_process = None diff --git a/src/serena/process_isolated_agent.py b/src/serena/process_isolated_agent.py index 768e45f6..f87856b6 100644 --- a/src/serena/process_isolated_agent.py +++ b/src/serena/process_isolated_agent.py @@ -436,6 +436,7 @@ class ProcessIsolatedSerenaAgent: # Use the default config loader self.serena_config = SerenaConfig.from_config_file() self.process: multiprocessing.Process | None = None + self.project = project self.conn: Connection | None = None def start(self) -> None: @@ -456,12 +457,14 @@ class ProcessIsolatedSerenaAgent: # Initialize the agent in the worker process try: + # TODO: instead pass config and project to worker, use at startup of agent there # Prepare initialization parameters, converting complex objects to dict if present init_params = self._init_params.copy() if init_params["serena_config"] is not None and hasattr(init_params["serena_config"], "to_dict"): # Convert SerenaConfigBase to dict for serialization serena_config_obj = init_params["serena_config"] init_params["serena_config"] = serena_config_obj.to_dict() # type: ignore + # init_params["project"] = self.project # Note: context and modes are not serializable across processes, # so they will be None and SerenaAgent will use defaults init_params["context"] = None