From 22de2b5f80232221abb6a1bd9c82d94d4c3cf6e9 Mon Sep 17 00:00:00 2001 From: Teryl Taylor Date: Mon, 11 Aug 2025 19:47:08 -0600 Subject: [PATCH 01/65] feat: add support for external plugins Signed-off-by: Teryl Taylor --- mcpgateway/config.py | 4 +- mcpgateway/plugins/__init__.py | 13 +- mcpgateway/plugins/framework/base.py | 92 ++-- mcpgateway/plugins/framework/constants.py | 27 ++ mcpgateway/plugins/framework/errors.py | 63 +++ mcpgateway/plugins/framework/external/mcp.py | 176 +++++++ mcpgateway/plugins/framework/loader/plugin.py | 11 +- mcpgateway/plugins/framework/manager.py | 50 +- mcpgateway/plugins/framework/models.py | 445 +++++++++++++++++- mcpgateway/plugins/framework/plugin_types.py | 369 --------------- mcpgateway/plugins/framework/utils.py | 16 +- mcpgateway/services/resource_service.py | 2 +- mcpgateway/services/tool_service.py | 5 +- mcpgateway/validators.py | 4 +- plugins/deny_filter/deny.py | 9 +- plugins/external/config.yaml | 22 + plugins/pii_filter/pii_filter.py | 5 +- plugins/regex_filter/search_replace.py | 14 +- plugins/resource_filter/resource_filter.py | 5 +- .../test_resource_plugin_integration.py | 10 +- .../framework/loader/test_plugin_loader.py | 7 +- .../plugins/framework/test_manager.py | 8 +- .../framework/test_manager_extended.py | 36 +- .../plugins/framework/test_resource_hooks.py | 33 +- .../plugins/framework/test_utils.py | 22 +- .../plugins/pii_filter/test_pii_filter.py | 18 +- .../resource_filter/test_resource_filter.py | 9 +- .../services/test_resource_service_plugins.py | 5 +- 28 files changed, 911 insertions(+), 569 deletions(-) create mode 100644 mcpgateway/plugins/framework/constants.py create mode 100644 mcpgateway/plugins/framework/errors.py create mode 100644 mcpgateway/plugins/framework/external/mcp.py delete mode 100644 mcpgateway/plugins/framework/plugin_types.py create mode 100644 plugins/external/config.yaml diff --git a/mcpgateway/config.py b/mcpgateway/config.py index 922479693..366204d7e 100644 --- a/mcpgateway/config.py +++ b/mcpgateway/config.py @@ -659,9 +659,7 @@ def validate_database(self) -> None: db_dir.mkdir(parents=True) # Validation patterns for safe display (configurable) - validation_dangerous_html_pattern: str = ( - r"<(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)\b|" - ) + validation_dangerous_html_pattern: str = r"<(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)\b|" validation_dangerous_js_pattern: str = r"(?i)(?:^|\s|[\"'`<>=])(javascript:|vbscript:|data:\s*[^,]*[;\s]*(javascript|vbscript)|\bon[a-z]+\s*=|<\s*script\b)" diff --git a/mcpgateway/plugins/__init__.py b/mcpgateway/plugins/__init__.py index d93e84e99..7a5234fdb 100644 --- a/mcpgateway/plugins/__init__.py +++ b/mcpgateway/plugins/__init__.py @@ -13,14 +13,7 @@ """ from mcpgateway.plugins.framework.manager import PluginManager -from mcpgateway.plugins.framework.models import PluginViolation -from mcpgateway.plugins.framework.plugin_types import GlobalContext, PluginViolationError, PromptPosthookPayload, PromptPrehookPayload +from mcpgateway.plugins.framework.models import GlobalContext, PluginViolation, PromptPosthookPayload, PromptPrehookPayload, ToolPostInvokePayload, ToolPreInvokePayload +from mcpgateway.plugins.framework.errors import PluginViolationError, PluginError -__all__ = [ - "GlobalContext", - "PluginManager", - "PluginViolation", - "PluginViolationError", - "PromptPosthookPayload", - "PromptPrehookPayload", -] +__all__ = ["GlobalContext", "PluginError", "PluginManager", "PluginViolation", "PluginViolationError", "PromptPosthookPayload", "PromptPrehookPayload", "ToolPostInvokePayload", "ToolPreInvokePayload"] diff --git a/mcpgateway/plugins/framework/base.py b/mcpgateway/plugins/framework/base.py index 3b70a01d5..e5cd87492 100644 --- a/mcpgateway/plugins/framework/base.py +++ b/mcpgateway/plugins/framework/base.py @@ -20,9 +20,12 @@ import uuid # First-Party -from mcpgateway.plugins.framework.models import HookType, PluginCondition, PluginConfig, PluginMode -from mcpgateway.plugins.framework.plugin_types import ( +from mcpgateway.plugins.framework.models import ( + HookType, + PluginCondition, + PluginConfig, PluginContext, + PluginMode, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, @@ -138,6 +141,9 @@ def conditions(self) -> list[PluginCondition] | None: """ return self._config.conditions + async def initialize(self) -> None: + """Initialize the plugin.""" + async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: """Plugin hook run before a prompt is retrieved and rendered. @@ -177,18 +183,14 @@ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginCo payload: The tool payload to be analyzed. context: Contextual information about the hook call. - Returns: - ToolPreInvokeResult with processing status and modified payload. - - Examples: - >>> from mcpgateway.plugins.framework.plugin_types import ToolPreInvokePayload, PluginContext, GlobalContext - >>> payload = ToolPreInvokePayload("calculator", {"operation": "add", "a": 5, "b": 3}) - >>> context = PluginContext(GlobalContext(request_id="123")) - >>> # In async context: - >>> # result = await plugin.tool_pre_invoke(payload, context) + Raises: + NotImplementedError: needs to be implemented by sub class. """ - # Default pass-through implementation - return ToolPreInvokeResult(continue_processing=True, modified_payload=payload) + raise NotImplementedError( + f"""'tool_pre_invoke' not implemented for plugin {self._config.name} + of plugin type {type(self)} + """ + ) async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: """Plugin hook run after a tool is invoked. @@ -197,18 +199,14 @@ async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: Plugin payload: The tool result payload to be analyzed. context: Contextual information about the hook call. - Returns: - ToolPostInvokeResult with processing status and modified result. - - Examples: - >>> from mcpgateway.plugins.framework.plugin_types import ToolPostInvokePayload, PluginContext, GlobalContext - >>> payload = ToolPostInvokePayload("calculator", {"result": 8, "status": "success"}) - >>> context = PluginContext(GlobalContext(request_id="123")) - >>> # In async context: - >>> # result = await plugin.tool_post_invoke(payload, context) + Raises: + NotImplementedError: needs to be implemented by sub class. """ - # Default pass-through implementation - return ToolPostInvokeResult(continue_processing=True, modified_payload=payload) + raise NotImplementedError( + f"""'tool_post_invoke' not implemented for plugin {self._config.name} + of plugin type {type(self)} + """ + ) async def resource_pre_fetch(self, payload, context): """Plugin hook run before a resource is fetched. @@ -217,22 +215,14 @@ async def resource_pre_fetch(self, payload, context): payload: The resource payload to be analyzed. context: Contextual information about the hook call. - Returns: - ResourcePreFetchResult with processing status and modified payload. - - Examples: - >>> from mcpgateway.plugins.framework.plugin_types import ResourcePreFetchPayload, PluginContext, GlobalContext - >>> payload = ResourcePreFetchPayload("file:///data.txt", {"cache": True}) - >>> context = PluginContext(GlobalContext(request_id="123")) - >>> # In async context: - >>> # result = await plugin.resource_pre_fetch(payload, context) + Raises: + NotImplementedError: needs to be implemented by sub class. """ - # Import here to avoid circular dependency - # First-Party - from mcpgateway.plugins.framework.plugin_types import ResourcePreFetchResult - - # Default pass-through implementation - return ResourcePreFetchResult(continue_processing=True, modified_payload=payload) + raise NotImplementedError( + f"""'resource_pre_fetch' not implemented for plugin {self._config.name} + of plugin type {type(self)} + """ + ) async def resource_post_fetch(self, payload, context): """Plugin hook run after a resource is fetched. @@ -241,24 +231,14 @@ async def resource_post_fetch(self, payload, context): payload: The resource content payload to be analyzed. context: Contextual information about the hook call. - Returns: - ResourcePostFetchResult with processing status and modified content. - - Examples: - >>> from mcpgateway.plugins.framework.plugin_types import ResourcePostFetchPayload, PluginContext, GlobalContext - >>> from mcpgateway.models import ResourceContent - >>> content = ResourceContent(type="resource", uri="file:///data.txt", text="Data") - >>> payload = ResourcePostFetchPayload("file:///data.txt", content) - >>> context = PluginContext(GlobalContext(request_id="123")) - >>> # In async context: - >>> # result = await plugin.resource_post_fetch(payload, context) + Raises: + NotImplementedError: needs to be implemented by sub class. """ - # Import here to avoid circular dependency - # First-Party - from mcpgateway.plugins.framework.plugin_types import ResourcePostFetchResult - - # Default pass-through implementation - return ResourcePostFetchResult(continue_processing=True, modified_payload=payload) + raise NotImplementedError( + f"""'resource_post_fetch' not implemented for plugin {self._config.name} + of plugin type {type(self)} + """ + ) async def shutdown(self) -> None: """Plugin cleanup code.""" diff --git a/mcpgateway/plugins/framework/constants.py b/mcpgateway/plugins/framework/constants.py new file mode 100644 index 000000000..af2abf44c --- /dev/null +++ b/mcpgateway/plugins/framework/constants.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +"""Plugins constants file. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +This module stores a collection of plugin constants used throughout the framework. +""" + +# Model constants. +# Specialized plugin types. +EXTERNAL_PLUGIN_TYPE = "external" + +# MCP related constants. +PYTHON_SUFFIX = ".py" +URL = "url" +SCRIPT = "script" +AFTER = "after" + +NAME = "name" +PYTHON = "python" +PLUGIN_NAME = "plugin_name" +PAYLOAD = "payload" +CONTEXT = "context" +GET_PLUGIN_CONFIG = "get_plugin_config" +IGNORE_CONFIG_EXTERNAL = "ignore_config_external" diff --git a/mcpgateway/plugins/framework/errors.py b/mcpgateway/plugins/framework/errors.py new file mode 100644 index 000000000..113990f90 --- /dev/null +++ b/mcpgateway/plugins/framework/errors.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +"""Pydantic models for plugins. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +This module implements the pydantic models associated with +the base plugin layer including configurations, and contexts. +""" + +# First-Party +from mcpgateway.plugins.framework.models import PluginErrorModel, PluginViolation + + +class PluginViolationError(Exception): + """A plugin violation error. + + Attributes: + violation (PluginViolation): the plugin violation. + message (str): the plugin violation reason. + """ + + def __init__(self, message: str, violation: PluginViolation | None = None): + """Initialize a plugin violation error. + + Args: + message: the reason for the violation error. + violation: the plugin violation object details. + """ + self.message = message + self.violation = violation + super().__init__(self.message) + + +class PluginError(Exception): + """A plugin error object for errors internal to the plugin. + + Attributes: + error (PluginErrorModel): the plugin error object. + """ + + def __init__(self, error: PluginErrorModel): + """Initialize a plugin violation error. + + Args: + error: the plugin error details. + """ + self.error = error + super().__init__(self.error.message) + + +def convert_exception_to_error(exception: Exception, plugin_name: str) -> PluginErrorModel: + """Converts an exception object into a PluginErrorModel. Primarily used for external plugin error handling. + + Args: + exception: The exception to be converted. + plugin_name: The name of the plugin on which the exception occurred. + + Returns: + A plugin error pydantic object that can be sent over HTTP. + """ + return PluginErrorModel(message=repr(exception), plugin_name=plugin_name) diff --git a/mcpgateway/plugins/framework/external/mcp.py b/mcpgateway/plugins/framework/external/mcp.py new file mode 100644 index 000000000..bcb9becfc --- /dev/null +++ b/mcpgateway/plugins/framework/external/mcp.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +"""External plugin client which connects to a remote server through MCP. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +Module that contains plugin MCP client code to serve external plugins. +""" + +# Standard +from contextlib import AsyncExitStack +import json +import logging +from typing import Any, Optional + +# Third-Party +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client +from mcp.client.streamable_http import streamablehttp_client + +# First-Party +from mcpgateway.plugins.framework.base import Plugin +from mcpgateway.plugins.framework.constants import ( + CONTEXT, + GET_PLUGIN_CONFIG, + IGNORE_CONFIG_EXTERNAL, + NAME, + PAYLOAD, + PLUGIN_NAME, + PYTHON, + PYTHON_SUFFIX, +) +from mcpgateway.plugins.framework.models import HookType, PluginConfig, PluginContext, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, PromptPrehookResult +from mcpgateway.schemas import TransportType + +logger = logging.getLogger(__name__) + + +class ExternalPlugin(Plugin): + """External plugin object for pre/post processing of inputs and outputs at various locations throughout the mcp gateway. The External Plugin connects to a remote MCP server that contains plugins.""" + + def __init__(self, config: PluginConfig) -> None: + """Initialize a plugin with a configuration and context. + + Args: + config: The plugin configuration + """ + super().__init__(config) + self._session: Optional[ClientSession] = None + self._exit_stack = AsyncExitStack() + self._http: Optional[Any] + self._stdio: Optional[Any] + self._write: Optional[Any] + + async def initialize(self) -> None: + """Initialize the plugin's connection to the MCP server. + + Raises: + ValueError: if unable to retrieve plugin configuration of external plugin. + """ + + if not self._config.mcp: + raise ValueError(f"The mcp section must be defined for external plugin {self.name}") + if self._config.mcp.proto == TransportType.STDIO: + await self.__connect_to_stdio_server(self._config.mcp.script) + elif self._config.mcp.proto == TransportType.STREAMABLEHTTP: + await self.__connect_to_http_server(self._config.mcp.url) + + config = await self.__get_plugin_config() + + if not config: + raise ValueError(f"Unable to retrieve configuration for external plugin {self.name}") + + current_config = self._config.model_dump(exclude_unset=True) + remote_config = config.model_dump(exclude_unset=True) + remote_config.update(current_config) + + context = {IGNORE_CONFIG_EXTERNAL: True} + + self._config = PluginConfig.model_validate(remote_config, context=context) + + async def __connect_to_stdio_server(self, server_script_path: str) -> None: + """Connect to an MCP plugin server via stdio. + + Args: + server_script_path: Path to the server script (.py). + + Raises: + ValueError: if stdio script is not a python script. + """ + is_python = server_script_path.endswith(PYTHON_SUFFIX) if server_script_path else False + if not (is_python): + raise ValueError("Server script must be a .py file") + + server_params = StdioServerParameters(command=PYTHON, args=[server_script_path], env=None) + + stdio_transport = await self._exit_stack.enter_async_context(stdio_client(server_params)) + self._stdio, self._write = stdio_transport + self._session = await self._exit_stack.enter_async_context(ClientSession(self._stdio, self._write)) + + await self._session.initialize() + + # List available tools + response = await self._session.list_tools() + tools = response.tools + logger.info("\nConnected to plugin MCP server (stdio) with tools: %s", " ".join([tool.name for tool in tools])) + + async def __connect_to_http_server(self, uri: str): + """Connect to an MCP plugin server via streamable http. + + Args: + uri: the URI of the mcp plugin server. + """ + + http_transport = await self._exit_stack.enter_async_context(streamablehttp_client(uri)) + self._http, self._write, _ = http_transport + self._session = await self._exit_stack.enter_async_context(ClientSession(self._http, self._write)) + + await self._session.initialize() + + # List available tools + response = await self._session.list_tools() + tools = response.tools + logger.info("\nConnected to plugin MCP (http) server with tools: %s", " ".join([tool.name for tool in tools])) + + async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: + """Plugin hook run before a prompt is retrieved and rendered. + + Args: + payload: The prompt payload to be analyzed. + context: contextual information about the hook call. Including why it was called. + + Returns: + The prompt prehook with name and arguments as modified or blocked by the plugin. + """ + + result = await self._session.call_tool(HookType.PROMPT_PRE_FETCH, {PLUGIN_NAME: self.name, PAYLOAD: payload, CONTEXT: context}) + for content in result.content: + res = json.loads(content.text) + return PromptPrehookResult.model_validate(res) + return PromptPrehookResult(continue_processing=True) + + async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult: + """Plugin hook run after a prompt is rendered. + + Args: + payload: The prompt payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + A set of prompt messages as modified or blocked by the plugin. + """ + + result = await self._session.call_tool(HookType.PROMPT_POST_FETCH, {PLUGIN_NAME: self.name, PAYLOAD: payload, CONTEXT: context}) + print(result) + for content in result.content: + res = json.loads(content.text) + return PromptPosthookResult.model_validate(res) + return PromptPosthookResult(continue_processing=True) + + async def __get_plugin_config(self) -> PluginConfig | None: + """Retrieve plugin configuration for the current plugin on the remote MCP server. + + Returns: + A plugin configuration for the current plugin from a remote MCP server. + """ + configs = await self._session.call_tool(GET_PLUGIN_CONFIG, {NAME: self.name}) + for content in configs.content: + conf = json.loads(content.text) + return PluginConfig.model_validate(conf) + return None + + async def shutdown(self) -> None: + """Plugin cleanup code.""" + await self._exit_stack.aclose() diff --git a/mcpgateway/plugins/framework/loader/plugin.py b/mcpgateway/plugins/framework/loader/plugin.py index f08a4dcd5..c7aa3cff1 100644 --- a/mcpgateway/plugins/framework/loader/plugin.py +++ b/mcpgateway/plugins/framework/loader/plugin.py @@ -14,6 +14,8 @@ # First-Party from mcpgateway.plugins.framework.base import Plugin +from mcpgateway.plugins.framework.constants import EXTERNAL_PLUGIN_TYPE +from mcpgateway.plugins.framework.external.mcp import ExternalPlugin from mcpgateway.plugins.framework.models import PluginConfig from mcpgateway.plugins.framework.utils import import_module, parse_class_name @@ -70,7 +72,10 @@ def __register_plugin_type(self, kind: str) -> None: kind: The fully-qualified type of the plugin to be registered. """ if kind not in self._plugin_types: - plugin_type = self.__get_plugin_type(kind) + if kind == EXTERNAL_PLUGIN_TYPE: + plugin_type = ExternalPlugin + else: + plugin_type = self.__get_plugin_type(kind) self._plugin_types[kind] = plugin_type async def load_and_instantiate_plugin(self, config: PluginConfig) -> Plugin | None: @@ -86,7 +91,9 @@ async def load_and_instantiate_plugin(self, config: PluginConfig) -> Plugin | No self.__register_plugin_type(config.kind) plugin_type = self._plugin_types[config.kind] if plugin_type: - return plugin_type(config) + plugin = plugin_type(config) + await plugin.initialize() + return plugin return None async def shutdown(self) -> None: diff --git a/mcpgateway/plugins/framework/manager.py b/mcpgateway/plugins/framework/manager.py index 095a7cfae..a11d1222d 100644 --- a/mcpgateway/plugins/framework/manager.py +++ b/mcpgateway/plugins/framework/manager.py @@ -20,7 +20,7 @@ >>> # await manager.initialize() # Called in async context >>> # Create test payload and context - >>> from mcpgateway.plugins.framework.plugin_types import PromptPrehookPayload, GlobalContext + >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload, GlobalContext >>> payload = PromptPrehookPayload(name="test", args={"user": "input"}) >>> context = GlobalContext(request_id="123") >>> # result, contexts = await manager.prompt_pre_fetch(payload, context) # Called in async context @@ -33,15 +33,19 @@ from typing import Any, Callable, Coroutine, Dict, Generic, Optional, Tuple, TypeVar # First-Party -from mcpgateway.plugins.framework.base import PluginRef +from mcpgateway.plugins.framework.base import Plugin, PluginRef from mcpgateway.plugins.framework.loader.config import ConfigLoader from mcpgateway.plugins.framework.loader.plugin import PluginLoader -from mcpgateway.plugins.framework.models import Config, HookType, PluginCondition, PluginMode, PluginViolation -from mcpgateway.plugins.framework.plugin_types import ( +from mcpgateway.plugins.framework.models import ( + Config, GlobalContext, + HookType, + PluginCondition, PluginContext, PluginContextTable, + PluginMode, PluginResult, + PluginViolation, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, @@ -95,7 +99,7 @@ class PluginExecutor(Generic[T]): - Metadata aggregation from multiple plugins Examples: - >>> from mcpgateway.plugins.framework.plugin_types import PromptPrehookPayload + >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload >>> executor = PluginExecutor[PromptPrehookPayload]() >>> # In async context: >>> # result, contexts = await executor.execute( @@ -178,7 +182,7 @@ async def execute( if local_contexts and local_context_key in local_contexts: local_context = local_contexts[local_context_key] else: - local_context = PluginContext(global_context) + local_context = PluginContext(request_id=global_context.request_id, user=global_context.user, tenant_id=global_context.tenant_id, server_id=global_context.server_id) res_local_contexts[local_context_key] = local_context try: @@ -282,7 +286,7 @@ async def pre_prompt_fetch(plugin: PluginRef, payload: PromptPrehookPayload, con Examples: >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.plugin_types import PromptPrehookPayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload, PluginContext, GlobalContext >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = PromptPrehookPayload(name="test", args={"key": "value"}) @@ -306,7 +310,7 @@ async def post_prompt_fetch(plugin: PluginRef, payload: PromptPosthookPayload, c Examples: >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.plugin_types import PromptPosthookPayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.models import PromptPosthookPayload, PluginContext, GlobalContext >>> from mcpgateway.models import PromptResult >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) @@ -332,7 +336,7 @@ async def pre_tool_invoke(plugin: PluginRef, payload: ToolPreInvokePayload, cont Examples: >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.plugin_types import ToolPreInvokePayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.models import ToolPreInvokePayload, PluginContext, GlobalContext >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = ToolPreInvokePayload(name="calculator", args={"operation": "add", "a": 5, "b": 3}) @@ -356,7 +360,7 @@ async def post_tool_invoke(plugin: PluginRef, payload: ToolPostInvokePayload, co Examples: >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.plugin_types import ToolPostInvokePayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.models import ToolPostInvokePayload, PluginContext, GlobalContext >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = ToolPostInvokePayload(name="calculator", result={"result": 8, "status": "success"}) @@ -380,7 +384,7 @@ async def pre_resource_fetch(plugin: PluginRef, payload: ResourcePreFetchPayload Examples: >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.plugin_types import ResourcePreFetchPayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.models import ResourcePreFetchPayload, PluginContext, GlobalContext >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = ResourcePreFetchPayload(uri="file:///data.txt", metadata={"cache": True}) @@ -404,7 +408,7 @@ async def post_resource_fetch(plugin: PluginRef, payload: ResourcePostFetchPaylo Examples: >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.plugin_types import ResourcePostFetchPayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.models import ResourcePostFetchPayload, PluginContext, GlobalContext >>> from mcpgateway.models import ResourceContent >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) @@ -440,7 +444,7 @@ class PluginManager: >>> # print(f"Loaded {manager.plugin_count} plugins") >>> >>> # Execute prompt hooks - >>> from mcpgateway.plugins.framework.plugin_types import PromptPrehookPayload, GlobalContext + >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload, GlobalContext >>> payload = PromptPrehookPayload(name="test", args={}) >>> context = GlobalContext(request_id="req-123") >>> # In async context: @@ -524,6 +528,18 @@ def initialized(self) -> bool: """ return self._initialized + def get_plugin(self, name: str) -> Optional[Plugin]: + """Get a plugin by name. + + Args: + name: the name of the plugin to return. + + Returns: + A plugin. + """ + plugin_ref = self._registry.get_plugin(name) + return plugin_ref.plugin if plugin_ref else None + async def initialize(self) -> None: """Initialize the plugin manager and load all configured plugins. @@ -646,7 +662,7 @@ async def prompt_pre_fetch( >>> # In async context: >>> # await manager.initialize() >>> - >>> from mcpgateway.plugins.framework.plugin_types import PromptPrehookPayload, GlobalContext + >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload, GlobalContext >>> payload = PromptPrehookPayload( ... name="greeting", ... args={"user": "Alice"} @@ -698,7 +714,7 @@ async def prompt_post_fetch( Examples: >>> # Continuing from prompt_pre_fetch example >>> from mcpgateway.models import PromptResult, Message, TextContent, Role - >>> from mcpgateway.plugins.framework.plugin_types import PromptPosthookPayload, GlobalContext + >>> from mcpgateway.plugins.framework.models import PromptPosthookPayload, GlobalContext >>> >>> # Create a proper Message with TextContent >>> message = Message( @@ -763,7 +779,7 @@ async def tool_pre_invoke( >>> # In async context: >>> # await manager.initialize() >>> - >>> from mcpgateway.plugins.framework.plugin_types import ToolPreInvokePayload, GlobalContext + >>> from mcpgateway.plugins.framework.models import ToolPreInvokePayload, GlobalContext >>> payload = ToolPreInvokePayload( ... name="calculator", ... args={"operation": "add", "a": 5, "b": 3} @@ -814,7 +830,7 @@ async def tool_post_invoke( Examples: >>> # Continuing from tool_pre_invoke example - >>> from mcpgateway.plugins.framework.plugin_types import ToolPostInvokePayload, GlobalContext + >>> from mcpgateway.plugins.framework.models import ToolPostInvokePayload, GlobalContext >>> >>> post_payload = ToolPostInvokePayload( ... name="calculator", diff --git a/mcpgateway/plugins/framework/models.py b/mcpgateway/plugins/framework/models.py index 7882586d2..e10728b66 100644 --- a/mcpgateway/plugins/framework/models.py +++ b/mcpgateway/plugins/framework/models.py @@ -11,10 +11,19 @@ # Standard from enum import Enum -from typing import Any, Optional +from pathlib import Path +from typing import Any, Generic, Optional, Self, TypeVar # Third-Party -from pydantic import BaseModel, PrivateAttr +from pydantic import BaseModel, field_serializer, field_validator, model_validator, PrivateAttr, ValidationInfo + +# First-Party +from mcpgateway.models import PromptResult +from mcpgateway.plugins.framework.constants import AFTER, EXTERNAL_PLUGIN_TYPE, IGNORE_CONFIG_EXTERNAL, PYTHON_SUFFIX, SCRIPT, URL +from mcpgateway.schemas import TransportType +from mcpgateway.validators import SecurityValidator + +T = TypeVar("T") class HookType(str, Enum): @@ -177,6 +186,23 @@ class PluginCondition(BaseModel): user_patterns: Optional[list[str]] = None content_types: Optional[list[str]] = None + @field_serializer("server_ids", "tenant_ids", "tools", "prompts") + def serialize_set(self, value: set[str] | None) -> list[str] | None: + """Serialize set objects in PluginCondition for MCP. + + Args: + value: a set of server ids, tenant ids, tools or prompts. + + Returns: + The set as a serializable list. + """ + if value: + values = [] + for key in value: + values.append(key) + return values + return None + class AppliedTo(BaseModel): """What tools/prompts/resources and fields the plugin will be applied to. @@ -192,6 +218,61 @@ class AppliedTo(BaseModel): resources: Optional[list[ResourceTemplate]] = None +class MCPConfig(BaseModel): + """An MCP configuration for external MCP plugin objects. + + Attributes: + type (TransportType): The MCP transport type. Can be SSE, STDIO, or STREAMABLEHTTP + url (Optional[str]): An MCP URL. Only valid when MCP transport type is SSE or STREAMABLEHTTP. + script (Optional[str]): The path and name to the STDIO script that runs the plugin server. Only valid for STDIO type. + """ + + proto: TransportType + url: Optional[str] = None + script: Optional[str] = None + + @field_validator(URL, mode=AFTER) + @classmethod + def validate_url(cls, url: str | None) -> str | None: + """Validate a MCP url for streamable HTTP connections. + + Args: + url: the url to be validated. + + Raises: + ValueError: if the URL fails validation. + + Returns: + The validated URL or None if none is set. + """ + if url: + result = SecurityValidator.validate_url(url) + return result + return url + + @field_validator(SCRIPT, mode=AFTER) + @classmethod + def validate_script(cls, script: str | None) -> str | None: + """Validate an MCP stdio script. + + Args: + script: the script to be validated. + + Raises: + ValueError: if the script doesn't exist or doesn't have a .py suffix. + + Returns: + The validated string or None if none is set. + """ + if script: + file_path = Path(script) + if not file_path.is_file(): + raise ValueError(f"MCP server script {script} does not exist.") + elif file_path.suffix != PYTHON_SUFFIX: + raise ValueError(f"MCP server script {script} does not have a .py suffix.") + return script + + class PluginConfig(BaseModel): """A plugin configuration. @@ -209,21 +290,68 @@ class PluginConfig(BaseModel): conditions (Optional[list[PluginCondition]]): the conditions on which the plugin is run. applied_to (Optional[list[AppliedTo]]): the tools, fields, that the plugin is applied to. config (dict[str, Any]): the plugin specific configurations. + mcp (Optional[MCPConfig]): MCP configuration for external plugin when kind is "external". """ name: str - description: str - author: str + description: Optional[str] = None + author: Optional[str] = None kind: str namespace: Optional[str] = None - version: str - hooks: list[HookType] - tags: list[str] + version: Optional[str] = None + hooks: Optional[list[HookType]] = None + tags: Optional[list[str]] = None mode: PluginMode = PluginMode.ENFORCE - priority: int = 100 # Lower = higher priority + priority: Optional[int] = None # Lower = higher priority conditions: Optional[list[PluginCondition]] = None # When to apply applied_to: Optional[list[AppliedTo]] = None # Fields to apply to. - config: dict[str, Any] = {} + config: Optional[dict[str, Any]] = None + mcp: Optional[MCPConfig] = None + + @model_validator(mode=AFTER) + def check_url_or_script_filled(self) -> Self: + """Checks to see that at least one of url or script are set depending on MCP server configuration. + + Raises: + ValueError: if the script attribute is not defined with STDIO set, or the URL not defined with HTTP transports. + + Returns: + The model after validation. + """ + if not self.mcp: + return self + if self.mcp.proto == TransportType.STDIO and not self.mcp.script: + raise ValueError(f"Plugin {self.name} has transport type set to SSE but no script value") + elif (self.mcp.proto == TransportType.STREAMABLEHTTP or self.mcp.proto == TransportType.SSE) and not self.mcp.url: + raise ValueError(f"Plugin {self.name} has transport type set to StreamableHTTP but no url value") + elif self.mcp.proto != TransportType.SSE and self.mcp.proto != TransportType.STREAMABLEHTTP and self.mcp.proto != TransportType.STDIO: + raise ValueError(f"Plugin {self.name} must set transport type to either SSE or STREAMABLEHTTP or STDIO") + return self + + @model_validator(mode=AFTER) + def check_config_and_external(self, info: ValidationInfo) -> Self: + """Checks to see that a plugin's 'config' section is not defined if the kind is 'external'. This is because developers cannot override items in the plugin config section for external plugins. + + Args: + info: the contextual information passed into the pydantic model during model validation. Used to determine validation sequence. + + Raises: + ValueError: if the script attribute is not defined with STDIO set, or the URL not defined with HTTP transports. + + Returns: + The model after validation. + """ + ignore_config_external = False + if info and info.context and IGNORE_CONFIG_EXTERNAL in info.context: + ignore_config_external = info.context[IGNORE_CONFIG_EXTERNAL] + + if not ignore_config_external and self.config and self.kind == EXTERNAL_PLUGIN_TYPE: + raise ValueError(f"""Cannot have {self.name} plugin defined as 'external' with 'config' set.""" """ 'config' section settings can only be set on the plugin server.""") + + if self.kind == EXTERNAL_PLUGIN_TYPE and not self.mcp: + raise ValueError(f"Must set 'mcp' section for external plugin {self.name}") + + return self class PluginManifest(BaseModel): @@ -246,6 +374,22 @@ class PluginManifest(BaseModel): default_config: dict[str, Any] +class PluginErrorModel(BaseModel): + """A plugin error, used to denote exceptions/errors inside external plugins. + + Attributes: + message (str): the reason for the error. + code (str): an error code. + details: (dict[str, Any]): additional error details. + plugin_name (str): the plugin name. + """ + + message: str + code: Optional[str] = "" + details: Optional[dict[str, Any]] = {} + plugin_name: str + + class PluginViolation(BaseModel): """A plugin violation, used to denote policy violations. @@ -332,3 +476,286 @@ class Config(BaseModel): plugins: Optional[list[PluginConfig]] = [] plugin_dirs: list[str] = [] plugin_settings: PluginSettings + + +class PromptPrehookPayload(BaseModel): + """A prompt payload for a prompt prehook. + + Attributes: + name (str): The name of the prompt template. + args (dic[str,str]): The prompt template arguments. + + Examples: + >>> payload = PromptPrehookPayload("test_prompt", {"user": "alice"}) + >>> payload.name + 'test_prompt' + >>> payload.args + {'user': 'alice'} + >>> payload2 = PromptPrehookPayload("empty", None) + >>> payload2.args + {} + >>> p = PromptPrehookPayload("greeting", {"name": "Bob", "time": "morning"}) + >>> p.name + 'greeting' + >>> p.args["name"] + 'Bob' + """ + + name: str + args: Optional[dict[str, str]] = {} + + +class PromptPosthookPayload(BaseModel): + """A prompt payload for a prompt posthook. + + Attributes: + name (str): The prompt name. + result (PromptResult): The prompt after its template is rendered. + + Examples: + >>> from mcpgateway.models import PromptResult, Message, TextContent + >>> msg = Message(role="user", content=TextContent(type="text", text="Hello World")) + >>> result = PromptResult(messages=[msg]) + >>> payload = PromptPosthookPayload("greeting", result) + >>> payload.name + 'greeting' + >>> payload.result.messages[0].content.text + 'Hello World' + >>> from mcpgateway.models import PromptResult, Message, TextContent + >>> msg = Message(role="assistant", content=TextContent(type="text", text="Test output")) + >>> r = PromptResult(messages=[msg]) + >>> p = PromptPosthookPayload("test", r) + >>> p.name + 'test' + """ + + name: str + result: PromptResult + + +class PluginResult(BaseModel, Generic[T]): + """A result of the plugin hook processing. The actual type is dependent on the hook. + + Attributes: + continue_processing (bool): Whether to stop processing. + modified_payload (Optional[Any]): The modified payload if the plugin is a transformer. + violation (Optional[PluginViolation]): violation object. + metadata (Optional[dict[str, Any]]): additional metadata. + + Examples: + >>> result = PluginResult() + >>> result.continue_processing + True + >>> result.metadata + {} + >>> from mcpgateway.plugins.framework.models import PluginViolation + >>> violation = PluginViolation( + ... reason="Test", description="Test desc", code="TEST", details={} + ... ) + >>> result2 = PluginResult(continue_processing=False, violation=violation) + >>> result2.continue_processing + False + >>> result2.violation.code + 'TEST' + >>> r = PluginResult(metadata={"key": "value"}) + >>> r.metadata["key"] + 'value' + >>> r2 = PluginResult(continue_processing=False) + >>> r2.continue_processing + False + """ + + continue_processing: bool = True + modified_payload: Optional[T] = None + violation: Optional[PluginViolation] = None + metadata: Optional[dict[str, Any]] = {} + + +PromptPrehookResult = PluginResult[PromptPrehookPayload] +PromptPosthookResult = PluginResult[PromptPosthookPayload] + + +class ToolPreInvokePayload(BaseModel): + """A tool payload for a tool pre-invoke hook. + + Args: + name: The tool name. + args: The tool arguments for invocation. + + Examples: + >>> payload = ToolPreInvokePayload("test_tool", {"input": "data"}) + >>> payload.name + 'test_tool' + >>> payload.args + {'input': 'data'} + >>> payload2 = ToolPreInvokePayload("empty", None) + >>> payload2.args + {} + >>> p = ToolPreInvokePayload("calculator", {"operation": "add", "a": 5, "b": 3}) + >>> p.name + 'calculator' + >>> p.args["operation"] + 'add' + + """ + + name: str + args: Optional[dict[str, Any]] = {} + + +class ToolPostInvokePayload(BaseModel): + """A tool payload for a tool post-invoke hook. + + Args: + name: The tool name. + result: The tool invocation result. + + Examples: + >>> payload = ToolPostInvokePayload("calculator", {"result": 8, "status": "success"}) + >>> payload.name + 'calculator' + >>> payload.result + {'result': 8, 'status': 'success'} + >>> p = ToolPostInvokePayload("analyzer", {"confidence": 0.95, "sentiment": "positive"}) + >>> p.name + 'analyzer' + >>> p.result["confidence"] + 0.95 + """ + + name: str + result: Any + + +ToolPreInvokeResult = PluginResult[ToolPreInvokePayload] +ToolPostInvokeResult = PluginResult[ToolPostInvokePayload] + + +class GlobalContext(BaseModel): + """The global context, which shared across all plugins. + + Attributes: + request_id (str): ID of the HTTP request. + user (str): user ID associated with the request. + tenant_id (str): tenant ID. + server_id (str): server ID. + + Examples: + >>> ctx = GlobalContext("req-123") + >>> ctx.request_id + 'req-123' + >>> ctx.user is None + True + >>> ctx2 = GlobalContext("req-456", user="alice", tenant_id="tenant1") + >>> ctx2.user + 'alice' + >>> ctx2.tenant_id + 'tenant1' + >>> c = GlobalContext("123", server_id="srv1") + >>> c.request_id + '123' + >>> c.server_id + 'srv1' + """ + + request_id: str + user: Optional[str] = None + tenant_id: Optional[str] = None + server_id: Optional[str] = None + + +class PluginContext(GlobalContext): + """The plugin's context, which lasts a request lifecycle. + + Attributes: + metadata: context metadata. + state: the inmemory state of the request. + """ + + state: dict[str, Any] = {} + metadata: dict[str, Any] = {} + + def get_state(self, key: str, default: Any = None) -> Any: + """Get value from shared state. + + Args: + key: The key to access the shared state. + default: A default value if one doesn't exist. + + Returns: + The state value. + """ + return self.state.get(key, default) + + def set_state(self, key: str, value: Any) -> None: + """Set value in shared state. + + Args: + key: the key to add to the state. + value: the value to add to the state. + """ + self.state[key] = value + + async def cleanup(self) -> None: + """Cleanup context resources.""" + self.state.clear() + self.metadata.clear() + + +PluginContextTable = dict[str, PluginContext] + + +class ResourcePreFetchPayload(BaseModel): + """A resource payload for a resource pre-fetch hook. + + Attributes: + uri: The resource URI. + metadata: Optional metadata for the resource request. + + Examples: + >>> payload = ResourcePreFetchPayload("file:///data.txt") + >>> payload.uri + 'file:///data.txt' + >>> payload2 = ResourcePreFetchPayload("http://api/data", {"Accept": "application/json"}) + >>> payload2.metadata + {'Accept': 'application/json'} + >>> p = ResourcePreFetchPayload("file:///docs/readme.md", {"version": "1.0"}) + >>> p.uri + 'file:///docs/readme.md' + >>> p.metadata["version"] + '1.0' + """ + + uri: str + metadata: Optional[dict[str, Any]] = {} + + +class ResourcePostFetchPayload(BaseModel): + """A resource payload for a resource post-fetch hook. + + Attributes: + uri: The resource URI. + content: The fetched resource content. + + Examples: + >>> from mcpgateway.models import ResourceContent + >>> content = ResourceContent(type="resource", uri="file:///data.txt", + ... text="Hello World") + >>> payload = ResourcePostFetchPayload("file:///data.txt", content) + >>> payload.uri + 'file:///data.txt' + >>> payload.content.text + 'Hello World' + >>> from mcpgateway.models import ResourceContent + >>> resource_content = ResourceContent(type="resource", uri="test://resource", text="Test data") + >>> p = ResourcePostFetchPayload("test://resource", resource_content) + >>> p.uri + 'test://resource' + """ + + uri: str + content: Any + + +ResourcePreFetchResult = PluginResult[ResourcePreFetchPayload] +ResourcePostFetchResult = PluginResult[ResourcePostFetchPayload] diff --git a/mcpgateway/plugins/framework/plugin_types.py b/mcpgateway/plugins/framework/plugin_types.py deleted file mode 100644 index 5705c8c65..000000000 --- a/mcpgateway/plugins/framework/plugin_types.py +++ /dev/null @@ -1,369 +0,0 @@ -# -*- coding: utf-8 -*- -"""Pydantic models for plugins. - -Copyright 2025 -SPDX-License-Identifier: Apache-2.0 -Authors: Teryl Taylor, Mihai Criveti - -This module implements the pydantic models associated with -the base plugin layer including configurations, and contexts. -""" - -# Standard -from typing import Any, Generic, Optional, TypeVar - -# First-Party -from mcpgateway.models import PromptResult -from mcpgateway.plugins.framework.models import PluginViolation - -T = TypeVar("T") - - -class PromptPrehookPayload: - """A prompt payload for a prompt prehook. - - Examples: - >>> payload = PromptPrehookPayload("test_prompt", {"user": "alice"}) - >>> payload.name - 'test_prompt' - >>> payload.args - {'user': 'alice'} - >>> payload2 = PromptPrehookPayload("empty", None) - >>> payload2.args - {} - """ - - def __init__(self, name: str, args: Optional[dict[str, str]]): - """Initialize a prompt prehook payload. - - Args: - name: The prompt name. - args: The prompt arguments for rendering. - - Examples: - >>> p = PromptPrehookPayload("greeting", {"name": "Bob", "time": "morning"}) - >>> p.name - 'greeting' - >>> p.args["name"] - 'Bob' - """ - self.name = name - self.args = args or {} - - -class PromptPosthookPayload: - """A prompt payload for a prompt posthook. - - Examples: - >>> from mcpgateway.models import PromptResult, Message, TextContent - >>> msg = Message(role="user", content=TextContent(type="text", text="Hello World")) - >>> result = PromptResult(messages=[msg]) - >>> payload = PromptPosthookPayload("greeting", result) - >>> payload.name - 'greeting' - >>> payload.result.messages[0].content.text - 'Hello World' - """ - - def __init__(self, name: str, result: PromptResult): - """Initialize a prompt posthook payload. - - Args: - name: The prompt name. - result: The prompt Prompt Result. - - Examples: - >>> from mcpgateway.models import PromptResult, Message, TextContent - >>> msg = Message(role="assistant", content=TextContent(type="text", text="Test output")) - >>> r = PromptResult(messages=[msg]) - >>> p = PromptPosthookPayload("test", r) - >>> p.name - 'test' - """ - self.name = name - self.result = result - - -class PluginResult(Generic[T]): - """A plugin result. - - Examples: - >>> result = PluginResult() - >>> result.continue_processing - True - >>> result.metadata - {} - >>> from mcpgateway.plugins.framework.models import PluginViolation - >>> violation = PluginViolation( - ... reason="Test", description="Test desc", code="TEST", details={} - ... ) - >>> result2 = PluginResult(continue_processing=False, violation=violation) - >>> result2.continue_processing - False - >>> result2.violation.code - 'TEST' - """ - - def __init__(self, continue_processing: bool = True, modified_payload: Optional[T] = None, violation: Optional[PluginViolation] = None, metadata: Optional[dict[str, Any]] = None): - """Initialize a plugin result object. - - Args: - continue_processing (bool): Whether to stop processing. - modified_payload (Optional[Any]): The modified payload if the plugin is a transformer. - violation (Optional[PluginViolation]): violation object. - metadata (Optional[dict[str, Any]]): additional metadata. - - Examples: - >>> r = PluginResult(metadata={"key": "value"}) - >>> r.metadata["key"] - 'value' - >>> r2 = PluginResult(continue_processing=False) - >>> r2.continue_processing - False - """ - self.continue_processing = continue_processing - self.modified_payload = modified_payload - self.violation = violation - self.metadata = metadata or {} - - -PromptPrehookResult = PluginResult[PromptPrehookPayload] -PromptPosthookResult = PluginResult[PromptPosthookPayload] - - -class ToolPreInvokePayload: - """A tool payload for a tool pre-invoke hook. - - Examples: - >>> payload = ToolPreInvokePayload("test_tool", {"input": "data"}) - >>> payload.name - 'test_tool' - >>> payload.args - {'input': 'data'} - >>> payload2 = ToolPreInvokePayload("empty", None) - >>> payload2.args - {} - """ - - def __init__(self, name: str, args: Optional[dict[str, Any]]): - """Initialize a tool pre-invoke payload. - - Args: - name: The tool name. - args: The tool arguments for invocation. - - Examples: - >>> p = ToolPreInvokePayload("calculator", {"operation": "add", "a": 5, "b": 3}) - >>> p.name - 'calculator' - >>> p.args["operation"] - 'add' - """ - self.name = name - self.args = args or {} - - -class ToolPostInvokePayload: - """A tool payload for a tool post-invoke hook. - - Examples: - >>> payload = ToolPostInvokePayload("calculator", {"result": 8, "status": "success"}) - >>> payload.name - 'calculator' - >>> payload.result - {'result': 8, 'status': 'success'} - """ - - def __init__(self, name: str, result: Any): - """Initialize a tool post-invoke payload. - - Args: - name: The tool name. - result: The tool invocation result. - - Examples: - >>> p = ToolPostInvokePayload("analyzer", {"confidence": 0.95, "sentiment": "positive"}) - >>> p.name - 'analyzer' - >>> p.result["confidence"] - 0.95 - """ - self.name = name - self.result = result - - -ToolPreInvokeResult = PluginResult[ToolPreInvokePayload] -ToolPostInvokeResult = PluginResult[ToolPostInvokePayload] - - -class ResourcePreFetchPayload: - """A resource payload for a resource pre-fetch hook. - - Examples: - >>> payload = ResourcePreFetchPayload("file:///data.txt") - >>> payload.uri - 'file:///data.txt' - >>> payload2 = ResourcePreFetchPayload("http://api/data", {"Accept": "application/json"}) - >>> payload2.metadata - {'Accept': 'application/json'} - """ - - def __init__(self, uri: str, metadata: Optional[dict[str, Any]] = None): - """Initialize a resource pre-fetch payload. - - Args: - uri: The resource URI. - metadata: Optional metadata for the resource request. - - Examples: - >>> p = ResourcePreFetchPayload("file:///docs/readme.md", {"version": "1.0"}) - >>> p.uri - 'file:///docs/readme.md' - >>> p.metadata["version"] - '1.0' - """ - self.uri = uri - self.metadata = metadata or {} - - -class ResourcePostFetchPayload: - """A resource payload for a resource post-fetch hook. - - Examples: - >>> from mcpgateway.models import ResourceContent - >>> content = ResourceContent(type="resource", uri="file:///data.txt", - ... text="Hello World") - >>> payload = ResourcePostFetchPayload("file:///data.txt", content) - >>> payload.uri - 'file:///data.txt' - >>> payload.content.text - 'Hello World' - """ - - def __init__(self, uri: str, content: Any): - """Initialize a resource post-fetch payload. - - Args: - uri: The resource URI. - content: The fetched resource content. - - Examples: - >>> from mcpgateway.models import ResourceContent - >>> resource_content = ResourceContent(type="resource", uri="test://resource", text="Test data") - >>> p = ResourcePostFetchPayload("test://resource", resource_content) - >>> p.uri - 'test://resource' - """ - self.uri = uri - self.content = content - - -ResourcePreFetchResult = PluginResult[ResourcePreFetchPayload] -ResourcePostFetchResult = PluginResult[ResourcePostFetchPayload] - - -class GlobalContext: - """The global context, which shared across all plugins. - - Examples: - >>> ctx = GlobalContext("req-123") - >>> ctx.request_id - 'req-123' - >>> ctx.user is None - True - >>> ctx2 = GlobalContext("req-456", user="alice", tenant_id="tenant1") - >>> ctx2.user - 'alice' - >>> ctx2.tenant_id - 'tenant1' - """ - - def __init__(self, request_id: str, user: Optional[str] = None, tenant_id: Optional[str] = None, server_id: Optional[str] = None) -> None: - """Initialize a global context. - - Args: - request_id (str): ID of the HTTP request. - user (str): user ID associated with the request. - tenant_id (str): tenant ID. - server_id (str): server ID. - - Examples: - >>> c = GlobalContext("123", server_id="srv1") - >>> c.request_id - '123' - >>> c.server_id - 'srv1' - """ - self.request_id = request_id - self.user = user - self.tenant_id = tenant_id - self.server_id = server_id - - -class PluginContext(GlobalContext): - """The plugin's context, which lasts a request lifecycle. - - Attributes: - metadata: context metadata. - state: the inmemory state of the request. - """ - - def __init__(self, gcontext: Optional[GlobalContext] = None) -> None: - """Initialize a plugin context. - - Args: - gcontext: the global context object. - """ - if gcontext: - super().__init__(gcontext.request_id, gcontext.user, gcontext.tenant_id, gcontext.server_id) - self.state: dict[str, Any] = {} # In-memory state - self.metadata: dict[str, Any] = {} - - def get_state(self, key: str, default: Any = None) -> Any: - """Get value from shared state. - - Args: - key: The key to access the shared state. - default: A default value if one doesn't exist. - - Returns: - The state value. - """ - return self.state.get(key, default) - - def set_state(self, key: str, value: Any) -> None: - """Set value in shared state. - - Args: - key: the key to add to the state. - value: the value to add to the state. - """ - self.state[key] = value - - async def cleanup(self) -> None: - """Cleanup context resources.""" - self.state.clear() - self.metadata.clear() - - -PluginContextTable = dict[str, PluginContext] - - -class PluginViolationError(Exception): - """A plugin violation error. - - Attributes: - violation (PluginViolation): the plugin violation. - message (str): the plugin violation reason. - """ - - def __init__(self, message: str, violation: PluginViolation | None = None): - """Initialize a plugin violation error. - - Args: - message: the reason for the violation error. - violation: the plugin violation object details. - """ - self.message = message - self.violation = violation - super().__init__(self.message) diff --git a/mcpgateway/plugins/framework/utils.py b/mcpgateway/plugins/framework/utils.py index cc7563a5f..37d76c30c 100644 --- a/mcpgateway/plugins/framework/utils.py +++ b/mcpgateway/plugins/framework/utils.py @@ -15,9 +15,9 @@ from types import ModuleType # First-Party -from mcpgateway.plugins.framework.models import PluginCondition -from mcpgateway.plugins.framework.plugin_types import ( +from mcpgateway.plugins.framework.models import ( GlobalContext, + PluginCondition, PromptPosthookPayload, PromptPrehookPayload, ResourcePostFetchPayload, @@ -84,7 +84,7 @@ def matches(condition: PluginCondition, context: GlobalContext) -> bool: Examples: >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.plugin_types import GlobalContext + >>> from mcpgateway.plugins.framework.models import GlobalContext >>> cond = PluginCondition(server_ids={"srv1", "srv2"}) >>> ctx = GlobalContext("req1", server_id="srv1") >>> matches(cond, ctx) @@ -125,7 +125,7 @@ def pre_prompt_matches(payload: PromptPrehookPayload, conditions: list[PluginCon Examples: >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.plugin_types import PromptPrehookPayload, GlobalContext + >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload, GlobalContext >>> payload = PromptPrehookPayload("greeting", {}) >>> cond = PluginCondition(prompts={"greeting"}) >>> ctx = GlobalContext("req1") @@ -187,7 +187,7 @@ def pre_tool_matches(payload: ToolPreInvokePayload, conditions: list[PluginCondi Examples: >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.plugin_types import ToolPreInvokePayload, GlobalContext + >>> from mcpgateway.plugins.framework.models import ToolPreInvokePayload, GlobalContext >>> payload = ToolPreInvokePayload("calculator", {}) >>> cond = PluginCondition(tools={"calculator"}) >>> ctx = GlobalContext("req1") @@ -224,7 +224,7 @@ def post_tool_matches(payload: ToolPostInvokePayload, conditions: list[PluginCon Examples: >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.plugin_types import ToolPostInvokePayload, GlobalContext + >>> from mcpgateway.plugins.framework.models import ToolPostInvokePayload, GlobalContext >>> payload = ToolPostInvokePayload("calculator", {"result": 8}) >>> cond = PluginCondition(tools={"calculator"}) >>> ctx = GlobalContext("req1") @@ -261,7 +261,7 @@ def pre_resource_matches(payload: ResourcePreFetchPayload, conditions: list[Plug Examples: >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.plugin_types import ResourcePreFetchPayload, GlobalContext + >>> from mcpgateway.plugins.framework.models import ResourcePreFetchPayload, GlobalContext >>> payload = ResourcePreFetchPayload("file:///data.txt") >>> cond = PluginCondition(resources={"file:///data.txt"}) >>> ctx = GlobalContext("req1") @@ -298,7 +298,7 @@ def post_resource_matches(payload: ResourcePostFetchPayload, conditions: list[Pl Examples: >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.plugin_types import ResourcePostFetchPayload, GlobalContext + >>> from mcpgateway.plugins.framework.models import ResourcePostFetchPayload, GlobalContext >>> from mcpgateway.models import ResourceContent >>> content = ResourceContent(type="resource", uri="file:///data.txt", text="Test") >>> payload = ResourcePostFetchPayload("file:///data.txt", content) diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index 18454cb64..d28ec8453 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -55,7 +55,7 @@ try: # First-Party from mcpgateway.plugins.framework.manager import PluginManager - from mcpgateway.plugins.framework.plugin_types import GlobalContext, ResourcePostFetchPayload, ResourcePreFetchPayload + from mcpgateway.plugins.framework.models import GlobalContext, ResourcePostFetchPayload, ResourcePreFetchPayload PLUGINS_AVAILABLE = True except ImportError: diff --git a/mcpgateway/services/tool_service.py b/mcpgateway/services/tool_service.py index ee0129afd..be04fe1d9 100644 --- a/mcpgateway/services/tool_service.py +++ b/mcpgateway/services/tool_service.py @@ -41,8 +41,7 @@ from mcpgateway.db import ToolMetric from mcpgateway.models import TextContent, ToolResult from mcpgateway.observability import create_span -from mcpgateway.plugins.framework.manager import PluginManager -from mcpgateway.plugins.framework.plugin_types import GlobalContext, PluginViolationError, ToolPostInvokePayload, ToolPreInvokePayload +from mcpgateway.plugins import GlobalContext, PluginManager, PluginViolationError, ToolPostInvokePayload, ToolPreInvokePayload from mcpgateway.schemas import ToolCreate, ToolRead, ToolUpdate, TopPerformer from mcpgateway.services.logging_service import LoggingService from mcpgateway.utils.create_slug import slugify @@ -685,7 +684,7 @@ async def invoke_tool(self, db: Session, name: str, arguments: Dict[str, Any], r if self._plugin_manager: try: - pre_result, context_table = await self._plugin_manager.tool_pre_invoke(payload=ToolPreInvokePayload(name, arguments), global_context=global_context, local_contexts=None) + pre_result, context_table = await self._plugin_manager.tool_pre_invoke(payload=ToolPreInvokePayload(name=name, args=arguments), global_context=global_context, local_contexts=None) if not pre_result.continue_processing: # Plugin blocked the request diff --git a/mcpgateway/validators.py b/mcpgateway/validators.py index 344b62b47..1e9a84d57 100644 --- a/mcpgateway/validators.py +++ b/mcpgateway/validators.py @@ -63,9 +63,7 @@ class SecurityValidator: """Configurable validation with MCP-compliant limits""" # Configurable patterns (from settings) - DANGEROUS_HTML_PATTERN = ( - settings.validation_dangerous_html_pattern - ) # Default: '<(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)\b|' + DANGEROUS_HTML_PATTERN = settings.validation_dangerous_html_pattern # Default: '<(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)\b|' DANGEROUS_JS_PATTERN = settings.validation_dangerous_js_pattern # Default: javascript:|vbscript:|on\w+\s*=|data:.*script ALLOWED_URL_SCHEMES = settings.validation_allowed_url_schemes # Default: ["http://", "https://", "ws://", "wss://"] diff --git a/plugins/deny_filter/deny.py b/plugins/deny_filter/deny.py index 339c928ff..550c31e29 100644 --- a/plugins/deny_filter/deny.py +++ b/plugins/deny_filter/deny.py @@ -15,8 +15,13 @@ # First-Party from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.models import PluginConfig, PluginViolation -from mcpgateway.plugins.framework.plugin_types import PluginContext, PromptPrehookPayload, PromptPrehookResult +from mcpgateway.plugins.framework.models import ( + PluginConfig, + PluginContext, + PluginViolation, + PromptPrehookPayload, + PromptPrehookResult +) from mcpgateway.services.logging_service import LoggingService # Initialize logging service first diff --git a/plugins/external/config.yaml b/plugins/external/config.yaml new file mode 100644 index 000000000..68a9c6d2b --- /dev/null +++ b/plugins/external/config.yaml @@ -0,0 +1,22 @@ +# plugins/config.yaml - Main plugin configuration file + +plugins: + - name: "DenyListPlugin" + kind: "external" + mcp: + proto: STREAMABLEHTTP + url: http://127.0.0.1:3000/mcp + +# Plugin directories to scan +plugin_dirs: + - "plugins/native" # Built-in plugins + - "plugins/custom" # Custom organization plugins + - "/etc/mcpgateway/plugins" # System-wide plugins + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/plugins/pii_filter/pii_filter.py b/plugins/pii_filter/pii_filter.py index 3aa34aad6..18f9014af 100644 --- a/plugins/pii_filter/pii_filter.py +++ b/plugins/pii_filter/pii_filter.py @@ -20,9 +20,10 @@ # First-Party from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.models import PluginConfig, PluginViolation -from mcpgateway.plugins.framework.plugin_types import ( +from mcpgateway.plugins.framework.models import ( + PluginConfig, PluginContext, + PluginViolation, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, diff --git a/plugins/regex_filter/search_replace.py b/plugins/regex_filter/search_replace.py index ff92f2a4c..1fe885a64 100644 --- a/plugins/regex_filter/search_replace.py +++ b/plugins/regex_filter/search_replace.py @@ -15,8 +15,18 @@ # First-Party from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.models import PluginConfig -from mcpgateway.plugins.framework.plugin_types import PluginContext, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, PromptPrehookResult, ToolPreInvokePayload, ToolPreInvokeResult, ToolPostInvokePayload, ToolPostInvokeResult +from mcpgateway.plugins.framework.models import ( + PluginConfig, + PluginContext, + PromptPosthookPayload, + PromptPosthookResult, + PromptPrehookPayload, + PromptPrehookResult, + ToolPostInvokePayload, + ToolPostInvokeResult, + ToolPreInvokePayload, + ToolPreInvokeResult +) class SearchReplace(BaseModel): diff --git a/plugins/resource_filter/resource_filter.py b/plugins/resource_filter/resource_filter.py index 98862e5e6..0c598bcf8 100644 --- a/plugins/resource_filter/resource_filter.py +++ b/plugins/resource_filter/resource_filter.py @@ -18,9 +18,10 @@ from urllib.parse import urlparse from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.models import PluginConfig, PluginMode -from mcpgateway.plugins.framework.plugin_types import ( +from mcpgateway.plugins.framework.models import ( + PluginConfig, PluginContext, + PluginMode, PluginViolation, ResourcePostFetchPayload, ResourcePostFetchResult, diff --git a/tests/integration/test_resource_plugin_integration.py b/tests/integration/test_resource_plugin_integration.py index d04df8762..08bdbab74 100644 --- a/tests/integration/test_resource_plugin_integration.py +++ b/tests/integration/test_resource_plugin_integration.py @@ -121,11 +121,9 @@ async def test_resource_filtering_integration(self, test_db): # Use real plugin manager but mock its initialization with patch("mcpgateway.services.resource_service.PluginManager") as MockPluginManager: from mcpgateway.plugins.framework.manager import PluginManager - from mcpgateway.plugins.framework.plugin_types import ( - GlobalContext, + from mcpgateway.plugins.framework.models import ( ResourcePostFetchPayload, ResourcePostFetchResult, - ResourcePreFetchPayload, ResourcePreFetchResult, ) @@ -137,6 +135,10 @@ def __init__(self, config_file): async def initialize(self): self._initialized = True + @property + def initialized(self) -> bool: + return self._initialized + async def resource_pre_fetch(self, payload, global_context): # Allow test:// protocol if payload.uri.startswith("test://"): @@ -148,7 +150,7 @@ async def resource_pre_fetch(self, payload, global_context): {"validated": True}, ) else: - from mcpgateway.plugins.framework.plugin_types import PluginViolation + from mcpgateway.plugins.framework.models import PluginViolation return ( ResourcePreFetchResult( diff --git a/tests/unit/mcpgateway/plugins/framework/loader/test_plugin_loader.py b/tests/unit/mcpgateway/plugins/framework/loader/test_plugin_loader.py index 6cb03b35b..7f2a3c852 100644 --- a/tests/unit/mcpgateway/plugins/framework/loader/test_plugin_loader.py +++ b/tests/unit/mcpgateway/plugins/framework/loader/test_plugin_loader.py @@ -15,8 +15,7 @@ from mcpgateway.models import Message, PromptResult, Role, TextContent from mcpgateway.plugins.framework.loader.config import ConfigLoader from mcpgateway.plugins.framework.loader.plugin import PluginLoader -from mcpgateway.plugins.framework.models import PluginMode -from mcpgateway.plugins.framework.plugin_types import GlobalContext, PluginContext, PromptPosthookPayload, PromptPrehookPayload +from mcpgateway.plugins.framework.models import PluginContext, PluginMode, PromptPosthookPayload, PromptPrehookPayload from plugins.regex_filter.search_replace import SearchReplaceConfig, SearchReplacePlugin from unittest.mock import patch, MagicMock @@ -54,8 +53,8 @@ async def test_plugin_loader_load(): assert plugin.hooks[0] == "prompt_pre_fetch" assert plugin.hooks[1] == "prompt_post_fetch" - context = PluginContext(GlobalContext(request_id="1", server_id="2")) - prompt = PromptPrehookPayload(name="test_prompt", args={"user": "What a crapshow!"}) + context = PluginContext(request_id="1", server_id="2") + prompt = PromptPrehookPayload(name="test_prompt", args = {"user": "What a crapshow!"}) result = await plugin.prompt_pre_fetch(prompt, context=context) assert len(result.modified_payload.args) == 1 assert result.modified_payload.args["user"] == "What a yikesshow!" diff --git a/tests/unit/mcpgateway/plugins/framework/test_manager.py b/tests/unit/mcpgateway/plugins/framework/test_manager.py index 66ab6c5ba..1f049405b 100644 --- a/tests/unit/mcpgateway/plugins/framework/test_manager.py +++ b/tests/unit/mcpgateway/plugins/framework/test_manager.py @@ -13,7 +13,13 @@ # First-Party from mcpgateway.models import Message, PromptResult, Role, TextContent from mcpgateway.plugins.framework.manager import PluginManager -from mcpgateway.plugins.framework.plugin_types import GlobalContext, PromptPosthookPayload, PromptPrehookPayload, ToolPostInvokePayload, ToolPreInvokePayload +from mcpgateway.plugins.framework.models import ( + GlobalContext, + PromptPosthookPayload, + PromptPrehookPayload, + ToolPostInvokePayload, + ToolPreInvokePayload +) from plugins.regex_filter.search_replace import SearchReplaceConfig diff --git a/tests/unit/mcpgateway/plugins/framework/test_manager_extended.py b/tests/unit/mcpgateway/plugins/framework/test_manager_extended.py index 0ebc5920b..ffc016932 100644 --- a/tests/unit/mcpgateway/plugins/framework/test_manager_extended.py +++ b/tests/unit/mcpgateway/plugins/framework/test_manager_extended.py @@ -13,10 +13,15 @@ from mcpgateway.models import Message, PromptResult, Role, TextContent from mcpgateway.plugins.framework.base import Plugin from mcpgateway.plugins.framework.manager import PluginManager -from mcpgateway.plugins.framework.models import Config, HookType, PluginCondition, PluginConfig, PluginMode, PluginViolation -from mcpgateway.plugins.framework.plugin_types import ( +from mcpgateway.plugins.framework.models import ( + Config, GlobalContext, + HookType, + PluginCondition, + PluginConfig, PluginContext, + PluginMode, + PluginViolation, PluginResult, PromptPosthookPayload, PromptPrehookPayload, @@ -447,7 +452,7 @@ async def test_manager_shutdown_behavior(): async def test_manager_payload_size_validation(): """Test payload size validation functionality.""" from mcpgateway.plugins.framework.manager import PayloadSizeError, MAX_PAYLOAD_SIZE, PluginExecutor - from mcpgateway.plugins.framework.plugin_types import PromptPrehookPayload, PromptPosthookPayload + from mcpgateway.plugins.framework.models import PromptPrehookPayload, PromptPosthookPayload # Test payload size validation directly on executor (covers lines 252, 258) executor = PluginExecutor[PromptPrehookPayload]() @@ -594,7 +599,7 @@ async def test_base_plugin_coverage(): """Test base plugin functionality for complete coverage.""" from mcpgateway.plugins.framework.base import Plugin, PluginRef from mcpgateway.plugins.framework.models import PluginConfig, HookType, PluginMode - from mcpgateway.plugins.framework.plugin_types import ( + from mcpgateway.plugins.framework.models import ( PluginContext, GlobalContext, PromptPrehookPayload, PromptPosthookPayload, ToolPreInvokePayload, ToolPostInvokePayload ) @@ -625,7 +630,7 @@ async def test_base_plugin_coverage(): assert plugin_ref.mode == PluginMode.ENFORCE # Default mode # Test NotImplementedError for prompt_pre_fetch (covers lines 151-155) - context = PluginContext(GlobalContext(request_id="test")) + context = PluginContext(request_id="test") payload = PromptPrehookPayload(name="test", args={}) with pytest.raises(NotImplementedError, match="'prompt_pre_fetch' not implemented"): @@ -641,30 +646,25 @@ async def test_base_plugin_coverage(): # Test default tool_pre_invoke implementation (covers line 191) tool_payload = ToolPreInvokePayload(name="test_tool", args={"key": "value"}) - tool_result = await plugin.tool_pre_invoke(tool_payload, context) - - assert tool_result.continue_processing is True - assert tool_result.modified_payload is tool_payload + with pytest.raises(NotImplementedError, match="'tool_pre_invoke' not implemented"): + await plugin.tool_pre_invoke(tool_payload, context) # Test default tool_post_invoke implementation (covers line 211) tool_post_payload = ToolPostInvokePayload(name="test_tool", result={"result": "success"}) - tool_post_result = await plugin.tool_post_invoke(tool_post_payload, context) - - assert tool_post_result.continue_processing is True - assert tool_post_result.modified_payload is tool_post_payload + with pytest.raises(NotImplementedError, match="'tool_post_invoke' not implemented"): + await plugin.tool_post_invoke(tool_post_payload, context) @pytest.mark.asyncio async def test_plugin_types_coverage(): """Test plugin types functionality for complete coverage.""" - from mcpgateway.plugins.framework.plugin_types import ( - PluginContext, GlobalContext, PluginViolationError + from mcpgateway.plugins.framework.models import ( + PluginContext, PluginViolation ) - from mcpgateway.plugins.framework.models import PluginViolation + from mcpgateway.plugins.framework.errors import PluginViolationError # Test PluginContext state methods (covers lines 266, 275) - global_ctx = GlobalContext(request_id="test", user="testuser") - plugin_ctx = PluginContext(global_ctx) + plugin_ctx = PluginContext(request_id="test", user="testuser") # Test get_state with default assert plugin_ctx.get_state("nonexistent", "default_value") == "default_value" diff --git a/tests/unit/mcpgateway/plugins/framework/test_resource_hooks.py b/tests/unit/mcpgateway/plugins/framework/test_resource_hooks.py index b7b67ed56..04ca09570 100644 --- a/tests/unit/mcpgateway/plugins/framework/test_resource_hooks.py +++ b/tests/unit/mcpgateway/plugins/framework/test_resource_hooks.py @@ -8,11 +8,14 @@ from mcpgateway.models import ResourceContent from mcpgateway.plugins.framework.base import Plugin, PluginRef from mcpgateway.plugins.framework.manager import PluginManager -from mcpgateway.plugins.framework.models import HookType, PluginCondition, PluginConfig, PluginMode # Registry is imported for mocking -from mcpgateway.plugins.framework.plugin_types import ( +from mcpgateway.plugins.framework.models import ( GlobalContext, + HookType, + PluginCondition, + PluginConfig, PluginContext, + PluginMode, PluginViolation, ResourcePostFetchPayload, ResourcePostFetchResult, @@ -52,14 +55,11 @@ async def test_plugin_resource_pre_fetch_default(self): ) plugin = Plugin(config) payload = ResourcePreFetchPayload(uri="file:///test.txt", metadata={}) - context = PluginContext(GlobalContext(request_id="test-123")) + context = PluginContext(request_id="test-123") - result = await plugin.resource_pre_fetch(payload, context) + with pytest.raises(NotImplementedError, match="'resource_pre_fetch' not implemented"): + await plugin.resource_pre_fetch(payload, context) - # ResourcePreFetchResult is a type alias, check actual type - assert result.__class__.__name__ == "PluginResult" - assert result.continue_processing is True - assert result.modified_payload == payload @pytest.mark.asyncio async def test_plugin_resource_post_fetch_default(self): @@ -76,14 +76,11 @@ async def test_plugin_resource_post_fetch_default(self): plugin = Plugin(config) content = ResourceContent(type="resource", uri="file:///test.txt", text="Test content") payload = ResourcePostFetchPayload(uri="file:///test.txt", content=content) - context = PluginContext(GlobalContext(request_id="test-123")) + context = PluginContext(request_id="test-123") - result = await plugin.resource_post_fetch(payload, context) + with pytest.raises(NotImplementedError, match="'resource_post_fetch' not implemented"): + await plugin.resource_post_fetch(payload, context) - # ResourcePostFetchResult is a type alias, check actual type - assert result.__class__.__name__ == "PluginResult" - assert result.continue_processing is True - assert result.modified_payload == payload @pytest.mark.asyncio async def test_resource_hook_blocking(self): @@ -113,7 +110,7 @@ async def resource_pre_fetch(self, payload, context): ) plugin = BlockingResourcePlugin(config) payload = ResourcePreFetchPayload(uri="file:///etc/passwd", metadata={}) - context = PluginContext(GlobalContext(request_id="test-123")) + context = PluginContext(request_id="test-123") result = await plugin.resource_pre_fetch(payload, context) @@ -160,7 +157,7 @@ async def resource_post_fetch(self, payload, context): text="Database config:\npassword: secret123\nport: 5432", ) payload = ResourcePostFetchPayload(uri="test://config", content=content) - context = PluginContext(GlobalContext(request_id="test-123")) + context = PluginContext(request_id="test-123") result = await plugin.resource_post_fetch(payload, context) @@ -447,7 +444,7 @@ async def resource_pre_fetch(self, payload, context): ) plugin = URIModifierPlugin(config) payload = ResourcePreFetchPayload(uri="test://resource", metadata={}) - context = PluginContext(GlobalContext(request_id="test-123")) + context = PluginContext(request_id="test-123") result = await plugin.resource_pre_fetch(payload, context) @@ -481,7 +478,7 @@ async def resource_pre_fetch(self, payload, context): ) plugin = MetadataEnricherPlugin(config) payload = ResourcePreFetchPayload(uri="test://resource", metadata={}) - context = PluginContext(GlobalContext(request_id="test-123", user="testuser")) + context = PluginContext(request_id="test-123", user="testuser") result = await plugin.resource_pre_fetch(payload, context) diff --git a/tests/unit/mcpgateway/plugins/framework/test_utils.py b/tests/unit/mcpgateway/plugins/framework/test_utils.py index 72606c213..b42004888 100644 --- a/tests/unit/mcpgateway/plugins/framework/test_utils.py +++ b/tests/unit/mcpgateway/plugins/framework/test_utils.py @@ -7,27 +7,11 @@ Unit tests for utilities. """ -# Standard import sys -# First-Party -from mcpgateway.plugins.framework.models import PluginCondition -from mcpgateway.plugins.framework.plugin_types import ( - GlobalContext, - PromptPrehookPayload, - PromptPosthookPayload, - ToolPreInvokePayload, - ToolPostInvokePayload, -) -from mcpgateway.plugins.framework.utils import ( - import_module, - matches, - parse_class_name, - post_prompt_matches, - post_tool_matches, - pre_prompt_matches, - pre_tool_matches, -) +from mcpgateway.plugins.framework.utils import import_module, matches, parse_class_name, post_prompt_matches, post_tool_matches, pre_prompt_matches, pre_tool_matches +from mcpgateway.plugins.framework.models import GlobalContext, PluginCondition, PromptPrehookPayload, PromptPosthookPayload, ToolPostInvokePayload, ToolPreInvokePayload + def test_server_ids(): diff --git a/tests/unit/mcpgateway/plugins/plugins/pii_filter/test_pii_filter.py b/tests/unit/mcpgateway/plugins/plugins/pii_filter/test_pii_filter.py index ef30286b6..7ad419954 100644 --- a/tests/unit/mcpgateway/plugins/plugins/pii_filter/test_pii_filter.py +++ b/tests/unit/mcpgateway/plugins/plugins/pii_filter/test_pii_filter.py @@ -11,10 +11,12 @@ # First-Party from mcpgateway.models import Message, PromptResult, Role, TextContent -from mcpgateway.plugins.framework.models import HookType, PluginConfig, PluginMode -from mcpgateway.plugins.framework.plugin_types import ( +from mcpgateway.plugins.framework.models import ( GlobalContext, + HookType, + PluginConfig, PluginContext, + PluginMode, PromptPosthookPayload, PromptPrehookPayload, ) @@ -245,7 +247,7 @@ def plugin_config(self) -> PluginConfig: async def test_prompt_pre_fetch_with_pii(self, plugin_config): """Test pre-fetch hook with PII detection.""" plugin = PIIFilterPlugin(plugin_config) - context = PluginContext(GlobalContext(request_id="test-1")) + context = PluginContext(request_id="test-1") # Create payload with PII payload = PromptPrehookPayload(name="test_prompt", args={"user_input": "My email is john@example.com and SSN is 123-45-6789", "safe_input": "This has no PII"}) @@ -269,7 +271,7 @@ async def test_prompt_pre_fetch_blocking(self, plugin_config): # Enable blocking plugin_config.config["block_on_detection"] = True plugin = PIIFilterPlugin(plugin_config) - context = PluginContext(GlobalContext(request_id="test-2")) + context = PluginContext(request_id="test-2") payload = PromptPrehookPayload(name="test_prompt", args={"input": "My SSN is 123-45-6789"}) @@ -285,7 +287,7 @@ async def test_prompt_pre_fetch_blocking(self, plugin_config): async def test_prompt_post_fetch(self, plugin_config): """Test post-fetch hook with PII in messages.""" plugin = PIIFilterPlugin(plugin_config) - context = PluginContext(GlobalContext(request_id="test-3")) + context = PluginContext(request_id="test-3") # Create messages with PII messages = [ @@ -314,7 +316,7 @@ async def test_prompt_post_fetch(self, plugin_config): async def test_no_pii_detection(self, plugin_config): """Test that clean text passes through unmodified.""" plugin = PIIFilterPlugin(plugin_config) - context = PluginContext(GlobalContext(request_id="test-4")) + context = PluginContext(request_id="test-4") payload = PromptPrehookPayload(name="test_prompt", args={"input": "This text has no sensitive information"}) @@ -331,7 +333,7 @@ async def test_custom_patterns(self, plugin_config): plugin_config.config["custom_patterns"] = [{"type": "custom", "pattern": r"\bEMP\d{6}\b", "description": "Employee ID", "mask_strategy": "redact", "enabled": True}] plugin = PIIFilterPlugin(plugin_config) - context = PluginContext(GlobalContext(request_id="test-5")) + context = PluginContext(request_id="test-5") payload = PromptPrehookPayload(name="test_prompt", args={"input": "Employee ID: EMP123456"}) @@ -349,7 +351,7 @@ async def test_permissive_mode(self, plugin_config): plugin_config.config["block_on_detection"] = True # Should be ignored in permissive mode plugin = PIIFilterPlugin(plugin_config) - context = PluginContext(GlobalContext(request_id="test-6")) + context = PluginContext(request_id="test-6") payload = PromptPrehookPayload(name="test_prompt", args={"input": "SSN: 123-45-6789"}) diff --git a/tests/unit/mcpgateway/plugins/plugins/resource_filter/test_resource_filter.py b/tests/unit/mcpgateway/plugins/plugins/resource_filter/test_resource_filter.py index 1a286ead1..d7b702e51 100644 --- a/tests/unit/mcpgateway/plugins/plugins/resource_filter/test_resource_filter.py +++ b/tests/unit/mcpgateway/plugins/plugins/resource_filter/test_resource_filter.py @@ -4,10 +4,11 @@ import pytest from mcpgateway.models import ResourceContent -from mcpgateway.plugins.framework.models import HookType, PluginCondition, PluginConfig, PluginMode -from mcpgateway.plugins.framework.plugin_types import ( - GlobalContext, +from mcpgateway.plugins.framework.models import ( + HookType, + PluginConfig, PluginContext, + PluginMode, ResourcePostFetchPayload, ResourcePreFetchPayload, ) @@ -49,7 +50,7 @@ def plugin(self, plugin_config): @pytest.fixture def context(self): """Create a plugin context.""" - return PluginContext(GlobalContext(request_id="test-123", user="testuser")) + return PluginContext(request_id="test-123", user="testuser") @pytest.mark.asyncio async def test_allowed_protocol(self, plugin, context): diff --git a/tests/unit/mcpgateway/services/test_resource_service_plugins.py b/tests/unit/mcpgateway/services/test_resource_service_plugins.py index 3d1bba03c..7d0d25b7d 100644 --- a/tests/unit/mcpgateway/services/test_resource_service_plugins.py +++ b/tests/unit/mcpgateway/services/test_resource_service_plugins.py @@ -7,11 +7,8 @@ from sqlalchemy.orm import Session from mcpgateway.models import ResourceContent -from mcpgateway.plugins.framework.plugin_types import ( - GlobalContext, +from mcpgateway.plugins.framework.models import ( PluginViolation, - ResourcePostFetchResult, - ResourcePreFetchResult, ) from mcpgateway.services.resource_service import ResourceError, ResourceNotFoundError, ResourceService From 397fee079a68f67bc6e615743408a98456809bb0 Mon Sep 17 00:00:00 2001 From: Teryl Taylor Date: Tue, 12 Aug 2025 21:43:28 -0700 Subject: [PATCH 02/65] feat(plugins): add external mcp server and associated test cases. Signed-off-by: Teryl Taylor --- mcpgateway/plugins/framework/base.py | 9 ++ .../external/{mcp.py => mcp/client.py} | 3 +- .../plugins/framework/external/mcp/server.py | 136 ++++++++++++++++++ mcpgateway/plugins/framework/loader/plugin.py | 2 +- plugins/external/config-stdio.yaml | 22 +++ plugins/resources/server/config-http.yaml | 9 ++ plugins/resources/server/config-stdio.yaml | 15 ++ .../configs/valid_stdio_external_plugin.yaml | 22 +++ ...valid_stdio_external_plugin_overrides.yaml | 25 ++++ .../valid_stdio_external_plugin_regex.yaml | 22 +++ .../external/mcp/test_client_stdio.py | 98 +++++++++++++ 11 files changed, 361 insertions(+), 2 deletions(-) rename mcpgateway/plugins/framework/external/{mcp.py => mcp/client.py} (99%) create mode 100644 mcpgateway/plugins/framework/external/mcp/server.py create mode 100644 plugins/external/config-stdio.yaml create mode 100644 plugins/resources/server/config-http.yaml create mode 100644 plugins/resources/server/config-stdio.yaml create mode 100644 tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml create mode 100644 tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml create mode 100644 tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml create mode 100644 tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py diff --git a/mcpgateway/plugins/framework/base.py b/mcpgateway/plugins/framework/base.py index e5cd87492..b41e8260d 100644 --- a/mcpgateway/plugins/framework/base.py +++ b/mcpgateway/plugins/framework/base.py @@ -96,6 +96,15 @@ def priority(self) -> int: """ return self._config.priority + @property + def config(self) -> PluginConfig: + """Return the plugin's configuration. + + Returns: + Plugin's configuration. + """ + return self._config + @property def mode(self) -> PluginMode: """Return the plugin's mode. diff --git a/mcpgateway/plugins/framework/external/mcp.py b/mcpgateway/plugins/framework/external/mcp/client.py similarity index 99% rename from mcpgateway/plugins/framework/external/mcp.py rename to mcpgateway/plugins/framework/external/mcp/client.py index bcb9becfc..4fbfa0c7e 100644 --- a/mcpgateway/plugins/framework/external/mcp.py +++ b/mcpgateway/plugins/framework/external/mcp/client.py @@ -12,6 +12,7 @@ from contextlib import AsyncExitStack import json import logging +import os from typing import Any, Optional # Third-Party @@ -93,7 +94,7 @@ async def __connect_to_stdio_server(self, server_script_path: str) -> None: if not (is_python): raise ValueError("Server script must be a .py file") - server_params = StdioServerParameters(command=PYTHON, args=[server_script_path], env=None) + server_params = StdioServerParameters(command=PYTHON, args=[server_script_path], env=os.environ) stdio_transport = await self._exit_stack.enter_async_context(stdio_client(server_params)) self._stdio, self._write = stdio_transport diff --git a/mcpgateway/plugins/framework/external/mcp/server.py b/mcpgateway/plugins/framework/external/mcp/server.py new file mode 100644 index 000000000..1fc6e19ea --- /dev/null +++ b/mcpgateway/plugins/framework/external/mcp/server.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +"""Plugin MCP Server. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +Module that contains plugin MCP server code to serve external plugins. +""" +# Standard +import asyncio +import logging +import os + +# Third-Party +from chuk_mcp_runtime.common.mcp_tool_decorator import mcp_tool +from chuk_mcp_runtime.entry import main_async + +# First-Party +from mcpgateway.plugins.framework.errors import convert_exception_to_error +from mcpgateway.plugins.framework.loader.config import ConfigLoader +from mcpgateway.plugins.framework.manager import DEFAULT_PLUGIN_TIMEOUT, PluginManager +from mcpgateway.plugins.framework.models import ( + PluginContext, + PluginErrorModel, + PromptPosthookPayload, + PromptPrehookPayload, +) + +logger = logging.getLogger(__name__) + +config_file = os.environ.get("CFMCP_PLUGIN_CONFIG", "resources/plugins/config.yaml") +global_plugin_manager = PluginManager(config_file) + + +async def initialize() -> None: + """Initialize the plugin manager with configured plugins.""" + await global_plugin_manager.initialize() + + +@mcp_tool(name="get_plugin_configs", description="Get the plugin configurations installed on the server") +async def get_plugin_configs() -> list[dict]: + """Return a list of plugin configurations for plugins currently installed on the MCP server. + + Returns: + A list of plugin configurations. + """ + config = ConfigLoader.load_config(config_file, use_jinja=False) + plugins: list[dict] = [] + for plug in config.plugins: + plugins.append(plug.model_dump()) + return plugins + + +@mcp_tool(name="get_plugin_config", description="Get the plugin configuration installed on the server given a plugin name") +async def get_plugin_config(name: str) -> dict: + """Return a plugin configuration give a plugin name. + + Args: + name: the name of the plugin of which to return the plugin configuration. + + Returns: + A list of plugin configurations. + """ + config = ConfigLoader.load_config(config_file, use_jinja=False) + for plug in config.plugins: + if plug.name.lower() == name.lower(): + return plug.model_dump() + return None + + +@mcp_tool(name="prompt_pre_fetch", description="Execute prompt prefetch hook for a plugin") +async def prompt_pre_fetch(plugin_name: str, payload: dict, context: dict) -> dict: + """Invoke the prompt pre fetch hook for a particular plugin. + + Args: + plugin_name: the name of the plugin to execute. + payload: the prompt name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. + + Returns: + The transformed or filtered response from the plugin hook. + """ + plugin_timeout = global_plugin_manager.config.plugin_settings.plugin_timeout if global_plugin_manager.config else DEFAULT_PLUGIN_TIMEOUT + plugin = global_plugin_manager.get_plugin(plugin_name) + try: + if plugin: + prepayload = PromptPrehookPayload.model_validate(payload) + precontext = PluginContext.model_validate(context) + result = await asyncio.wait_for(plugin.prompt_pre_fetch(prepayload, precontext), plugin_timeout) + return result.model_dump() + raise ValueError(f"Unable to retrieve plugin {plugin_name} to execute.") + except asyncio.TimeoutError: + return PluginErrorModel(message=f"Plugin {plugin_name} timed out from execution after {plugin_timeout} seconds.", plugin_name=plugin_name).model_dump() + except Exception as ex: + logger.exception(ex) + return convert_exception_to_error(ex, plugin_name=plugin_name).model_dump() + + +@mcp_tool(name="prompt_post_fetch", description="Execute prompt postfetch hook for a plugin") +async def prompt_post_fetch(plugin_name: str, payload: dict, context: dict) -> dict: + """Call plugin's prompt post-fetch hook. + + Args: + plugin_name: The name of the plugin to execute. + payload: The prompt payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin execution. + """ + plugin_timeout = global_plugin_manager.config.plugin_settings.plugin_timeout if global_plugin_manager.config else DEFAULT_PLUGIN_TIMEOUT + plugin = global_plugin_manager.get_plugin(plugin_name) + try: + if plugin: + postpayload = PromptPosthookPayload.model_validate(payload) + postcontext = PluginContext.model_validate(context) + result = await asyncio.wait_for(plugin.prompt_post_fetch(postpayload, postcontext), plugin_timeout) + return result.model_dump() + raise ValueError(f"Unable to retrieve plugin {plugin_name} to execute.") + except asyncio.TimeoutError: + return PluginErrorModel(message=f"Plugin {plugin_name} timed out from execution after {plugin_timeout} seconds.", plugin_name=plugin_name).model_dump() + except Exception as ex: + logger.exception(ex) + return convert_exception_to_error(ex, plugin_name=plugin_name).model_dump() + + +async def server_main(): + """Initialize plugin manager and run mcp server.""" + await initialize() + await main_async() + + +if __name__ == "__main__": + # launch + asyncio.run(server_main()) diff --git a/mcpgateway/plugins/framework/loader/plugin.py b/mcpgateway/plugins/framework/loader/plugin.py index c7aa3cff1..56f31e7d9 100644 --- a/mcpgateway/plugins/framework/loader/plugin.py +++ b/mcpgateway/plugins/framework/loader/plugin.py @@ -15,7 +15,7 @@ # First-Party from mcpgateway.plugins.framework.base import Plugin from mcpgateway.plugins.framework.constants import EXTERNAL_PLUGIN_TYPE -from mcpgateway.plugins.framework.external.mcp import ExternalPlugin +from mcpgateway.plugins.framework.external.mcp.client import ExternalPlugin from mcpgateway.plugins.framework.models import PluginConfig from mcpgateway.plugins.framework.utils import import_module, parse_class_name diff --git a/plugins/external/config-stdio.yaml b/plugins/external/config-stdio.yaml new file mode 100644 index 000000000..dcc412cff --- /dev/null +++ b/plugins/external/config-stdio.yaml @@ -0,0 +1,22 @@ +# plugins/config.yaml - Main plugin configuration file + +plugins: + - name: "DenyListPlugin" + kind: "external" + mcp: + proto: STDIO + script: mcpgateway/plugins/framework/external/mcp/server.py + +# Plugin directories to scan +plugin_dirs: + - "plugins/native" # Built-in plugins + - "plugins/custom" # Custom organization plugins + - "/etc/mcpgateway/plugins" # System-wide plugins + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/plugins/resources/server/config-http.yaml b/plugins/resources/server/config-http.yaml new file mode 100644 index 000000000..7de90dd61 --- /dev/null +++ b/plugins/resources/server/config-http.yaml @@ -0,0 +1,9 @@ +server: + type: "streamable-http" + +streamable-http: + host: "127.0.0.1" + port: 3000 + mcp_path: "/mcp" + json_response: true + stateless: true \ No newline at end of file diff --git a/plugins/resources/server/config-stdio.yaml b/plugins/resources/server/config-stdio.yaml new file mode 100644 index 000000000..773293672 --- /dev/null +++ b/plugins/resources/server/config-stdio.yaml @@ -0,0 +1,15 @@ +# MCP-arxiv-Search Configuration +host: + name: "mcp-plugin-server" + log_level: "INFO" + +server: + type: "stdio" + +mcp_servers: + echo_server: + enabled: true + location: "." + tools: + enabled: true + module: "plugins.framework.mcp.server" \ No newline at end of file diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml new file mode 100644 index 000000000..dcc412cff --- /dev/null +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml @@ -0,0 +1,22 @@ +# plugins/config.yaml - Main plugin configuration file + +plugins: + - name: "DenyListPlugin" + kind: "external" + mcp: + proto: STDIO + script: mcpgateway/plugins/framework/external/mcp/server.py + +# Plugin directories to scan +plugin_dirs: + - "plugins/native" # Built-in plugins + - "plugins/custom" # Custom organization plugins + - "/etc/mcpgateway/plugins" # System-wide plugins + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml new file mode 100644 index 000000000..4823f3e35 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml @@ -0,0 +1,25 @@ +# plugins/config.yaml - Main plugin configuration file + +plugins: + - name: "DenyListPlugin" + kind: "external" + description: "a different configuration." + priority: 150 + hooks: ["prompt_pre_fetch", "prompt_post_fetch"] + mcp: + proto: STDIO + script: mcpgateway/plugins/framework/external/mcp/server.py + +# Plugin directories to scan +plugin_dirs: + - "plugins/native" # Built-in plugins + - "plugins/custom" # Custom organization plugins + - "/etc/mcpgateway/plugins" # System-wide plugins + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml new file mode 100644 index 000000000..c34ff0749 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml @@ -0,0 +1,22 @@ +# plugins/config.yaml - Main plugin configuration file + +plugins: + - name: "ReplaceBadWordsPlugin" + kind: "external" + mcp: + proto: STDIO + script: mcpgateway/plugins/framework/external/mcp/server.py + +# Plugin directories to scan +plugin_dirs: + - "plugins/native" # Built-in plugins + - "plugins/custom" # Custom organization plugins + - "/etc/mcpgateway/plugins" # System-wide plugins + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py new file mode 100644 index 000000000..aabddd1d5 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py @@ -0,0 +1,98 @@ +""" +Tests for external client on stdio. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +""" +import os +import pytest + +from mcpgateway.models import Message, PromptResult, Role, TextContent +from mcpgateway.plugins.framework.loader.config import ConfigLoader +from mcpgateway.plugins.framework.loader.plugin import PluginLoader +from mcpgateway.plugins.framework.models import PluginContext, PromptPrehookPayload, PromptPosthookPayload + + +@pytest.mark.asyncio +async def test_client_load_stdio(): + os.environ["CFMCP_PLUGIN_CONFIG"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" + os.environ["PYTHONPATH"] = "." + config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml") + print(config) + + loader = PluginLoader() + plugin = await loader.load_and_instantiate_plugin(config.plugins[0]) + print(plugin) + prompt = PromptPrehookPayload(name="test_prompt", args = {"text": "That was innovative!"}) + result = await plugin.prompt_pre_fetch(prompt, PluginContext(request_id="1", server_id="2")) + assert result.violation + assert result.violation.reason == "Prompt not allowed" + assert result.violation.description == "A deny word was found in the prompt" + assert result.violation.code == "deny" + config = plugin.config + assert config.name == "DenyListPlugin" + assert config.description == "A plugin that implements a deny list filter." + assert config.priority == 100 + assert config.kind == "external" + await plugin.shutdown() + del os.environ["CFMCP_PLUGIN_CONFIG"] + del os.environ["PYTHONPATH"] + +async def test_client_load_stdio_overrides(): + os.environ["CFMCP_PLUGIN_CONFIG"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" + os.environ["PYTHONPATH"] = "." + config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml") + print(config) + + loader = PluginLoader() + plugin = await loader.load_and_instantiate_plugin(config.plugins[0]) + print(plugin) + prompt = PromptPrehookPayload(name="test_prompt", args = {"text": "That was innovative!"}) + result = await plugin.prompt_pre_fetch(prompt, PluginContext(request_id="1", server_id="2")) + assert result.violation + assert result.violation.reason == "Prompt not allowed" + assert result.violation.description == "A deny word was found in the prompt" + assert result.violation.code == "deny" + config = plugin.config + assert config.name == "DenyListPlugin" + assert config.description == "a different configuration." + assert config.priority == 150 + assert config.hooks[0] == "prompt_pre_fetch" + assert config.hooks[1] == "prompt_post_fetch" + assert config.kind == "external" + await plugin.shutdown() + del os.environ["CFMCP_PLUGIN_CONFIG"] + del os.environ["PYTHONPATH"] + +@pytest.mark.asyncio +async def test_client_load_stdio_post_prompt(): + os.environ["CFMCP_PLUGIN_CONFIG"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_single_plugin.yaml" + os.environ["PYTHONPATH"] = "." + config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml") + print(config) + + loader = PluginLoader() + plugin = await loader.load_and_instantiate_plugin(config.plugins[0]) + print(plugin) + prompt = PromptPrehookPayload(name="test_prompt", args = {"user": "What a crapshow!"}) + context = PluginContext(request_id="1", server_id="2") + result = await plugin.prompt_pre_fetch(prompt, context) + assert result.modified_payload.args["user"] == "What a yikesshow!" + config = plugin.config + assert config.name == "ReplaceBadWordsPlugin" + assert config.description == "A plugin for finding and replacing words." + assert config.priority == 150 + assert config.kind == "external" + + message = Message(content=TextContent(type="text", text="What the crud?"), role=Role.USER) + prompt_result = PromptResult(messages=[message]) + + payload_result = PromptPosthookPayload(name="test_prompt", result=prompt_result) + + result = await plugin.prompt_post_fetch(payload_result, context=context) + assert len(result.modified_payload.result.messages) == 1 + assert result.modified_payload.result.messages[0].content.text == "What the yikes?" + await plugin.shutdown() + await loader.shutdown() + del os.environ["CFMCP_PLUGIN_CONFIG"] + del os.environ["PYTHONPATH"] \ No newline at end of file From ad99a1d3129c4012a4e99c90e7b10fc2b5e34472 Mon Sep 17 00:00:00 2001 From: Teryl Taylor Date: Tue, 12 Aug 2025 21:50:49 -0700 Subject: [PATCH 03/65] fix(lint): fixed yamllint issues Signed-off-by: Teryl Taylor --- plugins/external/config-stdio.yaml | 2 +- plugins/resources/server/config-http.yaml | 2 +- plugins/resources/server/config-stdio.yaml | 2 +- .../plugins/fixtures/configs/valid_stdio_external_plugin.yaml | 2 +- .../fixtures/configs/valid_stdio_external_plugin_overrides.yaml | 2 +- .../fixtures/configs/valid_stdio_external_plugin_regex.yaml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/external/config-stdio.yaml b/plugins/external/config-stdio.yaml index dcc412cff..500da605e 100644 --- a/plugins/external/config-stdio.yaml +++ b/plugins/external/config-stdio.yaml @@ -4,7 +4,7 @@ plugins: - name: "DenyListPlugin" kind: "external" mcp: - proto: STDIO + proto: STDIO script: mcpgateway/plugins/framework/external/mcp/server.py # Plugin directories to scan diff --git a/plugins/resources/server/config-http.yaml b/plugins/resources/server/config-http.yaml index 7de90dd61..23d3922b6 100644 --- a/plugins/resources/server/config-http.yaml +++ b/plugins/resources/server/config-http.yaml @@ -6,4 +6,4 @@ streamable-http: port: 3000 mcp_path: "/mcp" json_response: true - stateless: true \ No newline at end of file + stateless: true diff --git a/plugins/resources/server/config-stdio.yaml b/plugins/resources/server/config-stdio.yaml index 773293672..6b1b536a8 100644 --- a/plugins/resources/server/config-stdio.yaml +++ b/plugins/resources/server/config-stdio.yaml @@ -12,4 +12,4 @@ mcp_servers: location: "." tools: enabled: true - module: "plugins.framework.mcp.server" \ No newline at end of file + module: "plugins.framework.mcp.server" diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml index dcc412cff..500da605e 100644 --- a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml @@ -4,7 +4,7 @@ plugins: - name: "DenyListPlugin" kind: "external" mcp: - proto: STDIO + proto: STDIO script: mcpgateway/plugins/framework/external/mcp/server.py # Plugin directories to scan diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml index 4823f3e35..294da3838 100644 --- a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml @@ -7,7 +7,7 @@ plugins: priority: 150 hooks: ["prompt_pre_fetch", "prompt_post_fetch"] mcp: - proto: STDIO + proto: STDIO script: mcpgateway/plugins/framework/external/mcp/server.py # Plugin directories to scan diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml index c34ff0749..375c5fb0b 100644 --- a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml @@ -4,7 +4,7 @@ plugins: - name: "ReplaceBadWordsPlugin" kind: "external" mcp: - proto: STDIO + proto: STDIO script: mcpgateway/plugins/framework/external/mcp/server.py # Plugin directories to scan From 6128d21270cc00316c465a065ab9eb6afb0067cc Mon Sep 17 00:00:00 2001 From: Teryl Taylor Date: Tue, 12 Aug 2025 21:53:58 -0700 Subject: [PATCH 04/65] fix(lint): fixed flake8 issue. Signed-off-by: Teryl Taylor --- mcpgateway/plugins/framework/external/mcp/server.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mcpgateway/plugins/framework/external/mcp/server.py b/mcpgateway/plugins/framework/external/mcp/server.py index 1fc6e19ea..3da623184 100644 --- a/mcpgateway/plugins/framework/external/mcp/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server.py @@ -78,6 +78,9 @@ async def prompt_pre_fetch(plugin_name: str, payload: dict, context: dict) -> di payload: the prompt name and arguments to be analyzed. context: the contextual and state information required for the execution of the hook. + Raises: + ValueError: if unable to retrieve a plugin. + Returns: The transformed or filtered response from the plugin hook. """ @@ -106,6 +109,9 @@ async def prompt_post_fetch(plugin_name: str, payload: dict, context: dict) -> d payload: The prompt payload to be analyzed. context: Contextual information about the hook call. + Raises: + ValueError: if unable to retrieve a plugin. + Returns: The result of the plugin execution. """ From fc41960f6160173c37e9ee9446d2dc27606888cf Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 11 Aug 2025 21:49:47 -0400 Subject: [PATCH 05/65] feat: define plugins cli and implement bootstrap command Signed-off-by: Frederico Araujo --- .env.example | 11 + Makefile | 4 +- mcpgateway/config.py | 4 + mcpgateway/plugins/tools/__init__.py | 12 + mcpgateway/plugins/tools/cli.py | 107 +++ pyproject.toml | 3 + uv.lock | 1141 +++++++++++++++++++------- 7 files changed, 1003 insertions(+), 279 deletions(-) create mode 100644 mcpgateway/plugins/tools/__init__.py create mode 100644 mcpgateway/plugins/tools/cli.py diff --git a/.env.example b/.env.example index bf7d904b1..01d5542f4 100644 --- a/.env.example +++ b/.env.example @@ -163,3 +163,14 @@ SKIP_SSL_VERIFY=false # Header Passthrough (WARNING: Security implications) ENABLE_HEADER_PASSTHROUGH=false DEFAULT_PASSTHROUGH_HEADERS=["X-Tenant-Id", "X-Trace-Id"] + +# Enable auto-completion for plugins CLI +PLUGINS_CLI_COMPLETION=false + +# Set markup mode for plugins CLI +# Valid options: +# rich: use rich markup +# markdown: allow markdown in help strings +# disabled: disable markup +# If unset (commented out), uses "rich" if rich is detected, otherwise disables it. +PLUGINS_CLI_MARKUP_MODE=rich \ No newline at end of file diff --git a/Makefile b/Makefile index 64f28e579..5426ec8a0 100644 --- a/Makefile +++ b/Makefile @@ -55,8 +55,8 @@ CONTAINER_MEMORY = 2048m CONTAINER_CPUS = 2 # Virtual-environment variables -VENVS_DIR := $(HOME)/.venv -VENV_DIR := $(VENVS_DIR)/$(PROJECT_NAME) +VENVS_DIR ?= $(HOME)/.venv +VENV_DIR ?= $(VENVS_DIR)/$(PROJECT_NAME) # ----------------------------------------------------------------------------- # OS Specific diff --git a/mcpgateway/config.py b/mcpgateway/config.py index 366204d7e..2a5865ba6 100644 --- a/mcpgateway/config.py +++ b/mcpgateway/config.py @@ -378,6 +378,10 @@ def _parse_federation_peers(cls, v): plugins_enabled: bool = Field(default=False, description="Enable the plugin framework") plugin_config_file: str = Field(default="plugins/config.yaml", description="Path to main plugin configuration file") + # Plugin CLI settings + plugins_cli_completion: bool = Field(default=False, description="Enable auto-completion for plugins CLI") + plugins_cli_markup_mode: str | None = Field(default=None, description="Set markup mode for plugins CLI") + # Development dev_mode: bool = False reload: bool = False diff --git a/mcpgateway/plugins/tools/__init__.py b/mcpgateway/plugins/tools/__init__.py new file mode 100644 index 000000000..a88d13091 --- /dev/null +++ b/mcpgateway/plugins/tools/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +"""Validation Package. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Fred Araujo + +Provides CLI tools for the MCP Gateway plugin system, including: +- bootstrap +- install +- package +""" diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py new file mode 100644 index 000000000..e4478b85b --- /dev/null +++ b/mcpgateway/plugins/tools/cli.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""mcpplugins CLI โ”€ command line tools for authoring and packaging plugins + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Fred Araujo + +This module is exposed as a **console-script** via: + + [project.scripts] + mcpplugins = "mcpgateway.plugins.tools.cli:main" + +so that a user can simply type `mcpplugins ...` to use the CLI. + +Features +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +* bootstrap: Creates a new plugin project from template โ”‚ +* install: Installs plugins into a Python environment โ”‚ +* package: Builds an MCP server to serve plugins as tools + +Typical usage +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +```console +$ mcpplugins --help +``` +""" + +# Standard +from pathlib import Path +from typing import Optional + +# Third-Party +from copier import Worker +import typer +from typing_extensions import Annotated + +# First-Party +from mcpgateway.config import settings + +# --------------------------------------------------------------------------- +# Configuration defaults +# --------------------------------------------------------------------------- +DEFAULT_TEMPLATE_URL = "https://github.com/IBM/mcp-context-forge-plugins-template.git" +DEFAULT_PROJECT_DIR = Path("./.") +DEFAULT_INSTALL_MANIFEST = Path("plugins/install.yaml") +DEFAULT_IMAGE_TAG = "contextforge-plugin:latest" # TBD: add plugin name and version +DEFAULT_IMAGE_BUILDER = "docker" +DEFAULT_CONTAINERFILE_PATH = Path("docker/Dockerfile") +DEFAULT_VCS_REF = "main" + +# --------------------------------------------------------------------------- +# CLI (overridable via environment variables) +# --------------------------------------------------------------------------- + +markup_mode = settings.plugins_cli_markup_mode or typer.core.DEFAULT_MARKUP_MODE +app = typer.Typer( + help="Command line tools for authoring and packaging plugins.", + add_completion=settings.plugins_cli_completion, + rich_markup_mode=None if markup_mode == "disabled" else markup_mode, +) + + +# --------------------------------------------------------------------------- +# Commands +# --------------------------------------------------------------------------- +@app.command(help="Creates a new plugin project from template.") +def bootstrap( + destination: Annotated[Path, typer.Option("--destination", "-d", help="The directory in which to bootstrap the plugin project.")] = DEFAULT_PROJECT_DIR, + template_url: Annotated[str, typer.Option("--template_url", "-u", help="The URL to the plugins copier template.")] = DEFAULT_TEMPLATE_URL, + vcs_ref: Annotated[str, typer.Option("--vcs_ref", "-r", help="The version control system tag/branch/commit to use for the template.")] = DEFAULT_VCS_REF, + answers_file: Optional[Annotated[typer.FileText, typer.Option("--answers_file", "-a", help="The answers file to be used for bootstrapping.")]] = None, + defaults: Annotated[str, typer.Option("--vcs_ref", "-r", help="Bootstrap with defaults.")] = False, +): + print("Boostrapping a plugin project from template.") + with Worker( + src_path=template_url, + dst_path=destination, + answers_file=answers_file, + defaults=defaults, + vcs_ref=vcs_ref, + ) as worker: + worker.run_copy() + + +@app.command(help="Installs plugins into a Python environment.") +def install( + install_manifest: Annotated[typer.FileText, typer.Option("--install_manifest", "-i", help="The install manifest describing which plugins to install.")] = DEFAULT_INSTALL_MANIFEST, +): + print("Installing plugins") + + +@app.command(help="Builds an MCP server to serve plugins as tools") +def package( + install_manifest: Annotated[typer.FileText, typer.Option("--install_manifest", "-i", help="The install manifest describing which plugins to install.")] = DEFAULT_INSTALL_MANIFEST, + image_tag: Annotated[str, typer.Option("--image_tag", "-t", help="The container image tag to generated container.")] = DEFAULT_IMAGE_TAG, + containerfile: Annotated[Path, typer.Option("--containerfile", "-c", help="The Dockerfile used to build the container.")] = DEFAULT_CONTAINERFILE_PATH, + builder: Annotated[str, typer.Option("--builder", "-b", help="The container builder, compatible with docker build.")] = DEFAULT_IMAGE_BUILDER, +): + print("Deleting user: Hiro Hamada") + + +def main() -> None: # noqa: D401 - imperative mood is fine here + app() + + +if __name__ == "__main__": + app() diff --git a/pyproject.toml b/pyproject.toml index 4fe8d3af6..1c12f67f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ maintainers = [ # ---------------------------------------------------------------- dependencies = [ "alembic>=1.16.4", + "copier>=9.9.0", "cryptography>=45.0.6", "fastapi>=0.116.1", "filelock>=3.19.1", @@ -67,6 +68,7 @@ dependencies = [ "sqlalchemy>=2.0.43", "sse-starlette>=3.0.2", "starlette>=0.47.2", + "typer>=0.16.0", "uvicorn>=0.35.0", "zeroconf>=0.147.0", ] @@ -236,6 +238,7 @@ Changelog = "https://github.com/IBM/mcp-context-forge/blob/main/CHANGELOG.md" # -------------------------------------------------------------------- [project.scripts] mcpgateway = "mcpgateway.cli:main" +mcpplugins = "mcpgateway.plugins.tools.cli:main" # -------------------------------------------------------------------- # ๐Ÿ”ง setuptools-specific configuration diff --git a/uv.lock b/uv.lock index 6c7a8d76b..c98a85e9c 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11, <3.14" resolution-markers = [ "python_full_version >= '3.13'", @@ -7,6 +7,96 @@ resolution-markers = [ "python_full_version < '3.12'", ] +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246, upload-time = "2025-07-29T05:50:15.937Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515, upload-time = "2025-07-29T05:50:17.442Z" }, + { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776, upload-time = "2025-07-29T05:50:19.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", size = 1741977, upload-time = "2025-07-29T05:50:21.665Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", size = 1690645, upload-time = "2025-07-29T05:50:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", size = 1789437, upload-time = "2025-07-29T05:50:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", size = 1828482, upload-time = "2025-07-29T05:50:26.693Z" }, + { url = "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", size = 1730944, upload-time = "2025-07-29T05:50:28.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", size = 1668020, upload-time = "2025-07-29T05:50:30.032Z" }, + { url = "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", size = 1716292, upload-time = "2025-07-29T05:50:31.983Z" }, + { url = "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", size = 1711451, upload-time = "2025-07-29T05:50:33.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", size = 1691634, upload-time = "2025-07-29T05:50:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", size = 1785238, upload-time = "2025-07-29T05:50:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", size = 1805701, upload-time = "2025-07-29T05:50:39.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", size = 1718758, upload-time = "2025-07-29T05:50:41.292Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/1006278d1ffd13a698e5dd4bfa01e5878f6bddefc296c8b62649753ff249/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", size = 428868, upload-time = "2025-07-29T05:50:43.063Z" }, + { url = "https://files.pythonhosted.org/packages/10/97/ad2b18700708452400278039272032170246a1bf8ec5d832772372c71f1a/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", size = 453273, upload-time = "2025-07-29T05:50:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, + { url = "https://files.pythonhosted.org/packages/f2/33/918091abcf102e39d15aba2476ad9e7bd35ddb190dcdd43a854000d3da0d/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315", size = 696741, upload-time = "2025-07-29T05:51:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/7495a81e39a998e400f3ecdd44a62107254803d1681d9189be5c2e4530cd/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd", size = 474407, upload-time = "2025-07-29T05:51:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/49/fc/a9576ab4be2dcbd0f73ee8675d16c707cfc12d5ee80ccf4015ba543480c9/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4", size = 466703, upload-time = "2025-07-29T05:51:22.948Z" }, + { url = "https://files.pythonhosted.org/packages/09/2f/d4bcc8448cf536b2b54eed48f19682031ad182faa3a3fee54ebe5b156387/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7", size = 1705532, upload-time = "2025-07-29T05:51:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f3/59406396083f8b489261e3c011aa8aee9df360a96ac8fa5c2e7e1b8f0466/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d", size = 1686794, upload-time = "2025-07-29T05:51:27.145Z" }, + { url = "https://files.pythonhosted.org/packages/dc/71/164d194993a8d114ee5656c3b7ae9c12ceee7040d076bf7b32fb98a8c5c6/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b", size = 1738865, upload-time = "2025-07-29T05:51:29.366Z" }, + { url = "https://files.pythonhosted.org/packages/1c/00/d198461b699188a93ead39cb458554d9f0f69879b95078dce416d3209b54/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d", size = 1788238, upload-time = "2025-07-29T05:51:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/85/b8/9e7175e1fa0ac8e56baa83bf3c214823ce250d0028955dfb23f43d5e61fd/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d", size = 1710566, upload-time = "2025-07-29T05:51:33.219Z" }, + { url = "https://files.pythonhosted.org/packages/59/e4/16a8eac9df39b48ae102ec030fa9f726d3570732e46ba0c592aeeb507b93/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645", size = 1624270, upload-time = "2025-07-29T05:51:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/cd84dee7b6ace0740908fd0af170f9fab50c2a41ccbc3806aabcb1050141/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461", size = 1677294, upload-time = "2025-07-29T05:51:37.215Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/d0f1f85e50d401eccd12bf85c46ba84f947a84839c8a1c2c5f6e8ab1eb50/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9", size = 1708958, upload-time = "2025-07-29T05:51:39.328Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/f6fa6c5790fb602538483aa5a1b86fcbad66244997e5230d88f9412ef24c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d", size = 1651553, upload-time = "2025-07-29T05:51:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/04/36/a6d36ad545fa12e61d11d1932eef273928b0495e6a576eb2af04297fdd3c/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693", size = 1727688, upload-time = "2025-07-29T05:51:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c8/f195e5e06608a97a4e52c5d41c7927301bf757a8e8bb5bbf8cef6c314961/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64", size = 1761157, upload-time = "2025-07-29T05:51:45.643Z" }, + { url = "https://files.pythonhosted.org/packages/05/6a/ea199e61b67f25ba688d3ce93f63b49b0a4e3b3d380f03971b4646412fc6/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51", size = 1710050, upload-time = "2025-07-29T05:51:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2e/ffeb7f6256b33635c29dbed29a22a723ff2dd7401fff42ea60cf2060abfb/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0", size = 422647, upload-time = "2025-07-29T05:51:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + [[package]] name = "aiosqlite" version = "0.21.0" @@ -491,58 +581,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b6/d9/0137658a353168ffa9d0fc14b812d3834772040858ddd1cb6eeaf09f7a44/cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d", size = 39177, upload-time = "2024-02-21T18:02:39.569Z" }, ] +[[package]] +name = "copier" +version = "9.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "dunamai" }, + { name = "funcy" }, + { name = "jinja2" }, + { name = "jinja2-ansible-filters" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "plumbum" }, + { name = "pydantic" }, + { name = "pygments" }, + { name = "pyyaml" }, + { name = "questionary" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/54/badd656b902a69915851caf0ad0dcb06682929f4e6a2f218acf15c373421/copier-9.9.0.tar.gz", hash = "sha256:db48d89bd30d957faa8c96fed757cb5718fdc0fe6b83bebb201424f448cb5216", size = 581833, upload-time = "2025-08-01T17:45:39.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/95/dd0da5d106b3000c91614010f33ff4f3b6dc5a77eb11d6f01cd000228892/copier-9.9.0-py3-none-any.whl", hash = "sha256:d3484178560456fc3f138ec67349872dd2b378051c2360f95d7f31005ea9794d", size = 55985, upload-time = "2025-08-01T17:45:38.06Z" }, +] + [[package]] name = "coverage" -version = "7.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/b7/c0465ca253df10a9e8dae0692a4ae6e9726d245390aaef92360e1d6d3832/coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b", size = 813556, upload-time = "2025-07-03T10:54:15.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/40/916786453bcfafa4c788abee4ccd6f592b5b5eca0cd61a32a4e5a7ef6e02/coverage-7.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a7a56a2964a9687b6aba5b5ced6971af308ef6f79a91043c05dd4ee3ebc3e9ba", size = 212152, upload-time = "2025-07-03T10:52:53.562Z" }, - { url = "https://files.pythonhosted.org/packages/9f/66/cc13bae303284b546a030762957322bbbff1ee6b6cb8dc70a40f8a78512f/coverage-7.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123d589f32c11d9be7fe2e66d823a236fe759b0096f5db3fb1b75b2fa414a4fa", size = 212540, upload-time = "2025-07-03T10:52:55.196Z" }, - { url = "https://files.pythonhosted.org/packages/0f/3c/d56a764b2e5a3d43257c36af4a62c379df44636817bb5f89265de4bf8bd7/coverage-7.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333b2e0ca576a7dbd66e85ab402e35c03b0b22f525eed82681c4b866e2e2653a", size = 245097, upload-time = "2025-07-03T10:52:56.509Z" }, - { url = "https://files.pythonhosted.org/packages/b1/46/bd064ea8b3c94eb4ca5d90e34d15b806cba091ffb2b8e89a0d7066c45791/coverage-7.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326802760da234baf9f2f85a39e4a4b5861b94f6c8d95251f699e4f73b1835dc", size = 242812, upload-time = "2025-07-03T10:52:57.842Z" }, - { url = "https://files.pythonhosted.org/packages/43/02/d91992c2b29bc7afb729463bc918ebe5f361be7f1daae93375a5759d1e28/coverage-7.9.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e7be4cfec248df38ce40968c95d3952fbffd57b400d4b9bb580f28179556d2", size = 244617, upload-time = "2025-07-03T10:52:59.239Z" }, - { url = "https://files.pythonhosted.org/packages/b7/4f/8fadff6bf56595a16d2d6e33415841b0163ac660873ed9a4e9046194f779/coverage-7.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b4a4cb73b9f2b891c1788711408ef9707666501ba23684387277ededab1097c", size = 244263, upload-time = "2025-07-03T10:53:00.601Z" }, - { url = "https://files.pythonhosted.org/packages/9b/d2/e0be7446a2bba11739edb9f9ba4eff30b30d8257370e237418eb44a14d11/coverage-7.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2c8937fa16c8c9fbbd9f118588756e7bcdc7e16a470766a9aef912dd3f117dbd", size = 242314, upload-time = "2025-07-03T10:53:01.932Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7d/dcbac9345000121b8b57a3094c2dfcf1ccc52d8a14a40c1d4bc89f936f80/coverage-7.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42da2280c4d30c57a9b578bafd1d4494fa6c056d4c419d9689e66d775539be74", size = 242904, upload-time = "2025-07-03T10:53:03.478Z" }, - { url = "https://files.pythonhosted.org/packages/41/58/11e8db0a0c0510cf31bbbdc8caf5d74a358b696302a45948d7c768dfd1cf/coverage-7.9.2-cp311-cp311-win32.whl", hash = "sha256:14fa8d3da147f5fdf9d298cacc18791818f3f1a9f542c8958b80c228320e90c6", size = 214553, upload-time = "2025-07-03T10:53:05.174Z" }, - { url = "https://files.pythonhosted.org/packages/3a/7d/751794ec8907a15e257136e48dc1021b1f671220ecccfd6c4eaf30802714/coverage-7.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:549cab4892fc82004f9739963163fd3aac7a7b0df430669b75b86d293d2df2a7", size = 215441, upload-time = "2025-07-03T10:53:06.472Z" }, - { url = "https://files.pythonhosted.org/packages/62/5b/34abcedf7b946c1c9e15b44f326cb5b0da852885312b30e916f674913428/coverage-7.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2667a2b913e307f06aa4e5677f01a9746cd08e4b35e14ebcde6420a9ebb4c62", size = 213873, upload-time = "2025-07-03T10:53:07.699Z" }, - { url = "https://files.pythonhosted.org/packages/53/d7/7deefc6fd4f0f1d4c58051f4004e366afc9e7ab60217ac393f247a1de70a/coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0", size = 212344, upload-time = "2025-07-03T10:53:09.3Z" }, - { url = "https://files.pythonhosted.org/packages/95/0c/ee03c95d32be4d519e6a02e601267769ce2e9a91fc8faa1b540e3626c680/coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3", size = 212580, upload-time = "2025-07-03T10:53:11.52Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9f/826fa4b544b27620086211b87a52ca67592622e1f3af9e0a62c87aea153a/coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1", size = 246383, upload-time = "2025-07-03T10:53:13.134Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b3/4477aafe2a546427b58b9c540665feff874f4db651f4d3cb21b308b3a6d2/coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615", size = 243400, upload-time = "2025-07-03T10:53:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/f8/c2/efffa43778490c226d9d434827702f2dfbc8041d79101a795f11cbb2cf1e/coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b", size = 245591, upload-time = "2025-07-03T10:53:15.872Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e7/a59888e882c9a5f0192d8627a30ae57910d5d449c80229b55e7643c078c4/coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9", size = 245402, upload-time = "2025-07-03T10:53:17.124Z" }, - { url = "https://files.pythonhosted.org/packages/92/a5/72fcd653ae3d214927edc100ce67440ed8a0a1e3576b8d5e6d066ed239db/coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f", size = 243583, upload-time = "2025-07-03T10:53:18.781Z" }, - { url = "https://files.pythonhosted.org/packages/5c/f5/84e70e4df28f4a131d580d7d510aa1ffd95037293da66fd20d446090a13b/coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d", size = 244815, upload-time = "2025-07-03T10:53:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/39/e7/d73d7cbdbd09fdcf4642655ae843ad403d9cbda55d725721965f3580a314/coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355", size = 214719, upload-time = "2025-07-03T10:53:21.521Z" }, - { url = "https://files.pythonhosted.org/packages/9f/d6/7486dcc3474e2e6ad26a2af2db7e7c162ccd889c4c68fa14ea8ec189c9e9/coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0", size = 215509, upload-time = "2025-07-03T10:53:22.853Z" }, - { url = "https://files.pythonhosted.org/packages/b7/34/0439f1ae2593b0346164d907cdf96a529b40b7721a45fdcf8b03c95fcd90/coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b", size = 213910, upload-time = "2025-07-03T10:53:24.472Z" }, - { url = "https://files.pythonhosted.org/packages/94/9d/7a8edf7acbcaa5e5c489a646226bed9591ee1c5e6a84733c0140e9ce1ae1/coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038", size = 212367, upload-time = "2025-07-03T10:53:25.811Z" }, - { url = "https://files.pythonhosted.org/packages/e8/9e/5cd6f130150712301f7e40fb5865c1bc27b97689ec57297e568d972eec3c/coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d", size = 212632, upload-time = "2025-07-03T10:53:27.075Z" }, - { url = "https://files.pythonhosted.org/packages/a8/de/6287a2c2036f9fd991c61cefa8c64e57390e30c894ad3aa52fac4c1e14a8/coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3", size = 245793, upload-time = "2025-07-03T10:53:28.408Z" }, - { url = "https://files.pythonhosted.org/packages/06/cc/9b5a9961d8160e3cb0b558c71f8051fe08aa2dd4b502ee937225da564ed1/coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14", size = 243006, upload-time = "2025-07-03T10:53:29.754Z" }, - { url = "https://files.pythonhosted.org/packages/49/d9/4616b787d9f597d6443f5588619c1c9f659e1f5fc9eebf63699eb6d34b78/coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6", size = 244990, upload-time = "2025-07-03T10:53:31.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/83/801cdc10f137b2d02b005a761661649ffa60eb173dcdaeb77f571e4dc192/coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b", size = 245157, upload-time = "2025-07-03T10:53:32.717Z" }, - { url = "https://files.pythonhosted.org/packages/c8/a4/41911ed7e9d3ceb0ffb019e7635468df7499f5cc3edca5f7dfc078e9c5ec/coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d", size = 243128, upload-time = "2025-07-03T10:53:34.009Z" }, - { url = "https://files.pythonhosted.org/packages/10/41/344543b71d31ac9cb00a664d5d0c9ef134a0fe87cb7d8430003b20fa0b7d/coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868", size = 244511, upload-time = "2025-07-03T10:53:35.434Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/3b68c77e4812105e2a060f6946ba9e6f898ddcdc0d2bfc8b4b152a9ae522/coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a", size = 214765, upload-time = "2025-07-03T10:53:36.787Z" }, - { url = "https://files.pythonhosted.org/packages/06/a2/7fac400f6a346bb1a4004eb2a76fbff0e242cd48926a2ce37a22a6a1d917/coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b", size = 215536, upload-time = "2025-07-03T10:53:38.188Z" }, - { url = "https://files.pythonhosted.org/packages/08/47/2c6c215452b4f90d87017e61ea0fd9e0486bb734cb515e3de56e2c32075f/coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694", size = 213943, upload-time = "2025-07-03T10:53:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/a3/46/e211e942b22d6af5e0f323faa8a9bc7c447a1cf1923b64c47523f36ed488/coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5", size = 213088, upload-time = "2025-07-03T10:53:40.874Z" }, - { url = "https://files.pythonhosted.org/packages/d2/2f/762551f97e124442eccd907bf8b0de54348635b8866a73567eb4e6417acf/coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b", size = 213298, upload-time = "2025-07-03T10:53:42.218Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b7/76d2d132b7baf7360ed69be0bcab968f151fa31abe6d067f0384439d9edb/coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3", size = 256541, upload-time = "2025-07-03T10:53:43.823Z" }, - { url = "https://files.pythonhosted.org/packages/a0/17/392b219837d7ad47d8e5974ce5f8dc3deb9f99a53b3bd4d123602f960c81/coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8", size = 252761, upload-time = "2025-07-03T10:53:45.19Z" }, - { url = "https://files.pythonhosted.org/packages/d5/77/4256d3577fe1b0daa8d3836a1ebe68eaa07dd2cbaf20cf5ab1115d6949d4/coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46", size = 254917, upload-time = "2025-07-03T10:53:46.931Z" }, - { url = "https://files.pythonhosted.org/packages/53/99/fc1a008eef1805e1ddb123cf17af864743354479ea5129a8f838c433cc2c/coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584", size = 256147, upload-time = "2025-07-03T10:53:48.289Z" }, - { url = "https://files.pythonhosted.org/packages/92/c0/f63bf667e18b7f88c2bdb3160870e277c4874ced87e21426128d70aa741f/coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e", size = 254261, upload-time = "2025-07-03T10:53:49.99Z" }, - { url = "https://files.pythonhosted.org/packages/8c/32/37dd1c42ce3016ff8ec9e4b607650d2e34845c0585d3518b2a93b4830c1a/coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac", size = 255099, upload-time = "2025-07-03T10:53:51.354Z" }, - { url = "https://files.pythonhosted.org/packages/da/2e/af6b86f7c95441ce82f035b3affe1cd147f727bbd92f563be35e2d585683/coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926", size = 215440, upload-time = "2025-07-03T10:53:52.808Z" }, - { url = "https://files.pythonhosted.org/packages/4d/bb/8a785d91b308867f6b2e36e41c569b367c00b70c17f54b13ac29bcd2d8c8/coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd", size = 216537, upload-time = "2025-07-03T10:53:54.273Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a0/a6bffb5e0f41a47279fd45a8f3155bf193f77990ae1c30f9c224b61cacb0/coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb", size = 214398, upload-time = "2025-07-03T10:53:56.715Z" }, - { url = "https://files.pythonhosted.org/packages/d7/85/f8bbefac27d286386961c25515431482a425967e23d3698b75a250872924/coverage-7.9.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:8a1166db2fb62473285bcb092f586e081e92656c7dfa8e9f62b4d39d7e6b5050", size = 204013, upload-time = "2025-07-03T10:54:12.084Z" }, - { url = "https://files.pythonhosted.org/packages/3c/38/bbe2e63902847cf79036ecc75550d0698af31c91c7575352eb25190d0fb3/coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4", size = 204005, upload-time = "2025-07-03T10:54:13.491Z" }, +version = "7.10.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/2c/253cc41cd0f40b84c1c34c5363e0407d73d4a1cae005fed6db3b823175bd/coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619", size = 822936, upload-time = "2025-08-10T21:27:39.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/04/810e506d7a19889c244d35199cbf3239a2f952b55580aa42ca4287409424/coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397", size = 216075, upload-time = "2025-08-10T21:25:39.891Z" }, + { url = "https://files.pythonhosted.org/packages/2e/50/6b3fbab034717b4af3060bdaea6b13dfdc6b1fad44b5082e2a95cd378a9a/coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85", size = 216476, upload-time = "2025-08-10T21:25:41.137Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/4368c624c1ed92659812b63afc76c492be7867ac8e64b7190b88bb26d43c/coverage-7.10.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1af4461b25fe92889590d438905e1fc79a95680ec2a1ff69a591bb3fdb6c7157", size = 246865, upload-time = "2025-08-10T21:25:42.408Z" }, + { url = "https://files.pythonhosted.org/packages/34/12/5608f76070939395c17053bf16e81fd6c06cf362a537ea9d07e281013a27/coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54", size = 248800, upload-time = "2025-08-10T21:25:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/7cc90c448a0ad724283cbcdfd66b8d23a598861a6a22ac2b7b8696491798/coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a", size = 250904, upload-time = "2025-08-10T21:25:45.384Z" }, + { url = "https://files.pythonhosted.org/packages/e6/70/9967b847063c1c393b4f4d6daab1131558ebb6b51f01e7df7150aa99f11d/coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84", size = 248597, upload-time = "2025-08-10T21:25:47.059Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fe/263307ce6878b9ed4865af42e784b42bb82d066bcf10f68defa42931c2c7/coverage-7.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0dc69c60224cda33d384572da945759756e3f06b9cdac27f302f53961e63160", size = 246647, upload-time = "2025-08-10T21:25:48.334Z" }, + { url = "https://files.pythonhosted.org/packages/8e/27/d27af83ad162eba62c4eb7844a1de6cf7d9f6b185df50b0a3514a6f80ddd/coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124", size = 247290, upload-time = "2025-08-10T21:25:49.945Z" }, + { url = "https://files.pythonhosted.org/packages/28/83/904ff27e15467a5622dbe9ad2ed5831b4a616a62570ec5924d06477dff5a/coverage-7.10.3-cp311-cp311-win32.whl", hash = "sha256:54e409dd64e5302b2a8fdf44ec1c26f47abd1f45a2dcf67bd161873ee05a59b8", size = 218521, upload-time = "2025-08-10T21:25:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/b8/29/bc717b8902faaccf0ca486185f0dcab4778561a529dde51cb157acaafa16/coverage-7.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:30c601610a9b23807c5e9e2e442054b795953ab85d525c3de1b1b27cebeb2117", size = 219412, upload-time = "2025-08-10T21:25:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7a/5a1a7028c11bb589268c656c6b3f2bbf06e0aced31bbdf7a4e94e8442cc0/coverage-7.10.3-cp311-cp311-win_arm64.whl", hash = "sha256:dabe662312a97958e932dee056f2659051d822552c0b866823e8ba1c2fe64770", size = 218091, upload-time = "2025-08-10T21:25:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/b8/62/13c0b66e966c43d7aa64dadc8cd2afa1f5a2bf9bb863bdabc21fb94e8b63/coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42", size = 216262, upload-time = "2025-08-10T21:25:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/59fdf79be7ac2f0206fc739032f482cfd3f66b18f5248108ff192741beae/coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294", size = 216496, upload-time = "2025-08-10T21:25:56.759Z" }, + { url = "https://files.pythonhosted.org/packages/34/b1/bc83788ba31bde6a0c02eb96bbc14b2d1eb083ee073beda18753fa2c4c66/coverage-7.10.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dd4564207b160d0d45c36a10bc0a3d12563028e8b48cd6459ea322302a156d7", size = 247989, upload-time = "2025-08-10T21:25:58.067Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/f8bdf88357956c844bd872e87cb16748a37234f7f48c721dc7e981145eb7/coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437", size = 250738, upload-time = "2025-08-10T21:25:59.406Z" }, + { url = "https://files.pythonhosted.org/packages/ae/df/6396301d332b71e42bbe624670af9376f63f73a455cc24723656afa95796/coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587", size = 251868, upload-time = "2025-08-10T21:26:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/21/d760b2df6139b6ef62c9cc03afb9bcdf7d6e36ed4d078baacffa618b4c1c/coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea", size = 249790, upload-time = "2025-08-10T21:26:02.009Z" }, + { url = "https://files.pythonhosted.org/packages/69/91/5dcaa134568202397fa4023d7066d4318dc852b53b428052cd914faa05e1/coverage-7.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a374d4e923814e8b72b205ef6b3d3a647bb50e66f3558582eda074c976923613", size = 247907, upload-time = "2025-08-10T21:26:03.757Z" }, + { url = "https://files.pythonhosted.org/packages/38/ed/70c0e871cdfef75f27faceada461206c1cc2510c151e1ef8d60a6fedda39/coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb", size = 249344, upload-time = "2025-08-10T21:26:05.11Z" }, + { url = "https://files.pythonhosted.org/packages/5f/55/c8a273ed503cedc07f8a00dcd843daf28e849f0972e4c6be4c027f418ad6/coverage-7.10.3-cp312-cp312-win32.whl", hash = "sha256:187ecdcac21f9636d570e419773df7bd2fda2e7fa040f812e7f95d0bddf5f79a", size = 218693, upload-time = "2025-08-10T21:26:06.534Z" }, + { url = "https://files.pythonhosted.org/packages/94/58/dd3cfb2473b85be0b6eb8c5b6d80b6fc3f8f23611e69ef745cef8cf8bad5/coverage-7.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a50ad2524ee7e4c2a95e60d2b0b83283bdfc745fe82359d567e4f15d3823eb5", size = 219501, upload-time = "2025-08-10T21:26:08.195Z" }, + { url = "https://files.pythonhosted.org/packages/56/af/7cbcbf23d46de6f24246e3f76b30df099d05636b30c53c158a196f7da3ad/coverage-7.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:c112f04e075d3495fa3ed2200f71317da99608cbb2e9345bdb6de8819fc30571", size = 218135, upload-time = "2025-08-10T21:26:09.584Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/239e4de9cc149c80e9cc359fab60592365b8c4cbfcad58b8a939d18c6898/coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a", size = 216298, upload-time = "2025-08-10T21:26:10.973Z" }, + { url = "https://files.pythonhosted.org/packages/56/da/28717da68f8ba68f14b9f558aaa8f3e39ada8b9a1ae4f4977c8f98b286d5/coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a", size = 216546, upload-time = "2025-08-10T21:26:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/de/bb/e1ade16b9e3f2d6c323faeb6bee8e6c23f3a72760a5d9af102ef56a656cb/coverage-7.10.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:488e9b50dc5d2aa9521053cfa706209e5acf5289e81edc28291a24f4e4488f46", size = 247538, upload-time = "2025-08-10T21:26:14.455Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4", size = 250141, upload-time = "2025-08-10T21:26:15.787Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ed/33efd8819895b10c66348bf26f011dd621e804866c996ea6893d682218df/coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a", size = 251415, upload-time = "2025-08-10T21:26:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/cb83826f313d07dc743359c9914d9bc460e0798da9a0e38b4f4fabc207ed/coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3", size = 249575, upload-time = "2025-08-10T21:26:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/ae963c7a8e9581c20fa4355ab8940ca272554d8102e872dbb932a644e410/coverage-7.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fe72cbdd12d9e0f4aca873fa6d755e103888a7f9085e4a62d282d9d5b9f7928c", size = 247466, upload-time = "2025-08-10T21:26:20.263Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/b68d1487c6af370b8d5ef223c6d7e250d952c3acfbfcdbf1a773aa0da9d2/coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21", size = 249084, upload-time = "2025-08-10T21:26:21.638Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/a0bcb561645c2c1e21758d8200443669d6560d2a2fb03955291110212ec4/coverage-7.10.3-cp313-cp313-win32.whl", hash = "sha256:24d0c13de473b04920ddd6e5da3c08831b1170b8f3b17461d7429b61cad59ae0", size = 218735, upload-time = "2025-08-10T21:26:23.009Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c3/78b4adddbc0feb3b223f62761e5f9b4c5a758037aaf76e0a5845e9e35e48/coverage-7.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:3564aae76bce4b96e2345cf53b4c87e938c4985424a9be6a66ee902626edec4c", size = 219531, upload-time = "2025-08-10T21:26:24.474Z" }, + { url = "https://files.pythonhosted.org/packages/70/1b/1229c0b2a527fa5390db58d164aa896d513a1fbb85a1b6b6676846f00552/coverage-7.10.3-cp313-cp313-win_arm64.whl", hash = "sha256:f35580f19f297455f44afcd773c9c7a058e52eb6eb170aa31222e635f2e38b87", size = 218162, upload-time = "2025-08-10T21:26:25.847Z" }, + { url = "https://files.pythonhosted.org/packages/fc/26/1c1f450e15a3bf3eaecf053ff64538a2612a23f05b21d79ce03be9ff5903/coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84", size = 217003, upload-time = "2025-08-10T21:26:27.231Z" }, + { url = "https://files.pythonhosted.org/packages/29/96/4b40036181d8c2948454b458750960956a3c4785f26a3c29418bbbee1666/coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e", size = 217238, upload-time = "2025-08-10T21:26:28.83Z" }, + { url = "https://files.pythonhosted.org/packages/62/23/8dfc52e95da20957293fb94d97397a100e63095ec1e0ef5c09dd8c6f591a/coverage-7.10.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ae22b97003c74186e034a93e4f946c75fad8c0ce8d92fbbc168b5e15ee2841f", size = 258561, upload-time = "2025-08-10T21:26:30.475Z" }, + { url = "https://files.pythonhosted.org/packages/59/95/00e7fcbeda3f632232f4c07dde226afe3511a7781a000aa67798feadc535/coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5", size = 260735, upload-time = "2025-08-10T21:26:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4c/f4666cbc4571804ba2a65b078ff0de600b0b577dc245389e0bc9b69ae7ca/coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8", size = 262960, upload-time = "2025-08-10T21:26:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a5/8a9e8a7b12a290ed98b60f73d1d3e5e9ced75a4c94a0d1a671ce3ddfff2a/coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1", size = 260515, upload-time = "2025-08-10T21:26:35.16Z" }, + { url = "https://files.pythonhosted.org/packages/86/11/bb59f7f33b2cac0c5b17db0d9d0abba9c90d9eda51a6e727b43bd5fce4ae/coverage-7.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:669fe0d4e69c575c52148511029b722ba8d26e8a3129840c2ce0522e1452b256", size = 258278, upload-time = "2025-08-10T21:26:36.539Z" }, + { url = "https://files.pythonhosted.org/packages/cc/22/3646f8903743c07b3e53fded0700fed06c580a980482f04bf9536657ac17/coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b", size = 259408, upload-time = "2025-08-10T21:26:37.954Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/6375e9d905da22ddea41cd85c30994b8b6f6c02e44e4c5744b76d16b026f/coverage-7.10.3-cp313-cp313t-win32.whl", hash = "sha256:cc0ee4b2ccd42cab7ee6be46d8a67d230cb33a0a7cd47a58b587a7063b6c6b0e", size = 219396, upload-time = "2025-08-10T21:26:39.426Z" }, + { url = "https://files.pythonhosted.org/packages/33/3b/7da37fd14412b8c8b6e73c3e7458fef6b1b05a37f990a9776f88e7740c89/coverage-7.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:03db599f213341e2960430984e04cf35fb179724e052a3ee627a068653cf4a7c", size = 220458, upload-time = "2025-08-10T21:26:40.905Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/59a9a70f17edab513c844ee7a5c63cf1057041a84cc725b46a51c6f8301b/coverage-7.10.3-cp313-cp313t-win_arm64.whl", hash = "sha256:46eae7893ba65f53c71284585a262f083ef71594f05ec5c85baf79c402369098", size = 218722, upload-time = "2025-08-10T21:26:42.362Z" }, + { url = "https://files.pythonhosted.org/packages/84/19/e67f4ae24e232c7f713337f3f4f7c9c58afd0c02866fb07c7b9255a19ed7/coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1", size = 207921, upload-time = "2025-08-10T21:27:38.254Z" }, ] [package.optional-dependencies] @@ -565,43 +678,43 @@ wheels = [ [[package]] name = "cryptography" -version = "45.0.5" +version = "45.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/1e/49527ac611af559665f71cbb8f92b332b5ec9c6fbc4e88b0f8e92f5e85df/cryptography-45.0.5.tar.gz", hash = "sha256:72e76caa004ab63accdf26023fccd1d087f6d90ec6048ff33ad0445abf7f605a", size = 744903, upload-time = "2025-07-02T13:06:25.941Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/fb/09e28bc0c46d2c547085e60897fea96310574c70fb21cd58a730a45f3403/cryptography-45.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:101ee65078f6dd3e5a028d4f19c07ffa4dd22cce6a20eaa160f8b5219911e7d8", size = 7043092, upload-time = "2025-07-02T13:05:01.514Z" }, - { url = "https://files.pythonhosted.org/packages/b1/05/2194432935e29b91fb649f6149c1a4f9e6d3d9fc880919f4ad1bcc22641e/cryptography-45.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3a264aae5f7fbb089dbc01e0242d3b67dffe3e6292e1f5182122bdf58e65215d", size = 4205926, upload-time = "2025-07-02T13:05:04.741Z" }, - { url = "https://files.pythonhosted.org/packages/07/8b/9ef5da82350175e32de245646b1884fc01124f53eb31164c77f95a08d682/cryptography-45.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e74d30ec9c7cb2f404af331d5b4099a9b322a8a6b25c4632755c8757345baac5", size = 4429235, upload-time = "2025-07-02T13:05:07.084Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e1/c809f398adde1994ee53438912192d92a1d0fc0f2d7582659d9ef4c28b0c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3af26738f2db354aafe492fb3869e955b12b2ef2e16908c8b9cb928128d42c57", size = 4209785, upload-time = "2025-07-02T13:05:09.321Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8b/07eb6bd5acff58406c5e806eff34a124936f41a4fb52909ffa4d00815f8c/cryptography-45.0.5-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e6c00130ed423201c5bc5544c23359141660b07999ad82e34e7bb8f882bb78e0", size = 3893050, upload-time = "2025-07-02T13:05:11.069Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ef/3333295ed58d900a13c92806b67e62f27876845a9a908c939f040887cca9/cryptography-45.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:dd420e577921c8c2d31289536c386aaa30140b473835e97f83bc71ea9d2baf2d", size = 4457379, upload-time = "2025-07-02T13:05:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9d/44080674dee514dbb82b21d6fa5d1055368f208304e2ab1828d85c9de8f4/cryptography-45.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d05a38884db2ba215218745f0781775806bde4f32e07b135348355fe8e4991d9", size = 4209355, upload-time = "2025-07-02T13:05:15.017Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d8/0749f7d39f53f8258e5c18a93131919ac465ee1f9dccaf1b3f420235e0b5/cryptography-45.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ad0caded895a00261a5b4aa9af828baede54638754b51955a0ac75576b831b27", size = 4456087, upload-time = "2025-07-02T13:05:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/09/d7/92acac187387bf08902b0bf0699816f08553927bdd6ba3654da0010289b4/cryptography-45.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9024beb59aca9d31d36fcdc1604dd9bbeed0a55bface9f1908df19178e2f116e", size = 4332873, upload-time = "2025-07-02T13:05:18.743Z" }, - { url = "https://files.pythonhosted.org/packages/03/c2/840e0710da5106a7c3d4153c7215b2736151bba60bf4491bdb421df5056d/cryptography-45.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91098f02ca81579c85f66df8a588c78f331ca19089763d733e34ad359f474174", size = 4564651, upload-time = "2025-07-02T13:05:21.382Z" }, - { url = "https://files.pythonhosted.org/packages/2e/92/cc723dd6d71e9747a887b94eb3827825c6c24b9e6ce2bb33b847d31d5eaa/cryptography-45.0.5-cp311-abi3-win32.whl", hash = "sha256:926c3ea71a6043921050eaa639137e13dbe7b4ab25800932a8498364fc1abec9", size = 2929050, upload-time = "2025-07-02T13:05:23.39Z" }, - { url = "https://files.pythonhosted.org/packages/1f/10/197da38a5911a48dd5389c043de4aec4b3c94cb836299b01253940788d78/cryptography-45.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:b85980d1e345fe769cfc57c57db2b59cff5464ee0c045d52c0df087e926fbe63", size = 3403224, upload-time = "2025-07-02T13:05:25.202Z" }, - { url = "https://files.pythonhosted.org/packages/fe/2b/160ce8c2765e7a481ce57d55eba1546148583e7b6f85514472b1d151711d/cryptography-45.0.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3562c2f23c612f2e4a6964a61d942f891d29ee320edb62ff48ffb99f3de9ae8", size = 7017143, upload-time = "2025-07-02T13:05:27.229Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e7/2187be2f871c0221a81f55ee3105d3cf3e273c0a0853651d7011eada0d7e/cryptography-45.0.5-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3fcfbefc4a7f332dece7272a88e410f611e79458fab97b5efe14e54fe476f4fd", size = 4197780, upload-time = "2025-07-02T13:05:29.299Z" }, - { url = "https://files.pythonhosted.org/packages/b9/cf/84210c447c06104e6be9122661159ad4ce7a8190011669afceeaea150524/cryptography-45.0.5-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:460f8c39ba66af7db0545a8c6f2eabcbc5a5528fc1cf6c3fa9a1e44cec33385e", size = 4420091, upload-time = "2025-07-02T13:05:31.221Z" }, - { url = "https://files.pythonhosted.org/packages/3e/6a/cb8b5c8bb82fafffa23aeff8d3a39822593cee6e2f16c5ca5c2ecca344f7/cryptography-45.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9b4cf6318915dccfe218e69bbec417fdd7c7185aa7aab139a2c0beb7468c89f0", size = 4198711, upload-time = "2025-07-02T13:05:33.062Z" }, - { url = "https://files.pythonhosted.org/packages/04/f7/36d2d69df69c94cbb2473871926daf0f01ad8e00fe3986ac3c1e8c4ca4b3/cryptography-45.0.5-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2089cc8f70a6e454601525e5bf2779e665d7865af002a5dec8d14e561002e135", size = 3883299, upload-time = "2025-07-02T13:05:34.94Z" }, - { url = "https://files.pythonhosted.org/packages/82/c7/f0ea40f016de72f81288e9fe8d1f6748036cb5ba6118774317a3ffc6022d/cryptography-45.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0027d566d65a38497bc37e0dd7c2f8ceda73597d2ac9ba93810204f56f52ebc7", size = 4450558, upload-time = "2025-07-02T13:05:37.288Z" }, - { url = "https://files.pythonhosted.org/packages/06/ae/94b504dc1a3cdf642d710407c62e86296f7da9e66f27ab12a1ee6fdf005b/cryptography-45.0.5-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:be97d3a19c16a9be00edf79dca949c8fa7eff621763666a145f9f9535a5d7f42", size = 4198020, upload-time = "2025-07-02T13:05:39.102Z" }, - { url = "https://files.pythonhosted.org/packages/05/2b/aaf0adb845d5dabb43480f18f7ca72e94f92c280aa983ddbd0bcd6ecd037/cryptography-45.0.5-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:7760c1c2e1a7084153a0f68fab76e754083b126a47d0117c9ed15e69e2103492", size = 4449759, upload-time = "2025-07-02T13:05:41.398Z" }, - { url = "https://files.pythonhosted.org/packages/91/e4/f17e02066de63e0100a3a01b56f8f1016973a1d67551beaf585157a86b3f/cryptography-45.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ff8728d8d890b3dda5765276d1bc6fb099252915a2cd3aff960c4c195745dd0", size = 4319991, upload-time = "2025-07-02T13:05:43.64Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2e/e2dbd629481b499b14516eed933f3276eb3239f7cee2dcfa4ee6b44d4711/cryptography-45.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7259038202a47fdecee7e62e0fd0b0738b6daa335354396c6ddebdbe1206af2a", size = 4554189, upload-time = "2025-07-02T13:05:46.045Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ea/a78a0c38f4c8736287b71c2ea3799d173d5ce778c7d6e3c163a95a05ad2a/cryptography-45.0.5-cp37-abi3-win32.whl", hash = "sha256:1e1da5accc0c750056c556a93c3e9cb828970206c68867712ca5805e46dc806f", size = 2911769, upload-time = "2025-07-02T13:05:48.329Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/28ac139109d9005ad3f6b6f8976ffede6706a6478e21c889ce36c840918e/cryptography-45.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:90cb0a7bb35959f37e23303b7eed0a32280510030daba3f7fdfbb65defde6a97", size = 3390016, upload-time = "2025-07-02T13:05:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/c0/71/9bdbcfd58d6ff5084687fe722c58ac718ebedbc98b9f8f93781354e6d286/cryptography-45.0.5-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8c4a6ff8a30e9e3d38ac0539e9a9e02540ab3f827a3394f8852432f6b0ea152e", size = 3587878, upload-time = "2025-07-02T13:06:06.339Z" }, - { url = "https://files.pythonhosted.org/packages/f0/63/83516cfb87f4a8756eaa4203f93b283fda23d210fc14e1e594bd5f20edb6/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bd4c45986472694e5121084c6ebbd112aa919a25e783b87eb95953c9573906d6", size = 4152447, upload-time = "2025-07-02T13:06:08.345Z" }, - { url = "https://files.pythonhosted.org/packages/22/11/d2823d2a5a0bd5802b3565437add16f5c8ce1f0778bf3822f89ad2740a38/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:982518cd64c54fcada9d7e5cf28eabd3ee76bd03ab18e08a48cad7e8b6f31b18", size = 4386778, upload-time = "2025-07-02T13:06:10.263Z" }, - { url = "https://files.pythonhosted.org/packages/5f/38/6bf177ca6bce4fe14704ab3e93627c5b0ca05242261a2e43ef3168472540/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:12e55281d993a793b0e883066f590c1ae1e802e3acb67f8b442e721e475e6463", size = 4151627, upload-time = "2025-07-02T13:06:13.097Z" }, - { url = "https://files.pythonhosted.org/packages/38/6a/69fc67e5266bff68a91bcb81dff8fb0aba4d79a78521a08812048913e16f/cryptography-45.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:5aa1e32983d4443e310f726ee4b071ab7569f58eedfdd65e9675484a4eb67bd1", size = 4385593, upload-time = "2025-07-02T13:06:15.689Z" }, - { url = "https://files.pythonhosted.org/packages/f6/34/31a1604c9a9ade0fdab61eb48570e09a796f4d9836121266447b0eaf7feb/cryptography-45.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e357286c1b76403dd384d938f93c46b2b058ed4dfcdce64a770f0537ed3feb6f", size = 3331106, upload-time = "2025-07-02T13:06:18.058Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d6/0d/d13399c94234ee8f3df384819dc67e0c5ce215fb751d567a55a1f4b028c7/cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719", size = 744949, upload-time = "2025-08-05T23:59:27.93Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/29/2793d178d0eda1ca4a09a7c4e09a5185e75738cc6d526433e8663b460ea6/cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74", size = 7042702, upload-time = "2025-08-05T23:58:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b6/cabd07410f222f32c8d55486c464f432808abaa1f12af9afcbe8f2f19030/cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f", size = 4206483, upload-time = "2025-08-05T23:58:27.132Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/f9c7d36a38b1cfeb1cc74849aabe9bf817990f7603ff6eb485e0d70e0b27/cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf", size = 4429679, upload-time = "2025-08-05T23:58:29.152Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2a/4434c17eb32ef30b254b9e8b9830cee4e516f08b47fdd291c5b1255b8101/cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5", size = 4210553, upload-time = "2025-08-05T23:58:30.596Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1d/09a5df8e0c4b7970f5d1f3aff1b640df6d4be28a64cae970d56c6cf1c772/cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2", size = 3894499, upload-time = "2025-08-05T23:58:32.03Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/120842ab20d9150a9d3a6bdc07fe2870384e82f5266d41c53b08a3a96b34/cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08", size = 4458484, upload-time = "2025-08-05T23:58:33.526Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/1bc3634d45ddfed0871bfba52cf8f1ad724761662a0c792b97a951fb1b30/cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402", size = 4210281, upload-time = "2025-08-05T23:58:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/ffb12c2d83d0ee625f124880a1f023b5878f79da92e64c37962bbbe35f3f/cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42", size = 4456890, upload-time = "2025-08-05T23:58:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8e/b3f3fe0dc82c77a0deb5f493b23311e09193f2268b77196ec0f7a36e3f3e/cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05", size = 4333247, upload-time = "2025-08-05T23:58:38.781Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a6/c3ef2ab9e334da27a1d7b56af4a2417d77e7806b2e0f90d6267ce120d2e4/cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453", size = 4565045, upload-time = "2025-08-05T23:58:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/31/c3/77722446b13fa71dddd820a5faab4ce6db49e7e0bf8312ef4192a3f78e2f/cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159", size = 2928923, upload-time = "2025-08-05T23:58:41.919Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/a025c3225188a811b82932a4dcc8457a26c3729d81578ccecbcce2cb784e/cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec", size = 3403805, upload-time = "2025-08-05T23:58:43.792Z" }, + { url = "https://files.pythonhosted.org/packages/5b/af/bcfbea93a30809f126d51c074ee0fac5bd9d57d068edf56c2a73abedbea4/cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0", size = 7020111, upload-time = "2025-08-05T23:58:45.316Z" }, + { url = "https://files.pythonhosted.org/packages/98/c6/ea5173689e014f1a8470899cd5beeb358e22bb3cf5a876060f9d1ca78af4/cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394", size = 4198169, upload-time = "2025-08-05T23:58:47.121Z" }, + { url = "https://files.pythonhosted.org/packages/ba/73/b12995edc0c7e2311ffb57ebd3b351f6b268fed37d93bfc6f9856e01c473/cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9", size = 4421273, upload-time = "2025-08-05T23:58:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/f7/6e/286894f6f71926bc0da67408c853dd9ba953f662dcb70993a59fd499f111/cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3", size = 4199211, upload-time = "2025-08-05T23:58:50.139Z" }, + { url = "https://files.pythonhosted.org/packages/de/34/a7f55e39b9623c5cb571d77a6a90387fe557908ffc44f6872f26ca8ae270/cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3", size = 3883732, upload-time = "2025-08-05T23:58:52.253Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b9/c6d32edbcba0cd9f5df90f29ed46a65c4631c4fbe11187feb9169c6ff506/cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301", size = 4450655, upload-time = "2025-08-05T23:58:53.848Z" }, + { url = "https://files.pythonhosted.org/packages/77/2d/09b097adfdee0227cfd4c699b3375a842080f065bab9014248933497c3f9/cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5", size = 4198956, upload-time = "2025-08-05T23:58:55.209Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/061ec6689207d54effdff535bbdf85cc380d32dd5377173085812565cf38/cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016", size = 4449859, upload-time = "2025-08-05T23:58:56.639Z" }, + { url = "https://files.pythonhosted.org/packages/41/ff/e7d5a2ad2d035e5a2af116e1a3adb4d8fcd0be92a18032917a089c6e5028/cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3", size = 4320254, upload-time = "2025-08-05T23:58:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/82/27/092d311af22095d288f4db89fcaebadfb2f28944f3d790a4cf51fe5ddaeb/cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9", size = 4554815, upload-time = "2025-08-05T23:59:00.283Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/aa2f4940262d588a8fdf4edabe4cda45854d00ebc6eaac12568b3a491a16/cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02", size = 2912147, upload-time = "2025-08-05T23:59:01.716Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bc/16e0276078c2de3ceef6b5a34b965f4436215efac45313df90d55f0ba2d2/cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b", size = 3390459, upload-time = "2025-08-05T23:59:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/61/69/c252de4ec047ba2f567ecb53149410219577d408c2aea9c989acae7eafce/cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983", size = 3584669, upload-time = "2025-08-05T23:59:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fe/deea71e9f310a31fe0a6bfee670955152128d309ea2d1c79e2a5ae0f0401/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427", size = 4153022, upload-time = "2025-08-05T23:59:16.954Z" }, + { url = "https://files.pythonhosted.org/packages/60/45/a77452f5e49cb580feedba6606d66ae7b82c128947aa754533b3d1bd44b0/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b", size = 4386802, upload-time = "2025-08-05T23:59:18.55Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b9/a2f747d2acd5e3075fdf5c145c7c3568895daaa38b3b0c960ef830db6cdc/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c", size = 4152706, upload-time = "2025-08-05T23:59:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/81/ec/381b3e8d0685a3f3f304a382aa3dfce36af2d76467da0fd4bb21ddccc7b2/cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385", size = 4386740, upload-time = "2025-08-05T23:59:21.525Z" }, + { url = "https://files.pythonhosted.org/packages/0a/76/cf8d69da8d0b5ecb0db406f24a63a3f69ba5e791a11b782aeeefef27ccbb/cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043", size = 3331874, upload-time = "2025-08-05T23:59:23.017Z" }, ] [[package]] @@ -723,6 +836,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/f4/65b8a29adab331611259b86cf1d87a64f523fed52aba5d4bbdb2be2aed43/dodgy-0.2.1-py3-none-any.whl", hash = "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6", size = 5362, upload-time = "2019-12-31T16:44:58.264Z" }, ] +[[package]] +name = "dunamai" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/2f/194d9a34c4d831c6563d2d990720850f0baef9ab60cb4ad8ae0eff6acd34/dunamai-1.25.0.tar.gz", hash = "sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1", size = 46155, upload-time = "2025-07-04T19:25:56.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/41/04e2a649058b0713b00d6c9bd22da35618bb157289e05d068e51fddf8d7e/dunamai-1.25.0-py3-none-any.whl", hash = "sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab", size = 27022, upload-time = "2025-07-04T19:25:54.863Z" }, +] + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -819,6 +944,92 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/86/b5/a43fed6fd0193585d17d6faa7b85317d4461f694aaed546098c69f856579/flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", size = 7350, upload-time = "2017-12-30T13:46:54.79Z" }, ] +[[package]] +name = "frozenlist" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/b1/b64018016eeb087db503b038296fd782586432b9c077fc5c7839e9cb6ef6/frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f", size = 45078, upload-time = "2025-06-09T23:02:35.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/7e/803dde33760128acd393a27eb002f2020ddb8d99d30a44bfbaab31c5f08a/frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a", size = 82251, upload-time = "2025-06-09T23:00:16.279Z" }, + { url = "https://files.pythonhosted.org/packages/75/a9/9c2c5760b6ba45eae11334db454c189d43d34a4c0b489feb2175e5e64277/frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750", size = 48183, upload-time = "2025-06-09T23:00:17.698Z" }, + { url = "https://files.pythonhosted.org/packages/47/be/4038e2d869f8a2da165f35a6befb9158c259819be22eeaf9c9a8f6a87771/frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd", size = 47107, upload-time = "2025-06-09T23:00:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/79/26/85314b8a83187c76a37183ceed886381a5f992975786f883472fcb6dc5f2/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2", size = 237333, upload-time = "2025-06-09T23:00:20.275Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/e5b64f7d2c92a41639ffb2ad44a6a82f347787abc0c7df5f49057cf11770/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f", size = 231724, upload-time = "2025-06-09T23:00:21.705Z" }, + { url = "https://files.pythonhosted.org/packages/20/fb/03395c0a43a5976af4bf7534759d214405fbbb4c114683f434dfdd3128ef/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30", size = 245842, upload-time = "2025-06-09T23:00:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/d0/15/c01c8e1dffdac5d9803507d824f27aed2ba76b6ed0026fab4d9866e82f1f/frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98", size = 239767, upload-time = "2025-06-09T23:00:25.103Z" }, + { url = "https://files.pythonhosted.org/packages/14/99/3f4c6fe882c1f5514b6848aa0a69b20cb5e5d8e8f51a339d48c0e9305ed0/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86", size = 224130, upload-time = "2025-06-09T23:00:27.061Z" }, + { url = "https://files.pythonhosted.org/packages/4d/83/220a374bd7b2aeba9d0725130665afe11de347d95c3620b9b82cc2fcab97/frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae", size = 235301, upload-time = "2025-06-09T23:00:29.02Z" }, + { url = "https://files.pythonhosted.org/packages/03/3c/3e3390d75334a063181625343e8daab61b77e1b8214802cc4e8a1bb678fc/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8", size = 234606, upload-time = "2025-06-09T23:00:30.514Z" }, + { url = "https://files.pythonhosted.org/packages/23/1e/58232c19608b7a549d72d9903005e2d82488f12554a32de2d5fb59b9b1ba/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31", size = 248372, upload-time = "2025-06-09T23:00:31.966Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/e4a567e01702a88a74ce8a324691e62a629bf47d4f8607f24bf1c7216e7f/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7", size = 229860, upload-time = "2025-06-09T23:00:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/73/a6/63b3374f7d22268b41a9db73d68a8233afa30ed164c46107b33c4d18ecdd/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5", size = 245893, upload-time = "2025-06-09T23:00:35.002Z" }, + { url = "https://files.pythonhosted.org/packages/6d/eb/d18b3f6e64799a79673c4ba0b45e4cfbe49c240edfd03a68be20002eaeaa/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898", size = 246323, upload-time = "2025-06-09T23:00:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f5/720f3812e3d06cd89a1d5db9ff6450088b8f5c449dae8ffb2971a44da506/frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56", size = 233149, upload-time = "2025-06-09T23:00:37.963Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/03efbf545e217d5db8446acfd4c447c15b7c8cf4dbd4a58403111df9322d/frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7", size = 39565, upload-time = "2025-06-09T23:00:39.753Z" }, + { url = "https://files.pythonhosted.org/packages/58/17/fe61124c5c333ae87f09bb67186d65038834a47d974fc10a5fadb4cc5ae1/frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d", size = 44019, upload-time = "2025-06-09T23:00:40.988Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a2/c8131383f1e66adad5f6ecfcce383d584ca94055a34d683bbb24ac5f2f1c/frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2", size = 81424, upload-time = "2025-06-09T23:00:42.24Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/02754159955088cb52567337d1113f945b9e444c4960771ea90eb73de8db/frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb", size = 47952, upload-time = "2025-06-09T23:00:43.481Z" }, + { url = "https://files.pythonhosted.org/packages/01/7a/0046ef1bd6699b40acd2067ed6d6670b4db2f425c56980fa21c982c2a9db/frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478", size = 46688, upload-time = "2025-06-09T23:00:44.793Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/a910bafe29c86997363fb4c02069df4ff0b5bc39d33c5198b4e9dd42d8f8/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8", size = 243084, upload-time = "2025-06-09T23:00:46.125Z" }, + { url = "https://files.pythonhosted.org/packages/64/3e/5036af9d5031374c64c387469bfcc3af537fc0f5b1187d83a1cf6fab1639/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08", size = 233524, upload-time = "2025-06-09T23:00:47.73Z" }, + { url = "https://files.pythonhosted.org/packages/06/39/6a17b7c107a2887e781a48ecf20ad20f1c39d94b2a548c83615b5b879f28/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4", size = 248493, upload-time = "2025-06-09T23:00:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/711d1337c7327d88c44d91dd0f556a1c47fb99afc060ae0ef66b4d24793d/frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b", size = 244116, upload-time = "2025-06-09T23:00:51.352Z" }, + { url = "https://files.pythonhosted.org/packages/24/fe/74e6ec0639c115df13d5850e75722750adabdc7de24e37e05a40527ca539/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e", size = 224557, upload-time = "2025-06-09T23:00:52.855Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/48421f62a6f77c553575201e89048e97198046b793f4a089c79a6e3268bd/frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca", size = 241820, upload-time = "2025-06-09T23:00:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fa/cb4a76bea23047c8462976ea7b7a2bf53997a0ca171302deae9d6dd12096/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df", size = 236542, upload-time = "2025-06-09T23:00:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/5d/32/476a4b5cfaa0ec94d3f808f193301debff2ea42288a099afe60757ef6282/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5", size = 249350, upload-time = "2025-06-09T23:00:58.468Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ba/9a28042f84a6bf8ea5dbc81cfff8eaef18d78b2a1ad9d51c7bc5b029ad16/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025", size = 225093, upload-time = "2025-06-09T23:01:00.015Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/3a32959e68f9cf000b04e79ba574527c17e8842e38c91d68214a37455786/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01", size = 245482, upload-time = "2025-06-09T23:01:01.474Z" }, + { url = "https://files.pythonhosted.org/packages/80/e8/edf2f9e00da553f07f5fa165325cfc302dead715cab6ac8336a5f3d0adc2/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08", size = 249590, upload-time = "2025-06-09T23:01:02.961Z" }, + { url = "https://files.pythonhosted.org/packages/1c/80/9a0eb48b944050f94cc51ee1c413eb14a39543cc4f760ed12657a5a3c45a/frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43", size = 237785, upload-time = "2025-06-09T23:01:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/f3/74/87601e0fb0369b7a2baf404ea921769c53b7ae00dee7dcfe5162c8c6dbf0/frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3", size = 39487, upload-time = "2025-06-09T23:01:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/c026e9a9fc17585a9d461f65d8593d281fedf55fbf7eb53f16c6df2392f9/frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a", size = 43874, upload-time = "2025-06-09T23:01:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/6b2cebdabdbd50367273c20ff6b57a3dfa89bd0762de02c3a1eb42cb6462/frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee", size = 79791, upload-time = "2025-06-09T23:01:09.368Z" }, + { url = "https://files.pythonhosted.org/packages/83/2e/5b70b6a3325363293fe5fc3ae74cdcbc3e996c2a11dde2fd9f1fb0776d19/frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d", size = 47165, upload-time = "2025-06-09T23:01:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/f4/25/a0895c99270ca6966110f4ad98e87e5662eab416a17e7fd53c364bf8b954/frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43", size = 45881, upload-time = "2025-06-09T23:01:12.296Z" }, + { url = "https://files.pythonhosted.org/packages/19/7c/71bb0bbe0832793c601fff68cd0cf6143753d0c667f9aec93d3c323f4b55/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d", size = 232409, upload-time = "2025-06-09T23:01:13.641Z" }, + { url = "https://files.pythonhosted.org/packages/c0/45/ed2798718910fe6eb3ba574082aaceff4528e6323f9a8570be0f7028d8e9/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee", size = 225132, upload-time = "2025-06-09T23:01:15.264Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e2/8417ae0f8eacb1d071d4950f32f229aa6bf68ab69aab797b72a07ea68d4f/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb", size = 237638, upload-time = "2025-06-09T23:01:16.752Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b7/2ace5450ce85f2af05a871b8c8719b341294775a0a6c5585d5e6170f2ce7/frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f", size = 233539, upload-time = "2025-06-09T23:01:18.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/b9/6989292c5539553dba63f3c83dc4598186ab2888f67c0dc1d917e6887db6/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60", size = 215646, upload-time = "2025-06-09T23:01:19.649Z" }, + { url = "https://files.pythonhosted.org/packages/72/31/bc8c5c99c7818293458fe745dab4fd5730ff49697ccc82b554eb69f16a24/frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00", size = 232233, upload-time = "2025-06-09T23:01:21.175Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/460db4d7ba0811b9ccb85af996019f5d70831f2f5f255f7cc61f86199795/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b", size = 227996, upload-time = "2025-06-09T23:01:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/f4b39e904c03927b7ecf891804fd3b4df3db29b9e487c6418e37988d6e9d/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c", size = 242280, upload-time = "2025-06-09T23:01:24.808Z" }, + { url = "https://files.pythonhosted.org/packages/b8/33/3f8d6ced42f162d743e3517781566b8481322be321b486d9d262adf70bfb/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949", size = 217717, upload-time = "2025-06-09T23:01:26.28Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e8/ad683e75da6ccef50d0ab0c2b2324b32f84fc88ceee778ed79b8e2d2fe2e/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca", size = 236644, upload-time = "2025-06-09T23:01:27.887Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/8d19ccdd3799310722195a72ac94ddc677541fb4bef4091d8e7775752360/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b", size = 238879, upload-time = "2025-06-09T23:01:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/c12bf657494c2fd1079a48b2db49fa4196325909249a52d8f09bc9123fd7/frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e", size = 232502, upload-time = "2025-06-09T23:01:31.287Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8b/e7f9dfde869825489382bc0d512c15e96d3964180c9499efcec72e85db7e/frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1", size = 39169, upload-time = "2025-06-09T23:01:35.503Z" }, + { url = "https://files.pythonhosted.org/packages/35/89/a487a98d94205d85745080a37860ff5744b9820a2c9acbcdd9440bfddf98/frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba", size = 43219, upload-time = "2025-06-09T23:01:36.784Z" }, + { url = "https://files.pythonhosted.org/packages/56/d5/5c4cf2319a49eddd9dd7145e66c4866bdc6f3dbc67ca3d59685149c11e0d/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d", size = 84345, upload-time = "2025-06-09T23:01:38.295Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/ec2c1e1dc16b85bc9d526009961953df9cec8481b6886debb36ec9107799/frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d", size = 48880, upload-time = "2025-06-09T23:01:39.887Z" }, + { url = "https://files.pythonhosted.org/packages/69/86/f9596807b03de126e11e7d42ac91e3d0b19a6599c714a1989a4e85eeefc4/frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b", size = 48498, upload-time = "2025-06-09T23:01:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cb/df6de220f5036001005f2d726b789b2c0b65f2363b104bbc16f5be8084f8/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146", size = 292296, upload-time = "2025-06-09T23:01:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/83/1f/de84c642f17c8f851a2905cee2dae401e5e0daca9b5ef121e120e19aa825/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74", size = 273103, upload-time = "2025-06-09T23:01:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/88/3c/c840bfa474ba3fa13c772b93070893c6e9d5c0350885760376cbe3b6c1b3/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1", size = 292869, upload-time = "2025-06-09T23:01:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1c/3efa6e7d5a39a1d5ef0abeb51c48fb657765794a46cf124e5aca2c7a592c/frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1", size = 291467, upload-time = "2025-06-09T23:01:47.234Z" }, + { url = "https://files.pythonhosted.org/packages/4f/00/d5c5e09d4922c395e2f2f6b79b9a20dab4b67daaf78ab92e7729341f61f6/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384", size = 266028, upload-time = "2025-06-09T23:01:48.819Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/72765be905619dfde25a7f33813ac0341eb6b076abede17a2e3fbfade0cb/frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb", size = 284294, upload-time = "2025-06-09T23:01:50.394Z" }, + { url = "https://files.pythonhosted.org/packages/88/67/c94103a23001b17808eb7dd1200c156bb69fb68e63fcf0693dde4cd6228c/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c", size = 281898, upload-time = "2025-06-09T23:01:52.234Z" }, + { url = "https://files.pythonhosted.org/packages/42/34/a3e2c00c00f9e2a9db5653bca3fec306349e71aff14ae45ecc6d0951dd24/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65", size = 290465, upload-time = "2025-06-09T23:01:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/f89b7fbce8b0b0c095d82b008afd0590f71ccb3dee6eee41791cf8cd25fd/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3", size = 266385, upload-time = "2025-06-09T23:01:55.769Z" }, + { url = "https://files.pythonhosted.org/packages/cd/45/e365fdb554159462ca12df54bc59bfa7a9a273ecc21e99e72e597564d1ae/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657", size = 288771, upload-time = "2025-06-09T23:01:57.4Z" }, + { url = "https://files.pythonhosted.org/packages/00/11/47b6117002a0e904f004d70ec5194fe9144f117c33c851e3d51c765962d0/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104", size = 288206, upload-time = "2025-06-09T23:01:58.936Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/5f9f3c3fd7f7746082ec67bcdc204db72dad081f4f83a503d33220a92973/frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf", size = 282620, upload-time = "2025-06-09T23:02:00.493Z" }, + { url = "https://files.pythonhosted.org/packages/0b/31/8fbc5af2d183bff20f21aa743b4088eac4445d2bb1cdece449ae80e4e2d1/frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81", size = 43059, upload-time = "2025-06-09T23:02:02.072Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ed/41956f52105b8dbc26e457c5705340c67c8cc2b79f394b79bffc09d0e938/frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e", size = 47516, upload-time = "2025-06-09T23:02:03.779Z" }, + { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, +] + +[[package]] +name = "funcy" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/b8/c6081521ff70afdff55cd9512b2220bbf4fa88804dae51d1b57b4b58ef32/funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb", size = 537931, upload-time = "2023-03-28T06:22:46.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0", size = 30891, upload-time = "2023-03-28T06:22:42.576Z" }, +] + [[package]] name = "gitdb" version = "4.0.12" @@ -1159,6 +1370,19 @@ 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" }, ] +[[package]] +name = "jinja2-ansible-filters" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/27/fa186af4b246eb869ffca8ffa42d92b05abaec08c99329e74d88b2c46ec7/jinja2-ansible-filters-1.3.2.tar.gz", hash = "sha256:07c10cf44d7073f4f01102ca12d9a2dc31b41d47e4c61ed92ef6a6d2669b356b", size = 16945, upload-time = "2022-06-30T14:08:50.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34", size = 18975, upload-time = "2022-06-30T14:08:49.571Z" }, +] + [[package]] name = "jq" version = "1.10.0" @@ -1216,7 +1440,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.24.0" +version = "4.25.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -1224,9 +1448,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/00/a297a868e9d0784450faa7365c2172a7d6110c763e30ba861867c32ae6a9/jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f", size = 356830, upload-time = "2025-07-18T15:39:45.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/c86cd8e011fe98803d7e382fd67c0df5ceab8d2b7ad8c5a81524f791551c/jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716", size = 89184, upload-time = "2025-07-18T15:39:42.956Z" }, ] [[package]] @@ -1495,7 +1719,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.11.0" +version = "1.12.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1510,17 +1734,18 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/f5/9506eb5578d5bbe9819ee8ba3198d0ad0e2fbe3bab8b257e4131ceb7dfb6/mcp-1.11.0.tar.gz", hash = "sha256:49a213df56bb9472ff83b3132a4825f5c8f5b120a90246f08b0dac6bedac44c8", size = 406907, upload-time = "2025-07-10T16:41:09.388Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/88/f6cb7e7c260cd4b4ce375f2b1614b33ce401f63af0f49f7141a2e9bf0a45/mcp-1.12.4.tar.gz", hash = "sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5", size = 431148, upload-time = "2025-08-07T20:31:18.082Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/9c/c9ca79f9c512e4113a5d07043013110bb3369fc7770040c61378c7fbcf70/mcp-1.11.0-py3-none-any.whl", hash = "sha256:58deac37f7483e4b338524b98bc949b7c2b7c33d978f5fafab5bde041c5e2595", size = 155880, upload-time = "2025-07-10T16:41:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/ad/68/316cbc54b7163fa22571dcf42c9cc46562aae0a021b974e0a8141e897200/mcp-1.12.4-py3-none-any.whl", hash = "sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789", size = 160145, upload-time = "2025-08-07T20:31:15.69Z" }, ] [[package]] name = "mcp-contextforge-gateway" -version = "0.3.1" +version = "0.5.0" source = { editable = "." } dependencies = [ { name = "alembic" }, + { name = "copier" }, { name = "cryptography" }, { name = "fastapi" }, { name = "filelock" }, @@ -1536,9 +1761,12 @@ dependencies = [ { name = "pydantic" }, { name = "pydantic-settings" }, { name = "pyjwt" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, { name = "sqlalchemy" }, { name = "sse-starlette" }, { name = "starlette" }, + { name = "typer" }, { name = "uvicorn" }, { name = "zeroconf" }, ] @@ -1557,6 +1785,7 @@ asyncpg = [ { name = "asyncpg" }, ] dev = [ + { name = "aiohttp" }, { name = "argparse-manpage" }, { name = "autoflake" }, { name = "bandit" }, @@ -1616,9 +1845,11 @@ dev = [ { name = "unimport" }, { name = "uv" }, { name = "vulture" }, + { name = "websockets" }, { name = "yamllint" }, ] dev-all = [ + { name = "aiohttp" }, { name = "argparse-manpage" }, { name = "autoflake" }, { name = "bandit" }, @@ -1678,6 +1909,7 @@ dev-all = [ { name = "unimport" }, { name = "uv" }, { name = "vulture" }, + { name = "websockets" }, { name = "yamllint" }, ] playwright = [ @@ -1695,6 +1927,7 @@ redis = [ [package.metadata] requires-dist = [ + { name = "aiohttp", marker = "extra == 'dev'", specifier = ">=3.12.15" }, { name = "aiosqlite", marker = "extra == 'aiosqlite'", specifier = ">=0.21.0" }, { name = "alembic", specifier = ">=1.16.4" }, { name = "alembic", marker = "extra == 'alembic'", specifier = ">=1.16.4" }, @@ -1707,9 +1940,10 @@ requires-dist = [ { name = "check-manifest", marker = "extra == 'dev'", specifier = ">=0.50" }, { name = "code2flow", marker = "extra == 'dev'", specifier = ">=2.5.1" }, { name = "cookiecutter", marker = "extra == 'dev'", specifier = ">=2.6.0" }, - { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.9.2" }, + { name = "copier", specifier = ">=9.9.0" }, + { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.10.2" }, { name = "coverage-badge", marker = "extra == 'dev'", specifier = ">=1.1.2" }, - { name = "cryptography", specifier = ">=45.0.5" }, + { name = "cryptography", specifier = ">=45.0.6" }, { name = "darglint", marker = "extra == 'dev'", specifier = ">=1.8.1" }, { name = "dlint", marker = "extra == 'dev'", specifier = ">=0.16.0" }, { name = "dodgy", marker = "extra == 'dev'", specifier = ">=0.2.1" }, @@ -1726,17 +1960,17 @@ requires-dist = [ { name = "jinja2", specifier = ">=3.1.6" }, { name = "jq", specifier = ">=1.10.0" }, { name = "jsonpath-ng", specifier = ">=1.7.0" }, - { name = "jsonschema", specifier = ">=4.24.0" }, - { name = "mcp", specifier = ">=1.11.0" }, - { name = "mcp-contextforge-gateway", extras = ["redis"], marker = "extra == 'all'", specifier = ">=0.3.1" }, - { name = "mcp-contextforge-gateway", extras = ["redis", "dev"], marker = "extra == 'dev-all'", specifier = ">=0.3.1" }, - { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.17.0" }, + { name = "jsonschema", specifier = ">=4.25.0" }, + { name = "mcp", specifier = ">=1.12.4" }, + { name = "mcp-contextforge-gateway", extras = ["redis"], marker = "extra == 'all'", specifier = ">=0.5.0" }, + { name = "mcp-contextforge-gateway", extras = ["redis", "dev"], marker = "extra == 'dev-all'", specifier = ">=0.5.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.17.1" }, { name = "parse", specifier = ">=1.20.2" }, { name = "pexpect", marker = "extra == 'dev'", specifier = ">=4.9.0" }, { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.9.0" }, { name = "pip-licenses", marker = "extra == 'dev'", specifier = ">=5.0.0" }, - { name = "playwright", marker = "extra == 'playwright'", specifier = ">=1.53.0" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.2.0" }, + { name = "playwright", marker = "extra == 'playwright'", specifier = ">=1.54.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.3.0" }, { name = "prospector", extras = ["with-everything"], marker = "extra == 'dev'", specifier = ">=1.17.2" }, { name = "psutil", specifier = ">=7.0.0" }, { name = "psycopg2-binary", marker = "extra == 'postgres'", specifier = ">=2.9.10" }, @@ -1744,15 +1978,15 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.10.1" }, { name = "pydocstyle", marker = "extra == 'dev'", specifier = ">=6.3.0" }, { name = "pyjwt", specifier = ">=2.10.1" }, - { name = "pylint", marker = "extra == 'dev'", specifier = ">=3.3.7" }, + { name = "pylint", marker = "extra == 'dev'", specifier = ">=3.3.8" }, { name = "pylint-pydantic", marker = "extra == 'dev'", specifier = ">=0.3.5" }, { name = "pyre-check", marker = "extra == 'dev'", specifier = ">=0.9.25" }, - { name = "pyrefly", marker = "extra == 'dev'", specifier = ">=0.24.2" }, + { name = "pyrefly", marker = "extra == 'dev'", specifier = ">=0.27.2" }, { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.403" }, { name = "pyroma", marker = "extra == 'dev'", specifier = ">=5.0" }, { name = "pyspelling", marker = "extra == 'dev'", specifier = ">=2.10" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.1" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.1.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.2.1" }, { name = "pytest-env", marker = "extra == 'dev'", specifier = ">=1.1.5" }, { name = "pytest-examples", marker = "extra == 'dev'", specifier = ">=0.0.18" }, @@ -1763,28 +1997,32 @@ requires-dist = [ { name = "pytest-timeout", marker = "extra == 'playwright'", specifier = ">=2.4.0" }, { name = "pytest-trio", marker = "extra == 'dev'", specifier = ">=0.8.0" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.8.0" }, + { name = "python-json-logger", specifier = ">=3.3.0" }, { name = "pytype", marker = "extra == 'dev'", specifier = ">=2024.10.11" }, { name = "pyupgrade", marker = "extra == 'dev'", specifier = ">=3.20.0" }, + { name = "pyyaml", specifier = ">=6.0.2" }, { name = "radon", marker = "extra == 'dev'", specifier = ">=6.0.1" }, - { name = "redis", marker = "extra == 'dev'", specifier = ">=6.2.0" }, - { name = "redis", marker = "extra == 'redis'", specifier = ">=6.2.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.12.3" }, - { name = "semgrep", marker = "extra == 'dev'", specifier = ">=1.128.1" }, + { name = "redis", marker = "extra == 'dev'", specifier = ">=6.4.0" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=6.4.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.12.8" }, + { name = "semgrep", marker = "extra == 'dev'", specifier = ">=1.131.0" }, { name = "settings-doc", marker = "extra == 'dev'", specifier = ">=4.3.2" }, { name = "snakeviz", marker = "extra == 'dev'", specifier = ">=2.2.2" }, - { name = "sqlalchemy", specifier = ">=2.0.41" }, - { name = "sse-starlette", specifier = ">=2.4.1" }, - { name = "starlette", specifier = ">=0.46.2" }, + { name = "sqlalchemy", specifier = ">=2.0.42" }, + { name = "sse-starlette", specifier = ">=3.0.2" }, + { name = "starlette", specifier = ">=0.47.2" }, { name = "tomlcheck", marker = "extra == 'dev'", specifier = ">=0.2.3" }, - { name = "tox", marker = "extra == 'dev'", specifier = ">=4.27.0" }, - { name = "tox-uv", marker = "extra == 'dev'", specifier = ">=1.26.1" }, + { name = "tox", marker = "extra == 'dev'", specifier = ">=4.28.4" }, + { name = "tox-uv", marker = "extra == 'dev'", specifier = ">=1.27.0" }, { name = "twine", marker = "extra == 'dev'", specifier = ">=6.1.0" }, - { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.1a14" }, + { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.1a17" }, + { name = "typer", specifier = ">=0.16.0" }, { name = "types-tabulate", marker = "extra == 'dev'", specifier = ">=0.9.0.20241207" }, { name = "unimport", marker = "extra == 'dev'", specifier = ">=1.2.1" }, - { name = "uv", marker = "extra == 'dev'", specifier = ">=0.7.21" }, + { name = "uv", marker = "extra == 'dev'", specifier = ">=0.8.8" }, { name = "uvicorn", specifier = ">=0.35.0" }, { name = "vulture", marker = "extra == 'dev'", specifier = ">=2.14" }, + { name = "websockets", marker = "extra == 'dev'", specifier = ">=15.0.1" }, { name = "yamllint", marker = "extra == 'dev'", specifier = ">=1.37.1" }, { name = "zeroconf", specifier = ">=0.147.0" }, ] @@ -1875,36 +2113,117 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432, upload-time = "2024-12-27T17:40:16.256Z" }, ] +[[package]] +name = "multidict" +version = "6.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/7f/90a7f01e2d005d6653c689039977f6856718c75c5579445effb7e60923d1/multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", size = 76472, upload-time = "2025-08-11T12:06:29.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a3/bed07bc9e2bb302ce752f1dabc69e884cd6a676da44fb0e501b246031fdd/multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", size = 44634, upload-time = "2025-08-11T12:06:30.374Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4b/ceeb4f8f33cf81277da464307afeaf164fb0297947642585884f5cad4f28/multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", size = 44282, upload-time = "2025-08-11T12:06:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/436a5da8702b06866189b69f655ffdb8f70796252a8772a77815f1812679/multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", size = 229696, upload-time = "2025-08-11T12:06:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0e/915160be8fecf1fca35f790c08fb74ca684d752fcba62c11daaf3d92c216/multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", size = 246665, upload-time = "2025-08-11T12:06:34.448Z" }, + { url = "https://files.pythonhosted.org/packages/08/ee/2f464330acd83f77dcc346f0b1a0eaae10230291450887f96b204b8ac4d3/multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", size = 225485, upload-time = "2025-08-11T12:06:35.672Z" }, + { url = "https://files.pythonhosted.org/packages/71/cc/9a117f828b4d7fbaec6adeed2204f211e9caf0a012692a1ee32169f846ae/multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", size = 257318, upload-time = "2025-08-11T12:06:36.98Z" }, + { url = "https://files.pythonhosted.org/packages/25/77/62752d3dbd70e27fdd68e86626c1ae6bccfebe2bb1f84ae226363e112f5a/multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", size = 254689, upload-time = "2025-08-11T12:06:38.233Z" }, + { url = "https://files.pythonhosted.org/packages/00/6e/fac58b1072a6fc59af5e7acb245e8754d3e1f97f4f808a6559951f72a0d4/multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", size = 246709, upload-time = "2025-08-11T12:06:39.517Z" }, + { url = "https://files.pythonhosted.org/packages/01/ef/4698d6842ef5e797c6db7744b0081e36fb5de3d00002cc4c58071097fac3/multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", size = 243185, upload-time = "2025-08-11T12:06:40.796Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c9/d82e95ae1d6e4ef396934e9b0e942dfc428775f9554acf04393cce66b157/multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", size = 237838, upload-time = "2025-08-11T12:06:42.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/f94af5c36baaa75d44fab9f02e2a6bcfa0cd90acb44d4976a80960759dbc/multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", size = 246368, upload-time = "2025-08-11T12:06:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/4a/fe/29f23460c3d995f6a4b678cb2e9730e7277231b981f0b234702f0177818a/multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", size = 253339, upload-time = "2025-08-11T12:06:45.597Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/fd59449204426187b82bf8a75f629310f68c6adc9559dc922d5abe34797b/multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", size = 246933, upload-time = "2025-08-11T12:06:46.841Z" }, + { url = "https://files.pythonhosted.org/packages/19/52/d5d6b344f176a5ac3606f7a61fb44dc746e04550e1a13834dff722b8d7d6/multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", size = 242225, upload-time = "2025-08-11T12:06:48.588Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d3/5b2281ed89ff4d5318d82478a2a2450fcdfc3300da48ff15c1778280ad26/multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", size = 41306, upload-time = "2025-08-11T12:06:49.95Z" }, + { url = "https://files.pythonhosted.org/packages/74/7d/36b045c23a1ab98507aefd44fd8b264ee1dd5e5010543c6fccf82141ccef/multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", size = 46029, upload-time = "2025-08-11T12:06:51.082Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5e/553d67d24432c5cd52b49047f2d248821843743ee6d29a704594f656d182/multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", size = 43017, upload-time = "2025-08-11T12:06:52.243Z" }, + { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload-time = "2025-08-11T12:06:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload-time = "2025-08-11T12:06:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload-time = "2025-08-11T12:06:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload-time = "2025-08-11T12:06:57.213Z" }, + { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload-time = "2025-08-11T12:06:58.946Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload-time = "2025-08-11T12:07:00.301Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload-time = "2025-08-11T12:07:01.638Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload-time = "2025-08-11T12:07:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload-time = "2025-08-11T12:07:04.564Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload-time = "2025-08-11T12:07:05.914Z" }, + { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload-time = "2025-08-11T12:07:08.301Z" }, + { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload-time = "2025-08-11T12:07:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload-time = "2025-08-11T12:07:11.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload-time = "2025-08-11T12:07:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload-time = "2025-08-11T12:07:14.57Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload-time = "2025-08-11T12:07:15.904Z" }, + { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload-time = "2025-08-11T12:07:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload-time = "2025-08-11T12:07:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/e1db626f64f60008320aab00fbe4f23fc3300d75892a3381275b3d284580/multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e", size = 75848, upload-time = "2025-08-11T12:07:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/4c/aa/8b6f548d839b6c13887253af4e29c939af22a18591bfb5d0ee6f1931dae8/multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657", size = 45060, upload-time = "2025-08-11T12:07:21.163Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/f5e97e5d99a729bc2aa58eb3ebfa9f1e56a9b517cc38c60537c81834a73f/multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da", size = 43269, upload-time = "2025-08-11T12:07:22.392Z" }, + { url = "https://files.pythonhosted.org/packages/dc/31/d54eb0c62516776f36fe67f84a732f97e0b0e12f98d5685bebcc6d396910/multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa", size = 237158, upload-time = "2025-08-11T12:07:23.636Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1c/8a10c1c25b23156e63b12165a929d8eb49a6ed769fdbefb06e6f07c1e50d/multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f", size = 257076, upload-time = "2025-08-11T12:07:25.049Z" }, + { url = "https://files.pythonhosted.org/packages/ad/86/90e20b5771d6805a119e483fd3d1e8393e745a11511aebca41f0da38c3e2/multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0", size = 240694, upload-time = "2025-08-11T12:07:26.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/49/484d3e6b535bc0555b52a0a26ba86e4d8d03fd5587d4936dc59ba7583221/multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879", size = 266350, upload-time = "2025-08-11T12:07:27.94Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b4/aa4c5c379b11895083d50021e229e90c408d7d875471cb3abf721e4670d6/multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a", size = 267250, upload-time = "2025-08-11T12:07:29.303Z" }, + { url = "https://files.pythonhosted.org/packages/80/e5/5e22c5bf96a64bdd43518b1834c6d95a4922cc2066b7d8e467dae9b6cee6/multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f", size = 254900, upload-time = "2025-08-11T12:07:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/58b27fed927c07035abc02befacab42491e7388ca105e087e6e0215ead64/multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5", size = 252355, upload-time = "2025-08-11T12:07:32.205Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a1/dad75d23a90c29c02b5d6f3d7c10ab36c3197613be5d07ec49c7791e186c/multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438", size = 250061, upload-time = "2025-08-11T12:07:33.623Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1a/ac2216b61c7f116edab6dc3378cca6c70dc019c9a457ff0d754067c58b20/multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e", size = 249675, upload-time = "2025-08-11T12:07:34.958Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/1916af833b800d13883e452e8e0977c065c4ee3ab7a26941fbfdebc11895/multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7", size = 261247, upload-time = "2025-08-11T12:07:36.588Z" }, + { url = "https://files.pythonhosted.org/packages/c5/65/d1f84fe08ac44a5fc7391cbc20a7cedc433ea616b266284413fd86062f8c/multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812", size = 257960, upload-time = "2025-08-11T12:07:39.735Z" }, + { url = "https://files.pythonhosted.org/packages/13/b5/29ec78057d377b195ac2c5248c773703a6b602e132a763e20ec0457e7440/multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a", size = 250078, upload-time = "2025-08-11T12:07:41.525Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/7e79d38f70a872cae32e29b0d77024bef7834b0afb406ddae6558d9e2414/multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69", size = 41708, upload-time = "2025-08-11T12:07:43.405Z" }, + { url = "https://files.pythonhosted.org/packages/9d/34/746696dffff742e97cd6a23da953e55d0ea51fa601fa2ff387b3edcfaa2c/multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf", size = 45912, upload-time = "2025-08-11T12:07:45.082Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/3bac136181e271e29170d8d71929cdeddeb77f3e8b6a0c08da3a8e9da114/multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605", size = 43076, upload-time = "2025-08-11T12:07:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/64/94/0a8e63e36c049b571c9ae41ee301ada29c3fee9643d9c2548d7d558a1d99/multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb", size = 82812, upload-time = "2025-08-11T12:07:48.402Z" }, + { url = "https://files.pythonhosted.org/packages/25/1a/be8e369dfcd260d2070a67e65dd3990dd635cbd735b98da31e00ea84cd4e/multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e", size = 48313, upload-time = "2025-08-11T12:07:49.679Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/dd4ade298674b2f9a7b06a32c94ffbc0497354df8285f27317c66433ce3b/multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f", size = 46777, upload-time = "2025-08-11T12:07:51.318Z" }, + { url = "https://files.pythonhosted.org/packages/89/db/98aa28bc7e071bfba611ac2ae803c24e96dd3a452b4118c587d3d872c64c/multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773", size = 229321, upload-time = "2025-08-11T12:07:52.965Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bc/01ddda2a73dd9d167bd85d0e8ef4293836a8f82b786c63fb1a429bc3e678/multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e", size = 249954, upload-time = "2025-08-11T12:07:54.423Z" }, + { url = "https://files.pythonhosted.org/packages/06/78/6b7c0f020f9aa0acf66d0ab4eb9f08375bac9a50ff5e3edb1c4ccd59eafc/multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0", size = 228612, upload-time = "2025-08-11T12:07:55.914Z" }, + { url = "https://files.pythonhosted.org/packages/00/44/3faa416f89b2d5d76e9d447296a81521e1c832ad6e40b92f990697b43192/multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395", size = 257528, upload-time = "2025-08-11T12:07:57.371Z" }, + { url = "https://files.pythonhosted.org/packages/05/5f/77c03b89af0fcb16f018f668207768191fb9dcfb5e3361a5e706a11db2c9/multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45", size = 256329, upload-time = "2025-08-11T12:07:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e9/ed750a2a9afb4f8dc6f13dc5b67b514832101b95714f1211cd42e0aafc26/multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb", size = 247928, upload-time = "2025-08-11T12:08:01.037Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b5/e0571bc13cda277db7e6e8a532791d4403dacc9850006cb66d2556e649c0/multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5", size = 245228, upload-time = "2025-08-11T12:08:02.96Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a3/69a84b0eccb9824491f06368f5b86e72e4af54c3067c37c39099b6687109/multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141", size = 235869, upload-time = "2025-08-11T12:08:04.746Z" }, + { url = "https://files.pythonhosted.org/packages/a9/9d/28802e8f9121a6a0804fa009debf4e753d0a59969ea9f70be5f5fdfcb18f/multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d", size = 243446, upload-time = "2025-08-11T12:08:06.332Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/6c98add069b4878c1d66428a5f5149ddb6d32b1f9836a826ac764b9940be/multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d", size = 252299, upload-time = "2025-08-11T12:08:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/3a/09/8fe02d204473e14c0af3affd50af9078839dfca1742f025cca765435d6b4/multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0", size = 246926, upload-time = "2025-08-11T12:08:09.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/3d/7b1e10d774a6df5175ecd3c92bff069e77bed9ec2a927fdd4ff5fe182f67/multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92", size = 243383, upload-time = "2025-08-11T12:08:10.981Z" }, + { url = "https://files.pythonhosted.org/packages/50/b0/a6fae46071b645ae98786ab738447de1ef53742eaad949f27e960864bb49/multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e", size = 47775, upload-time = "2025-08-11T12:08:12.439Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0a/2436550b1520091af0600dff547913cb2d66fbac27a8c33bc1b1bccd8d98/multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4", size = 53100, upload-time = "2025-08-11T12:08:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/43ac51faff934086db9c072a94d327d71b7d8b40cd5dcb47311330929ef0/multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad", size = 45501, upload-time = "2025-08-11T12:08:15.173Z" }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, +] + [[package]] name = "mypy" -version = "1.17.0" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, - { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, - { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, - { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, - { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, - { url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" }, - { url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" }, - { url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" }, - { url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" }, - { url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" }, - { url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" }, - { url = "https://files.pythonhosted.org/packages/be/7b/5f8ab461369b9e62157072156935cec9d272196556bdc7c2ff5f4c7c0f9b/mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06", size = 11070019, upload-time = "2025-07-14T20:32:07.99Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f8/c49c9e5a2ac0badcc54beb24e774d2499748302c9568f7f09e8730e953fa/mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a", size = 10114457, upload-time = "2025-07-14T20:33:47.285Z" }, - { url = "https://files.pythonhosted.org/packages/89/0c/fb3f9c939ad9beed3e328008b3fb90b20fda2cddc0f7e4c20dbefefc3b33/mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889", size = 11857838, upload-time = "2025-07-14T20:33:14.462Z" }, - { url = "https://files.pythonhosted.org/packages/4c/66/85607ab5137d65e4f54d9797b77d5a038ef34f714929cf8ad30b03f628df/mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba", size = 12731358, upload-time = "2025-07-14T20:32:25.579Z" }, - { url = "https://files.pythonhosted.org/packages/73/d0/341dbbfb35ce53d01f8f2969facbb66486cee9804048bf6c01b048127501/mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658", size = 12917480, upload-time = "2025-07-14T20:34:21.868Z" }, - { url = "https://files.pythonhosted.org/packages/64/63/70c8b7dbfc520089ac48d01367a97e8acd734f65bd07813081f508a8c94c/mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c", size = 9589666, upload-time = "2025-07-14T20:34:16.841Z" }, - { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, + { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, + { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, + { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/5b/82/aec2fc9b9b149f372850291827537a508d6c4d3664b1750a324b91f71355/mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7", size = 11075338, upload-time = "2025-07-31T07:53:38.873Z" }, + { url = "https://files.pythonhosted.org/packages/07/ac/ee93fbde9d2242657128af8c86f5d917cd2887584cf948a8e3663d0cd737/mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81", size = 10113066, upload-time = "2025-07-31T07:54:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/946a1e0be93f17f7caa56c45844ec691ca153ee8b62f21eddda336a2d203/mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6", size = 11875473, upload-time = "2025-07-31T07:53:14.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0f/478b4dce1cb4f43cf0f0d00fba3030b21ca04a01b74d1cd272a528cf446f/mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849", size = 12744296, upload-time = "2025-07-31T07:53:03.896Z" }, + { url = "https://files.pythonhosted.org/packages/ca/70/afa5850176379d1b303f992a828de95fc14487429a7139a4e0bdd17a8279/mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14", size = 12914657, upload-time = "2025-07-31T07:54:08.576Z" }, + { url = "https://files.pythonhosted.org/packages/53/f9/4a83e1c856a3d9c8f6edaa4749a4864ee98486e9b9dbfbc93842891029c2/mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a", size = 9593320, upload-time = "2025-07-31T07:53:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, ] [[package]] @@ -2285,21 +2604,21 @@ wheels = [ [[package]] name = "playwright" -version = "1.53.0" +version = "1.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet" }, { name = "pyee" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/e2/2f107be74419280749723bd1197c99351f4b8a0a25e974b9764affb940b2/playwright-1.53.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:48a1a15ce810f0ffe512b6050de9871ea193b41dd3cc1bbed87b8431012419ba", size = 40392498, upload-time = "2025-06-25T21:48:34.17Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d5/e8c57a4f6fd46059fb2d51da2d22b47afc886b42400f06b742cd4a9ba131/playwright-1.53.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a701f9498a5b87e3f929ec01cea3109fbde75821b19c7ba4bba54f6127b94f76", size = 38647035, upload-time = "2025-06-25T21:48:38.414Z" }, - { url = "https://files.pythonhosted.org/packages/4d/f3/da18cd7c22398531316e58fd131243fd9156fe7765aae239ae542a5d07d2/playwright-1.53.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:f765498341c4037b4c01e742ae32dd335622f249488ccd77ca32d301d7c82c61", size = 40392502, upload-time = "2025-06-25T21:48:42.293Z" }, - { url = "https://files.pythonhosted.org/packages/92/32/5d871c3753fbee5113eefc511b9e44c0006a27f2301b4c6bffa4346fbd94/playwright-1.53.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:db19cb5b58f3b15cad3e2419f4910c053e889202fc202461ee183f1530d1db60", size = 45848364, upload-time = "2025-06-25T21:48:45.849Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6b/9942f86661ff41332f9299db4950623123e60ca71e4fb6e6942fc0212624/playwright-1.53.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9276c9c935fc062f51f4f5107e56420afd6d9a524348dc437793dc2e34c742e3", size = 45235174, upload-time = "2025-06-25T21:48:49.579Z" }, - { url = "https://files.pythonhosted.org/packages/51/63/28b3f2d36e6a95e88f033d2aa7af06083f6f4aa0d9764759d96033cd053e/playwright-1.53.0-py3-none-win32.whl", hash = "sha256:36eedec101724ff5a000cddab87dd9a72a39f9b3e65a687169c465484e667c06", size = 35415131, upload-time = "2025-06-25T21:48:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/a9/b5/4ca25974a90d16cfd4a9a953ee5a666cf484a0bdacb4eed484e5cab49e66/playwright-1.53.0-py3-none-win_amd64.whl", hash = "sha256:d68975807a0fd997433537f1dcf2893cda95884a39dc23c6f591b8d5f691e9e8", size = 35415138, upload-time = "2025-06-25T21:48:57.082Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/b42ff2116df5d07ccad2dc4eeb20af92c975a1fbc7cd3ed37b678468b813/playwright-1.53.0-py3-none-win_arm64.whl", hash = "sha256:fcfd481f76568d7b011571160e801b47034edd9e2383c43d83a5fb3f35c67885", size = 31188568, upload-time = "2025-06-25T21:49:00.194Z" }, + { url = "https://files.pythonhosted.org/packages/f3/09/33d5bfe393a582d8dac72165a9e88b274143c9df411b65ece1cc13f42988/playwright-1.54.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:bf3b845af744370f1bd2286c2a9536f474cc8a88dc995b72ea9a5be714c9a77d", size = 40439034, upload-time = "2025-07-22T13:58:04.816Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7b/51882dc584f7aa59f446f2bb34e33c0e5f015de4e31949e5b7c2c10e54f0/playwright-1.54.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:780928b3ca2077aea90414b37e54edd0c4bbb57d1aafc42f7aa0b3fd2c2fac02", size = 38702308, upload-time = "2025-07-22T13:58:08.211Z" }, + { url = "https://files.pythonhosted.org/packages/73/a1/7aa8ae175b240c0ec8849fcf000e078f3c693f9aa2ffd992da6550ea0dff/playwright-1.54.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:81d0b6f28843b27f288cfe438af0a12a4851de57998009a519ea84cee6fbbfb9", size = 40439037, upload-time = "2025-07-22T13:58:11.37Z" }, + { url = "https://files.pythonhosted.org/packages/34/a9/45084fd23b6206f954198296ce39b0acf50debfdf3ec83a593e4d73c9c8a/playwright-1.54.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:09919f45cc74c64afb5432646d7fef0d19fff50990c862cb8d9b0577093f40cc", size = 45920135, upload-time = "2025-07-22T13:58:14.494Z" }, + { url = "https://files.pythonhosted.org/packages/02/d4/6a692f4c6db223adc50a6e53af405b45308db39270957a6afebddaa80ea2/playwright-1.54.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ae206c55737e8e3eae51fb385d61c0312eeef31535643bb6232741b41b6fdc", size = 45302695, upload-time = "2025-07-22T13:58:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/4ee60a1c3714321db187bebbc40d52cea5b41a856925156325058b5fca5a/playwright-1.54.0-py3-none-win32.whl", hash = "sha256:0b108622ffb6906e28566f3f31721cd57dda637d7e41c430287804ac01911f56", size = 35469309, upload-time = "2025-07-22T13:58:21.917Z" }, + { url = "https://files.pythonhosted.org/packages/aa/77/8f8fae05a242ef639de963d7ae70a69d0da61d6d72f1207b8bbf74ffd3e7/playwright-1.54.0-py3-none-win_amd64.whl", hash = "sha256:9e5aee9ae5ab1fdd44cd64153313a2045b136fcbcfb2541cc0a3d909132671a2", size = 35469311, upload-time = "2025-07-22T13:58:24.707Z" }, + { url = "https://files.pythonhosted.org/packages/33/ff/99a6f4292a90504f2927d34032a4baf6adb498dc3f7cf0f3e0e22899e310/playwright-1.54.0-py3-none-win_arm64.whl", hash = "sha256:a975815971f7b8dca505c441a4c56de1aeb56a211290f8cc214eeef5524e8d75", size = 31239119, upload-time = "2025-07-22T13:58:27.56Z" }, ] [[package]] @@ -2311,6 +2630,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "plumbum" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/5d/49ba324ad4ae5b1a4caefafbce7a1648540129344481f2ed4ef6bb68d451/plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219", size = 319083, upload-time = "2024-10-05T05:59:27.059Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/9d/d03542c93bb3d448406731b80f39c3d5601282f778328c22c77d270f4ed4/plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5", size = 127970, upload-time = "2024-10-05T05:59:25.102Z" }, +] + [[package]] name = "ply" version = "3.11" @@ -2322,7 +2653,7 @@ wheels = [ [[package]] name = "pre-commit" -version = "4.2.0" +version = "4.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -2331,9 +2662,9 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, ] [[package]] @@ -2348,6 +2679,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/c7/5613524e606ea1688b3bdbf48aa64bafb6d0a4ac3750274c43b6158a390f/prettytable-3.16.0-py3-none-any.whl", hash = "sha256:b5eccfabb82222f5aa46b798ff02a8452cf530a352c31bddfa29be41242863aa", size = 33863, upload-time = "2025-03-24T19:39:02.359Z" }, ] +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "propcache" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139, upload-time = "2025-06-09T22:56:06.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207, upload-time = "2025-06-09T22:54:05.399Z" }, + { url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648, upload-time = "2025-06-09T22:54:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496, upload-time = "2025-06-09T22:54:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288, upload-time = "2025-06-09T22:54:10.466Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456, upload-time = "2025-06-09T22:54:11.828Z" }, + { url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429, upload-time = "2025-06-09T22:54:13.823Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472, upload-time = "2025-06-09T22:54:15.232Z" }, + { url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480, upload-time = "2025-06-09T22:54:17.104Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530, upload-time = "2025-06-09T22:54:18.512Z" }, + { url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230, upload-time = "2025-06-09T22:54:19.947Z" }, + { url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754, upload-time = "2025-06-09T22:54:21.716Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430, upload-time = "2025-06-09T22:54:23.17Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884, upload-time = "2025-06-09T22:54:25.539Z" }, + { url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480, upload-time = "2025-06-09T22:54:26.892Z" }, + { url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757, upload-time = "2025-06-09T22:54:28.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500, upload-time = "2025-06-09T22:54:29.4Z" }, + { url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674, upload-time = "2025-06-09T22:54:30.551Z" }, + { url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570, upload-time = "2025-06-09T22:54:32.296Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094, upload-time = "2025-06-09T22:54:33.929Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958, upload-time = "2025-06-09T22:54:35.186Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894, upload-time = "2025-06-09T22:54:36.708Z" }, + { url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672, upload-time = "2025-06-09T22:54:38.062Z" }, + { url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395, upload-time = "2025-06-09T22:54:39.634Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510, upload-time = "2025-06-09T22:54:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949, upload-time = "2025-06-09T22:54:43.038Z" }, + { url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258, upload-time = "2025-06-09T22:54:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036, upload-time = "2025-06-09T22:54:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684, upload-time = "2025-06-09T22:54:47.63Z" }, + { url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562, upload-time = "2025-06-09T22:54:48.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142, upload-time = "2025-06-09T22:54:50.424Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711, upload-time = "2025-06-09T22:54:52.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479, upload-time = "2025-06-09T22:54:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286, upload-time = "2025-06-09T22:54:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425, upload-time = "2025-06-09T22:54:55.642Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846, upload-time = "2025-06-09T22:54:57.246Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871, upload-time = "2025-06-09T22:54:58.975Z" }, + { url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720, upload-time = "2025-06-09T22:55:00.471Z" }, + { url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203, upload-time = "2025-06-09T22:55:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365, upload-time = "2025-06-09T22:55:03.199Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016, upload-time = "2025-06-09T22:55:04.518Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596, upload-time = "2025-06-09T22:55:05.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977, upload-time = "2025-06-09T22:55:07.792Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220, upload-time = "2025-06-09T22:55:09.173Z" }, + { url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642, upload-time = "2025-06-09T22:55:10.62Z" }, + { url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789, upload-time = "2025-06-09T22:55:12.029Z" }, + { url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880, upload-time = "2025-06-09T22:55:13.45Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220, upload-time = "2025-06-09T22:55:15.284Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678, upload-time = "2025-06-09T22:55:16.445Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560, upload-time = "2025-06-09T22:55:17.598Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676, upload-time = "2025-06-09T22:55:18.922Z" }, + { url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701, upload-time = "2025-06-09T22:55:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934, upload-time = "2025-06-09T22:55:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316, upload-time = "2025-06-09T22:55:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619, upload-time = "2025-06-09T22:55:24.651Z" }, + { url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896, upload-time = "2025-06-09T22:55:26.049Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111, upload-time = "2025-06-09T22:55:27.381Z" }, + { url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334, upload-time = "2025-06-09T22:55:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026, upload-time = "2025-06-09T22:55:30.184Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724, upload-time = "2025-06-09T22:55:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868, upload-time = "2025-06-09T22:55:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322, upload-time = "2025-06-09T22:55:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778, upload-time = "2025-06-09T22:55:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175, upload-time = "2025-06-09T22:55:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857, upload-time = "2025-06-09T22:55:39.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, +] + [[package]] name = "prospector" version = "1.17.2" @@ -2672,7 +3088,7 @@ wheels = [ [[package]] name = "pylint" -version = "3.3.7" +version = "3.3.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astroid" }, @@ -2683,9 +3099,9 @@ dependencies = [ { name = "platformdirs" }, { name = "tomlkit" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1c/e4/83e487d3ddd64ab27749b66137b26dc0c5b5c161be680e6beffdc99070b3/pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559", size = 1520709, upload-time = "2025-05-04T17:07:51.089Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/58/1f614a84d3295c542e9f6e2c764533eea3f318f4592dc1ea06c797114767/pylint-3.3.8.tar.gz", hash = "sha256:26698de19941363037e2937d3db9ed94fb3303fdadf7d98847875345a8bb6b05", size = 1523947, upload-time = "2025-08-09T09:12:57.234Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/83/bff755d09e31b5d25cc7fdc4bf3915d1a404e181f1abf0359af376845c24/pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d", size = 522565, upload-time = "2025-05-04T17:07:48.714Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1a/711e93a7ab6c392e349428ea56e794a3902bb4e0284c1997cff2d7efdbc1/pylint-3.3.8-py3-none-any.whl", hash = "sha256:7ef94aa692a600e82fabdd17102b73fc226758218c97473c7ad67bd4cb905d83", size = 523153, upload-time = "2025-08-09T09:12:54.836Z" }, ] [[package]] @@ -2805,18 +3221,18 @@ wheels = [ [[package]] name = "pyrefly" -version = "0.24.2" +version = "0.28.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/c3/17be94de732d01d86a671eb1e93608000a0594e60d05a14c5d9f13dbe21d/pyrefly-0.24.2.tar.gz", hash = "sha256:671b9933c2a3f646983de68bc0422736f7ce364c4f645f742559423b0b9b5150", size = 1129442, upload-time = "2025-07-15T02:40:19.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/9f/861d01445e02e0e668619f3bc7d0f2fa2524dedd328a40072afb71845876/pyrefly-0.28.0.tar.gz", hash = "sha256:6b1431e86d35c434ef286058fe2ea8ef2e968f70a706dc6296904a4850940ed5", size = 1213997, upload-time = "2025-08-11T17:34:34.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/cd/07862f0afd79e215617494495510d06cb7cc5907f5f32594498e7bb64f7e/pyrefly-0.24.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7e6bd1b88ec53b3f1ce2ece844016d7e7f0848a77022857a7fa6674a49abcc13", size = 6049599, upload-time = "2025-07-15T02:40:03.363Z" }, - { url = "https://files.pythonhosted.org/packages/b7/ce/680ce3c12a9d8cf0312207d1eee31947e5ceae169680fe2e341718f299be/pyrefly-0.24.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:83aa9013f2299dfc8ce11adec30a63be71528484c45e603375efe7496cb0538e", size = 5634851, upload-time = "2025-07-15T02:40:05.742Z" }, - { url = "https://files.pythonhosted.org/packages/0f/12/3846ceefaeccb6209b4bcb3518143039effbe7f16f864377949b78952814/pyrefly-0.24.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bf1689032b78f8f653244cd323ee1e06a0efb6192c4d7a415d1e85aedd37905", size = 5852019, upload-time = "2025-07-15T02:40:07.69Z" }, - { url = "https://files.pythonhosted.org/packages/a0/a3/cab8503091f244aa243995cb8745842198d71eb71225abe9ba8a1de78024/pyrefly-0.24.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8404b804a5a1bc4a54cc8e58bceacdf49d7221531843c068547241d8f476af24", size = 6546257, upload-time = "2025-07-15T02:40:09.729Z" }, - { url = "https://files.pythonhosted.org/packages/e0/06/b2881239f4a22c800003feaa3e653d6f635ea8979db506545e3c43bbf606/pyrefly-0.24.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d09f166a46e43655ea812611887ca16a0c54386296f4c9333f3f5fc7236709", size = 6296266, upload-time = "2025-07-15T02:40:11.667Z" }, - { url = "https://files.pythonhosted.org/packages/66/39/c414c1a30c24badb5153dd2d1ddb974d5b5662f80be9f1fed626fcfe6479/pyrefly-0.24.2-py3-none-win32.whl", hash = "sha256:6c602df48dcfa3240f9076c7d1e9cf9dc2d94c90ee5b4c6745f3734125a2cf3a", size = 5833755, upload-time = "2025-07-15T02:40:13.539Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2b/36d211dd03b86cb6216968f64081437837507debe30f58834a97091eda83/pyrefly-0.24.2-py3-none-win_amd64.whl", hash = "sha256:9ed4690716eb47077082d4e99624e0a1165b9ac93300c8d823f42cae12ec1ef4", size = 6207616, upload-time = "2025-07-15T02:40:15.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/9a/d51db168fe6bdae00b813582287d251e116666f07cb388b62d1715808891/pyrefly-0.24.2-py3-none-win_arm64.whl", hash = "sha256:96ba49c02f374d716b8674409aa653093dad5263cf4e429a1d5ec603064db715", size = 5867507, upload-time = "2025-07-15T02:40:16.793Z" }, + { url = "https://files.pythonhosted.org/packages/63/54/84e8cb476e143e5766432e9b1c7f2207928766472454720e11c36ccd2eba/pyrefly-0.28.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2a37dae101147fcd10e82fd7769f3658abd60fbc335d09820a4e0d78e0ab691a", size = 6423862, upload-time = "2025-08-11T17:34:20.321Z" }, + { url = "https://files.pythonhosted.org/packages/ea/51/1d0979e3bac99549bb1323279039536e63a7d1d6a035f00ed19a60ad5e79/pyrefly-0.28.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0931bc81650c1209310cbac576eba554fbd318ea9eaeae097181e9db07196197", size = 5993928, upload-time = "2025-08-11T17:34:22.219Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c9/a62dca991532a3e9bc02f16c7e8d5dc0cbae70ea21fbc9520ac38fe3ace5/pyrefly-0.28.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eeb470ed84d0e0ff63c5fb7573120ac822b7dfb9cf51c9a9b6bd75eceb64728", size = 6213273, upload-time = "2025-08-11T17:34:24.013Z" }, + { url = "https://files.pythonhosted.org/packages/6e/96/e0b05163e2eaef1ee74be3e5fce9bc4a43e53a355eab5382fb589f0df57f/pyrefly-0.28.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a80b8213d4dc4c3d139dbdbac64c2a4324f876f3ed79f76cec66223b7394afc3", size = 6983753, upload-time = "2025-08-11T17:34:25.836Z" }, + { url = "https://files.pythonhosted.org/packages/99/ff/6e95277b489d38fbb2345f68bdc392988817ca892f71d98995095947d056/pyrefly-0.28.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bda267113d4dcb284818ddf5a64236fdcb6ddf28343493f8162efdcaf104929", size = 6687047, upload-time = "2025-08-11T17:34:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/36/55/a839dcfdc8e119d8120a8a71737dc3a4c6b2bdbcaa69db0413a1723afce4/pyrefly-0.28.0-py3-none-win32.whl", hash = "sha256:cb2cd5867510a990683172b05b73dff7a3822ea1e41bb5bb3ce194f2763b4a0c", size = 6206654, upload-time = "2025-08-11T17:34:29.202Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a9/d0d5af3446e6306926931cc83f0a3442c1a558160665dec5fc8527fd65c8/pyrefly-0.28.0-py3-none-win_amd64.whl", hash = "sha256:22dfcb5be34ddc08c43d45cca4c13487bfe63bc2eba9fd039bb0c4410f29c6e4", size = 6619285, upload-time = "2025-08-11T17:34:31.067Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0d/80d1f0310aaa0224e3e453721036beb49f0fa740d6a09eabf73008f30ab6/pyrefly-0.28.0-py3-none-win_arm64.whl", hash = "sha256:c9e900b2a2e784665fd270fdafc241b77b9cfc5a8ec78166656d750c2fde9c58", size = 6256284, upload-time = "2025-08-11T17:34:32.613Z" }, ] [[package]] @@ -2904,14 +3320,14 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, ] [[package]] @@ -3096,6 +3512,15 @@ 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" }, ] +[[package]] +name = "python-json-logger" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/d3144a0bceede957f961e975f3752760fbe390d57fbe194baf709d8f1f7b/python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84", size = 16642, upload-time = "2025-03-07T07:08:27.301Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163, upload-time = "2025-03-07T07:08:25.627Z" }, +] + [[package]] name = "python-multipart" version = "0.0.20" @@ -3227,6 +3652,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] +[[package]] +name = "questionary" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/b8/d16eb579277f3de9e56e5ad25280fab52fc5774117fb70362e8c2e016559/questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587", size = 26775, upload-time = "2024-12-29T11:49:17.802Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/3f/11dd4cd4f39e05128bfd20138faea57bec56f9ffba6185d276e3107ba5b2/questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec", size = 36747, upload-time = "2024-12-29T11:49:16.734Z" }, +] + [[package]] name = "radon" version = "6.0.1" @@ -3256,14 +3693,14 @@ wheels = [ [[package]] name = "redis" -version = "6.2.0" +version = "6.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/9a/0551e01ba52b944f97480721656578c8a7c46b51b99d66814f85fe3a4f3e/redis-6.2.0.tar.gz", hash = "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977", size = 4639129, upload-time = "2025-05-28T05:01:18.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/67/e60968d3b0e077495a8fee89cf3f2373db98e528288a48f1ee44967f6e8c/redis-6.2.0-py3-none-any.whl", hash = "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e", size = 278659, upload-time = "2025-05-28T05:01:16.955Z" }, + { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, ] [[package]] @@ -3466,27 +3903,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9b/ce/8d7dbedede481245b489b769d27e2934730791a9a82765cb94566c6e6abd/ruff-0.12.4.tar.gz", hash = "sha256:13efa16df6c6eeb7d0f091abae50f58e9522f3843edb40d56ad52a5a4a4b6873", size = 5131435, upload-time = "2025-07-17T17:27:19.138Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/9f/517bc5f61bad205b7f36684ffa5415c013862dee02f55f38a217bdbe7aa4/ruff-0.12.4-py3-none-linux_armv6l.whl", hash = "sha256:cb0d261dac457ab939aeb247e804125a5d521b21adf27e721895b0d3f83a0d0a", size = 10188824, upload-time = "2025-07-17T17:26:31.412Z" }, - { url = "https://files.pythonhosted.org/packages/28/83/691baae5a11fbbde91df01c565c650fd17b0eabed259e8b7563de17c6529/ruff-0.12.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:55c0f4ca9769408d9b9bac530c30d3e66490bd2beb2d3dae3e4128a1f05c7442", size = 10884521, upload-time = "2025-07-17T17:26:35.084Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8d/756d780ff4076e6dd035d058fa220345f8c458391f7edfb1c10731eedc75/ruff-0.12.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a8224cc3722c9ad9044da7f89c4c1ec452aef2cfe3904365025dd2f51daeae0e", size = 10277653, upload-time = "2025-07-17T17:26:37.897Z" }, - { url = "https://files.pythonhosted.org/packages/8d/97/8eeee0f48ece153206dce730fc9e0e0ca54fd7f261bb3d99c0a4343a1892/ruff-0.12.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9949d01d64fa3672449a51ddb5d7548b33e130240ad418884ee6efa7a229586", size = 10485993, upload-time = "2025-07-17T17:26:40.68Z" }, - { url = "https://files.pythonhosted.org/packages/49/b8/22a43d23a1f68df9b88f952616c8508ea6ce4ed4f15353b8168c48b2d7e7/ruff-0.12.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:be0593c69df9ad1465e8a2d10e3defd111fdb62dcd5be23ae2c06da77e8fcffb", size = 10022824, upload-time = "2025-07-17T17:26:43.564Z" }, - { url = "https://files.pythonhosted.org/packages/cd/70/37c234c220366993e8cffcbd6cadbf332bfc848cbd6f45b02bade17e0149/ruff-0.12.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7dea966bcb55d4ecc4cc3270bccb6f87a337326c9dcd3c07d5b97000dbff41c", size = 11524414, upload-time = "2025-07-17T17:26:46.219Z" }, - { url = "https://files.pythonhosted.org/packages/14/77/c30f9964f481b5e0e29dd6a1fae1f769ac3fd468eb76fdd5661936edd262/ruff-0.12.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afcfa3ab5ab5dd0e1c39bf286d829e042a15e966b3726eea79528e2e24d8371a", size = 12419216, upload-time = "2025-07-17T17:26:48.883Z" }, - { url = "https://files.pythonhosted.org/packages/6e/79/af7fe0a4202dce4ef62c5e33fecbed07f0178f5b4dd9c0d2fcff5ab4a47c/ruff-0.12.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c057ce464b1413c926cdb203a0f858cd52f3e73dcb3270a3318d1630f6395bb3", size = 11976756, upload-time = "2025-07-17T17:26:51.754Z" }, - { url = "https://files.pythonhosted.org/packages/09/d1/33fb1fc00e20a939c305dbe2f80df7c28ba9193f7a85470b982815a2dc6a/ruff-0.12.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64b90d1122dc2713330350626b10d60818930819623abbb56535c6466cce045", size = 11020019, upload-time = "2025-07-17T17:26:54.265Z" }, - { url = "https://files.pythonhosted.org/packages/64/f4/e3cd7f7bda646526f09693e2e02bd83d85fff8a8222c52cf9681c0d30843/ruff-0.12.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abc48f3d9667fdc74022380b5c745873499ff827393a636f7a59da1515e7c57", size = 11277890, upload-time = "2025-07-17T17:26:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/5e/d0/69a85fb8b94501ff1a4f95b7591505e8983f38823da6941eb5b6badb1e3a/ruff-0.12.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2b2449dc0c138d877d629bea151bee8c0ae3b8e9c43f5fcaafcd0c0d0726b184", size = 10348539, upload-time = "2025-07-17T17:26:59.381Z" }, - { url = "https://files.pythonhosted.org/packages/16/a0/91372d1cb1678f7d42d4893b88c252b01ff1dffcad09ae0c51aa2542275f/ruff-0.12.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:56e45bb11f625db55f9b70477062e6a1a04d53628eda7784dce6e0f55fd549eb", size = 10009579, upload-time = "2025-07-17T17:27:02.462Z" }, - { url = "https://files.pythonhosted.org/packages/23/1b/c4a833e3114d2cc0f677e58f1df6c3b20f62328dbfa710b87a1636a5e8eb/ruff-0.12.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:478fccdb82ca148a98a9ff43658944f7ab5ec41c3c49d77cd99d44da019371a1", size = 10942982, upload-time = "2025-07-17T17:27:05.343Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ce/ce85e445cf0a5dd8842f2f0c6f0018eedb164a92bdf3eda51984ffd4d989/ruff-0.12.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0fc426bec2e4e5f4c4f182b9d2ce6a75c85ba9bcdbe5c6f2a74fcb8df437df4b", size = 11343331, upload-time = "2025-07-17T17:27:08.652Z" }, - { url = "https://files.pythonhosted.org/packages/35/cf/441b7fc58368455233cfb5b77206c849b6dfb48b23de532adcc2e50ccc06/ruff-0.12.4-py3-none-win32.whl", hash = "sha256:4de27977827893cdfb1211d42d84bc180fceb7b72471104671c59be37041cf93", size = 10267904, upload-time = "2025-07-17T17:27:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7e/20af4a0df5e1299e7368d5ea4350412226afb03d95507faae94c80f00afd/ruff-0.12.4-py3-none-win_amd64.whl", hash = "sha256:fe0b9e9eb23736b453143d72d2ceca5db323963330d5b7859d60d101147d461a", size = 11209038, upload-time = "2025-07-17T17:27:14.417Z" }, - { url = "https://files.pythonhosted.org/packages/11/02/8857d0dfb8f44ef299a5dfd898f673edefb71e3b533b3b9d2db4c832dd13/ruff-0.12.4-py3-none-win_arm64.whl", hash = "sha256:0618ec4442a83ab545e5b71202a5c0ed7791e8471435b94e655b570a5031a98e", size = 10469336, upload-time = "2025-07-17T17:27:16.913Z" }, +version = "0.12.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, + { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, + { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, + { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, + { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, + { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, + { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, + { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, ] [[package]] @@ -3504,7 +3941,7 @@ wheels = [ [[package]] name = "semgrep" -version = "1.128.1" +version = "1.131.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -3530,13 +3967,13 @@ dependencies = [ { name = "urllib3" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/2e/23e726076385a65b6b352adafb872be44e54955bebabd0008c347df1ae26/semgrep-1.128.1.tar.gz", hash = "sha256:ce51d80c0e667442c05a27d118c197fb278eff33685f0ae30aa4f91f128f0e5f", size = 41849475, upload-time = "2025-07-09T23:48:02.847Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/2b/25a016b731474eeb687baeb367662287fe2f4fb3d75ce84e099e7f0c3be7/semgrep-1.131.0.tar.gz", hash = "sha256:a56edd14610c44d74eb535447b7b7b938396077b42fbac176b0a7a7af7efff07", size = 42016368, upload-time = "2025-07-31T21:10:46.744Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/db/313a633431a0e297311c7b5ab18e82f9de7f4067c9c88d8271275fba878b/semgrep-1.128.1-cp39.cp310.cp311.py39.py310.py311-none-macosx_10_14_x86_64.whl", hash = "sha256:1c52c1ed2ebcc60052bccd9b6f8e814189ed54d400323828ca05bc1166840e1a", size = 34785909, upload-time = "2025-07-09T23:47:47.208Z" }, - { url = "https://files.pythonhosted.org/packages/d5/30/8b728657adf99f6d50f2c0ce447028e58d902d77033998f80c653a6bc3a8/semgrep-1.128.1-cp39.cp310.cp311.py39.py310.py311-none-macosx_11_0_arm64.whl", hash = "sha256:858fdb9d64c64a0ab594958f2ff22dd9decce81c49b7c1b0f06f860004a39f69", size = 39674292, upload-time = "2025-07-09T23:47:50.733Z" }, - { url = "https://files.pythonhosted.org/packages/37/2d/1a3084633ab2b8bf6af2f25369664c9ababdaef1b22ba6ff07d58e0e4091/semgrep-1.128.1-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d78aa08f045ac694ac24d4a39a370ba5974338aaea32a3304a239c328230249", size = 52063929, upload-time = "2025-07-09T23:47:53.761Z" }, - { url = "https://files.pythonhosted.org/packages/57/ef/1d84cbc5f82904734f244f899a928094ba4b15ba0bfebe324c539c295712/semgrep-1.128.1-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c592629c3a2a5efb3571f82dbc3134aaf64b2404faf95843d4f024d0ed52c13", size = 48223293, upload-time = "2025-07-09T23:47:56.963Z" }, - { url = "https://files.pythonhosted.org/packages/f8/a4/508b60e5f89f250da11565bf98c3d1c12930bef7fc265d6e2b0811a26835/semgrep-1.128.1-cp39.cp310.cp311.py39.py310.py311-none-win_amd64.whl", hash = "sha256:b52882bd238f4d25cd518018d962d79e6af352b51b70dfd09ba367f105252350", size = 42439092, upload-time = "2025-07-09T23:48:00.126Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/44a027208d85cac5d1130439ed3ecfedd55fe88231169c5e839b52d92ba1/semgrep-1.131.0-cp39.cp310.cp311.py39.py310.py311-none-macosx_10_14_x86_64.whl", hash = "sha256:160498fcc086998c438340e3ff439b38753fa2091d8c97c37568d4ce156a0a4e", size = 34859330, upload-time = "2025-07-31T21:10:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/80/4e/004cf9bd7301f2c9cb5b7e844d1398ba994b3261641bcfc57c7edf0b4289/semgrep-1.131.0-cp39.cp310.cp311.py39.py310.py311-none-macosx_11_0_arm64.whl", hash = "sha256:69a9dab95626a6d017e2e51cbac8909d3c5c7a99ba1879f5b5915df5342e37a8", size = 39757782, upload-time = "2025-07-31T21:10:31.607Z" }, + { url = "https://files.pythonhosted.org/packages/6d/60/72cd60d880fc835f7e9721b7a0623b5fab731ea9b1f12829eb91be6f070a/semgrep-1.131.0-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46b20a7fbc12cd069260402bdfcb72e93934361e94cdceb1e1ca3bd4c28f9857", size = 52121458, upload-time = "2025-07-31T21:10:35.088Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/d0f4ed63a86bed7ce96792198106d5b33c7e2b5f34c78abd5c33772a2bb4/semgrep-1.131.0-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2d11da8d7ca6f22d59fe131ac238dc387002e9197950877b6b8a27f5ba2692", size = 48266980, upload-time = "2025-07-31T21:10:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/82/8c/6f62d97501220d54e75a74c1caee164b74bc2f56abcc2e83cf1dd5d83c71/semgrep-1.131.0-cp39.cp310.cp311.py39.py310.py311-none-win_amd64.whl", hash = "sha256:c7ff0ffb30fe6b6101d040c1881f9c4ac9efb91655ee5d27c50d180a840be566", size = 42603915, upload-time = "2025-07-31T21:10:43.39Z" }, ] [[package]] @@ -3581,6 +4018,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] +[[package]] +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" } +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" }, +] + [[package]] name = "six" version = "1.17.0" @@ -3649,63 +4095,64 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.41" +version = "2.0.43" 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/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424, upload-time = "2025-05-14T17:10:32.339Z" } -wheels = [ - { 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, upload-time = "2025-05-14T17:48:20.444Z" }, - { 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, upload-time = "2025-05-14T17:48:21.634Z" }, - { 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, upload-time = "2025-05-14T17:51:56.205Z" }, - { 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, upload-time = "2025-05-14T17:55:26.928Z" }, - { 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, upload-time = "2025-05-14T17:51:59.384Z" }, - { 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, upload-time = "2025-05-14T17:55:29.901Z" }, - { url = "https://files.pythonhosted.org/packages/04/61/c0d4607f7799efa8b8ea3c49b4621e861c8f5c41fd4b5b636c534fcb7d73/sqlalchemy-2.0.41-cp311-cp311-win32.whl", hash = "sha256:293cd444d82b18da48c9f71cd7005844dbbd06ca19be1ccf6779154439eec0b8", size = 2086475, upload-time = "2025-05-14T17:56:02.095Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8e/8344f8ae1cb6a479d0741c02cd4f666925b2bf02e2468ddaf5ce44111f30/sqlalchemy-2.0.41-cp311-cp311-win_amd64.whl", hash = "sha256:3d3549fc3e40667ec7199033a4e40a2f669898a00a7b18a931d3efb4c7900504", size = 2110903, upload-time = "2025-05-14T17:56:03.499Z" }, - { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645, upload-time = "2025-05-14T17:55:24.854Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399, upload-time = "2025-05-14T17:55:28.097Z" }, - { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269, upload-time = "2025-05-14T17:50:38.227Z" }, - { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364, upload-time = "2025-05-14T17:51:49.829Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072, upload-time = "2025-05-14T17:50:39.774Z" }, - { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074, upload-time = "2025-05-14T17:51:51.736Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514, upload-time = "2025-05-14T17:55:49.915Z" }, - { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557, upload-time = "2025-05-14T17:55:51.349Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491, upload-time = "2025-05-14T17:55:31.177Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827, upload-time = "2025-05-14T17:55:34.921Z" }, - { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224, upload-time = "2025-05-14T17:50:41.418Z" }, - { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045, upload-time = "2025-05-14T17:51:54.722Z" }, - { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357, upload-time = "2025-05-14T17:50:43.483Z" }, - { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511, upload-time = "2025-05-14T17:51:57.308Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420, upload-time = "2025-05-14T17:55:52.69Z" }, - { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329, upload-time = "2025-05-14T17:55:54.495Z" }, - { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224, upload-time = "2025-05-14T17:39:42.154Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/d7/bc/d59b5d97d27229b0e009bd9098cd81af71c2fa5549c580a0a67b9bed0496/sqlalchemy-2.0.43.tar.gz", hash = "sha256:788bfcef6787a7764169cfe9859fe425bf44559619e1d9f56f5bddf2ebf6f417", size = 9762949, upload-time = "2025-08-11T14:24:58.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/77/fa7189fe44114658002566c6fe443d3ed0ec1fa782feb72af6ef7fbe98e7/sqlalchemy-2.0.43-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:52d9b73b8fb3e9da34c2b31e6d99d60f5f99fd8c1225c9dad24aeb74a91e1d29", size = 2136472, upload-time = "2025-08-11T15:52:21.789Z" }, + { url = "https://files.pythonhosted.org/packages/99/ea/92ac27f2fbc2e6c1766bb807084ca455265707e041ba027c09c17d697867/sqlalchemy-2.0.43-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f42f23e152e4545157fa367b2435a1ace7571cab016ca26038867eb7df2c3631", size = 2126535, upload-time = "2025-08-11T15:52:23.109Z" }, + { url = "https://files.pythonhosted.org/packages/94/12/536ede80163e295dc57fff69724caf68f91bb40578b6ac6583a293534849/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fb1a8c5438e0c5ea51afe9c6564f951525795cf432bed0c028c1cb081276685", size = 3297521, upload-time = "2025-08-11T15:50:33.536Z" }, + { url = "https://files.pythonhosted.org/packages/03/b5/cacf432e6f1fc9d156eca0560ac61d4355d2181e751ba8c0cd9cb232c8c1/sqlalchemy-2.0.43-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db691fa174e8f7036afefe3061bc40ac2b770718be2862bfb03aabae09051aca", size = 3297343, upload-time = "2025-08-11T15:57:51.186Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/d4c9b526f18457667de4c024ffbc3a0920c34237b9e9dd298e44c7c00ee5/sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2b3b4927d0bc03d02ad883f402d5de201dbc8894ac87d2e981e7d87430e60d", size = 3232113, upload-time = "2025-08-11T15:50:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/aa/79/c0121b12b1b114e2c8a10ea297a8a6d5367bc59081b2be896815154b1163/sqlalchemy-2.0.43-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d3d9b904ad4a6b175a2de0738248822f5ac410f52c2fd389ada0b5262d6a1e3", size = 3258240, upload-time = "2025-08-11T15:57:52.983Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/a2f9be96fb382f3ba027ad42f00dbe30fdb6ba28cda5f11412eee346bec5/sqlalchemy-2.0.43-cp311-cp311-win32.whl", hash = "sha256:5cda6b51faff2639296e276591808c1726c4a77929cfaa0f514f30a5f6156921", size = 2101248, upload-time = "2025-08-11T15:55:01.855Z" }, + { url = "https://files.pythonhosted.org/packages/ee/13/744a32ebe3b4a7a9c7ea4e57babae7aa22070d47acf330d8e5a1359607f1/sqlalchemy-2.0.43-cp311-cp311-win_amd64.whl", hash = "sha256:c5d1730b25d9a07727d20ad74bc1039bbbb0a6ca24e6769861c1aa5bf2c4c4a8", size = 2126109, upload-time = "2025-08-11T15:55:04.092Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/20c78f1081446095450bdc6ee6cc10045fce67a8e003a5876b6eaafc5cc4/sqlalchemy-2.0.43-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20d81fc2736509d7a2bd33292e489b056cbae543661bb7de7ce9f1c0cd6e7f24", size = 2134891, upload-time = "2025-08-11T15:51:13.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/0a/3d89034ae62b200b4396f0f95319f7d86e9945ee64d2343dcad857150fa2/sqlalchemy-2.0.43-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b9fc27650ff5a2c9d490c13c14906b918b0de1f8fcbb4c992712d8caf40e83", size = 2123061, upload-time = "2025-08-11T15:51:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/cb/10/2711f7ff1805919221ad5bee205971254845c069ee2e7036847103ca1e4c/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6772e3ca8a43a65a37c88e2f3e2adfd511b0b1da37ef11ed78dea16aeae85bd9", size = 3320384, upload-time = "2025-08-11T15:52:35.088Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0e/3d155e264d2ed2778484006ef04647bc63f55b3e2d12e6a4f787747b5900/sqlalchemy-2.0.43-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a113da919c25f7f641ffbd07fbc9077abd4b3b75097c888ab818f962707eb48", size = 3329648, upload-time = "2025-08-11T15:56:34.153Z" }, + { url = "https://files.pythonhosted.org/packages/5b/81/635100fb19725c931622c673900da5efb1595c96ff5b441e07e3dd61f2be/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4286a1139f14b7d70141c67a8ae1582fc2b69105f1b09d9573494eb4bb4b2687", size = 3258030, upload-time = "2025-08-11T15:52:36.933Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ed/a99302716d62b4965fded12520c1cbb189f99b17a6d8cf77611d21442e47/sqlalchemy-2.0.43-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:529064085be2f4d8a6e5fab12d36ad44f1909a18848fcfbdb59cc6d4bbe48efe", size = 3294469, upload-time = "2025-08-11T15:56:35.553Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a2/3a11b06715149bf3310b55a98b5c1e84a42cfb949a7b800bc75cb4e33abc/sqlalchemy-2.0.43-cp312-cp312-win32.whl", hash = "sha256:b535d35dea8bbb8195e7e2b40059e2253acb2b7579b73c1b432a35363694641d", size = 2098906, upload-time = "2025-08-11T15:55:00.645Z" }, + { url = "https://files.pythonhosted.org/packages/bc/09/405c915a974814b90aa591280623adc6ad6b322f61fd5cff80aeaef216c9/sqlalchemy-2.0.43-cp312-cp312-win_amd64.whl", hash = "sha256:1c6d85327ca688dbae7e2b06d7d84cfe4f3fffa5b5f9e21bb6ce9d0e1a0e0e0a", size = 2126260, upload-time = "2025-08-11T15:55:02.965Z" }, + { url = "https://files.pythonhosted.org/packages/41/1c/a7260bd47a6fae7e03768bf66451437b36451143f36b285522b865987ced/sqlalchemy-2.0.43-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e7c08f57f75a2bb62d7ee80a89686a5e5669f199235c6d1dac75cd59374091c3", size = 2130598, upload-time = "2025-08-11T15:51:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/8e/84/8a337454e82388283830b3586ad7847aa9c76fdd4f1df09cdd1f94591873/sqlalchemy-2.0.43-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:14111d22c29efad445cd5021a70a8b42f7d9152d8ba7f73304c4d82460946aaa", size = 2118415, upload-time = "2025-08-11T15:51:17.256Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ff/22ab2328148492c4d71899d62a0e65370ea66c877aea017a244a35733685/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21b27b56eb2f82653168cefe6cb8e970cdaf4f3a6cb2c5e3c3c1cf3158968ff9", size = 3248707, upload-time = "2025-08-11T15:52:38.444Z" }, + { url = "https://files.pythonhosted.org/packages/dc/29/11ae2c2b981de60187f7cbc84277d9d21f101093d1b2e945c63774477aba/sqlalchemy-2.0.43-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5a9da957c56e43d72126a3f5845603da00e0293720b03bde0aacffcf2dc04f", size = 3253602, upload-time = "2025-08-11T15:56:37.348Z" }, + { url = "https://files.pythonhosted.org/packages/b8/61/987b6c23b12c56d2be451bc70900f67dd7d989d52b1ee64f239cf19aec69/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d79f9fdc9584ec83d1b3c75e9f4595c49017f5594fee1a2217117647225d738", size = 3183248, upload-time = "2025-08-11T15:52:39.865Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/29d216002d4593c2ce1c0ec2cec46dda77bfbcd221e24caa6e85eff53d89/sqlalchemy-2.0.43-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9df7126fd9db49e3a5a3999442cc67e9ee8971f3cb9644250107d7296cb2a164", size = 3219363, upload-time = "2025-08-11T15:56:39.11Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e4/bd78b01919c524f190b4905d47e7630bf4130b9f48fd971ae1c6225b6f6a/sqlalchemy-2.0.43-cp313-cp313-win32.whl", hash = "sha256:7f1ac7828857fcedb0361b48b9ac4821469f7694089d15550bbcf9ab22564a1d", size = 2096718, upload-time = "2025-08-11T15:55:05.349Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a5/ca2f07a2a201f9497de1928f787926613db6307992fe5cda97624eb07c2f/sqlalchemy-2.0.43-cp313-cp313-win_amd64.whl", hash = "sha256:971ba928fcde01869361f504fcff3b7143b47d30de188b11c6357c0505824197", size = 2123200, upload-time = "2025-08-11T15:55:07.932Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/13bdde6521f322861fab67473cec4b1cc8999f3871953531cf61945fad92/sqlalchemy-2.0.43-py3-none-any.whl", hash = "sha256:1681c21dd2ccee222c2fe0bef671d1aef7c504087c9c4e800371cfcc8ac966fc", size = 1924759, upload-time = "2025-08-11T15:39:53.024Z" }, ] [[package]] name = "sse-starlette" -version = "2.4.1" +version = "3.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/3e/eae74d8d33e3262bae0a7e023bb43d8bdd27980aa3557333f4632611151f/sse_starlette-2.4.1.tar.gz", hash = "sha256:7c8a800a1ca343e9165fc06bbda45c78e4c6166320707ae30b416c42da070926", size = 18635, upload-time = "2025-07-06T09:41:33.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/42/6f/22ed6e33f8a9e76ca0a412405f31abb844b779d52c5f96660766edcd737c/sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a", size = 20985, upload-time = "2025-07-27T09:07:44.565Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/f1/6c7eaa8187ba789a6dd6d74430307478d2a91c23a5452ab339b6fbe15a08/sse_starlette-2.4.1-py3-none-any.whl", hash = "sha256:08b77ea898ab1a13a428b2b6f73cfe6d0e607a7b4e15b9bb23e4a37b087fd39a", size = 10824, upload-time = "2025-07-06T09:41:32.321Z" }, + { url = "https://files.pythonhosted.org/packages/ef/10/c78f463b4ef22eef8491f218f692be838282cd65480f6e423d7730dfd1fb/sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a", size = 11297, upload-time = "2025-07-27T09:07:43.268Z" }, ] [[package]] name = "starlette" -version = "0.46.2" +version = "0.47.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, ] [[package]] @@ -3849,7 +4296,7 @@ wheels = [ [[package]] name = "tox" -version = "4.27.0" +version = "4.28.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, @@ -3862,23 +4309,23 @@ dependencies = [ { name = "pyproject-api" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/b7/19c01717747076f63c54d871ada081cd711a7c9a7572f2225675c3858b94/tox-4.27.0.tar.gz", hash = "sha256:b97d5ecc0c0d5755bcc5348387fef793e1bfa68eb33746412f4c60881d7f5f57", size = 198351, upload-time = "2025-06-17T15:17:50.585Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/01/321c98e3cc584fd101d869c85be2a8236a41a84842bc6af5c078b10c2126/tox-4.28.4.tar.gz", hash = "sha256:b5b14c6307bd8994ff1eba5074275826620325ee1a4f61316959d562bfd70b9d", size = 199692, upload-time = "2025-07-31T21:20:26.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/3a/30889167f41ecaffb957ec4409e1cbc1d5d558a5bbbdfb734a5b9911930f/tox-4.27.0-py3-none-any.whl", hash = "sha256:2b8a7fb986b82aa2c830c0615082a490d134e0626dbc9189986da46a313c4f20", size = 173441, upload-time = "2025-06-17T15:17:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/564a33093e41a585e2e997220986182c037bc998abf03a0eb4a7a67c4eff/tox-4.28.4-py3-none-any.whl", hash = "sha256:8d4ad9ee916ebbb59272bb045e154a10fa12e3bbdcf94cc5185cbdaf9b241f99", size = 174058, upload-time = "2025-07-31T21:20:24.836Z" }, ] [[package]] name = "tox-uv" -version = "1.26.1" +version = "1.27.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tox" }, { name = "uv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/00/98e564731fc361cc2f1e39c58d2feb0b4c9f9a7cb06f0c769cdeb9a98004/tox_uv-1.26.1.tar.gz", hash = "sha256:241cc530b4a80436c4487977c8303d9aace398c6561d5e7d8845606fa7d482ab", size = 21849, upload-time = "2025-06-23T20:17:54.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/01/3bf884009509b7eef169bf3628e4eb470be7be269486cdc4ed411215e74a/tox_uv-1.27.0.tar.gz", hash = "sha256:93e432728c51f8659830dc6f5096528dc7726d7524ea7cb60f69ab00dbdd7fc8", size = 22253, upload-time = "2025-08-07T15:32:27.788Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/0b/e47c1bb2bc9e20b22a6913ea2162b7bb5729d38924fa2c1d4eaf95d3b36f/tox_uv-1.26.1-py3-none-any.whl", hash = "sha256:edc25b254e5cdbb13fc5d23d6d05b511dee562ab72b0e99da4a874a78018c38e", size = 16661, upload-time = "2025-06-23T20:17:52.492Z" }, + { url = "https://files.pythonhosted.org/packages/60/eb/669ff92885ec3fd497f9f1eaa33d4b98bad3f3c665258f0f607142f72c0c/tox_uv-1.27.0-py3-none-any.whl", hash = "sha256:479ca855db5ad555818498181a6fbaf6979624a1eae84ef63031279cfd00cbf5", size = 16880, upload-time = "2025-08-07T15:32:26.357Z" }, ] [[package]] @@ -3929,27 +4376,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.1a14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/c2/743ef78f8ddcdd8a8e074b6ad6eb01fa5d8c110f5e25e0142087b175b212/ty-0.0.1a14.tar.gz", hash = "sha256:a9ecac10c63a7c193c78ef1a01956c7c579e4d8498d3ec77543fe31a5a9e3912", size = 3176178, upload-time = "2025-07-08T11:57:34.171Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/10/4c29110e6571b00206962b219afa38e661001011bfc1b36bec4f74fdf51d/ty-0.0.1a14-py3-none-linux_armv6l.whl", hash = "sha256:165acd7a7c49d9cbd20b8fa72a7dab0ccceae935593618aba9218950fdbb0e08", size = 6969793, upload-time = "2025-07-08T11:57:07.349Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bc/1d64ef953a53474725847a8bd3418e422218ed7024fe2bd9e3c9c2c859ce/ty-0.0.1a14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54c357059f0c0d27f1efc544c0c9475f918d5cd579e7fa38365c5e2e4ef5868a", size = 7076030, upload-time = "2025-07-08T11:57:09.286Z" }, - { url = "https://files.pythonhosted.org/packages/de/b6/672bac4f24fab47def3c672a0a97d1eafbd6baef61b99ebefb805944c2a9/ty-0.0.1a14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c729efdddcfc7fe66df284a321cb2b8ea8c36f7e0a322176f0b5ffd3ca45db90", size = 6699358, upload-time = "2025-07-08T11:57:10.621Z" }, - { url = "https://files.pythonhosted.org/packages/99/1e/0a8d7a49c324584451309f96fc85f462e2b2052c216e82a2f689f9e477c0/ty-0.0.1a14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e593d549118e3794e11ab09e610520654a4c91a8c2ffaff8c31845067c2cf883", size = 6834240, upload-time = "2025-07-08T11:57:12.284Z" }, - { url = "https://files.pythonhosted.org/packages/5b/97/e57c49e7d5216af907ca83e4e4ede7471cef53284f90f8fe11f6051031d8/ty-0.0.1a14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:679f715d9d4b5558231eafe6c4441a0a53c245a669ac2ba049c0996e538f7e88", size = 6810434, upload-time = "2025-07-08T11:57:13.643Z" }, - { url = "https://files.pythonhosted.org/packages/1b/0f/293680a83e7c86354b97a7e8cb08896338370eb169383c7b687aa3311f96/ty-0.0.1a14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:541487c8b896c0094a32e91f96b2d6ee9bc3c1f1c0a9c5c781eef0002ca437a4", size = 7622086, upload-time = "2025-07-08T11:57:15.14Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9c/f5d730903d65a0eb989d793a1d7e5a62f765841c45bef325403edf342810/ty-0.0.1a14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:aa9ffb8a518762e78b0c6780475e89bfc8e040fb9832918b6bf79c5e8e96da92", size = 8073710, upload-time = "2025-07-08T11:57:16.512Z" }, - { url = "https://files.pythonhosted.org/packages/0e/09/f39d0f626df4841d39bdc8ae9052f91f65b45beff281bdf29a53efea79bf/ty-0.0.1a14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bae3288461e3db28dd19f12d4f71d88f5be201283f6d8790d467cb87d9717bd", size = 7716213, upload-time = "2025-07-08T11:57:18.136Z" }, - { url = "https://files.pythonhosted.org/packages/68/5a/afdab1afec623ecede8108733228703bb4cb25fa8210ba16427fcd1c0429/ty-0.0.1a14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:032ff6d43272c5a87ef2cf56773a4e44c370d98cdd129bc4b352d6c63c5348c7", size = 7559696, upload-time = "2025-07-08T11:57:19.816Z" }, - { url = "https://files.pythonhosted.org/packages/a7/26/e422f8ed5ca63b50bf87de5b4bc8fd15cb203a8d2690b2f447ccff0c74fb/ty-0.0.1a14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd7739be014ff5dee50b3a83252c7953cf278567079bde0e33c5203ff576d9b", size = 7375483, upload-time = "2025-07-08T11:57:21.525Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f2/bb88da6ba64bfe5edff636738861a6b78050611da9540e372635e599ee4c/ty-0.0.1a14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5776cf7ea1000be79cd70d44fafe15b3695bac1a115806421402dfa79ba9953e", size = 6726122, upload-time = "2025-07-08T11:57:24.116Z" }, - { url = "https://files.pythonhosted.org/packages/93/96/62566786e9124b8f0f2fbced5e57046534e5835af83f9efb4a0c861f6a61/ty-0.0.1a14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10d62346a56a0d21c809ae885ca7513a1c54da8c94029664eeb68fbdd3effc02", size = 6839605, upload-time = "2025-07-08T11:57:25.705Z" }, - { url = "https://files.pythonhosted.org/packages/12/26/40b7e7388a5308cc53dd440cb24d1a95b3efd07c4a374fce9cd0fe777049/ty-0.0.1a14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7a5bb97658bf3fa054833bf3511499e1422a27dee7a3c10aff56d4daa3821d6d", size = 7268362, upload-time = "2025-07-08T11:57:27.012Z" }, - { url = "https://files.pythonhosted.org/packages/14/50/11f275d7cb413104dea1360a997ad373839ed0108980912605e59188afba/ty-0.0.1a14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a89c075ae1238d215de2343909952e872306791fbad96a8ffd119a83a3e0a914", size = 7435560, upload-time = "2025-07-08T11:57:28.673Z" }, - { url = "https://files.pythonhosted.org/packages/11/b6/468c98f9520515fd9a65ce5a51c9aa39f7f944cb8f320328839cef65d1f1/ty-0.0.1a14-py3-none-win32.whl", hash = "sha256:6c3e87f4549a1bf7524df074a4fe9a5815aff25e536c5fc6f1518b72e74a17cf", size = 6573723, upload-time = "2025-07-08T11:57:30.285Z" }, - { url = "https://files.pythonhosted.org/packages/1c/82/a771661d3d64e17688063dc5573e8eebc0683f581c843b840f5b03d108f7/ty-0.0.1a14-py3-none-win_amd64.whl", hash = "sha256:0ed6145dae24b68638037d7c82f094b22adfb48114678120cf392092973fad96", size = 7181298, upload-time = "2025-07-08T11:57:31.642Z" }, - { url = "https://files.pythonhosted.org/packages/9d/d0/68b106ddc25239d4a7114e64211aa5ad5d27488c1a318ab8ad057b88b4a7/ty-0.0.1a14-py3-none-win_arm64.whl", hash = "sha256:67717fbbb501c9deb11141662688804513082992aaeb5fdc6a3b7cd8e77eea8e", size = 6788031, upload-time = "2025-07-08T11:57:32.943Z" }, +version = "0.0.1a17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/59/f29cf1adc5c5dd6e739e08138dfa2435d3210841c6b6aa4d5bee7203cabf/ty-0.0.1a17.tar.gz", hash = "sha256:8bd0c5722c630b46a136ffc8f273f47d46cf00d9df2b0c72f1bfd28d1908a7c2", size = 4037064, upload-time = "2025-08-06T12:13:55.862Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/76/1275e4b02a74dbff40408c608dcccb758245173bd794618dadcb092e384f/ty-0.0.1a17-py3-none-linux_armv6l.whl", hash = "sha256:c16b109f05ab34f084b98b9da84c795a23780c9a2f44c237464e52efc5f97139", size = 7970950, upload-time = "2025-08-06T12:13:24.031Z" }, + { url = "https://files.pythonhosted.org/packages/df/15/10947e3a0993b02acb563fa958fb9334937615195dbe6efd17c889d1925d/ty-0.0.1a17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:59e2db756b2b727723ebee58c2e00f172e00a60083a49c8522a19e81887dbc71", size = 8117684, upload-time = "2025-08-06T12:13:26.035Z" }, + { url = "https://files.pythonhosted.org/packages/c0/73/1f982b361b0f161dad3181739f6dc010252e17d5eb8eea625d88f03deb23/ty-0.0.1a17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:79b6d76d64f86414d482f08e09433bedd3e489a1973fa1226b457d4935b592a3", size = 7721774, upload-time = "2025-08-06T12:13:27.528Z" }, + { url = "https://files.pythonhosted.org/packages/d2/fc/a8837d4c1e395730157b16f379f4204035bb75e3fc815a1238c02bab2655/ty-0.0.1a17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdbc144e7d7a5c8dc102715870bf211b51efe581e952a933a0fcba2df9d6ac8d", size = 7841709, upload-time = "2025-08-06T12:13:29.726Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/1f69a9aa9f3092c7c1e3bf8e8d2d3db4a7a03108432fc02af24e313c8deb/ty-0.0.1a17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:677388fbe5b9e75764dd8b520eff9c3273f9749ece5425eb34e6fa1359430e3b", size = 7811651, upload-time = "2025-08-06T12:13:31.448Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d8/561283da06dd8f7da44543af9e4a7fde1716f1fe174dde073f78ea291b35/ty-0.0.1a17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdaa4baee9f559ee5bb36b66ad0635e2e4308c6937e8e655a4d4ae1bcf736ad0", size = 8702850, upload-time = "2025-08-06T12:13:34.484Z" }, + { url = "https://files.pythonhosted.org/packages/8a/10/da263a67fea576027b65d78a7d2a55d9829aa22b17e0e10d4201b8d6bd8c/ty-0.0.1a17-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a0637f80301e7a961b87df2e01db22e8f764cd46d371a73b9a968e1894c334ab", size = 9188621, upload-time = "2025-08-06T12:13:36.206Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/e0d2c55df43ecf1bd5f11859fd9f8dd8536643ce1433aec6b8c8bf3b2865/ty-0.0.1a17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:749622726587e758fbbb530d1ab293b7489476cd5502c3ac5da5d6b042bb6a1b", size = 8795061, upload-time = "2025-08-06T12:13:37.99Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3c/ad62544ad7982cb4029f7901953ede9a27c7f6a3afef207fef6a4c6ebfce/ty-0.0.1a17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f1cb91c4b79127193829ab2d1bda79a86ab782fcbdf2988b5a3af37a44a7ae2", size = 8643000, upload-time = "2025-08-06T12:13:40.075Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cb/029bf9f24bb5c5c7c4b139d1f131b19530303fcdd8141607a8d065a87f74/ty-0.0.1a17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7310e783c54f4f9b2380e050b2e992ccbebcf7e73748689f1d8408199cc5a14e", size = 8432265, upload-time = "2025-08-06T12:13:41.631Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6c/b4c7ba46218953a822f17813ed2d86238a04ca7937de286841a2c18ff857/ty-0.0.1a17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:96681e474600811066bf42e3e2cfac7603b7ca1da065fb4f852f94f3df9c944a", size = 7730711, upload-time = "2025-08-06T12:13:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/74/fd/f3aa541e1b7e1d0becf9f28e44e986ae5eb556f827a5312011026938d197/ty-0.0.1a17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ddc72485af203a70267c522ff6ab6cf648ea0a065a7fa542e9e9552c83cdaee", size = 7836927, upload-time = "2025-08-06T12:13:44.614Z" }, + { url = "https://files.pythonhosted.org/packages/94/06/7d8b4b52af385a20705cc063a7f9c144aae3b6aaef165ad2fcc029c9f755/ty-0.0.1a17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7b05f97cc5149b01cb979b9b6b2d773055fb482e490d7169d7c3a213de07ade5", size = 8304523, upload-time = "2025-08-06T12:13:45.983Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a6/e14d4600339a6654e9ccc90ad9662a116f9544e0afb8d0abf1c99d6a2c2d/ty-0.0.1a17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8f80d9b5bc8681fe072ede10d4052035ec1f54c194532932e6c4230a2a5526e5", size = 8492400, upload-time = "2025-08-06T12:13:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/ab/90/19dac956ab9f1ad04b4d1a38df856b44f237b3eda5af5b76a29439e64165/ty-0.0.1a17-py3-none-win32.whl", hash = "sha256:c3ba585145c4a019cb31001a1d0bd606e01fe01b285a20188a42eba165dddd50", size = 7617341, upload-time = "2025-08-06T12:13:49.093Z" }, + { url = "https://files.pythonhosted.org/packages/f5/94/08e2b3f6bc0af97abcd3fcc8ea28797a627296613256ae37e98043c871ca/ty-0.0.1a17-py3-none-win_amd64.whl", hash = "sha256:7d00b569ebd4635c58840d2ed9e1d2d8b36f496619c0bc0c8d1777767786b508", size = 8230727, upload-time = "2025-08-06T12:13:50.705Z" }, + { url = "https://files.pythonhosted.org/packages/98/c6/207bbc2f3bb71df4b1aeabe8e9c31a1cd22c72aff0ab9c1a832b9ae54f6e/ty-0.0.1a17-py3-none-win_arm64.whl", hash = "sha256:636eacc1dceaf09325415a70a03cd57eae53e5c7f281813aaa943a698a45cddb", size = 7782847, upload-time = "2025-08-06T12:13:54.243Z" }, ] [[package]] @@ -3980,6 +4427,21 @@ datetime = [ { name = "pytz" }, ] +[[package]] +name = "typer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, +] + [[package]] name = "types-python-dateutil" version = "2.9.0.20250516" @@ -4057,27 +4519,28 @@ wheels = [ [[package]] name = "uv" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/6c/0f42f6f59af910ab67ce5acb7f11e6341275cfe9cfa0b8a2ae97303e5775/uv-0.8.0.tar.gz", hash = "sha256:5d4b05056cc923e579007aede5ad1c3cf2c22628a89585f503b724521036748c", size = 3395163, upload-time = "2025-07-17T22:51:40.756Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/0a/07735385f63229c5a6079044861a7462b1f9ff02dc7c6a891d296ffed9b0/uv-0.8.0-py3-none-linux_armv6l.whl", hash = "sha256:7f1a7f9b10299d9db15acac6cdffc5af23c2b0fd6e56add6d6e5d100a82b5c1f", size = 17839329, upload-time = "2025-07-17T22:50:56.938Z" }, - { url = "https://files.pythonhosted.org/packages/8e/c1/160b81f8c34bf5ea6bddde96f80f3f000d083482f2612a98725a9f714707/uv-0.8.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6aeecfef8955dafcbad24ef5258341f2fcdf969949f74ccaa16a7bf9c3ec44b4", size = 17952798, upload-time = "2025-07-17T22:51:02.635Z" }, - { url = "https://files.pythonhosted.org/packages/9d/98/9a89983caa05cf998eea3dac1e6cff2e0ab8099be0695fd8b9dc6a5038a0/uv-0.8.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2d0ebf05eaee75921b3f23e7401a56bc0732bcdabb7469081ab00769340a93b4", size = 16618272, upload-time = "2025-07-17T22:51:04.941Z" }, - { url = "https://files.pythonhosted.org/packages/6e/8a/ed8c00d04c621b693c53cc9570ecddf766f8ff2078d6182eba55d0c20b10/uv-0.8.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:b784215c688c4eb54df62fb7506ba7c763fb8c9ba8457cd5dd48f0679f5d0328", size = 17199728, upload-time = "2025-07-17T22:51:06.853Z" }, - { url = "https://files.pythonhosted.org/packages/c1/08/fd29a5f93576f81a4d912e60d98fcb78e727c293f57b5a703e121d1875f2/uv-0.8.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:422e6c61b1555478f34300663f0057d5d4fea788c50eae31b332d0cec2a71536", size = 17561205, upload-time = "2025-07-17T22:51:09.528Z" }, - { url = "https://files.pythonhosted.org/packages/f3/28/80a4c04e0c843b16c2406a9a699dea4d2ac0f4863295194a7e202b917afa/uv-0.8.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19bd1fc94de3714c41d9f9f4dbcfdd2feeca00439b2fb2212ece249649b85c72", size = 18337053, upload-time = "2025-07-17T22:51:12.064Z" }, - { url = "https://files.pythonhosted.org/packages/00/43/8c86a21efced9d2617311b456a1e8ad76f4eba2d4809fe5a8d4639719949/uv-0.8.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c039a27387033c84eb07023c941cb6c3f4d71336ada54b83866929af9db75839", size = 19484516, upload-time = "2025-07-17T22:51:14.336Z" }, - { url = "https://files.pythonhosted.org/packages/38/0b/e74a16000ad8e5811ed648bb1801377b311640ed5b7033959fb5c80ab826/uv-0.8.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5deec88789d0c03eba6af2e615e310eaa4426deb5c690f15e54f09d4a8ad0d", size = 19292816, upload-time = "2025-07-17T22:51:16.757Z" }, - { url = "https://files.pythonhosted.org/packages/91/73/c8ee97f38adee10abfa7040ea5e3f5c37e0be2e67437346ba4dcce211d01/uv-0.8.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d3fcbd19654f168e1cae3054ed75cfc0c80a452d0668f90fc579135de6d7588e", size = 18835921, upload-time = "2025-07-17T22:51:18.948Z" }, - { url = "https://files.pythonhosted.org/packages/bf/94/b8609393606e2f80dec8d6e2a26b79d703857a9d761487cdd05d081d628f/uv-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb939490ce24d5f88ddebf2ceaf30c613b2bd27f2e67ae62ec0960baa5f8412d", size = 18708625, upload-time = "2025-07-17T22:51:22.37Z" }, - { url = "https://files.pythonhosted.org/packages/2a/44/8754d0a27b4d52d8cce9a5d2e50196dc14a5b7c0739858eabf4abfec1740/uv-0.8.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:57af365c459d8e05274363cb10f4781a5e15b7b6f56d1427fd5b04302aa3d244", size = 17464028, upload-time = "2025-07-17T22:51:24.855Z" }, - { url = "https://files.pythonhosted.org/packages/1c/17/ce98535a2f167feeea4d3b5f266239ebfe11ba5ceb1be3aad9772b35e9e0/uv-0.8.0-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:9fb57d550887e9858b8c7c0417e5aa3e9629c2ec851f1567f1cde2ba9bf2ee79", size = 17503466, upload-time = "2025-07-17T22:51:27.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/37/3990cf8a19012010cd1fafce7934c0aaa8375712c8bc037e0c3ef148df0c/uv-0.8.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:7bd1ff23f8585e0e80024341aeb896b6b5ce94d43d3a95142be8e4bb3f1354b4", size = 17843023, upload-time = "2025-07-17T22:51:29.44Z" }, - { url = "https://files.pythonhosted.org/packages/e0/91/b88ffc2355fe6c2d1695f42a4605ff0f2773d5bd1a62699757c84ccc6496/uv-0.8.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:29757a08499e4c4462efa4fcba664c8850eb7ab8ec04852582adb901591dcc50", size = 18862516, upload-time = "2025-07-17T22:51:31.887Z" }, - { url = "https://files.pythonhosted.org/packages/37/39/c470b8de6e250fb8f146c3f72c396a9e9f457cfbb04618f430cc52a3a84f/uv-0.8.0-py3-none-win32.whl", hash = "sha256:84b03d7b34e1a8e62b34d13e88f434e3f1773a0841f7ba3603ca23d360529e84", size = 17757431, upload-time = "2025-07-17T22:51:34.24Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4e/4a69b1baa14dfee113f76c823ffa4e79cd6bc6452c24454382a6fa793f2a/uv-0.8.0-py3-none-win_amd64.whl", hash = "sha256:2f47fca8cb0301408ec1ae7f3e0388afb36fc36188d1775dbd8daf336bf5be6f", size = 19493722, upload-time = "2025-07-17T22:51:36.672Z" }, - { url = "https://files.pythonhosted.org/packages/79/95/2803b563c61cd9b26f89a15b248d7e2ee5bbfbac892966ebd09111613f38/uv-0.8.0-py3-none-win_arm64.whl", hash = "sha256:aefc09b9a580f7f41a2358462215538f3806de60c6f20ade4a25ee4d678267e1", size = 18090168, upload-time = "2025-07-17T22:51:38.946Z" }, +version = "0.8.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/d0/4cd8ac2c7938da78c8e9ca791205f80e74b0f5a680f2a2d50323d54961d0/uv-0.8.8.tar.gz", hash = "sha256:6880e96cd994e53445d364206ddb4b2fff89fd2fbc74a74bef4a6f86384b07d9", size = 3477036, upload-time = "2025-08-09T00:26:00.883Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/d5/49e188db80f3d8b1969bdbcb8a5468a3796827f15d773241204f206a9ff6/uv-0.8.8-py3-none-linux_armv6l.whl", hash = "sha256:fcdbee030de120478db1a4bb3e3bbf04eec572527ea9107ecf064a808259b6c9", size = 18470316, upload-time = "2025-08-09T00:25:11.956Z" }, + { url = "https://files.pythonhosted.org/packages/01/50/add1afadccd141d0d72b54e5146f8181fcc6efd1567a17c5b1edec444010/uv-0.8.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:461e8fb83931755cf0596bf1b8ccbfe02765e81a0d392c495c07685d6b6591f9", size = 18468770, upload-time = "2025-08-09T00:25:15.391Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ac/3c6dc8781d37ef9854f412322caffac2978dd3fa1bf806f7daebcfebf2be/uv-0.8.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:58056e5ccebb0a1aad27bd89d0ccc5b65c086d5a7f6b0ac16a9dde030b63cf14", size = 17200419, upload-time = "2025-08-09T00:25:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9e/c30ea1f634673d234999985984afbe96c3d2a4381986e36df0bb46c0f21b/uv-0.8.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5b4c56a620137f562e1d7b09eac6c9d4adeb876aefc51be27973257fcb426c9d", size = 17779351, upload-time = "2025-08-09T00:25:20.891Z" }, + { url = "https://files.pythonhosted.org/packages/2f/89/f2885c6e97a265b4b18050df6285f56c81b603a867a63fcd8f2caa04d95c/uv-0.8.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5fc33adb91c4e3db550648aa30c2b97e8e4d8b8842ead7784a9e76dae3cb14dc", size = 18139292, upload-time = "2025-08-09T00:25:23.352Z" }, + { url = "https://files.pythonhosted.org/packages/38/5f/98dad16987919e7dc02f2566026a263ea6307bf57e8de0008dde4717d9cf/uv-0.8.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19a82d6738d3aa58e6646b9d6c343d103abf0c4caf97a68d16a8cab55282e4be", size = 18932468, upload-time = "2025-08-09T00:25:25.691Z" }, + { url = "https://files.pythonhosted.org/packages/56/99/52d0d9f53cc5df11b1a459e743bd7b2f4660d49f125a63640eb85ce993e0/uv-0.8.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9dce4de70098cb5b98feea9ef0b8f7db5d6b9deea003a926bc044a793872d719", size = 20251614, upload-time = "2025-08-09T00:25:28.122Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/0698099a905b4a07b8fa9d6838e0680de707216ccf003433ca1b4afff224/uv-0.8.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1038324c178d2d7407a4005c4c3294cbad6a02368ba5a85242308de62a6f4e12", size = 19916222, upload-time = "2025-08-09T00:25:30.732Z" }, + { url = "https://files.pythonhosted.org/packages/7f/29/8384e0f3f3536ef376d94b7ab177753179906a6c2f5bab893e3fb9525b45/uv-0.8.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bd016beea3935f9148b3d2482e3d60dee36f0260f9e99d4f57acfd978c1142a", size = 19238516, upload-time = "2025-08-09T00:25:33.637Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f1/6c107deccd6e66eb1c46776d8cef4ca9274aac73cec1b14453fe85e18a54/uv-0.8.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0a2b5ebc96aba2b0bf54283d2906b40f32949298cbc6ec48648097ddeac5c5d", size = 19232295, upload-time = "2025-08-09T00:25:37.154Z" }, + { url = "https://files.pythonhosted.org/packages/c5/96/9f5e935cd970102c67ce2a753ac721665fb4477c262e86afa0ab385cefff/uv-0.8.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e529dc0a1be5e896d299e4eae4599fa68909f8cb3e6c5ee1a46f66c9048e3334", size = 18046917, upload-time = "2025-08-09T00:25:39.72Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/97f371add0a02e5e37156ac0fea908ab4a1160fdf716d0e6c257b6767122/uv-0.8.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5d58d986c3b6a9ce0fb48cd48b3aee6cb1b1057f928d598432e75a4fcaa370f4", size = 18949133, upload-time = "2025-08-09T00:25:42.139Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/ea988ae9d8c5531454ea6904290e229624c9ea830a5c37b91ec74ebde9a4/uv-0.8.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:e117e1230559058fd286292dd5839e8e82d1aaf05763bf4a496e91fe07b69fa1", size = 18080018, upload-time = "2025-08-09T00:25:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/ff/14/3b16af331b79ae826d00a73e98f26f7f660dabedc0f82acb99069601b355/uv-0.8.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:372934fd94193c98dec59bd379cf39e73f906ae6162cbfb66686f32afd75fa0f", size = 18437896, upload-time = "2025-08-09T00:25:49.162Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b6/c866684da5571dbf42e9a60b6587a62adc8a2eb592f07411d3b29cb09871/uv-0.8.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9330c924faa9df00a5e78b54561ecf4e5eac1211066f027620dbe85bd6f479ce", size = 19341221, upload-time = "2025-08-09T00:25:51.444Z" }, + { url = "https://files.pythonhosted.org/packages/49/ea/55a0eff462b2ec5a6327dd87c401c53306406c830fa8f2cabd2af79dd97f/uv-0.8.8-py3-none-win32.whl", hash = "sha256:65113735aa3427d3897e2f537da1331d1391735c6eecb9b820da6a15fd2f6738", size = 18244601, upload-time = "2025-08-09T00:25:53.696Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c0/f56ddb1b2276405618e3d2522018c962c010fc71f97f385d01b7e1dcd8df/uv-0.8.8-py3-none-win_amd64.whl", hash = "sha256:66189ca0b4051396aa19a6f036351477656073d0fd01618051faca699e1b3cdc", size = 20233481, upload-time = "2025-08-09T00:25:56.247Z" }, + { url = "https://files.pythonhosted.org/packages/ac/1a/70dc4c730c19f3af40be9450b98b801e03cd6d16609743013f7258f69a29/uv-0.8.8-py3-none-win_arm64.whl", hash = "sha256:1d829486e88ebbf7895306ff09a8b6014d3af7a18e27d751979ee37bf3a27832", size = 18786215, upload-time = "2025-08-09T00:25:58.941Z" }, ] [[package]] @@ -4146,6 +4609,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, ] +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + [[package]] name = "wrapt" version = "1.17.2" @@ -4212,6 +4717,88 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dd/b9/be7a4cfdf47e03785f657f94daea8123e838d817be76c684298305bd789f/yamllint-1.37.1-py3-none-any.whl", hash = "sha256:364f0d79e81409f591e323725e6a9f4504c8699ddf2d7263d8d2b539cd66a583", size = 68813, upload-time = "2025-05-04T08:25:52.552Z" }, ] +[[package]] +name = "yarl" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833, upload-time = "2025-06-10T00:43:07.393Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070, upload-time = "2025-06-10T00:43:09.538Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818, upload-time = "2025-06-10T00:43:11.575Z" }, + { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003, upload-time = "2025-06-10T00:43:14.088Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537, upload-time = "2025-06-10T00:43:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358, upload-time = "2025-06-10T00:43:18.704Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362, upload-time = "2025-06-10T00:43:20.888Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979, upload-time = "2025-06-10T00:43:23.169Z" }, + { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274, upload-time = "2025-06-10T00:43:27.111Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294, upload-time = "2025-06-10T00:43:28.96Z" }, + { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169, upload-time = "2025-06-10T00:43:30.701Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776, upload-time = "2025-06-10T00:43:32.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341, upload-time = "2025-06-10T00:43:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988, upload-time = "2025-06-10T00:43:36.489Z" }, + { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113, upload-time = "2025-06-10T00:43:38.592Z" }, + { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485, upload-time = "2025-06-10T00:43:41.038Z" }, + { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686, upload-time = "2025-06-10T00:43:42.692Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, + { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, + { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, + { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, + { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, + { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, + { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, + { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, + { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, + { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, + { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, + { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, + { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, + { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, + { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, + { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +] + [[package]] name = "zeroconf" version = "0.147.0" From 2f3201fdabec08e14d53cf2e310eac6348a17b70 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 11 Aug 2025 23:32:49 -0400 Subject: [PATCH 06/65] fix: implement install and package CLI commands Signed-off-by: Frederico Araujo --- mcpgateway/plugins/tools/cli.py | 30 ++++++++++++++++++++------ mcpgateway/plugins/tools/models.py | 34 ++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) create mode 100644 mcpgateway/plugins/tools/models.py diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index e4478b85b..3e2c056ea 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -26,16 +26,20 @@ """ # Standard +import os from pathlib import Path +import subprocess from typing import Optional # Third-Party from copier import Worker import typer from typing_extensions import Annotated +import yaml # First-Party from mcpgateway.config import settings +from mcpgateway.plugins.tools.models import InstallManifest # --------------------------------------------------------------------------- # Configuration defaults @@ -45,8 +49,10 @@ DEFAULT_INSTALL_MANIFEST = Path("plugins/install.yaml") DEFAULT_IMAGE_TAG = "contextforge-plugin:latest" # TBD: add plugin name and version DEFAULT_IMAGE_BUILDER = "docker" +DEFAULT_BUILD_CONTEXT = "." DEFAULT_CONTAINERFILE_PATH = Path("docker/Dockerfile") DEFAULT_VCS_REF = "main" +DEFAULT_INSTALLER = "uv pip install" # --------------------------------------------------------------------------- # CLI (overridable via environment variables) @@ -71,7 +77,6 @@ def bootstrap( answers_file: Optional[Annotated[typer.FileText, typer.Option("--answers_file", "-a", help="The answers file to be used for bootstrapping.")]] = None, defaults: Annotated[str, typer.Option("--vcs_ref", "-r", help="Bootstrap with defaults.")] = False, ): - print("Boostrapping a plugin project from template.") with Worker( src_path=template_url, dst_path=destination, @@ -85,23 +90,36 @@ def bootstrap( @app.command(help="Installs plugins into a Python environment.") def install( install_manifest: Annotated[typer.FileText, typer.Option("--install_manifest", "-i", help="The install manifest describing which plugins to install.")] = DEFAULT_INSTALL_MANIFEST, + installer: Annotated[str, typer.Option("--installer", "-c", help="The install command to install plugins.")] = DEFAULT_INSTALLER, ): - print("Installing plugins") + typer.echo(f"Installing plugin packages from {install_manifest.name}") + data = yaml.safe_load(install_manifest) + manifest = InstallManifest.model_validate(data) + for pkg in manifest.packages: + typer.echo(f"Installing plugin package {pkg.package} from {pkg.repository}") + repository = os.path.expandvars(pkg.repository) + if pkg.extras: + cmd = f"{installer} \"{pkg.package}[{','.join(pkg.extras)}]@{repository}\"" + else: + cmd = f'{installer} "{pkg.package}@{repository}"' + subprocess.run(cmd, shell=True) @app.command(help="Builds an MCP server to serve plugins as tools") def package( - install_manifest: Annotated[typer.FileText, typer.Option("--install_manifest", "-i", help="The install manifest describing which plugins to install.")] = DEFAULT_INSTALL_MANIFEST, image_tag: Annotated[str, typer.Option("--image_tag", "-t", help="The container image tag to generated container.")] = DEFAULT_IMAGE_TAG, containerfile: Annotated[Path, typer.Option("--containerfile", "-c", help="The Dockerfile used to build the container.")] = DEFAULT_CONTAINERFILE_PATH, builder: Annotated[str, typer.Option("--builder", "-b", help="The container builder, compatible with docker build.")] = DEFAULT_IMAGE_BUILDER, + build_context: Annotated[Path, typer.Option("--build_context", "-p", help="The container builder context, specified as a path.")] = DEFAULT_BUILD_CONTEXT, ): - print("Deleting user: Hiro Hamada") + typer.echo("Building MCP server image") + cmd = f"{builder} -f {containerfile} -t {image_tag} {build_context}" + subprocess.run(cmd, shell=True) def main() -> None: # noqa: D401 - imperative mood is fine here app() -if __name__ == "__main__": - app() +if __name__ == "__main__": # pragma: no cover - executed only when run directly + main() diff --git a/mcpgateway/plugins/tools/models.py b/mcpgateway/plugins/tools/models.py new file mode 100644 index 000000000..244a94e8a --- /dev/null +++ b/mcpgateway/plugins/tools/models.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +"""MCP Plugins CLI models for schema validation. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Fred Araujo + +This module defines models for schema validation. +""" + +# Standard + +# Third-Party +from pydantic import BaseModel + + +class InstallManifestPackage(BaseModel): + """ + A single install manifest record containing the specification of what plugin + packages and dependencies to be installed from a repository. + """ + + package: str + repository: str + extras: list[str] | None = None + + +class InstallManifest(BaseModel): + """ + An install manifest containing a list of records describing what plugin + packages and dependencies to be installed. + """ + + packages: list[InstallManifestPackage] From 650da9a0fe9892ba0091643a8f65427770f4d63c Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 11 Aug 2025 23:50:01 -0400 Subject: [PATCH 07/65] fix: remote avoid insecure shell=True in subprocess invocation Signed-off-by: Frederico Araujo --- mcpgateway/plugins/tools/cli.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index 3e2c056ea..c1fcfaac8 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -98,11 +98,12 @@ def install( for pkg in manifest.packages: typer.echo(f"Installing plugin package {pkg.package} from {pkg.repository}") repository = os.path.expandvars(pkg.repository) + cmd = installer.split(" ") if pkg.extras: - cmd = f"{installer} \"{pkg.package}[{','.join(pkg.extras)}]@{repository}\"" + cmd.append(f"{pkg.package}[{','.join(pkg.extras)}]@{repository}") else: - cmd = f'{installer} "{pkg.package}@{repository}"' - subprocess.run(cmd, shell=True) + cmd.append(f"{pkg.package}@{repository}") + subprocess.run(cmd) @app.command(help="Builds an MCP server to serve plugins as tools") @@ -113,8 +114,9 @@ def package( build_context: Annotated[Path, typer.Option("--build_context", "-p", help="The container builder context, specified as a path.")] = DEFAULT_BUILD_CONTEXT, ): typer.echo("Building MCP server image") - cmd = f"{builder} -f {containerfile} -t {image_tag} {build_context}" - subprocess.run(cmd, shell=True) + cmd = builder.split(" ") + cmd.extend(["-f", containerfile, "-t", image_tag, build_context]) + subprocess.run(cmd) def main() -> None: # noqa: D401 - imperative mood is fine here From 4f7b88be6f0e1e5747a0b10d4ad1456be1889bd3 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 12:22:06 -0400 Subject: [PATCH 08/65] feat: add external plugin template Signed-off-by: Frederico Araujo --- mcpgateway/plugins/tools/cli.py | 4 +- plugin_templates/external/.ruff.toml | 63 ++++++++ plugin_templates/external/MANIFEST.in.jinja | 69 ++++++++ plugin_templates/external/Makefile.jinja | 149 ++++++++++++++++++ plugin_templates/external/README.md.jinja | 30 ++++ plugin_templates/external/__init__.py.jinja | 24 +++ plugin_templates/external/copier.yml | 29 ++++ plugin_templates/external/plugin.py.jinja | 35 ++++ .../external/pyproject.toml.jinja | 87 ++++++++++ .../resources/config/config.yaml.jinja | 11 ++ plugin_templates/external/tests/pytest.ini | 13 ++ ...ce(' ', '_').replace('-', '_') }}.py.jinja | 16 ++ 12 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 plugin_templates/external/.ruff.toml create mode 100644 plugin_templates/external/MANIFEST.in.jinja create mode 100644 plugin_templates/external/Makefile.jinja create mode 100644 plugin_templates/external/README.md.jinja create mode 100644 plugin_templates/external/__init__.py.jinja create mode 100644 plugin_templates/external/copier.yml create mode 100644 plugin_templates/external/plugin.py.jinja create mode 100644 plugin_templates/external/pyproject.toml.jinja create mode 100644 plugin_templates/external/resources/config/config.yaml.jinja create mode 100644 plugin_templates/external/tests/pytest.ini create mode 100644 plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index c1fcfaac8..48b92b203 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -44,7 +44,9 @@ # --------------------------------------------------------------------------- # Configuration defaults # --------------------------------------------------------------------------- -DEFAULT_TEMPLATE_URL = "https://github.com/IBM/mcp-context-forge-plugins-template.git" +DEFAULT_TEMPLATE_BASE_URL = "https://github.com/IBM/mcp-context-forge.git" +DEFAULT_TEMPLATE_TYPE = "external" +DEFAULT_TEMPLATE_URL = f"{DEFAULT_TEMPLATE_BASE_URL}::plugin_templates/{DEFAULT_TEMPLATE_TYPE}" DEFAULT_PROJECT_DIR = Path("./.") DEFAULT_INSTALL_MANIFEST = Path("plugins/install.yaml") DEFAULT_IMAGE_TAG = "contextforge-plugin:latest" # TBD: add plugin name and version diff --git a/plugin_templates/external/.ruff.toml b/plugin_templates/external/.ruff.toml new file mode 100644 index 000000000..443a275df --- /dev/null +++ b/plugin_templates/external/.ruff.toml @@ -0,0 +1,63 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", + "docs", + "test" +] + +# 200 line length +line-length = 200 +indent-width = 4 + +# Assume Python 3.11 +target-version = "py311" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" diff --git a/plugin_templates/external/MANIFEST.in.jinja b/plugin_templates/external/MANIFEST.in.jinja new file mode 100644 index 000000000..80fc569e3 --- /dev/null +++ b/plugin_templates/external/MANIFEST.in.jinja @@ -0,0 +1,69 @@ +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# MANIFEST.in - source-distribution contents for {{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }} +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# 1๏ธโƒฃ Core project files that SDists/Wheels should always carry +include LICENSE +include README.md +include plugins/README.md +include pyproject.toml +include Containerfile +include __init__ + +# 2๏ธโƒฃ Top-level config, examples and helper scripts +include *.py +include *.md +include *.example +include *.lock +include *.properties +include *.toml +include *.yaml +include *.yml +include *.json +include *.sh +include *.txt +recursive-include async_testing *.py +recursive-include async_testing *.yaml + +# 3๏ธโƒฃ Tooling/lint configuration dot-files (explicit so they're not lost) +include .env.make +include .interrogaterc +include .jshintrc +include whitesource.config +include .darglint +include .dockerignore +include .flake8 +include .htmlhintrc +include .pycodestyle +include .pylintrc +include .whitesource +include .coveragerc +# include .gitignore # purely optional but many projects ship it +include .bumpversion.cfg +include .yamllint +include .editorconfig +include .snyk + +# 4๏ธโƒฃ Runtime data that lives *inside* the package at import time +recursive-include resources/plugins *.yaml +recursive-include {{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }} *.yaml + +# 5๏ธโƒฃ (Optional) include MKDocs-based docs in the sdist +# graft docs + +# 6๏ธโƒฃ Never publish caches, compiled or build outputs, deployment, agent_runtimes, etc. +global-exclude __pycache__ *.py[cod] *.so *.dylib +prune build +prune dist +prune .eggs +prune *.egg-info +prune charts +prune k8s +prune .devcontainer +exclude CLAUDE.* +exclude llms-full.txt + +# Exclude deployment, mcp-servers and agent_runtimes +prune deployment +prune mcp-servers +prune agent_runtimes diff --git a/plugin_templates/external/Makefile.jinja b/plugin_templates/external/Makefile.jinja new file mode 100644 index 000000000..d549d5c47 --- /dev/null +++ b/plugin_templates/external/Makefile.jinja @@ -0,0 +1,149 @@ + +REQUIRED_BUILD_BINS := uv + +SHELL := /bin/bash +.SHELLFLAGS := -eu -o pipefail -c + +# Project variables +PACKAGE_NAME = denylist +CF_PROJECT_NAME = mcpgateway +TARGET ?= src + +# Virtual-environment variables +VENVS_DIR := $(HOME)/.venv +VENV_DIR := $(VENVS_DIR)/$(CF_PROJECT_NAME) + +### Linters +black: + @echo "๐ŸŽจ black $(TARGET)..." && $(VENV_DIR)/bin/black -l 200 $(TARGET) + +black-check: + @echo "๐ŸŽจ black --check $(TARGET)..." && $(VENV_DIR)/bin/black -l 200 --check --diff $(TARGET) + +ruff: + @echo "โšก ruff $(TARGET)..." && $(VENV_DIR)/bin/ruff check $(TARGET) && $(VENV_DIR)/bin/ruff format $(TARGET) + +ruff-check: + @echo "โšก ruff check $(TARGET)..." && $(VENV_DIR)/bin/ruff check $(TARGET) + +ruff-fix: + @echo "โšก ruff check --fix $(TARGET)..." && $(VENV_DIR)/bin/ruff check --fix $(TARGET) + +ruff-format: + @echo "โšก ruff format $(TARGET)..." && $(VENV_DIR)/bin/ruff format $(TARGET) + +### Targets +.PHONY: install +install: + $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`))) + @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m uv pip install ." + +.PHONY: uninstall +uninstall: + pip uninstall $(PACKAGE_NAME) + +.PHONY: dist +dist: clean ## Build wheel + sdist into ./dist + @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv + @/bin/bash -eu -c "\ + source $(VENV_DIR)/bin/activate && \ + python3 -m pip install --quiet --upgrade pip build && \ + python3 -m build" + @echo '๐Ÿ›  Wheel & sdist written to ./dist' + +.PHONY: wheel +wheel: ## Build wheel only + @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv + @/bin/bash -eu -c "\ + source $(VENV_DIR)/bin/activate && \ + python3 -m pip install --quiet --upgrade pip build && \ + python3 -m build -w" + @echo '๐Ÿ›  Wheel written to ./dist' + +.PHONY: sdist +sdist: ## Build source distribution only + @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv + @/bin/bash -eu -c "\ + source $(VENV_DIR)/bin/activate && \ + python3 -m pip install --quiet --upgrade pip build && \ + python3 -m build -s" + @echo '๐Ÿ›  Source distribution written to ./dist' + +.PHONY: verify +verify: dist ## Build, run metadata & manifest checks + @/bin/bash -c "source $(VENV_DIR)/bin/activate && \ + twine check dist/* && \ + check-manifest && \ + pyroma -d ." + @echo "โœ… Package verified - ready to publish." + +.PHONY: lint-fix +lint-fix: + @# Handle file arguments + @target_file="$(word 2,$(MAKECMDGOALS))"; \ + if [ -n "$$target_file" ] && [ "$$target_file" != "" ]; then \ + actual_target="$$target_file"; \ + else \ + actual_target="$(TARGET)"; \ + fi; \ + for target in $$(echo $$actual_target); do \ + if [ ! -e "$$target" ]; then \ + echo "โŒ File/directory not found: $$target"; \ + exit 1; \ + fi; \ + done; \ + echo "๐Ÿ”ง Fixing lint issues in $$actual_target..."; \ + $(MAKE) --no-print-directory black TARGET="$$actual_target"; \ + $(MAKE) --no-print-directory isort TARGET="$$actual_target"; \ + $(MAKE) --no-print-directory ruff-fix TARGET="$$actual_target" + +.PHONY: lint-check +lint-check: + @# Handle file arguments + @target_file="$(word 2,$(MAKECMDGOALS))"; \ + if [ -n "$$target_file" ] && [ "$$target_file" != "" ]; then \ + actual_target="$$target_file"; \ + else \ + actual_target="$(TARGET)"; \ + fi; \ + for target in $$(echo $$actual_target); do \ + if [ ! -e "$$target" ]; then \ + echo "โŒ File/directory not found: $$target"; \ + exit 1; \ + fi; \ + done; \ + echo "๐Ÿ”ง Fixing lint issues in $$actual_target..."; \ + $(MAKE) --no-print-directory black-check TARGET="$$actual_target"; \ + $(MAKE) --no-print-directory isort-check TARGET="$$actual_target"; \ + $(MAKE) --no-print-directory ruff-check TARGET="$$actual_target" + +.PHONY: lock +lock: + $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`. Please run `make init`))) + uv lock + +.PHONY: test +test: + pytest tests + +.PHONY: clean +clean: + find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete + rm -rf *.egg-info src/*.egg-info .pytest_cache tests/.pytest_cache build dist .ruff_cache .coverage + +.PHONY: help +help: + @echo "This Makefile is offered for convenience." + @echo "" + @echo "The following are the valid targets for this Makefile:" + @echo "...install Install package from sources" + @echo "...uninstall Uninstall package" + @echo "...dist Clean-build wheel *and* sdist into ./dist + @echo "...wheel Build wheel only + @echo "...sdist Build source distribution only + @echo "...verify Build + twine + check-manifest + pyroma (no upload) + @echo "...lock Lock dependencies" + @echo "...lint-fix Check and fix lint errors" + @echo "...lint-check Check for lint errors" + @echo "...test Run all tests" + @echo "...clean Remove all artifacts and builds" diff --git a/plugin_templates/external/README.md.jinja b/plugin_templates/external/README.md.jinja new file mode 100644 index 000000000..dcdc59a79 --- /dev/null +++ b/plugin_templates/external/README.md.jinja @@ -0,0 +1,30 @@ +# {{plugin_name}} for Context Forge MCP Gateway + +{{description}}. + + +## Installation + + +## Setting up the development environment + + +## Testing + +Test modules are created under the `tests` directory. + +To run all tests, use the following command: + +``` +make test +``` + +**Note:** To enable logging, set `log_cli = true` in `tests/pytest.ini`. + +## Code Linting + +Before checking in any code for the project, please lint the code. This can be done using: + +``` +make lint-fix +``` diff --git a/plugin_templates/external/__init__.py.jinja b/plugin_templates/external/__init__.py.jinja new file mode 100644 index 000000000..6ba2422aa --- /dev/null +++ b/plugin_templates/external/__init__.py.jinja @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +"""MCP Gateway {{plugin_name}} Plugin - {{description}}. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: {{author}} + +""" +import importlib.metadata + +# Package version +try: + __version__ = importlib.metadata.version("{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}") +except Exception: + __version__ = {{version}} + +__author__ = {{author}} +__copyright__ = "Copyright 2025" +__license__ = "Apache 2.0" +__description__ = "{{description}}" +__url__ = "https://ibm.github.io/mcp-context-forge/" +__download_url__ = "https://github.com/IBM/mcp-context-forge" +__packages__ = ["{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}"] + diff --git a/plugin_templates/external/copier.yml b/plugin_templates/external/copier.yml new file mode 100644 index 000000000..1437334aa --- /dev/null +++ b/plugin_templates/external/copier.yml @@ -0,0 +1,29 @@ +_exclude: + - CHANGELOG.md + - copier.yml + - .copier-answers.yml + - .git + - .github/CODEOWNERS + +plugin_name: + type: str + help: What is the plugin name? (e.g., MyFilter) + +version: + type: str + help: What is the initial version of the plugin? + default: 0.1.0 + +author: + type: str + help: Who is the author? + default: "[[_copier_conf.git_user.name]]" + +e-mail: + type: str + help: What's the author's email? + default: "[[_copier_conf.git_user.email]]" + +description: + type: str + help: Write a plugin description diff --git a/plugin_templates/external/plugin.py.jinja b/plugin_templates/external/plugin.py.jinja new file mode 100644 index 000000000..aedb845db --- /dev/null +++ b/plugin_templates/external/plugin.py.jinja @@ -0,0 +1,35 @@ +"""A custom skill.""" +from logging import Logger +from typing import Dict + +from skills_sdk.plugins.core.base import BaseSkill +from skills_sdk.plugins.core.schemas import TaskIn, TaskOut + + +{% set class_parts = plugin_name.replace(' ', '_').replace('-','_').split('_') -%} +{% if class_parts|length > 1 -%} +{% set class_name = class_parts|map('capitalize')|join -%} +{% else -%} +{% set class_name = class_parts|join -%} +{% endif -%} +class {{ class_name }}(BaseSkill): + """A custom skill for ...""" + + def __init__(self, logger: Logger, config: Dict = None) -> None: + """Entry init block for ... + + Args: + logger: logger that the skill can make use of + config: the skill configuration + """ + super().__init__(logger, config) + + def run(self, input: TaskIn) -> TaskOut: + """Start the main skill flow. + + Args: + input: input object for the skill + Returns: + The skill output + """ + return TaskOut(output="implement me") diff --git a/plugin_templates/external/pyproject.toml.jinja b/plugin_templates/external/pyproject.toml.jinja new file mode 100644 index 000000000..0944de2d1 --- /dev/null +++ b/plugin_templates/external/pyproject.toml.jinja @@ -0,0 +1,87 @@ +# ---------------------------------------------------------------- +# ๐Ÿ’ก Build system (PEP 517) +# - setuptools โ‰ฅ 77 gives SPDX licence support (PEP 639) +# - wheel is needed by most build front-ends +# ---------------------------------------------------------------- +[build-system] +requires = ["setuptools>=77", "wheel"] +build-backend = "setuptools.build_meta" + +# ---------------------------------------------------------------- +# ๐Ÿ“ฆ Core project metadata (PEP 621) +# ---------------------------------------------------------------- +[project] +name = "{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}" +version = "{{version}}" +description = "{{description}}" +keywords = ["MCP","API","gateway","tools", + "agents","agentic ai","model context protocol","multi-agent","fastapi", + "json-rpc","sse","websocket","federation","security","authentication" +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Framework :: FastAPI", + "Framework :: AsyncIO", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Topic :: Software Development :: Libraries :: Application Frameworks" +] +readme = "README.md" +requires-python = ">=3.11,<3.14" + +# SPDX licence expression + explicit licence file (PEP 639) +license = "Apache-2.0" +license-files = ["LICENSE"] + +# Maintainers +maintainers = [ + {name = "{{author}}", email = "{{email}}"} +] + +# Authors +authors = [ + {name = "{{author}}", email = "{{email}}"} +] + +# URLs +urls = [ + Homepage = "https://ibm.github.io/mcp-context-forge/" + Documentation = "https://ibm.github.io/mcp-context-forge/" + Repository = "https://github.com/IBM/mcp-context-forge" + "Bug Tracker" = "https://github.com/IBM/mcp-context-forge/issues" + Changelog = "https://github.com/IBM/mcp-context-forge/blob/main/CHANGELOG.md" +] + +# ---------------------------------------------------------------- +# Runtime dependencies +# ---------------------------------------------------------------- +packages = [{include = "{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}", from = "src"}] +dependencies = [ + "chuk-mcp-runtime>=0.6.5", + "chuk-mcp[full]>=0.5.1", + "mcp-contextforge-gateway", +] + +[tool.uv.sources] +mcp-contextforge-gateway = { git = "https://github.com/terylt/mcp-context-forge.git", rev = "feat/remote_plugin_support" } + +# -------------------------------------------------------------------- +# ๐Ÿ”ง setuptools-specific configuration +# -------------------------------------------------------------------- +[tool.setuptools] +include-package-data = true # ensure wheels include the data files + +# Automatic discovery: keep every package that starts with "{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}" +[tool.setuptools.packages.find] +include = ["{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}*"] +exclude = ["tests*"] + +## Runtime data files ------------------------------------------------ +[tool.setuptools.package-data] +{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }} = [ + "resources/plugins/config.yaml", +] diff --git a/plugin_templates/external/resources/config/config.yaml.jinja b/plugin_templates/external/resources/config/config.yaml.jinja new file mode 100644 index 000000000..e22032313 --- /dev/null +++ b/plugin_templates/external/resources/config/config.yaml.jinja @@ -0,0 +1,11 @@ +logging: + # Setting the log level: CRITICAL, ERROR, WARNING, INFO, DEBUG + level: 'DEBUG' +plugin: + configpath: src/{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }} + codepaths: + - src + - resources/plugins + plugins: + - {{plugin_name}} + - {{plugin_name}}Custom diff --git a/plugin_templates/external/tests/pytest.ini b/plugin_templates/external/tests/pytest.ini new file mode 100644 index 000000000..d08a0a52f --- /dev/null +++ b/plugin_templates/external/tests/pytest.ini @@ -0,0 +1,13 @@ +[pytest] +log_cli = false +log_cli_level = INFO +log_cli_format = %(asctime)s [%(module)s] [%(levelname)s] %(message)s +log_cli_date_format = %Y-%m-%d %H:%M:%S +log_level = INFO +log_format = %(asctime)s [%(module)s] [%(levelname)s] %(message)s +log_date_format = %Y-%m-%d %H:%M:%S +addopts = --cov --cov-report term-missing +env_files = .env +pythonpath = src +filterwarnings = + ignore::DeprecationWarning:pydantic.* \ No newline at end of file diff --git a/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja b/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja new file mode 100644 index 000000000..106f3ec86 --- /dev/null +++ b/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja @@ -0,0 +1,16 @@ +"""Tests for custom skill.""" +import pytest +from skills_sdk.loader.plugin import PluginLoader + + +@pytest.fixture(scope="module", autouse=True) +def plugin(): + """Loads plugin loader and skill.""" + loader = PluginLoader("./resources/config/config.yaml") + plugin = loader.get_skill("{{ plugin_name }}") + yield plugin + + +def test_{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}(plugin): + """Implement me.""" + assert True From ba20be2205575d7738f9d3711f21111d1942f742 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 14:55:11 -0400 Subject: [PATCH 09/65] feat: move copier config to repository root Signed-off-by: Frederico Araujo --- .gitignore | 1 + plugin_templates/external/copier.yml => copier.yml | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) rename plugin_templates/external/copier.yml => copier.yml (71%) diff --git a/.gitignore b/.gitignore index 4944c598f..d75a78298 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,7 @@ command_outputs.md .ica.env ./snakefood.png ./snakefood.svg +./playground .attic .vscode/settings.json diff --git a/plugin_templates/external/copier.yml b/copier.yml similarity index 71% rename from plugin_templates/external/copier.yml rename to copier.yml index 1437334aa..17ff5fd39 100644 --- a/plugin_templates/external/copier.yml +++ b/copier.yml @@ -1,9 +1,11 @@ -_exclude: - - CHANGELOG.md - - copier.yml - - .copier-answers.yml - - .git - - .github/CODEOWNERS +_subdirectory: plugin_templates/{{template_type}} + +template_type: + type: str + help: What plugin template would you like to use? + choices: + - native + - external plugin_name: type: str From a0c87c64592d8a04cc3629de4280d4f4b32f507a Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 15:09:30 -0400 Subject: [PATCH 10/65] feat: update copier template Signed-off-by: Frederico Araujo --- copier.yml | 8 ++++---- mcpgateway/plugins/tools/cli.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/copier.yml b/copier.yml index 17ff5fd39..0f159f6e8 100644 --- a/copier.yml +++ b/copier.yml @@ -19,12 +19,12 @@ version: author: type: str help: Who is the author? - default: "[[_copier_conf.git_user.name]]" + default: {{_copier_conf.git_user.name}} -e-mail: +email: type: str - help: What's the author's email? - default: "[[_copier_conf.git_user.email]]" + help: What's the author's e-mail? + default: {{_copier_conf.git_user.email}} description: type: str diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index 48b92b203..c1065e65a 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -77,7 +77,7 @@ def bootstrap( template_url: Annotated[str, typer.Option("--template_url", "-u", help="The URL to the plugins copier template.")] = DEFAULT_TEMPLATE_URL, vcs_ref: Annotated[str, typer.Option("--vcs_ref", "-r", help="The version control system tag/branch/commit to use for the template.")] = DEFAULT_VCS_REF, answers_file: Optional[Annotated[typer.FileText, typer.Option("--answers_file", "-a", help="The answers file to be used for bootstrapping.")]] = None, - defaults: Annotated[str, typer.Option("--vcs_ref", "-r", help="Bootstrap with defaults.")] = False, + defaults: Annotated[bool, typer.Option("--defaults", help="Bootstrap with defaults.")] = False, ): with Worker( src_path=template_url, From 36ef96614c17e2a3438e6fe7561d022045432cce Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 15:28:29 -0400 Subject: [PATCH 11/65] feat: get default author from git config Signed-off-by: Frederico Araujo --- copier.yml | 4 ++-- mcpgateway/plugins/tools/cli.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/copier.yml b/copier.yml index 0f159f6e8..b4bf7064a 100644 --- a/copier.yml +++ b/copier.yml @@ -19,12 +19,12 @@ version: author: type: str help: Who is the author? - default: {{_copier_conf.git_user.name}} + default: {{_copier_conf.data.default_author_name}} email: type: str help: What's the author's e-mail? - default: {{_copier_conf.git_user.email}} + default: {{_copier_conf.data.default_author_email}} description: type: str diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index c1065e65a..fc1a6370c 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -47,6 +47,8 @@ DEFAULT_TEMPLATE_BASE_URL = "https://github.com/IBM/mcp-context-forge.git" DEFAULT_TEMPLATE_TYPE = "external" DEFAULT_TEMPLATE_URL = f"{DEFAULT_TEMPLATE_BASE_URL}::plugin_templates/{DEFAULT_TEMPLATE_TYPE}" +DEFAULT_AUTHOR_NAME = "" +DEFAULT_AUTHOR_EMAIL = "" DEFAULT_PROJECT_DIR = Path("./.") DEFAULT_INSTALL_MANIFEST = Path("plugins/install.yaml") DEFAULT_IMAGE_TAG = "contextforge-plugin:latest" # TBD: add plugin name and version @@ -67,6 +69,28 @@ rich_markup_mode=None if markup_mode == "disabled" else markup_mode, ) +# --------------------------------------------------------------------------- +# Utility functions +# --------------------------------------------------------------------------- + + +def git_user_name() -> str: + """Return the current git user name from the environment.""" + try: + res = subprocess.run(["git", "config", "user.name"], stdout=subprocess.PIPE) + return res.stdout.strip().decode() if not res.returncode else DEFAULT_AUTHOR_NAME + except Exception: + return DEFAULT_AUTHOR_NAME + + +def git_user_email() -> str: + """Return the current git user email from the environment.""" + try: + res = subprocess.run(["git", "config", "user.email"], stdout=subprocess.PIPE) + return res.stdout.strip().decode() if not res.returncode else DEFAULT_AUTHOR_EMAIL + except Exception: + return DEFAULT_AUTHOR_EMAIL + # --------------------------------------------------------------------------- # Commands @@ -85,6 +109,7 @@ def bootstrap( answers_file=answers_file, defaults=defaults, vcs_ref=vcs_ref, + data={"default_author_name": git_user_name(), "default_author_email": git_user_email()}, ) as worker: worker.run_copy() From 72e8e0806dc083477e4188ca0bc79f6e1ab0e9b8 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 15:36:52 -0400 Subject: [PATCH 12/65] feat: update copier settings Signed-off-by: Frederico Araujo --- copier.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/copier.yml b/copier.yml index b4bf7064a..515b4b1c8 100644 --- a/copier.yml +++ b/copier.yml @@ -19,12 +19,12 @@ version: author: type: str help: Who is the author? - default: {{_copier_conf.data.default_author_name}} + default: {{ _copier_conf.data['default_author_name'] }} email: type: str help: What's the author's e-mail? - default: {{_copier_conf.data.default_author_email}} + default: {{ _copier_conf.data['default_author_email'] }} description: type: str From 04aa529caa48938de0215e7203764e0cf12749b4 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 20:41:06 -0400 Subject: [PATCH 13/65] fix: copier config syntax Signed-off-by: Frederico Araujo --- copier.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/copier.yml b/copier.yml index 515b4b1c8..3100bb0a0 100644 --- a/copier.yml +++ b/copier.yml @@ -19,12 +19,12 @@ version: author: type: str help: Who is the author? - default: {{ _copier_conf.data['default_author_name'] }} + default: "{{ _copier_conf.data.default_author_name }}" email: type: str help: What's the author's e-mail? - default: {{ _copier_conf.data['default_author_email'] }} + default: "{{ _copier_conf.data.default_author_email }}" description: type: str From f9ba5cf9d2ccfac753fb2a1f522daab9ab1fc3de Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 21:15:32 -0400 Subject: [PATCH 14/65] feat: add external plugin template modules Signed-off-by: Frederico Araujo --- .../resources/config/config.yaml.jinja | 45 +++++++--- .../__init__.py.jinja | 0 .../plugin-manifest.yaml.jinja | 9 ++ .../plugin.py.jinja | 90 +++++++++++++++++++ 4 files changed, 133 insertions(+), 11 deletions(-) rename plugin_templates/external/{ => {{ plugin_name.lower().replace(' ', '_') }}}/__init__.py.jinja (100%) create mode 100644 plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin-manifest.yaml.jinja create mode 100644 plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja diff --git a/plugin_templates/external/resources/config/config.yaml.jinja b/plugin_templates/external/resources/config/config.yaml.jinja index e22032313..69882cf5d 100644 --- a/plugin_templates/external/resources/config/config.yaml.jinja +++ b/plugin_templates/external/resources/config/config.yaml.jinja @@ -1,11 +1,34 @@ -logging: - # Setting the log level: CRITICAL, ERROR, WARNING, INFO, DEBUG - level: 'DEBUG' -plugin: - configpath: src/{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }} - codepaths: - - src - - resources/plugins - plugins: - - {{plugin_name}} - - {{plugin_name}}Custom +plugins: + - name: "{{ plugin_name }}" + {% set class_parts = plugin_name.replace(' ', '_').replace('-','_').split('_') -%} + {% if class_parts|length > 1 -%} + {% set class_name = class_parts|map('capitalize')|join -%} + {% else -%} + {% set class_name = class_parts|join -%} + {% endif -%} + kind: "{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}.plugin..{{ class_name }}" + description: "{{ description }}" + version: "{{ version }}" + author: "{{ author }}" + hooks: ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke"] + tags: ["plugin"] + mode: "enforce" # enforce | permissive | disabled + priority: 150 + conditions: + # Apply to specific tools/servers + - server_ids: [] # Apply to all servers + tenant_ids: [] # Apply to all tenants + config: + # Plugin config dict passed to the plugin constructor + +# Plugin directories to scan +plugin_dirs: + - "{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}" + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/plugin_templates/external/__init__.py.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja similarity index 100% rename from plugin_templates/external/__init__.py.jinja rename to plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin-manifest.yaml.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin-manifest.yaml.jinja new file mode 100644 index 000000000..5cc75c018 --- /dev/null +++ b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin-manifest.yaml.jinja @@ -0,0 +1,9 @@ +description: "{{description}}" +author: "{{author}}" +version: "{{version}}" +available_hooks: + - "prompt_pre_hook" + - "prompt_post_hook" + - "tool_pre_hook" + - "tool_post_hook" +default_configs: diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja new file mode 100644 index 000000000..0ee2ab3e5 --- /dev/null +++ b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +"""Simple example plugin for implementing a deny filter. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Fred Araujo + +This module loads configurations for plugins. +""" +# First-Party +from mcpgateway.plugins.framework.base import Plugin +from mcpgateway.plugins.framework.models import PluginConfig +from mcpgateway.plugins.framework.plugin_types import ( + PluginContext, + PromptPosthookPayload, + PromptPosthookResult, + PromptPrehookPayload, + PromptPrehookResult, + ToolPostInvokePayload, + ToolPostInvokeResult, + ToolPreInvokePayload, + ToolPreInvokeResult, +) + +{% set class_parts = plugin_name.replace(' ', '_').replace('-','_').split('_') -%} +{% if class_parts|length > 1 -%} +{% set class_name = class_parts|map('capitalize')|join -%} +{% else -%} +{% set class_name = class_parts|join -%} +{% endif -%} +class {{ class_name }}(Plugin): + """{{ description. }}""" + + def __init__(self, config: PluginConfig): + """Entry init block for plugin. + + Args: + logger: logger that the skill can make use of + config: the skill configuration + """ + super().__init__(config) + + + async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: + """The plugin hook run before a prompt is retrieved and rendered. + + Args: + payload: The prompt payload to be analyzed. + context: contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the prompt can proceed. + """ + pass + + async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult: + """Plugin hook run after a prompt is rendered. + + Args: + payload: The prompt payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the prompt can proceed. + """ + pass + + async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: + """Plugin hook run before a tool is invoked. + + Args: + payload: The tool payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the tool can proceed. + """ + pass + + async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: + """Plugin hook run after a tool is invoked. + + Args: + payload: The tool result payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the tool result should proceed. + """ + pass \ No newline at end of file From 442ee9e1ec2a5880d9bb52057c7ad7855a8b3cbe Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 21:18:52 -0400 Subject: [PATCH 15/65] fix: template syntax Signed-off-by: Frederico Araujo --- .../__init__.py.jinja | 1 - .../plugin.py.jinja | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja index 6ba2422aa..bf349e627 100644 --- a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja +++ b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """MCP Gateway {{plugin_name}} Plugin - {{description}}. Copyright 2025 diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja index 0ee2ab3e5..760d50485 100644 --- a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja +++ b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- -"""Simple example plugin for implementing a deny filter. +"""{{ description }}. Copyright 2025 SPDX-License-Identifier: Apache-2.0 -Authors: Fred Araujo +Authors: {{ author }} This module loads configurations for plugins. """ From 8e1dd2a90776dc2587e776ebe6350bd8c9892b4a Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 21:23:42 -0400 Subject: [PATCH 16/65] fix: template syntax Signed-off-by: Frederico Araujo --- plugin_templates/external/resources/config/config.yaml.jinja | 2 +- .../{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin_templates/external/resources/config/config.yaml.jinja b/plugin_templates/external/resources/config/config.yaml.jinja index 69882cf5d..ba5c19abe 100644 --- a/plugin_templates/external/resources/config/config.yaml.jinja +++ b/plugin_templates/external/resources/config/config.yaml.jinja @@ -6,7 +6,7 @@ plugins: {% else -%} {% set class_name = class_parts|join -%} {% endif -%} - kind: "{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}.plugin..{{ class_name }}" + kind: "{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}.plugin.{{ class_name }}" description: "{{ description }}" version: "{{ version }}" author: "{{ author }}" diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja index 760d50485..c74df9821 100644 --- a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja +++ b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja @@ -28,7 +28,7 @@ from mcpgateway.plugins.framework.plugin_types import ( {% set class_name = class_parts|join -%} {% endif -%} class {{ class_name }}(Plugin): - """{{ description. }}""" + """{{ description }}.""" def __init__(self, config: PluginConfig): """Entry init block for plugin. From 9203231778ff0d85362ebc74765a329f7e8f83dc Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 21:27:58 -0400 Subject: [PATCH 17/65] fix: make template Signed-off-by: Frederico Araujo --- plugin_templates/external/Makefile.jinja | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin_templates/external/Makefile.jinja b/plugin_templates/external/Makefile.jinja index d549d5c47..d94c9e654 100644 --- a/plugin_templates/external/Makefile.jinja +++ b/plugin_templates/external/Makefile.jinja @@ -7,7 +7,7 @@ SHELL := /bin/bash # Project variables PACKAGE_NAME = denylist CF_PROJECT_NAME = mcpgateway -TARGET ?= src +TARGET ?= {{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }} # Virtual-environment variables VENVS_DIR := $(HOME)/.venv @@ -129,7 +129,7 @@ test: .PHONY: clean clean: find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete - rm -rf *.egg-info src/*.egg-info .pytest_cache tests/.pytest_cache build dist .ruff_cache .coverage + rm -rf *.egg-info .pytest_cache tests/.pytest_cache build dist .ruff_cache .coverage .PHONY: help help: From 98c792483bb8a1eb1c01ce80b674c48ddd893154 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 22:19:17 -0400 Subject: [PATCH 18/65] feat: fix template issue Signed-off-by: Frederico Araujo --- plugin_templates/external/MANIFEST.in.jinja | 2 -- plugin_templates/external/Makefile.jinja | 2 -- .../external/pyproject.toml.jinja | 15 ++++++------ ...ce(' ', '_').replace('-', '_') }}.py.jinja | 19 +++++++++++---- .../__init__.py.jinja | 6 ++--- .../plugin.py.jinja | 23 ++++++++++--------- 6 files changed, 36 insertions(+), 31 deletions(-) diff --git a/plugin_templates/external/MANIFEST.in.jinja b/plugin_templates/external/MANIFEST.in.jinja index 80fc569e3..d4d969bf6 100644 --- a/plugin_templates/external/MANIFEST.in.jinja +++ b/plugin_templates/external/MANIFEST.in.jinja @@ -5,10 +5,8 @@ # 1๏ธโƒฃ Core project files that SDists/Wheels should always carry include LICENSE include README.md -include plugins/README.md include pyproject.toml include Containerfile -include __init__ # 2๏ธโƒฃ Top-level config, examples and helper scripts include *.py diff --git a/plugin_templates/external/Makefile.jinja b/plugin_templates/external/Makefile.jinja index d94c9e654..fac44d339 100644 --- a/plugin_templates/external/Makefile.jinja +++ b/plugin_templates/external/Makefile.jinja @@ -94,7 +94,6 @@ lint-fix: done; \ echo "๐Ÿ”ง Fixing lint issues in $$actual_target..."; \ $(MAKE) --no-print-directory black TARGET="$$actual_target"; \ - $(MAKE) --no-print-directory isort TARGET="$$actual_target"; \ $(MAKE) --no-print-directory ruff-fix TARGET="$$actual_target" .PHONY: lint-check @@ -114,7 +113,6 @@ lint-check: done; \ echo "๐Ÿ”ง Fixing lint issues in $$actual_target..."; \ $(MAKE) --no-print-directory black-check TARGET="$$actual_target"; \ - $(MAKE) --no-print-directory isort-check TARGET="$$actual_target"; \ $(MAKE) --no-print-directory ruff-check TARGET="$$actual_target" .PHONY: lock diff --git a/plugin_templates/external/pyproject.toml.jinja b/plugin_templates/external/pyproject.toml.jinja index 0944de2d1..953be2c85 100644 --- a/plugin_templates/external/pyproject.toml.jinja +++ b/plugin_templates/external/pyproject.toml.jinja @@ -48,18 +48,17 @@ authors = [ ] # URLs -urls = [ - Homepage = "https://ibm.github.io/mcp-context-forge/" - Documentation = "https://ibm.github.io/mcp-context-forge/" - Repository = "https://github.com/IBM/mcp-context-forge" - "Bug Tracker" = "https://github.com/IBM/mcp-context-forge/issues" - Changelog = "https://github.com/IBM/mcp-context-forge/blob/main/CHANGELOG.md" -] +[project.urls] +Homepage = "https://ibm.github.io/mcp-context-forge/" +Documentation = "https://ibm.github.io/mcp-context-forge/" +Repository = "https://github.com/IBM/mcp-context-forge" +"Bug Tracker" = "https://github.com/IBM/mcp-context-forge/issues" +Changelog = "https://github.com/IBM/mcp-context-forge/blob/main/CHANGELOG.md" # ---------------------------------------------------------------- # Runtime dependencies # ---------------------------------------------------------------- -packages = [{include = "{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}", from = "src"}] + dependencies = [ "chuk-mcp-runtime>=0.6.5", "chuk-mcp[full]>=0.5.1", diff --git a/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja b/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja index 106f3ec86..d78b81dcb 100644 --- a/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja +++ b/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja @@ -1,16 +1,25 @@ -"""Tests for custom skill.""" +"""Tests for plugin.""" + +# Third-Party import pytest -from skills_sdk.loader.plugin import PluginLoader + +# First-Party +from mcpgateway.plugins.framework.manager import PluginManager @pytest.fixture(scope="module", autouse=True) def plugin(): """Loads plugin loader and skill.""" - loader = PluginLoader("./resources/config/config.yaml") - plugin = loader.get_skill("{{ plugin_name }}") + plugin = PluginManager("./resources/config/config.yaml") + await plugin.initialize() yield plugin + await plugin.shutdown() -def test_{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}(plugin): +def test_prompt_pre_hook(plugin): """Implement me.""" + # payload = PromptPrehookPayload(...) + # global_context = GlobalContext(request_id="...") + # result, contexts = await plugin.prompt_pre_fetch(payload, global_context) + # assert result.modified_payload ... assert True diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja index bf349e627..4f59e09d7 100644 --- a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja +++ b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja @@ -5,19 +5,19 @@ SPDX-License-Identifier: Apache-2.0 Authors: {{author}} """ + import importlib.metadata # Package version try: __version__ = importlib.metadata.version("{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}") except Exception: - __version__ = {{version}} + __version__ = "{{version}}" -__author__ = {{author}} +__author__ = "{{author}}" __copyright__ = "Copyright 2025" __license__ = "Apache 2.0" __description__ = "{{description}}" __url__ = "https://ibm.github.io/mcp-context-forge/" __download_url__ = "https://github.com/IBM/mcp-context-forge" __packages__ = ["{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}"] - diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja index c74df9821..f0aa56630 100644 --- a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja +++ b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja @@ -6,21 +6,23 @@ Authors: {{ author }} This module loads configurations for plugins. """ + # First-Party from mcpgateway.plugins.framework.base import Plugin from mcpgateway.plugins.framework.models import PluginConfig from mcpgateway.plugins.framework.plugin_types import ( - PluginContext, - PromptPosthookPayload, - PromptPosthookResult, - PromptPrehookPayload, - PromptPrehookResult, - ToolPostInvokePayload, - ToolPostInvokeResult, - ToolPreInvokePayload, - ToolPreInvokeResult, + PluginContext, + PromptPosthookPayload, + PromptPosthookResult, + PromptPrehookPayload, + PromptPrehookResult, + ToolPostInvokePayload, + ToolPostInvokeResult, + ToolPreInvokePayload, + ToolPreInvokeResult, ) + {% set class_parts = plugin_name.replace(' ', '_').replace('-','_').split('_') -%} {% if class_parts|length > 1 -%} {% set class_name = class_parts|map('capitalize')|join -%} @@ -39,7 +41,6 @@ class {{ class_name }}(Plugin): """ super().__init__(config) - async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: """The plugin hook run before a prompt is retrieved and rendered. @@ -86,4 +87,4 @@ class {{ class_name }}(Plugin): Returns: The result of the plugin's analysis, including whether the tool result should proceed. """ - pass \ No newline at end of file + pass From 6b4edb94a63202de25474678f1c58ede195ef072 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Tue, 12 Aug 2025 22:28:25 -0400 Subject: [PATCH 19/65] fix: toml template Signed-off-by: Frederico Araujo --- .../external/pyproject.toml.jinja | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/plugin_templates/external/pyproject.toml.jinja b/plugin_templates/external/pyproject.toml.jinja index 953be2c85..20bd9cdae 100644 --- a/plugin_templates/external/pyproject.toml.jinja +++ b/plugin_templates/external/pyproject.toml.jinja @@ -32,21 +32,23 @@ classifiers = [ ] readme = "README.md" requires-python = ">=3.11,<3.14" - -# SPDX licence expression + explicit licence file (PEP 639) license = "Apache-2.0" license-files = ["LICENSE"] -# Maintainers maintainers = [ {name = "{{author}}", email = "{{email}}"} ] -# Authors authors = [ {name = "{{author}}", email = "{{email}}"} ] +dependencies = [ + "chuk-mcp-runtime>=0.6.5", + "chuk-mcp[full]>=0.5.1", + "mcp-contextforge-gateway", +] + # URLs [project.urls] Homepage = "https://ibm.github.io/mcp-context-forge/" @@ -55,16 +57,6 @@ Repository = "https://github.com/IBM/mcp-context-forge" "Bug Tracker" = "https://github.com/IBM/mcp-context-forge/issues" Changelog = "https://github.com/IBM/mcp-context-forge/blob/main/CHANGELOG.md" -# ---------------------------------------------------------------- -# Runtime dependencies -# ---------------------------------------------------------------- - -dependencies = [ - "chuk-mcp-runtime>=0.6.5", - "chuk-mcp[full]>=0.5.1", - "mcp-contextforge-gateway", -] - [tool.uv.sources] mcp-contextforge-gateway = { git = "https://github.com/terylt/mcp-context-forge.git", rev = "feat/remote_plugin_support" } From c1dcdc35235d409e977aacd19af247e90e66bb77 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 13 Aug 2025 16:27:19 -0400 Subject: [PATCH 20/65] fix: plugin mcp server initialization Signed-off-by: Frederico Araujo --- mcpgateway/plugins/framework/__init__.py | 19 +++++++++++++++++++ .../framework/external/mcp/__init__.py | 11 +++++++++++ .../plugins/framework/external/mcp/server.py | 16 +++++++++++----- .../plugins/framework/loader/__init__.py | 11 +++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 mcpgateway/plugins/framework/__init__.py create mode 100644 mcpgateway/plugins/framework/external/mcp/__init__.py create mode 100644 mcpgateway/plugins/framework/loader/__init__.py diff --git a/mcpgateway/plugins/framework/__init__.py b/mcpgateway/plugins/framework/__init__.py new file mode 100644 index 000000000..d9d6ebfa7 --- /dev/null +++ b/mcpgateway/plugins/framework/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +"""Plugin framework package. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Fred Araujo + +Exposes plugin framework components: +- Plugin (base class) +- PluginManager (loader) +- Models +""" + +# from mcpgateway.plugins.framework.base import Plugin +# # from mcpgateway.plugins.framework.external.mcp.server import run_plugin_mcp_server +# from mcpgateway.plugins.framework.loader.config import ConfigLoader +# from mcpgateway.plugins.framework.manager import PluginManager + +# __all__ = ["ConfigLoader", "Plugin", "PluginManager", "run_plugin_mcp_server"] diff --git a/mcpgateway/plugins/framework/external/mcp/__init__.py b/mcpgateway/plugins/framework/external/mcp/__init__.py new file mode 100644 index 000000000..385097dd1 --- /dev/null +++ b/mcpgateway/plugins/framework/external/mcp/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +"""External plugins package. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Fred Araujo + +Exposes external plugin components: +- server +- external plugin client +""" diff --git a/mcpgateway/plugins/framework/external/mcp/server.py b/mcpgateway/plugins/framework/external/mcp/server.py index 3da623184..0cc2059ae 100644 --- a/mcpgateway/plugins/framework/external/mcp/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server.py @@ -30,12 +30,18 @@ logger = logging.getLogger(__name__) config_file = os.environ.get("CFMCP_PLUGIN_CONFIG", "resources/plugins/config.yaml") -global_plugin_manager = PluginManager(config_file) +global_plugin_manager = None async def initialize() -> None: """Initialize the plugin manager with configured plugins.""" - await global_plugin_manager.initialize() + try: + global global_plugin_manager + global_plugin_manager = PluginManager(config_file) + await global_plugin_manager.initialize() + except Exception: + logger.exception("Could not initialize external plugin server.") + raise @mcp_tool(name="get_plugin_configs", description="Get the plugin configurations installed on the server") @@ -131,12 +137,12 @@ async def prompt_post_fetch(plugin_name: str, payload: dict, context: dict) -> d return convert_exception_to_error(ex, plugin_name=plugin_name).model_dump() -async def server_main(): +async def run_plugin_mcp_server(): """Initialize plugin manager and run mcp server.""" await initialize() await main_async() -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover - executed only when run directly # launch - asyncio.run(server_main()) + asyncio.run(run_plugin_mcp_server()) diff --git a/mcpgateway/plugins/framework/loader/__init__.py b/mcpgateway/plugins/framework/loader/__init__.py new file mode 100644 index 000000000..385097dd1 --- /dev/null +++ b/mcpgateway/plugins/framework/loader/__init__.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +"""External plugins package. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Fred Araujo + +Exposes external plugin components: +- server +- external plugin client +""" From 2cf7b46bc5a563dfe26af995dc208d5fbcbe0357 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Wed, 13 Aug 2025 16:28:09 -0400 Subject: [PATCH 21/65] feat: init module for plugin framework Signed-off-by: Frederico Araujo --- mcpgateway/plugins/framework/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mcpgateway/plugins/framework/__init__.py b/mcpgateway/plugins/framework/__init__.py index d9d6ebfa7..5726b6d21 100644 --- a/mcpgateway/plugins/framework/__init__.py +++ b/mcpgateway/plugins/framework/__init__.py @@ -11,9 +11,9 @@ - Models """ -# from mcpgateway.plugins.framework.base import Plugin -# # from mcpgateway.plugins.framework.external.mcp.server import run_plugin_mcp_server -# from mcpgateway.plugins.framework.loader.config import ConfigLoader -# from mcpgateway.plugins.framework.manager import PluginManager +from mcpgateway.plugins.framework.base import Plugin +from mcpgateway.plugins.framework.external.mcp.server import run_plugin_mcp_server +from mcpgateway.plugins.framework.loader.config import ConfigLoader +from mcpgateway.plugins.framework.manager import PluginManager -# __all__ = ["ConfigLoader", "Plugin", "PluginManager", "run_plugin_mcp_server"] +__all__ = ["ConfigLoader", "Plugin", "PluginManager", "run_plugin_mcp_server"] From aecc11b5403419f7f1b06e3f64d3e42c74962b0c Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Thu, 14 Aug 2025 18:47:58 -0400 Subject: [PATCH 22/65] feat: add chuck runtime and container wrapping Signed-off-by: Frederico Araujo --- plugin_templates/external/.dockerignore | 363 ++++++++++++++++++ plugin_templates/external/Containerfile | 47 +++ plugin_templates/external/Makefile.jinja | 296 +++++++++++++- plugin_templates/external/plugin.py.jinja | 35 -- .../external/pyproject.toml.jinja | 1 - .../resources/runtime/config.yaml.jinja | 71 ++++ plugin_templates/external/run-server.sh | 43 +++ .../__init__.py.jinja | 0 .../plugin-manifest.yaml.jinja | 0 .../plugin.py.jinja | 4 +- 10 files changed, 813 insertions(+), 47 deletions(-) create mode 100644 plugin_templates/external/.dockerignore create mode 100644 plugin_templates/external/Containerfile delete mode 100644 plugin_templates/external/plugin.py.jinja create mode 100644 plugin_templates/external/resources/runtime/config.yaml.jinja create mode 100755 plugin_templates/external/run-server.sh rename plugin_templates/external/{{{ plugin_name.lower().replace(' ', '_') }} => {{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}}/__init__.py.jinja (100%) rename plugin_templates/external/{{{ plugin_name.lower().replace(' ', '_') }} => {{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}}/plugin-manifest.yaml.jinja (100%) rename plugin_templates/external/{{{ plugin_name.lower().replace(' ', '_') }} => {{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}}/plugin.py.jinja (95%) diff --git a/plugin_templates/external/.dockerignore b/plugin_templates/external/.dockerignore new file mode 100644 index 000000000..e9a71f900 --- /dev/null +++ b/plugin_templates/external/.dockerignore @@ -0,0 +1,363 @@ +# syntax=docker/dockerfile:1 +#---------------------------------------------------------------------- +# Docker Build Context Optimization +# +# This .dockerignore file excludes unnecessary files from the Docker +# build context to improve build performance and security. +#---------------------------------------------------------------------- + +#---------------------------------------------------------------------- +# 1. Development and source directories (not needed in production) +#---------------------------------------------------------------------- +agent_runtimes/ +charts/ +deployment/ +docs/ +deployment/k8s/ +mcp-servers/ +tests/ +test/ +attic/ +*.md +.benchmarks/ + +# Development environment directories +.devcontainer/ +.github/ +.vscode/ +.idea/ + +#---------------------------------------------------------------------- +# 2. Version control +#---------------------------------------------------------------------- +.git/ +.gitignore +.gitattributes +.gitmodules + +#---------------------------------------------------------------------- +# 3. Python build artifacts and caches +#---------------------------------------------------------------------- +# Byte-compiled files +__pycache__/ +*.py[cod] +*.pyc +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +.wily/ + +# PyInstaller +*.manifest +*.spec + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ +.pytype/ + +# Cython debug symbols +cython_debug/ + +#---------------------------------------------------------------------- +# 4. Virtual environments +#---------------------------------------------------------------------- +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.python37/ +.python39/ +.python-version + +# PDM +pdm.lock +.pdm.toml +.pdm-python + +#---------------------------------------------------------------------- +# 5. Package managers and dependencies +#---------------------------------------------------------------------- +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.npm +.yarn + +# pip +pip-log.txt +pip-delete-this-directory.txt + +#---------------------------------------------------------------------- +# 6. Docker and container files (avoid recursive copies) +#---------------------------------------------------------------------- +Dockerfile +Dockerfile.* +Containerfile +Containerfile.* +docker-compose.yml +docker-compose.*.yml +podman-compose*.yaml +.dockerignore + +#---------------------------------------------------------------------- +# 7. IDE and editor files +#---------------------------------------------------------------------- +# JetBrains +.idea/ +*.iml +*.iws +*.ipr + +# VSCode +.vscode/ +*.code-workspace + +# Vim +*.swp +*.swo +*~ + +# Emacs +*~ +\#*\# +.\#* + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +#---------------------------------------------------------------------- +# 8. Build tools and CI/CD configurations +#---------------------------------------------------------------------- +# Testing configurations +.coveragerc +.pylintrc +.flake8 +pytest.ini +tox.ini +.pytest.ini + +# Linting and formatting +.hadolint.yaml +.pre-commit-config.yaml +.pycodestyle +.pyre_configuration +.pyspelling.yaml +.ruff.toml +.shellcheckrc + +# Build configurations +Makefile +setup.cfg +pyproject.toml.bak +MANIFEST.in + +# CI/CD +.travis.* +.gitlab-ci.yml +.circleci/ +.github/ +azure-pipelines.yml +Jenkinsfile + +# Code quality +sonar-code.properties +sonar-project.properties +.scannerwork/ +whitesource.config +.whitesource + +# Other tools +.bumpversion.cfg +.editorconfig +mypy.ini + +#---------------------------------------------------------------------- +# 9. Application runtime files (should not be in image) +#---------------------------------------------------------------------- +# Databases +*.db +*.sqlite +*.sqlite3 +mcp.db +db.sqlite3 + +# Logs +*.log +logs/ +log/ + +# Certificates and secrets +certs/ +*.pem +*.key +*.crt +*.csr +.env +.env.* + +# Generated files +public/ +static/ +media/ + +# Application instances +instance/ +local_settings.py + +#---------------------------------------------------------------------- +# 10. Framework-specific files +#---------------------------------------------------------------------- +# Django +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +media/ + +# Flask +instance/ +.webassets-cache + +# Scrapy +.scrapy + +# Sphinx documentation +docs/_build/ +docs/build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints +*.ipynb + +# IPython +profile_default/ +ipython_config.py + +# celery +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +#---------------------------------------------------------------------- +# 11. Backup and temporary files +#---------------------------------------------------------------------- +*.bak +*.backup +*.tmp +*.temp +*.orig +*.rej +.backup/ +backup/ +tmp/ +temp/ + +#---------------------------------------------------------------------- +# 12. Documentation and miscellaneous +#---------------------------------------------------------------------- +*.md +!README.md +LICENSE +CHANGELOG +AUTHORS +CONTRIBUTORS +TODO +TODO.md +DEVELOPING.md +CONTRIBUTING.md + +# Spelling +.spellcheck-en.txt +*.dic + +# Shell scripts (if not needed in container) +test.sh +scripts/test/ +scripts/dev/ + +#---------------------------------------------------------------------- +# 13. OS-specific files +#---------------------------------------------------------------------- +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# Linux +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* + +#---------------------------------------------------------------------- +# End of .dockerignore +#---------------------------------------------------------------------- diff --git a/plugin_templates/external/Containerfile b/plugin_templates/external/Containerfile new file mode 100644 index 000000000..d2d5f6748 --- /dev/null +++ b/plugin_templates/external/Containerfile @@ -0,0 +1,47 @@ +# syntax=docker/dockerfile:1.7 +ARG UBI=python-312-minimal + +FROM registry.access.redhat.com/ubi9/${UBI} AS builder + +ARG PYTHON_VERSION=3.12 + +ARG VERSION +ARG COMMIT_ID +ARG SKILLS_SDK_COMMIT_ID +ARG SKILLS_SDK_VERSION +ARG BUILD_TIME_SKILLS_INSTALL + +ENV APP_HOME=/app + +USER 0 + +# Image pre-requisites +RUN INSTALL_PKGS="git make gcc gcc-c++ python${PYTHON_VERSION}-devel" && \ + microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \ + microdnf -y clean all --enablerepo='*' + +# Setup alias from HOME to APP_HOME +RUN mkdir -p ${APP_HOME} && \ + chown -R 1001:0 ${APP_HOME} && \ + ln -s ${HOME} ${APP_HOME} && \ + mkdir -p ${HOME}/resources/config && \ + chown -R 1001:0 ${HOME}/resources/config + +USER 1001 + +# Install plugin package +COPY . . +RUN pip install --no-cache-dir uv && python -m uv pip install . + +# Make default cache directory writable +RUN mkdir -p -m 0776 ${HOME}/.cache + +# Update labels +LABEL maintainer="Context Forge MCP Gateway Team" \ + name="mcp/mcppluginserver" \ + version="${VERSION}" \ + url="https://github.com/IBM/mcp-context-forge" \ + description="MCP Plugin Server for the Context Forge MCP Gateway" + +# App entrypoint +ENTRYPOINT ["sh", "-c", "${HOME}/run-server.sh"] diff --git a/plugin_templates/external/Makefile.jinja b/plugin_templates/external/Makefile.jinja index fac44d339..fca7bfa24 100644 --- a/plugin_templates/external/Makefile.jinja +++ b/plugin_templates/external/Makefile.jinja @@ -5,15 +5,18 @@ SHELL := /bin/bash .SHELLFLAGS := -eu -o pipefail -c # Project variables -PACKAGE_NAME = denylist -CF_PROJECT_NAME = mcpgateway +PACKAGE_NAME = {{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }} +PROJECT_NAME = {{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }} TARGET ?= {{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }} # Virtual-environment variables VENVS_DIR := $(HOME)/.venv -VENV_DIR := $(VENVS_DIR)/$(CF_PROJECT_NAME) +VENV_DIR := $(VENVS_DIR)/$(PROJECT_NAME) + +# ============================================================================= +# Linters +# ============================================================================= -### Linters black: @echo "๐ŸŽจ black $(TARGET)..." && $(VENV_DIR)/bin/black -l 200 $(TARGET) @@ -32,7 +35,262 @@ ruff-fix: ruff-format: @echo "โšก ruff format $(TARGET)..." && $(VENV_DIR)/bin/ruff format $(TARGET) -### Targets +# ============================================================================= +# Container runtime configuration and operations +# ============================================================================= + +# Container resource limits +CONTAINER_MEMORY = 2048m +CONTAINER_CPUS = 2 + +# Auto-detect container runtime if not specified - DEFAULT TO DOCKER +CONTAINER_RUNTIME ?= $(shell command -v docker >/dev/null 2>&1 && echo docker || echo podman) + +# Alternative: Always default to docker unless explicitly overridden +# CONTAINER_RUNTIME ?= docker + +# Container port +CONTAINER_PORT ?= 8000 +CONTAINER_INTERNAL_PORT ?= 8000 + +print-runtime: + @echo Using container runtime: $(CONTAINER_RUNTIME) + +# Base image name (without any prefix) +IMAGE_BASE ?= mcpgateway/$(PROJECT_NAME) +IMAGE_TAG ?= latest + +# Handle runtime-specific image naming +ifeq ($(CONTAINER_RUNTIME),podman) + # Podman adds localhost/ prefix for local builds + IMAGE_LOCAL := localhost/$(IMAGE_BASE):$(IMAGE_TAG) + IMAGE_LOCAL_DEV := localhost/$(IMAGE_BASE)-dev:$(IMAGE_TAG) + IMAGE_PUSH := $(IMAGE_BASE):$(IMAGE_TAG) +else + # Docker doesn't add prefix + IMAGE_LOCAL := $(IMAGE_BASE):$(IMAGE_TAG) + IMAGE_LOCAL_DEV := $(IMAGE_BASE)-dev:$(IMAGE_TAG) + IMAGE_PUSH := $(IMAGE_BASE):$(IMAGE_TAG) +endif + +print-image: + @echo "๐Ÿณ Container Runtime: $(CONTAINER_RUNTIME)" + @echo "Using image: $(IMAGE_LOCAL)" + @echo "Development image: $(IMAGE_LOCAL_DEV)" + @echo "Push image: $(IMAGE_PUSH)" + +# Function to get the actual image name as it appears in image list +define get_image_name +$(shell $(CONTAINER_RUNTIME) images --format "{{.Repository}}:{{.Tag}}" | grep -E "(localhost/)?$(IMAGE_BASE):$(IMAGE_TAG)" | head -1) +endef + +# Function to normalize image name for operations +define normalize_image +$(if $(findstring localhost/,$(1)),$(1),$(if $(filter podman,$(CONTAINER_RUNTIME)),localhost/$(1),$(1))) +endef + +# Containerfile to use (can be overridden) +#CONTAINER_FILE ?= Containerfile +CONTAINER_FILE ?= $(shell [ -f "Containerfile" ] && echo "Containerfile" || echo "Dockerfile") + +# Define COMMA for the conditional Z flag +COMMA := , + +container-info: + @echo "๐Ÿณ Container Runtime Configuration" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "Runtime: $(CONTAINER_RUNTIME)" + @echo "Base Image: $(IMAGE_BASE)" + @echo "Tag: $(IMAGE_TAG)" + @echo "Local Image: $(IMAGE_LOCAL)" + @echo "Push Image: $(IMAGE_PUSH)" + @echo "Actual Image: $(call get_image_name)" + @echo "Container File: $(CONTAINER_FILE)" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + +# Auto-detect platform based on uname +PLATFORM ?= linux/$(shell uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') + +container-build: + @echo "๐Ÿ”จ Building with $(CONTAINER_RUNTIME) for platform $(PLATFORM)..." + $(CONTAINER_RUNTIME) build \ + --platform=$(PLATFORM) \ + -f $(CONTAINER_FILE) \ + --tag $(IMAGE_BASE):$(IMAGE_TAG) \ + . + @echo "โœ… Built image: $(call get_image_name)" + $(CONTAINER_RUNTIME) images $(IMAGE_BASE):$(IMAGE_TAG) + +container-run: container-check-image + @echo "๐Ÿš€ Running with $(CONTAINER_RUNTIME)..." + -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true + -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true + $(CONTAINER_RUNTIME) run --name $(PROJECT_NAME) \ + --env-file=.env \ + -p $(CONTAINER_PORT):$(CONTAINER_INTERNAL_PORT) \ + --restart=always \ + --memory=$(CONTAINER_MEMORY) --cpus=$(CONTAINER_CPUS) \ + --health-cmd="curl --fail http://localhost:$(CONTAINER_INTERNAL_PORT)/health || exit 1" \ + --health-interval=1m --health-retries=3 \ + --health-start-period=30s --health-timeout=10s \ + -d $(call get_image_name) + @sleep 2 + @echo "โœ… Container started" + @echo "๐Ÿ” Health check status:" + @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo "No health check configured" + +container-run-host: container-check-image + @echo "๐Ÿš€ Running with $(CONTAINER_RUNTIME)..." + -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true + -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true + $(CONTAINER_RUNTIME) run --name $(PROJECT_NAME) \ + --env-file=.env \ + --network=host \ + -p $(CONTAINER_PORT):$(CONTAINER_INTERNAL_PORT) \ + --restart=always \ + --memory=$(CONTAINER_MEMORY) --cpus=$(CONTAINER_CPUS) \ + --health-cmd="curl --fail http://localhost:$(CONTAINER_INTERNAL_PORT)/health || exit 1" \ + --health-interval=1m --health-retries=3 \ + --health-start-period=30s --health-timeout=10s \ + -d $(call get_image_name) + @sleep 2 + @echo "โœ… Container started" + @echo "๐Ÿ” Health check status:" + @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo "No health check configured" + +container-push: container-check-image + @echo "๐Ÿ“ค Preparing to push image..." + @# For Podman, we need to remove localhost/ prefix for push + @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ + actual_image=$$($(CONTAINER_RUNTIME) images --format "{{.Repository}}:{{.Tag}}" | grep -E "$(IMAGE_BASE):$(IMAGE_TAG)" | head -1); \ + if echo "$$actual_image" | grep -q "^localhost/"; then \ + echo "๐Ÿท๏ธ Tagging for push (removing localhost/ prefix)..."; \ + $(CONTAINER_RUNTIME) tag "$$actual_image" $(IMAGE_PUSH); \ + fi; \ + fi + $(CONTAINER_RUNTIME) push $(IMAGE_PUSH) + @echo "โœ… Pushed: $(IMAGE_PUSH)" + +container-check-image: + @echo "๐Ÿ” Checking for image..." + @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ + if ! $(CONTAINER_RUNTIME) image exists $(IMAGE_LOCAL) 2>/dev/null && \ + ! $(CONTAINER_RUNTIME) image exists $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null; then \ + echo "โŒ Image not found: $(IMAGE_LOCAL)"; \ + echo "๐Ÿ’ก Run 'make container-build' first"; \ + exit 1; \ + fi; \ + else \ + if ! $(CONTAINER_RUNTIME) images -q $(IMAGE_LOCAL) 2>/dev/null | grep -q . && \ + ! $(CONTAINER_RUNTIME) images -q $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null | grep -q .; then \ + echo "โŒ Image not found: $(IMAGE_LOCAL)"; \ + echo "๐Ÿ’ก Run 'make container-build' first"; \ + exit 1; \ + fi; \ + fi + @echo "โœ… Image found" + +container-stop: + @echo "๐Ÿ›‘ Stopping container..." + -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true + -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true + @echo "โœ… Container stopped and removed" + +container-logs: + @echo "๐Ÿ“œ Streaming logs (Ctrl+C to exit)..." + $(CONTAINER_RUNTIME) logs -f $(PROJECT_NAME) + +container-shell: + @echo "๐Ÿ”ง Opening shell in container..." + @if ! $(CONTAINER_RUNTIME) ps -q -f name=$(PROJECT_NAME) | grep -q .; then \ + echo "โŒ Container $(PROJECT_NAME) is not running"; \ + echo "๐Ÿ’ก Run 'make container-run' first"; \ + exit 1; \ + fi + @$(CONTAINER_RUNTIME) exec -it $(PROJECT_NAME) /bin/bash 2>/dev/null || \ + $(CONTAINER_RUNTIME) exec -it $(PROJECT_NAME) /bin/sh + +container-health: + @echo "๐Ÿฅ Checking container health..." + @if ! $(CONTAINER_RUNTIME) ps -q -f name=$(PROJECT_NAME) | grep -q .; then \ + echo "โŒ Container $(PROJECT_NAME) is not running"; \ + exit 1; \ + fi + @echo "Status: $$($(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo 'No health check')" + @echo "Logs:" + @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{range .State.Health.Log}}{{.Output}}{{end}}' 2>/dev/null || true + +container-build-multi: + @echo "๐Ÿ”จ Building multi-architecture image..." + @if [ "$(CONTAINER_RUNTIME)" = "docker" ]; then \ + if ! docker buildx inspect $(PROJECT_NAME)-builder >/dev/null 2>&1; then \ + echo "๐Ÿ“ฆ Creating buildx builder..."; \ + docker buildx create --name $(PROJECT_NAME)-builder; \ + fi; \ + docker buildx use $(PROJECT_NAME)-builder; \ + docker buildx build \ + --platform=linux/amd64,linux/arm64 \ + -f $(CONTAINER_FILE) \ + --tag $(IMAGE_BASE):$(IMAGE_TAG) \ + --push \ + .; \ + elif [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ + echo "๐Ÿ“ฆ Building manifest with Podman..."; \ + $(CONTAINER_RUNTIME) build --platform=linux/amd64,linux/arm64 \ + -f $(CONTAINER_FILE) \ + --manifest $(IMAGE_BASE):$(IMAGE_TAG) \ + .; \ + echo "๐Ÿ’ก To push: podman manifest push $(IMAGE_BASE):$(IMAGE_TAG)"; \ + else \ + echo "โŒ Multi-arch builds require Docker buildx or Podman"; \ + exit 1; \ + fi + +# Helper targets for debugging image issues +image-list: + @echo "๐Ÿ“‹ Images matching $(IMAGE_BASE):" + @$(CONTAINER_RUNTIME) images --format "table {{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Created}}\t{{.Size}}" | \ + grep -E "(IMAGE|$(IMAGE_BASE))" || echo "No matching images found" + +image-clean: + @echo "๐Ÿงน Removing all $(IMAGE_BASE) images..." + @$(CONTAINER_RUNTIME) images --format "{{.Repository}}:{{.Tag}}" | \ + grep -E "(localhost/)?$(IMAGE_BASE)" | \ + xargs $(XARGS_FLAGS) $(CONTAINER_RUNTIME) rmi -f 2>/dev/null + @echo "โœ… Images cleaned" + +# Fix image naming issues +image-retag: + @echo "๐Ÿท๏ธ Retagging images for consistency..." + @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ + if $(CONTAINER_RUNTIME) image exists $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null; then \ + $(CONTAINER_RUNTIME) tag $(IMAGE_BASE):$(IMAGE_TAG) $(IMAGE_LOCAL) 2>/dev/null || true; \ + fi; \ + else \ + if $(CONTAINER_RUNTIME) images -q $(IMAGE_LOCAL) 2>/dev/null | grep -q .; then \ + $(CONTAINER_RUNTIME) tag $(IMAGE_LOCAL) $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null || true; \ + fi; \ + fi + @echo "โœ… Images retagged" # This always shows success + +# Runtime switching helpers +use-docker: + @echo "export CONTAINER_RUNTIME=docker" + @echo "๐Ÿ’ก Run: export CONTAINER_RUNTIME=docker" + +use-podman: + @echo "export CONTAINER_RUNTIME=podman" + @echo "๐Ÿ’ก Run: export CONTAINER_RUNTIME=podman" + +show-runtime: + @echo "Current runtime: $(CONTAINER_RUNTIME)" + @echo "Detected from: $$(command -v $(CONTAINER_RUNTIME) || echo 'not found')" # Added + @echo "To switch: make use-docker or make use-podman" + +# ============================================================================= +# Targets +# ============================================================================= + .PHONY: install install: $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`))) @@ -124,6 +382,22 @@ lock: test: pytest tests +.PHONY: serve +serve: + @echo "Implement me." + +.PHONY: build +build: + @$(MAKE) container-build + +.PHONY: start +start: + @$(MAKE) container-run + +.PHONY: stop +stop: + @$(MAKE) container-stop + .PHONY: clean clean: find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete @@ -136,10 +410,14 @@ help: @echo "The following are the valid targets for this Makefile:" @echo "...install Install package from sources" @echo "...uninstall Uninstall package" - @echo "...dist Clean-build wheel *and* sdist into ./dist - @echo "...wheel Build wheel only - @echo "...sdist Build source distribution only - @echo "...verify Build + twine + check-manifest + pyroma (no upload) + @echo "...dist Clean-build wheel *and* sdist into ./dist" + @echo "...wheel Build wheel only" + @echo "...sdist Build source distribution only" + @echo "...verify Build + twine + check-manifest + pyroma (no upload)" + @echo "...serve Start API server locally" + @echo "...build Build API server container image" + @echo "...start Start the API server container" + @echo "...start Stop the API server container" @echo "...lock Lock dependencies" @echo "...lint-fix Check and fix lint errors" @echo "...lint-check Check for lint errors" diff --git a/plugin_templates/external/plugin.py.jinja b/plugin_templates/external/plugin.py.jinja deleted file mode 100644 index aedb845db..000000000 --- a/plugin_templates/external/plugin.py.jinja +++ /dev/null @@ -1,35 +0,0 @@ -"""A custom skill.""" -from logging import Logger -from typing import Dict - -from skills_sdk.plugins.core.base import BaseSkill -from skills_sdk.plugins.core.schemas import TaskIn, TaskOut - - -{% set class_parts = plugin_name.replace(' ', '_').replace('-','_').split('_') -%} -{% if class_parts|length > 1 -%} -{% set class_name = class_parts|map('capitalize')|join -%} -{% else -%} -{% set class_name = class_parts|join -%} -{% endif -%} -class {{ class_name }}(BaseSkill): - """A custom skill for ...""" - - def __init__(self, logger: Logger, config: Dict = None) -> None: - """Entry init block for ... - - Args: - logger: logger that the skill can make use of - config: the skill configuration - """ - super().__init__(logger, config) - - def run(self, input: TaskIn) -> TaskOut: - """Start the main skill flow. - - Args: - input: input object for the skill - Returns: - The skill output - """ - return TaskOut(output="implement me") diff --git a/plugin_templates/external/pyproject.toml.jinja b/plugin_templates/external/pyproject.toml.jinja index 20bd9cdae..a61e6df3e 100644 --- a/plugin_templates/external/pyproject.toml.jinja +++ b/plugin_templates/external/pyproject.toml.jinja @@ -45,7 +45,6 @@ authors = [ dependencies = [ "chuk-mcp-runtime>=0.6.5", - "chuk-mcp[full]>=0.5.1", "mcp-contextforge-gateway", ] diff --git a/plugin_templates/external/resources/runtime/config.yaml.jinja b/plugin_templates/external/resources/runtime/config.yaml.jinja new file mode 100644 index 000000000..629f5e7d9 --- /dev/null +++ b/plugin_templates/external/resources/runtime/config.yaml.jinja @@ -0,0 +1,71 @@ +# config.yaml +host: + name: "{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}" + log_level: "INFO" + +server: + type: "streamable-http" # "stdio" or "sse" or "streamable-http" + #auth: "bearer" # this line is needed to enable bearer auth + +# Logging configuration - controls all logging behavior +logging: + level: "WARNING" # Changed from INFO to WARNING for quieter default + format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + reset_handlers: true + quiet_libraries: true + + # Specific logger overrides to silence noisy components + loggers: + # Your existing overrides + "chuk_mcp_runtime.proxy": "WARNING" + "chuk_mcp_runtime.proxy.manager": "WARNING" + "chuk_mcp_runtime.proxy.tool_wrapper": "WARNING" + "chuk_tool_processor.mcp.stream_manager": "WARNING" + "chuk_tool_processor.mcp.register": "WARNING" + "chuk_tool_processor.mcp.setup_stdio": "WARNING" + "chuk_mcp_runtime.common.tool_naming": "WARNING" + "chuk_mcp_runtime.common.openai_compatibility": "WARNING" + + # NEW: Add the noisy loggers you're seeing + "chuk_sessions.session_manager": "ERROR" + "chuk_mcp_runtime.session.native": "ERROR" + "chuk_mcp_runtime.tools.artifacts": "ERROR" + "chuk_mcp_runtime.tools.session": "ERROR" + "chuk_artifacts.store": "ERROR" + "chuk_mcp_runtime.entry": "WARNING" # Keep some info but less chatty + "chuk_mcp_runtime.server": "WARNING" # Server start/stop messages + +# optional overrides +sse: + host: "0.0.0.0" + port: 8000 + sse_path: "/sse" + message_path: "/messages/" + health_path: "/health" + log_level: "info" + access_log: true + +streamable-http: + host: "0.0.0.0" + port: 8000 + mcp_path: "/mcp" + stateless: true + json_response: true + health_path: "/health" + log_level: "info" + access_log: true + +proxy: + enabled: false + namespace: "proxy" + openai_compatible: false # โ† set to true if you want underscores + +# Session tools (disabled by default - must enable explicitly) +session_tools: + enabled: false # Must explicitly enable + +# Artifact storage (disabled by default - must enable explicitly) +artifacts: + enabled: false # Must explicitly enable + storage_provider: "filesystem" + session_provider: "memory" \ No newline at end of file diff --git a/plugin_templates/external/run-server.sh b/plugin_templates/external/run-server.sh new file mode 100755 index 000000000..6a72aeb0b --- /dev/null +++ b/plugin_templates/external/run-server.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +#โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# Script : run-server.sh +# Purpose: Launch the MCP Gateway's Plugin API +# +# Description: +# This script launches an API server using +# chuck runtime. +# +# Environment Variables: +# API_SERVER_SCRIPT : Path to the server script (optional, auto-detected) +# CFMCP_PLUGIN_CONFIG : Path to the plugin config (optional, default: ./resources/plugins/config.yaml) +# CHUK_MCP_CONFIG_PATH : Path to the chuck-mcp-runtime config (optional, default: ./resources/runtime/config.yaml) +# +# Usage: +# ./run-server.sh # Run server +#โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +# Exit immediately on error, undefined variable, or pipe failure +set -euo pipefail + +#โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# SECTION 1: Script Location Detection +# Determine the absolute path of the API server script +#โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +if [[ -z "${API_SERVER_SCRIPT:-}" ]]; then + API_SERVER_SCRIPT="$(python -c 'import mcpgateway.plugins.framework.external.mcp.server as server; print(server.__file__)')" + echo "โœ“ API server script path auto-detected: ${API_SERVER_SCRIPT}" +else + echo "โœ“ Using provided API server script path: ${API_SERVER_SCRIPT}" +fi + +#โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# SECTION 2: Run the API server +# Run the API server from configuration +#โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +CFMCP_PLUGIN_CONFIG=${CFMCP_PLUGIN_CONFIG:-./resources/plugins/config.yaml} +CHUK_MCP_CONFIG_PATH=${CHUK_MCP_CONFIG_PATH:-./resources/runtime/config.yaml} + +echo "โœ“ Using plugin config from: ${CFMCP_PLUGIN_CONFIG}" +echo "โœ“ Running API server with config from: ${CHUK_MCP_CONFIG_PATH}" +python ${API_SERVER_SCRIPT} \ No newline at end of file diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/__init__.py.jinja similarity index 100% rename from plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/__init__.py.jinja rename to plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/__init__.py.jinja diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin-manifest.yaml.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin-manifest.yaml.jinja similarity index 100% rename from plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin-manifest.yaml.jinja rename to plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin-manifest.yaml.jinja diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin.py.jinja similarity index 95% rename from plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja rename to plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin.py.jinja index f0aa56630..d42a2dca7 100644 --- a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_') }}/plugin.py.jinja +++ b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin.py.jinja @@ -9,8 +9,8 @@ This module loads configurations for plugins. # First-Party from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.models import PluginConfig -from mcpgateway.plugins.framework.plugin_types import ( +from mcpgateway.plugins.framework.models import ( + PluginConfig, PluginContext, PromptPosthookPayload, PromptPosthookResult, From 7b0c3825b3fa5aadd505d885a5ba55683f7e7d95 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Thu, 14 Aug 2025 18:56:47 -0400 Subject: [PATCH 23/65] fix: makefile template Signed-off-by: Frederico Araujo --- plugin_templates/external/Makefile.jinja | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugin_templates/external/Makefile.jinja b/plugin_templates/external/Makefile.jinja index fca7bfa24..7d40e9148 100644 --- a/plugin_templates/external/Makefile.jinja +++ b/plugin_templates/external/Makefile.jinja @@ -79,6 +79,8 @@ print-image: @echo "Development image: $(IMAGE_LOCAL_DEV)" @echo "Push image: $(IMAGE_PUSH)" +{% raw %} + # Function to get the actual image name as it appears in image list define get_image_name $(shell $(CONTAINER_RUNTIME) images --format "{{.Repository}}:{{.Tag}}" | grep -E "(localhost/)?$(IMAGE_BASE):$(IMAGE_TAG)" | head -1) @@ -287,6 +289,8 @@ show-runtime: @echo "Detected from: $$(command -v $(CONTAINER_RUNTIME) || echo 'not found')" # Added @echo "To switch: make use-docker or make use-podman" +{% endraw %} + # ============================================================================= # Targets # ============================================================================= From d216957b484ef7cdbffd5fa9c235440e3e20a0a1 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Thu, 14 Aug 2025 19:03:06 -0400 Subject: [PATCH 24/65] fix: plugins config path Signed-off-by: Frederico Araujo --- .../external/resources/{config => plugins}/config.yaml.jinja | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plugin_templates/external/resources/{config => plugins}/config.yaml.jinja (100%) diff --git a/plugin_templates/external/resources/config/config.yaml.jinja b/plugin_templates/external/resources/plugins/config.yaml.jinja similarity index 100% rename from plugin_templates/external/resources/config/config.yaml.jinja rename to plugin_templates/external/resources/plugins/config.yaml.jinja From dccd5709897724a0f3a0d20602994db3f1207d47 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Thu, 14 Aug 2025 19:12:04 -0400 Subject: [PATCH 25/65] feat: add .env.template Signed-off-by: Frederico Araujo --- .../plugins/framework/external/mcp/server.py | 2 +- plugin_templates/external/.env.template | 316 ++++++++++++++++++ plugin_templates/external/run-server.sh | 6 +- 3 files changed, 320 insertions(+), 4 deletions(-) create mode 100644 plugin_templates/external/.env.template diff --git a/mcpgateway/plugins/framework/external/mcp/server.py b/mcpgateway/plugins/framework/external/mcp/server.py index 0cc2059ae..e4ecd0b20 100644 --- a/mcpgateway/plugins/framework/external/mcp/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server.py @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) -config_file = os.environ.get("CFMCP_PLUGIN_CONFIG", "resources/plugins/config.yaml") +config_file = os.environ.get("PLUGINS_CONFIG_PATH", os.path.join(".", "resources", "plugins", "config.yaml")) global_plugin_manager = None diff --git a/plugin_templates/external/.env.template b/plugin_templates/external/.env.template new file mode 100644 index 000000000..bffafc8ef --- /dev/null +++ b/plugin_templates/external/.env.template @@ -0,0 +1,316 @@ +##################################### +# Basic Application Settings +##################################### + +# The public-facing name of your gateway (used in docs/UI) +APP_NAME=MCP_Gateway + +# Bind address for HTTP server (0.0.0.0 for all interfaces) +HOST=0.0.0.0 + +# TCP port to listen on +PORT=4444 + +# SQLAlchemy connection string; leave as-is for local dev +# Examples: +# sqlite:///./mcp.db (local file-based) +# postgresql://user:pass@localhost:5432/mcp +DATABASE_URL=sqlite:///./mcp.db + +# Optional root path for deployments under a subpath (e.g., /api) +# Leave empty for default (/) +# Example: APP_ROOT_PATH=/gateway +APP_ROOT_PATH= + +##################################### +# Database Connection Pooling +##################################### + +# Size of the SQLAlchemy connection pool (default is 200) +DB_POOL_SIZE=200 + +# Number of extra connections allowed beyond pool size +DB_MAX_OVERFLOW=10 + +# Max time (in seconds) to wait for a connection from pool +DB_POOL_TIMEOUT=30 + +# Recycle database connections after N seconds +DB_POOL_RECYCLE=3600 + +# Maximum number of times to boot database connection for cold start +DB_MAX_RETRIES=3 + +# Interval time for next retry of database connection +DB_RETRY_INTERVAL_MS=2000 + +##################################### +# Cache Backend +##################################### + +# Backend for caching (memory, redis, database, or none) +CACHE_TYPE=database + +# If using Redis: provide full connection URL +# Example: redis://localhost:6379/0 +REDIS_URL=redis://localhost:6379/0 + +# Prefix for cache keys (recommended to avoid collisions) +CACHE_PREFIX=mcpgw: + +# TTL for user sessions (in seconds) +SESSION_TTL=3600 + +# TTL for ephemeral messages (like completions) in seconds +MESSAGE_TTL=600 + +# Maximum number of times to boot redis connection for cold start +REDIS_MAX_RETRIES=3 + +# Interval time for next retry of redis connection +REDIS_RETRY_INTERVAL_MS=2000 + +##################################### +# Protocol Settings +##################################### + +# MCP protocol version supported by this gateway +PROTOCOL_VERSION=2025-03-26 + +##################################### +# Authentication +##################################### + +# Admin UI basic-auth credentials +BASIC_AUTH_USER=admin +BASIC_AUTH_PASSWORD=changeme + +# Require any form of authentication (true or false) +AUTH_REQUIRED=true + +# Secret used to sign JWTs (use long random value in prod) +JWT_SECRET_KEY=my-test-key + +# Algorithm used to sign JWTs (e.g., HS256) +JWT_ALGORITHM=HS256 + +# Expiry time for generated JWT tokens (in minutes; e.g. 7 days) +TOKEN_EXPIRY=10080 + +# Require all JWT tokens to have expiration claims (true or false) +REQUIRE_TOKEN_EXPIRATION=false + + +# Used to derive an AES encryption key for secure auth storage +# Must be a non-empty string (e.g. passphrase or random secret) +AUTH_ENCRYPTION_SECRET=my-test-salt + +##################################### +# Admin UI and API Toggles +##################################### + +# Enable the visual Admin UI (true/false) +MCPGATEWAY_UI_ENABLED=true + +# Enable the Admin API endpoints (true/false) +MCPGATEWAY_ADMIN_API_ENABLED=true + +##################################### +# Security and CORS +##################################### + +# Skip TLS certificate checks for upstream requests (not recommended in prod) +SKIP_SSL_VERIFY=false + +# CORS origin allowlist (use JSON array of URLs) +# Example: ["http://localhost:3000"] +# Do not quote this value. Start with [] to ensure it's valid JSON. +ALLOWED_ORIGINS='["http://localhost", "http://localhost:4444"]' + +# Enable CORS handling in the gateway +CORS_ENABLED=true + +# Enable HTTP Basic Auth for docs endpoints (in addition to Bearer token auth) +# Uses the same credentials as BASIC_AUTH_USER and BASIC_AUTH_PASSWORD +DOCS_ALLOW_BASIC_AUTH=false + +##################################### +# Retry Config for HTTP Requests +##################################### + +RETRY_MAX_ATTEMPTS=3 +# seconds +RETRY_BASE_DELAY=1.0 +# seconds +RETRY_MAX_DELAY=60.0 +# fraction of delay +RETRY_JITTER_MAX=0.5 + +##################################### +# Logging +##################################### + +# Logging verbosity level: DEBUG, INFO, WARNING, ERROR, CRITICAL +LOG_LEVEL=INFO + +# Log format: json or text +LOG_FORMAT=json + +# Optional path to write logs to file (leave empty to log to stdout) +#LOG_FILE=./logs/gateway.log + +##################################### +# Transport Configuration +##################################### + +# Transport mechanisms to expose (comma-separated): http, ws, sse, stdio, all +TRANSPORT_TYPE=all + +# WebSocket ping interval (seconds) +WEBSOCKET_PING_INTERVAL=30 + +# SSE client retry timeout (milliseconds) +SSE_RETRY_TIMEOUT=5000 + + +##################################### +# Streamabe HTTP Transport Configuration +##################################### + +# Set False to use stateless sessions without event store and True for stateful sessions +USE_STATEFUL_SESSIONS=false + +# Set true for JSON responses, false for SSE streams +JSON_RESPONSE_ENABLED=true + + +##################################### +# Federation +##################################### + +# Enable federated mode (discovery + peer syncing) +FEDERATION_ENABLED=true + +# Automatically discover peer gateways (e.g., via mDNS) +FEDERATION_DISCOVERY=false + +# Explicit list of peers (JSON array of URLs). Do not quote this value. +FEDERATION_PEERS='[]' + +# Timeout for federated requests (in seconds) +FEDERATION_TIMEOUT=30 + +# Frequency of syncing peers (in seconds) +FEDERATION_SYNC_INTERVAL=300 + +##################################### +# Resources +##################################### + +# Maximum number of resources to keep in memory cache +RESOURCE_CACHE_SIZE=1000 + +# Time-to-live for resource cache (seconds) +RESOURCE_CACHE_TTL=3600 + +# Maximum allowed size for a resource (in bytes, e.g., 10MB) +MAX_RESOURCE_SIZE=10485760 + +##################################### +# Tools +##################################### + +# Max execution time for tools (in seconds) +TOOL_TIMEOUT=60 + +# Number of retry attempts for failed tools +MAX_TOOL_RETRIES=3 + +# Max number of tool invocations per minute +TOOL_RATE_LIMIT=100 + +# Number of tools that can run simultaneously +TOOL_CONCURRENT_LIMIT=10 + +##################################### +# Prompts +##################################### + +# How many prompt templates to cache +PROMPT_CACHE_SIZE=100 + +# Max size (in bytes) for a prompt template +MAX_PROMPT_SIZE=102400 + +# Timeout for rendering prompt templates (in seconds) +PROMPT_RENDER_TIMEOUT=10 + +##################################### +# Gateway Validation +##################################### + +# Timeout for gateway validation (in seconds) +GATEWAY_VALIDATION_TIMEOUT=5 + +##################################### +# Health Checks +##################################### + +# How often to poll peer gateways (in seconds) +HEALTH_CHECK_INTERVAL=60 + +# Timeout for a single health check request (seconds) +HEALTH_CHECK_TIMEOUT=10 + +# Number of failed checks before marking peer unhealthy +UNHEALTHY_THRESHOLD=3 + +##################################### +# Lock file Settings +##################################### + +# This path is append with the system temp directory. +# It is used to ensure that only one instance of the gateway health Check can run at a time. +# saved dir in /tmp/gateway_healthcheck_init.lock +FILELOCK_NAME=gateway_healthcheck_init.lock + +##################################### +# Development Settings +##################################### + +# Enable dev mode features (e.g. schema debug) +DEV_MODE=false + +# Reload app on code changes (dev only) +RELOAD=false + +# Enable verbose logging/debug traces +DEBUG=false + +# Gateway tool name separator +GATEWAY_TOOL_NAME_SEPARATOR=- +VALID_SLUG_SEPARATOR_REGEXP= r"^(-{1,2}|[_.])$" + +##################################### +# Plugins Settings +##################################### + +# Enable the plugin framework +PLUGINS_ENABLED=false + +# Enable auto-completion for plugins CLI +PLUGINS_CLI_COMPLETION=false + +# Set markup mode for plugins CLI +# Valid options: +# rich: use rich markup +# markdown: allow markdown in help strings +# disabled: disable markup +# If unset (commented out), uses "rich" if rich is detected, otherwise disables it. +PLUGINS_CLI_MARKUP_MODE=rich + +# Configuration path for plugin loader +PLUGINS_CONFIG=./resources/plugins/config.yaml + +# Configuration path for chuck mcp runtime +CHUK_MCP_CONFIG_PATH=./resources/runtime/config.yaml \ No newline at end of file diff --git a/plugin_templates/external/run-server.sh b/plugin_templates/external/run-server.sh index 6a72aeb0b..57959dea5 100755 --- a/plugin_templates/external/run-server.sh +++ b/plugin_templates/external/run-server.sh @@ -9,7 +9,7 @@ # # Environment Variables: # API_SERVER_SCRIPT : Path to the server script (optional, auto-detected) -# CFMCP_PLUGIN_CONFIG : Path to the plugin config (optional, default: ./resources/plugins/config.yaml) +# PLUGINS_CONFIG_PATH : Path to the plugin config (optional, default: ./resources/plugins/config.yaml) # CHUK_MCP_CONFIG_PATH : Path to the chuck-mcp-runtime config (optional, default: ./resources/runtime/config.yaml) # # Usage: @@ -35,9 +35,9 @@ fi # Run the API server from configuration #โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -CFMCP_PLUGIN_CONFIG=${CFMCP_PLUGIN_CONFIG:-./resources/plugins/config.yaml} +PLUGINS_CONFIG_PATH=${PLUGINS_CONFIG_PATH:-./resources/plugins/config.yaml} CHUK_MCP_CONFIG_PATH=${CHUK_MCP_CONFIG_PATH:-./resources/runtime/config.yaml} -echo "โœ“ Using plugin config from: ${CFMCP_PLUGIN_CONFIG}" +echo "โœ“ Using plugin config from: ${PLUGINS_CONFIG_PATH}" echo "โœ“ Running API server with config from: ${CHUK_MCP_CONFIG_PATH}" python ${API_SERVER_SCRIPT} \ No newline at end of file From d9169b750e00b2f583d05d5a3e5e950ffa7b58c6 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 15 Aug 2025 18:01:48 -0400 Subject: [PATCH 26/65] feat: add tools and resources support Signed-off-by: Frederico Araujo --- mcpgateway/plugins/framework/base.py | 5 +- .../plugins/framework/external/mcp/server.py | 164 +++++++++++++++--- 2 files changed, 146 insertions(+), 23 deletions(-) diff --git a/mcpgateway/plugins/framework/base.py b/mcpgateway/plugins/framework/base.py index b41e8260d..b0a05740d 100644 --- a/mcpgateway/plugins/framework/base.py +++ b/mcpgateway/plugins/framework/base.py @@ -30,6 +30,7 @@ PromptPosthookResult, PromptPrehookPayload, PromptPrehookResult, + ResourcePreFetchResult, ToolPostInvokePayload, ToolPostInvokeResult, ToolPreInvokePayload, @@ -217,7 +218,7 @@ async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: Plugin """ ) - async def resource_pre_fetch(self, payload, context): + async def resource_pre_fetch(self, payload, context) -> ResourcePreFetchResult: """Plugin hook run before a resource is fetched. Args: @@ -233,7 +234,7 @@ async def resource_pre_fetch(self, payload, context): """ ) - async def resource_post_fetch(self, payload, context): + async def resource_post_fetch(self, payload, context) -> ResourcePreFetchResult: """Plugin hook run after a resource is fetched. Args: diff --git a/mcpgateway/plugins/framework/external/mcp/server.py b/mcpgateway/plugins/framework/external/mcp/server.py index e4ecd0b20..b138cb584 100644 --- a/mcpgateway/plugins/framework/external/mcp/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server.py @@ -4,29 +4,47 @@ Copyright 2025 SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor + Fred Araujo Module that contains plugin MCP server code to serve external plugins. """ + # Standard import asyncio import logging import os +from typing import Any, Callable, Dict, Type, TypeVar # Third-Party from chuk_mcp_runtime.common.mcp_tool_decorator import mcp_tool from chuk_mcp_runtime.entry import main_async +from pydantic import BaseModel # First-Party +from mcpgateway.plugins.framework import Plugin from mcpgateway.plugins.framework.errors import convert_exception_to_error from mcpgateway.plugins.framework.loader.config import ConfigLoader from mcpgateway.plugins.framework.manager import DEFAULT_PLUGIN_TIMEOUT, PluginManager from mcpgateway.plugins.framework.models import ( PluginContext, PluginErrorModel, + PluginResult, PromptPosthookPayload, + PromptPosthookResult, PromptPrehookPayload, + PromptPrehookResult, + ResourcePostFetchPayload, + ResourcePostFetchResult, + ResourcePreFetchPayload, + ResourcePreFetchResult, + ToolPostInvokePayload, + ToolPostInvokeResult, + ToolPreInvokePayload, + ToolPreInvokeResult, ) +P = TypeVar("P", bound=BaseModel) + logger = logging.getLogger(__name__) config_file = os.environ.get("PLUGINS_CONFIG_PATH", os.path.join(".", "resources", "plugins", "config.yaml")) @@ -75,11 +93,14 @@ async def get_plugin_config(name: str) -> dict: return None -@mcp_tool(name="prompt_pre_fetch", description="Execute prompt prefetch hook for a plugin") -async def prompt_pre_fetch(plugin_name: str, payload: dict, context: dict) -> dict: - """Invoke the prompt pre fetch hook for a particular plugin. +async def _invoke_hook( + payload_model: Type[P], hook_function: Callable[[Plugin], Callable[[P, PluginContext], PluginResult]], plugin_name: str, payload: Dict[str, Any], context: Dict[str, Any] +) -> dict: + """Invoke a plugin hook. Args: + payload_model: the type of the payload accepted for the hook. + hook_function: the hook function to be invoked. plugin_name: the name of the plugin to execute. payload: the prompt name and arguments to be analyzed. context: the contextual and state information required for the execution of the hook. @@ -94,9 +115,9 @@ async def prompt_pre_fetch(plugin_name: str, payload: dict, context: dict) -> di plugin = global_plugin_manager.get_plugin(plugin_name) try: if plugin: - prepayload = PromptPrehookPayload.model_validate(payload) - precontext = PluginContext.model_validate(context) - result = await asyncio.wait_for(plugin.prompt_pre_fetch(prepayload, precontext), plugin_timeout) + _payload = payload_model.model_validate(payload) + _context = PluginContext.model_validate(context) + result = await asyncio.wait_for(hook_function(plugin, _payload, _context), plugin_timeout) return result.model_dump() raise ValueError(f"Unable to retrieve plugin {plugin_name} to execute.") except asyncio.TimeoutError: @@ -106,8 +127,30 @@ async def prompt_pre_fetch(plugin_name: str, payload: dict, context: dict) -> di return convert_exception_to_error(ex, plugin_name=plugin_name).model_dump() +@mcp_tool(name="prompt_pre_fetch", description="Execute prompt prefetch hook for a plugin") +async def prompt_pre_fetch(plugin_name: str, payload: Dict[str, Any], context: Dict[str, Any]) -> dict: + """Invoke the prompt pre fetch hook for a particular plugin. + + Args: + plugin_name: the name of the plugin to execute. + payload: the prompt name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. + + Raises: + ValueError: if unable to retrieve a plugin. + + Returns: + The transformed or filtered response from the plugin hook. + """ + + def prompt_pre_fetch_func(plugin: Plugin, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: + return plugin.prompt_pre_fetch(payload, context) + + return await _invoke_hook(PromptPrehookPayload, prompt_pre_fetch_func, plugin_name, payload, context) + + @mcp_tool(name="prompt_post_fetch", description="Execute prompt postfetch hook for a plugin") -async def prompt_post_fetch(plugin_name: str, payload: dict, context: dict) -> dict: +async def prompt_post_fetch(plugin_name: str, payload: Dict[str, Any], context: Dict[str, Any]) -> dict: """Call plugin's prompt post-fetch hook. Args: @@ -121,20 +164,99 @@ async def prompt_post_fetch(plugin_name: str, payload: dict, context: dict) -> d Returns: The result of the plugin execution. """ - plugin_timeout = global_plugin_manager.config.plugin_settings.plugin_timeout if global_plugin_manager.config else DEFAULT_PLUGIN_TIMEOUT - plugin = global_plugin_manager.get_plugin(plugin_name) - try: - if plugin: - postpayload = PromptPosthookPayload.model_validate(payload) - postcontext = PluginContext.model_validate(context) - result = await asyncio.wait_for(plugin.prompt_post_fetch(postpayload, postcontext), plugin_timeout) - return result.model_dump() - raise ValueError(f"Unable to retrieve plugin {plugin_name} to execute.") - except asyncio.TimeoutError: - return PluginErrorModel(message=f"Plugin {plugin_name} timed out from execution after {plugin_timeout} seconds.", plugin_name=plugin_name).model_dump() - except Exception as ex: - logger.exception(ex) - return convert_exception_to_error(ex, plugin_name=plugin_name).model_dump() + + def prompt_post_fetch_func(plugin: Plugin, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult: + return plugin.prompt_post_fetch(payload, context) + + return await _invoke_hook(PromptPosthookPayload, prompt_post_fetch_func, plugin_name, payload, context) + + +@mcp_tool(name="tool_pre_invoke", description="Execute tool pre-invoke hook for a plugin") +async def tool_pre_invoke(plugin_name: str, payload: Dict[str, Any], context: Dict[str, Any]) -> dict: + """Invoke the tool pre-invoke hook for a particular plugin. + + Args: + plugin_name: the name of the plugin to execute. + payload: the tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. + + Raises: + ValueError: if unable to retrieve a plugin. + + Returns: + The transformed or filtered response from the plugin hook. + """ + + def tool_pre_invoke_func(plugin: Plugin, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: + return plugin.tool_pre_invoke(payload, context) + + return await _invoke_hook(ToolPreInvokePayload, tool_pre_invoke_func, plugin_name, payload, context) + + +@mcp_tool(name="tool_post_invoke", description="Execute tool post-invoke hook for a plugin") +async def tool_post_invoke(plugin_name: str, payload: Dict[str, Any], context: Dict[str, Any]) -> dict: + """Invoke the tool post-invoke hook for a particular plugin. + + Args: + plugin_name: the name of the plugin to execute. + payload: the tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. + + Raises: + ValueError: if unable to retrieve a plugin. + + Returns: + The transformed or filtered response from the plugin hook. + """ + + def tool_post_invoke_func(plugin: Plugin, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: + return plugin.tool_post_invoke(payload, context) + + return await _invoke_hook(ToolPostInvokePayload, tool_post_invoke_func, plugin_name, payload, context) + + +@mcp_tool(name="resource_pre_fetch", description="Execute resource prefetch hook for a plugin") +async def resource_pre_fetch(plugin_name: str, payload: Dict[str, Any], context: Dict[str, Any]) -> dict: + """Invoke the resource pre fetch hook for a particular plugin. + + Args: + plugin_name: the name of the plugin to execute. + payload: the resource name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. + + Raises: + ValueError: if unable to retrieve a plugin. + + Returns: + The transformed or filtered response from the plugin hook. + """ + + def resource_pre_fetch_func(plugin: Plugin, payload: ResourcePreFetchPayload, context: PluginContext) -> ResourcePreFetchResult: + return plugin.resource_pre_fetch(payload, context) + + return await _invoke_hook(ResourcePreFetchPayload, resource_pre_fetch_func, plugin_name, payload, context) + + +@mcp_tool(name="resource_post_fetch", description="Execute resource postfetch hook for a plugin") +async def resource_post_fetch(plugin_name: str, payload: Dict[str, Any], context: Dict[str, Any]) -> dict: + """Call plugin's resource post-fetch hook. + + Args: + plugin_name: The name of the plugin to execute. + payload: The resource payload to be analyzed. + context: Contextual information about the hook call. + + Raises: + ValueError: if unable to retrieve a plugin. + + Returns: + The result of the plugin execution. + """ + + def resource_post_fetch_func(plugin: Plugin, payload: ResourcePostFetchPayload, context: PluginContext) -> ResourcePostFetchResult: + return plugin.resource_post_fetch(payload, context) + + return await _invoke_hook(ResourcePostFetchPayload, resource_post_fetch_func, plugin_name, payload, context) async def run_plugin_mcp_server(): From dbcafab92f40dfebe0d993421d74305125af0519 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 15 Aug 2025 18:10:45 -0400 Subject: [PATCH 27/65] fix: lint yaml Signed-off-by: Frederico Araujo --- copier.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/copier.yml b/copier.yml index 3100bb0a0..7f9a7148d 100644 --- a/copier.yml +++ b/copier.yml @@ -9,7 +9,7 @@ template_type: plugin_name: type: str - help: What is the plugin name? (e.g., MyFilter) + help: What is the plugin name? (e.g., MyFilter) version: type: str From 860dc80e795e19a6794bc408ea8240026105fe18 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 15 Aug 2025 19:28:10 -0400 Subject: [PATCH 28/65] chore: cleanups Signed-off-by: Frederico Araujo --- .../plugins/framework/external/mcp/server.py | 44 +-- plugin_templates/external/.env.template | 293 ------------------ .../external/pyproject.toml.jinja | 2 +- 3 files changed, 23 insertions(+), 316 deletions(-) diff --git a/mcpgateway/plugins/framework/external/mcp/server.py b/mcpgateway/plugins/framework/external/mcp/server.py index b138cb584..827ec7c46 100644 --- a/mcpgateway/plugins/framework/external/mcp/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server.py @@ -81,7 +81,7 @@ async def get_plugin_config(name: str) -> dict: """Return a plugin configuration give a plugin name. Args: - name: the name of the plugin of which to return the plugin configuration. + name: The name of the plugin of which to return the plugin configuration. Returns: A list of plugin configurations. @@ -99,14 +99,14 @@ async def _invoke_hook( """Invoke a plugin hook. Args: - payload_model: the type of the payload accepted for the hook. - hook_function: the hook function to be invoked. - plugin_name: the name of the plugin to execute. - payload: the prompt name and arguments to be analyzed. - context: the contextual and state information required for the execution of the hook. + payload_model: The type of the payload accepted for the hook. + hook_function: The hook function to be invoked. + plugin_name: The name of the plugin to execute. + payload: The prompt name and arguments to be analyzed. + context: The contextual and state information required for the execution of the hook. Raises: - ValueError: if unable to retrieve a plugin. + ValueError: If unable to retrieve a plugin. Returns: The transformed or filtered response from the plugin hook. @@ -132,12 +132,12 @@ async def prompt_pre_fetch(plugin_name: str, payload: Dict[str, Any], context: D """Invoke the prompt pre fetch hook for a particular plugin. Args: - plugin_name: the name of the plugin to execute. - payload: the prompt name and arguments to be analyzed. - context: the contextual and state information required for the execution of the hook. + plugin_name: The name of the plugin to execute. + payload: The prompt name and arguments to be analyzed. + context: The contextual and state information required for the execution of the hook. Raises: - ValueError: if unable to retrieve a plugin. + ValueError: If unable to retrieve a plugin. Returns: The transformed or filtered response from the plugin hook. @@ -176,12 +176,12 @@ async def tool_pre_invoke(plugin_name: str, payload: Dict[str, Any], context: Di """Invoke the tool pre-invoke hook for a particular plugin. Args: - plugin_name: the name of the plugin to execute. - payload: the tool name and arguments to be analyzed. - context: the contextual and state information required for the execution of the hook. + plugin_name: The name of the plugin to execute. + payload: The tool name and arguments to be analyzed. + context: The contextual and state information required for the execution of the hook. Raises: - ValueError: if unable to retrieve a plugin. + ValueError: If unable to retrieve a plugin. Returns: The transformed or filtered response from the plugin hook. @@ -198,12 +198,12 @@ async def tool_post_invoke(plugin_name: str, payload: Dict[str, Any], context: D """Invoke the tool post-invoke hook for a particular plugin. Args: - plugin_name: the name of the plugin to execute. - payload: the tool name and arguments to be analyzed. + plugin_name: The name of the plugin to execute. + payload: The tool name and arguments to be analyzed. context: the contextual and state information required for the execution of the hook. Raises: - ValueError: if unable to retrieve a plugin. + ValueError: If unable to retrieve a plugin. Returns: The transformed or filtered response from the plugin hook. @@ -220,12 +220,12 @@ async def resource_pre_fetch(plugin_name: str, payload: Dict[str, Any], context: """Invoke the resource pre fetch hook for a particular plugin. Args: - plugin_name: the name of the plugin to execute. - payload: the resource name and arguments to be analyzed. - context: the contextual and state information required for the execution of the hook. + plugin_name: The name of the plugin to execute. + payload: The resource name and arguments to be analyzed. + context: The contextual and state information required for the execution of the hook. Raises: - ValueError: if unable to retrieve a plugin. + ValueError: If unable to retrieve a plugin. Returns: The transformed or filtered response from the plugin hook. diff --git a/plugin_templates/external/.env.template b/plugin_templates/external/.env.template index bffafc8ef..89b6fe43b 100644 --- a/plugin_templates/external/.env.template +++ b/plugin_templates/external/.env.template @@ -1,296 +1,3 @@ -##################################### -# Basic Application Settings -##################################### - -# The public-facing name of your gateway (used in docs/UI) -APP_NAME=MCP_Gateway - -# Bind address for HTTP server (0.0.0.0 for all interfaces) -HOST=0.0.0.0 - -# TCP port to listen on -PORT=4444 - -# SQLAlchemy connection string; leave as-is for local dev -# Examples: -# sqlite:///./mcp.db (local file-based) -# postgresql://user:pass@localhost:5432/mcp -DATABASE_URL=sqlite:///./mcp.db - -# Optional root path for deployments under a subpath (e.g., /api) -# Leave empty for default (/) -# Example: APP_ROOT_PATH=/gateway -APP_ROOT_PATH= - -##################################### -# Database Connection Pooling -##################################### - -# Size of the SQLAlchemy connection pool (default is 200) -DB_POOL_SIZE=200 - -# Number of extra connections allowed beyond pool size -DB_MAX_OVERFLOW=10 - -# Max time (in seconds) to wait for a connection from pool -DB_POOL_TIMEOUT=30 - -# Recycle database connections after N seconds -DB_POOL_RECYCLE=3600 - -# Maximum number of times to boot database connection for cold start -DB_MAX_RETRIES=3 - -# Interval time for next retry of database connection -DB_RETRY_INTERVAL_MS=2000 - -##################################### -# Cache Backend -##################################### - -# Backend for caching (memory, redis, database, or none) -CACHE_TYPE=database - -# If using Redis: provide full connection URL -# Example: redis://localhost:6379/0 -REDIS_URL=redis://localhost:6379/0 - -# Prefix for cache keys (recommended to avoid collisions) -CACHE_PREFIX=mcpgw: - -# TTL for user sessions (in seconds) -SESSION_TTL=3600 - -# TTL for ephemeral messages (like completions) in seconds -MESSAGE_TTL=600 - -# Maximum number of times to boot redis connection for cold start -REDIS_MAX_RETRIES=3 - -# Interval time for next retry of redis connection -REDIS_RETRY_INTERVAL_MS=2000 - -##################################### -# Protocol Settings -##################################### - -# MCP protocol version supported by this gateway -PROTOCOL_VERSION=2025-03-26 - -##################################### -# Authentication -##################################### - -# Admin UI basic-auth credentials -BASIC_AUTH_USER=admin -BASIC_AUTH_PASSWORD=changeme - -# Require any form of authentication (true or false) -AUTH_REQUIRED=true - -# Secret used to sign JWTs (use long random value in prod) -JWT_SECRET_KEY=my-test-key - -# Algorithm used to sign JWTs (e.g., HS256) -JWT_ALGORITHM=HS256 - -# Expiry time for generated JWT tokens (in minutes; e.g. 7 days) -TOKEN_EXPIRY=10080 - -# Require all JWT tokens to have expiration claims (true or false) -REQUIRE_TOKEN_EXPIRATION=false - - -# Used to derive an AES encryption key for secure auth storage -# Must be a non-empty string (e.g. passphrase or random secret) -AUTH_ENCRYPTION_SECRET=my-test-salt - -##################################### -# Admin UI and API Toggles -##################################### - -# Enable the visual Admin UI (true/false) -MCPGATEWAY_UI_ENABLED=true - -# Enable the Admin API endpoints (true/false) -MCPGATEWAY_ADMIN_API_ENABLED=true - -##################################### -# Security and CORS -##################################### - -# Skip TLS certificate checks for upstream requests (not recommended in prod) -SKIP_SSL_VERIFY=false - -# CORS origin allowlist (use JSON array of URLs) -# Example: ["http://localhost:3000"] -# Do not quote this value. Start with [] to ensure it's valid JSON. -ALLOWED_ORIGINS='["http://localhost", "http://localhost:4444"]' - -# Enable CORS handling in the gateway -CORS_ENABLED=true - -# Enable HTTP Basic Auth for docs endpoints (in addition to Bearer token auth) -# Uses the same credentials as BASIC_AUTH_USER and BASIC_AUTH_PASSWORD -DOCS_ALLOW_BASIC_AUTH=false - -##################################### -# Retry Config for HTTP Requests -##################################### - -RETRY_MAX_ATTEMPTS=3 -# seconds -RETRY_BASE_DELAY=1.0 -# seconds -RETRY_MAX_DELAY=60.0 -# fraction of delay -RETRY_JITTER_MAX=0.5 - -##################################### -# Logging -##################################### - -# Logging verbosity level: DEBUG, INFO, WARNING, ERROR, CRITICAL -LOG_LEVEL=INFO - -# Log format: json or text -LOG_FORMAT=json - -# Optional path to write logs to file (leave empty to log to stdout) -#LOG_FILE=./logs/gateway.log - -##################################### -# Transport Configuration -##################################### - -# Transport mechanisms to expose (comma-separated): http, ws, sse, stdio, all -TRANSPORT_TYPE=all - -# WebSocket ping interval (seconds) -WEBSOCKET_PING_INTERVAL=30 - -# SSE client retry timeout (milliseconds) -SSE_RETRY_TIMEOUT=5000 - - -##################################### -# Streamabe HTTP Transport Configuration -##################################### - -# Set False to use stateless sessions without event store and True for stateful sessions -USE_STATEFUL_SESSIONS=false - -# Set true for JSON responses, false for SSE streams -JSON_RESPONSE_ENABLED=true - - -##################################### -# Federation -##################################### - -# Enable federated mode (discovery + peer syncing) -FEDERATION_ENABLED=true - -# Automatically discover peer gateways (e.g., via mDNS) -FEDERATION_DISCOVERY=false - -# Explicit list of peers (JSON array of URLs). Do not quote this value. -FEDERATION_PEERS='[]' - -# Timeout for federated requests (in seconds) -FEDERATION_TIMEOUT=30 - -# Frequency of syncing peers (in seconds) -FEDERATION_SYNC_INTERVAL=300 - -##################################### -# Resources -##################################### - -# Maximum number of resources to keep in memory cache -RESOURCE_CACHE_SIZE=1000 - -# Time-to-live for resource cache (seconds) -RESOURCE_CACHE_TTL=3600 - -# Maximum allowed size for a resource (in bytes, e.g., 10MB) -MAX_RESOURCE_SIZE=10485760 - -##################################### -# Tools -##################################### - -# Max execution time for tools (in seconds) -TOOL_TIMEOUT=60 - -# Number of retry attempts for failed tools -MAX_TOOL_RETRIES=3 - -# Max number of tool invocations per minute -TOOL_RATE_LIMIT=100 - -# Number of tools that can run simultaneously -TOOL_CONCURRENT_LIMIT=10 - -##################################### -# Prompts -##################################### - -# How many prompt templates to cache -PROMPT_CACHE_SIZE=100 - -# Max size (in bytes) for a prompt template -MAX_PROMPT_SIZE=102400 - -# Timeout for rendering prompt templates (in seconds) -PROMPT_RENDER_TIMEOUT=10 - -##################################### -# Gateway Validation -##################################### - -# Timeout for gateway validation (in seconds) -GATEWAY_VALIDATION_TIMEOUT=5 - -##################################### -# Health Checks -##################################### - -# How often to poll peer gateways (in seconds) -HEALTH_CHECK_INTERVAL=60 - -# Timeout for a single health check request (seconds) -HEALTH_CHECK_TIMEOUT=10 - -# Number of failed checks before marking peer unhealthy -UNHEALTHY_THRESHOLD=3 - -##################################### -# Lock file Settings -##################################### - -# This path is append with the system temp directory. -# It is used to ensure that only one instance of the gateway health Check can run at a time. -# saved dir in /tmp/gateway_healthcheck_init.lock -FILELOCK_NAME=gateway_healthcheck_init.lock - -##################################### -# Development Settings -##################################### - -# Enable dev mode features (e.g. schema debug) -DEV_MODE=false - -# Reload app on code changes (dev only) -RELOAD=false - -# Enable verbose logging/debug traces -DEBUG=false - -# Gateway tool name separator -GATEWAY_TOOL_NAME_SEPARATOR=- -VALID_SLUG_SEPARATOR_REGEXP= r"^(-{1,2}|[_.])$" - ##################################### # Plugins Settings ##################################### diff --git a/plugin_templates/external/pyproject.toml.jinja b/plugin_templates/external/pyproject.toml.jinja index a61e6df3e..b6d493ed8 100644 --- a/plugin_templates/external/pyproject.toml.jinja +++ b/plugin_templates/external/pyproject.toml.jinja @@ -57,7 +57,7 @@ Repository = "https://github.com/IBM/mcp-context-forge" Changelog = "https://github.com/IBM/mcp-context-forge/blob/main/CHANGELOG.md" [tool.uv.sources] -mcp-contextforge-gateway = { git = "https://github.com/terylt/mcp-context-forge.git", rev = "feat/remote_plugin_support" } +mcp-contextforge-gateway = { git = "https://github.com/araujof/mcp-context-forge.git", rev = "feat/720-plugins-cli" } # -------------------------------------------------------------------- # ๐Ÿ”ง setuptools-specific configuration From b16397f8c82583c92b763c3d15faa9991d18a8de Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 15 Aug 2025 21:03:26 -0400 Subject: [PATCH 29/65] feat: update manifest.in Signed-off-by: Frederico Araujo --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 989c90da0..2662aa441 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -30,6 +30,7 @@ include *.txt recursive-include async_testing *.py recursive-include async_testing *.yaml recursive-include examples *.yaml +recursive-include plugin_templates * # 3๏ธโƒฃ Tooling/lint configuration dot-files (explicit so they're not lost) include .env.make From c01a2dbc8cd2f3f29d35532712bb458dcae04c69 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 15 Aug 2025 22:18:22 -0400 Subject: [PATCH 30/65] chore: linting Signed-off-by: Frederico Araujo --- mcpgateway/plugins/tools/cli.py | 12 +- mcpgateway/services/prompt_service.py | 2 +- pyproject.toml | 14 +- uv.lock | 1413 +++++++++++++++---------- 4 files changed, 901 insertions(+), 540 deletions(-) diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index fc1a6370c..99498c04e 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -75,7 +75,11 @@ def git_user_name() -> str: - """Return the current git user name from the environment.""" + """Return the current git user name from the environment. + + Returns: + The git user name configured in the user's environment. + """ try: res = subprocess.run(["git", "config", "user.name"], stdout=subprocess.PIPE) return res.stdout.strip().decode() if not res.returncode else DEFAULT_AUTHOR_NAME @@ -84,7 +88,11 @@ def git_user_name() -> str: def git_user_email() -> str: - """Return the current git user email from the environment.""" + """Return the current git user email from the environment. + + Returns: + The git user email configured in the user's environment. + """ try: res = subprocess.run(["git", "config", "user.email"], stdout=subprocess.PIPE) return res.stdout.strip().decode() if not res.returncode else DEFAULT_AUTHOR_EMAIL diff --git a/mcpgateway/services/prompt_service.py b/mcpgateway/services/prompt_service.py index ebe38bd02..9db0d2cbb 100644 --- a/mcpgateway/services/prompt_service.py +++ b/mcpgateway/services/prompt_service.py @@ -500,7 +500,7 @@ async def get_prompt( request_id = uuid.uuid4().hex global_context = GlobalContext(request_id=request_id, user=user, server_id=server_id, tenant_id=tenant_id) try: - pre_result, context_table = await self._plugin_manager.prompt_pre_fetch(payload=PromptPrehookPayload(name, arguments), global_context=global_context, local_contexts=None) + pre_result, context_table = await self._plugin_manager.prompt_pre_fetch(payload=PromptPrehookPayload(name=name, args=arguments), global_context=global_context, local_contexts=None) if not pre_result.continue_processing: # Plugin blocked the request diff --git a/pyproject.toml b/pyproject.toml index 1c12f67f4..3ed2f11ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,10 +106,10 @@ alembic = [ # Observability dependencies (optional) observability = [ - "opentelemetry-api>=1.36.0", - "opentelemetry-exporter-otlp>=1.36.0", - "opentelemetry-exporter-otlp-proto-grpc>=1.36.0", - "opentelemetry-sdk>=1.36.0", + "opentelemetry-api>=1.25.0", + "opentelemetry-exporter-otlp>=1.25.0", + "opentelemetry-exporter-otlp-proto-grpc>=1.25.0", + "opentelemetry-sdk>=1.25.0", ] # Additional observability backends (optional) @@ -137,6 +137,11 @@ asyncpg = [ "asyncpg>=0.30.0", ] +# Chuck runtime (optional) +chuck = [ + "chuk-mcp-runtime>=0.6.5", +] + # Optional dependency groups (development) dev = [ "aiohttp>=3.12.15", @@ -146,6 +151,7 @@ dev = [ "black>=25.1.0", "bump2version>=1.0.1", "check-manifest>=0.50", + "chuk-mcp-runtime>=0.6.5", "code2flow>=2.5.1", "cookiecutter>=2.6.0", "coverage>=7.10.3", diff --git a/uv.lock b/uv.lock index c98a85e9c..e520e5a2b 100644 --- a/uv.lock +++ b/uv.lock @@ -7,6 +7,51 @@ resolution-markers = [ "python_full_version < '3.12'", ] +[[package]] +name = "aioboto3" +version = "15.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiobotocore", extra = ["boto3"] }, + { name = "aiofiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/b1/b0331786c50f6ef881f9a71c3441ccf7b64c7eed210297d882c37ce31713/aioboto3-15.1.0.tar.gz", hash = "sha256:37763bbc6321ceb479106dc63bc84c8fdb59dd02540034a12941aebef2057c5c", size = 234664, upload-time = "2025-08-14T19:49:15.35Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/b0/28e3ac89e7119b1cb4e6830664060b96a2b5761291e92a10fb3044b5a11d/aioboto3-15.1.0-py3-none-any.whl", hash = "sha256:66006142a2ccc7d6d07aa260ba291c4922b6767d270ba42f95c59e85d8b3e645", size = 35791, upload-time = "2025-08-14T19:49:14.14Z" }, +] + +[[package]] +name = "aiobotocore" +version = "2.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "aioitertools" }, + { name = "botocore" }, + { name = "jmespath" }, + { name = "multidict" }, + { name = "python-dateutil" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/ca/ac82c0c699815b6d5b4017f3d8fb2c2d49537f4937f4a0bdf58b4c75d321/aiobotocore-2.24.0.tar.gz", hash = "sha256:b32c0c45d38c22a18ce395a0b5448606c5260603296a152895b5bdb40ab3139d", size = 119597, upload-time = "2025-08-08T18:26:50.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/68/b29577197aa2e54b50d6f214524790cc1cb27d289585ad7c7bdfe5125285/aiobotocore-2.24.0-py3-none-any.whl", hash = "sha256:72bb1f8eb1b962779a95e1bcc9cf35bc33196ad763b622a40ae7fa9d2e95c87c", size = 84971, upload-time = "2025-08-08T18:26:48.777Z" }, +] + +[package.optional-dependencies] +boto3 = [ + { name = "boto3" }, +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -84,6 +129,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/8e/78ee35774201f38d5e1ba079c9958f7629b1fd079459aea9467441dbfbf5/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84", size = 449067, upload-time = "2025-07-29T05:51:52.549Z" }, ] +[[package]] +name = "aioitertools" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/de/38491a84ab323b47c7f86e94d2830e748780525f7a10c8600b67ead7e9ea/aioitertools-0.12.0.tar.gz", hash = "sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b", size = 19369, upload-time = "2024-09-02T03:33:40.349Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/13/58b70a580de00893223d61de8fea167877a3aed97d4a5e1405c9159ef925/aioitertools-0.12.0-py3-none-any.whl", hash = "sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796", size = 24345, upload-time = "2024-09-02T03:34:59.454Z" }, +] + [[package]] name = "aiosignal" version = "1.4.0" @@ -134,23 +188,23 @@ wheels = [ [[package]] name = "anyio" -version = "4.9.0" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } 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/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, ] [[package]] name = "argparse-manpage" -version = "4.6" +version = "4.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3d/b7/cacb63bd461c83050b3b0efffc9151077b69de900ef5559ffeee72fa825b/argparse-manpage-4.6.tar.gz", hash = "sha256:0b659d70fd142876da41c2918bd6de4d027875720b0e4672d6443b51198dbb62", size = 58674, upload-time = "2024-06-05T11:46:30.578Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/6e/2907db04890c23728eecfcb04c37f66cba24d7903fe0ff6b8dc84d943ca5/argparse_manpage-4.7.tar.gz", hash = "sha256:1deab76b212ac8753cbb67b9d2d2bc0949bbc338bb1cc3547f0890cb34108b32", size = 58871, upload-time = "2025-08-15T10:24:20.601Z" } [[package]] name = "arrow" @@ -167,11 +221,11 @@ wheels = [ [[package]] name = "astroid" -version = "3.3.10" +version = "3.3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/c2/9b2de9ed027f9fe5734a6c0c0a601289d796b3caaf1e372e23fa88a73047/astroid-3.3.10.tar.gz", hash = "sha256:c332157953060c6deb9caa57303ae0d20b0fbdb2e59b4a4f2a6ba49d0a7961ce", size = 398941, upload-time = "2025-05-10T13:33:10.405Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/74/dfb75f9ccd592bbedb175d4a32fc643cf569d7c218508bfbd6ea7ef9c091/astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce", size = 400439, upload-time = "2025-07-13T18:04:23.177Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/58/5260205b9968c20b6457ed82f48f9e3d6edf2f1f95103161798b73aeccf0/astroid-3.3.10-py3-none-any.whl", hash = "sha256:104fb9cb9b27ea95e847a94c003be03a9e039334a8ebca5ee27dafaf5c5711eb", size = 275388, upload-time = "2025-05-10T13:33:08.391Z" }, + { url = "https://files.pythonhosted.org/packages/af/0f/3b8fdc946b4d9cc8cc1e8af42c4e409468c84441b933d037e101b3d72d86/astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec", size = 275612, upload-time = "2025-07-13T18:04:21.07Z" }, ] [[package]] @@ -183,6 +237,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, ] +[[package]] +name = "asyncio" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/ea/26c489a11f7ca862d5705db67683a7361ce11c23a7b98fc6c2deaeccede2/asyncio-4.0.0.tar.gz", hash = "sha256:570cd9e50db83bc1629152d4d0b7558d6451bb1bfd5dfc2e935d96fc2f40329b", size = 5371, upload-time = "2025-08-05T02:51:46.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/64/eff2564783bd650ca25e15938d1c5b459cda997574a510f7de69688cb0b4/asyncio-4.0.0-py3-none-any.whl", hash = "sha256:c1eddb0659231837046809e68103969b2bef8b0400d59cfa6363f6b5ed8cc88b", size = 5555, upload-time = "2025-08-05T02:51:45.767Z" }, +] + [[package]] name = "asyncpg" version = "0.30.0" @@ -331,27 +394,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" }, ] +[[package]] +name = "boto3" +version = "1.39.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/2e/ed75ea3ee0fd1afacc3379bc2b7457c67a6b0f0e554e1f7ccbdbaed2351b/boto3-1.39.11.tar.gz", hash = "sha256:3027edf20642fe1d5f9dc50a420d0fe2733073ed6a9f0f047b60fe08c3682132", size = 111869, upload-time = "2025-07-22T19:26:50.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/66/88566a6484e746c0b075f7c9bb248e8548eda0a486de4460d150a41e2d57/boto3-1.39.11-py3-none-any.whl", hash = "sha256:af8f1dad35eceff7658fab43b39b0f55892b6e3dd12308733521cc24dd2c9a02", size = 139900, upload-time = "2025-07-22T19:26:48.706Z" }, +] + +[[package]] +name = "botocore" +version = "1.39.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/d0/9d64261186cff650fe63168441edb4f4cd33f085a74c0c54455630a71f91/botocore-1.39.11.tar.gz", hash = "sha256:953b12909d6799350e346ab038e55b6efe622c616f80aef74d7a6683ffdd972c", size = 14217749, upload-time = "2025-07-22T19:26:40.723Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/2c/8a0b02d60a1dbbae7faa5af30484b016aa3023f9833dfc0d19b0b770dd6a/botocore-1.39.11-py3-none-any.whl", hash = "sha256:1545352931a8a186f3e977b1e1a4542d7d434796e274c3c62efd0210b5ea76dc", size = 13876276, upload-time = "2025-07-22T19:26:35.164Z" }, +] + [[package]] name = "bracex" -version = "2.5.post1" +version = "2.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/57418c4404cd22fe6275b8301ca2b46a8cdaa8157938017a9ae0b3edf363/bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6", size = 26641, upload-time = "2024-09-28T21:41:22.017Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/02/8db98cdc1a58e0abd6716d5e63244658e6e63513c65f469f34b6f1053fd0/bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6", size = 11558, upload-time = "2024-09-28T21:41:21.016Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, ] [[package]] name = "build" -version = "1.2.2.post1" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, { name = "packaging" }, { name = "pyproject-hooks" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382, upload-time = "2025-08-01T21:27:07.844Z" }, ] [[package]] @@ -392,11 +483,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.6.15" +version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] @@ -464,50 +555,44 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { 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, upload-time = "2025-05-02T08:32:11.945Z" }, - { 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, upload-time = "2025-05-02T08:32:13.946Z" }, - { 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, upload-time = "2025-05-02T08:32:15.873Z" }, - { 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, upload-time = "2025-05-02T08:32:17.283Z" }, - { 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, upload-time = "2025-05-02T08:32:18.807Z" }, - { 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, upload-time = "2025-05-02T08:32:20.333Z" }, - { 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, upload-time = "2025-05-02T08:32:21.86Z" }, - { 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, upload-time = "2025-05-02T08:32:23.434Z" }, - { 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, upload-time = "2025-05-02T08:32:24.993Z" }, - { 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, upload-time = "2025-05-02T08:32:26.435Z" }, - { 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, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] [[package]] @@ -523,6 +608,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/55/92207fa9b92ac2ade5593b1280f804f2590a680b7fe96775eb26074eec6b/check_manifest-0.50-py3-none-any.whl", hash = "sha256:6ab3e3aa72a008da3314b432f4c768c9647b4d6d8032f9e1a4672a572118e48c", size = 20385, upload-time = "2024-10-09T08:09:59.963Z" }, ] +[[package]] +name = "chuk-artifacts" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aioboto3" }, + { name = "asyncio" }, + { name = "chuk-sessions" }, + { name = "dotenv" }, + { name = "ibm-cos-sdk" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/da/577ac92baa94c9cc5a7b20e7adf843ac09a62ef50b6fa280fbf530264678/chuk_artifacts-0.4.1.tar.gz", hash = "sha256:b8ff717312e298e33b29873fede7849fe31f6faf0b497c60b7258ab388569454", size = 93916, upload-time = "2025-06-23T15:39:29.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/3a/93ebe6e2c4edb7b2f91b9998e4e3f174ff7cd4e188c3d6d60a4d548be060/chuk_artifacts-0.4.1-py3-none-any.whl", hash = "sha256:0ee6ea2ab1d64b35a79cfc449ad0b313d1b95e4a8fc16b0cb252ae75185286a4", size = 43172, upload-time = "2025-06-23T15:39:28.337Z" }, +] + +[[package]] +name = "chuk-mcp-runtime" +version = "0.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "chuk-artifacts" }, + { name = "chuk-sessions" }, + { name = "cryptography" }, + { name = "mcp" }, + { name = "pydantic" }, + { name = "pyjwt" }, + { name = "pyyaml" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/e1/feb4878fa4656239c185044607934e0a9dcb828e9588e7e5e5312434c954/chuk_mcp_runtime-0.6.5.tar.gz", hash = "sha256:ee90c93ec745d1835f40647ceeaa104085ebff7f7e8fdb68be229c57000b333f", size = 70932, upload-time = "2025-07-21T13:58:45.22Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/db/566184f985eaa232bb1ff9b2afb19bb78f0e5e6fe636940a00f9ee14aa03/chuk_mcp_runtime-0.6.5-py3-none-any.whl", hash = "sha256:e6d05dc7294f67657e9694bb784ab07b40c9c8d8c79758c29333fb87191f695d", size = 66849, upload-time = "2025-07-21T13:58:44.065Z" }, +] + +[[package]] +name = "chuk-sessions" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "pyyaml" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/5b/95d17807944c922671ac8538c03b1f334c8ce7e473d62ccf3d3bf3833c01/chuk_sessions-0.4.2-py3-none-any.whl", hash = "sha256:aaa49cbd59ec0cb22c9fbfaed99fa28f9fbfeac3c9f54d15242f5ec17deed740", size = 11895, upload-time = "2025-06-23T08:46:46.603Z" }, +] + [[package]] name = "click" version = "8.1.8" @@ -800,11 +935,11 @@ wheels = [ [[package]] name = "distlib" -version = "0.3.9" +version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] [[package]] @@ -820,11 +955,11 @@ wheels = [ [[package]] name = "docutils" -version = "0.21.2" +version = "0.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/86/5b41c32ecedcfdb4c77b28b6cb14234f252075f8cdb254531727a35547dd/docutils-0.22.tar.gz", hash = "sha256:ba9d57750e92331ebe7c08a1bbf7a7f8143b86c476acd51528b042216a6aad0f", size = 2277984, upload-time = "2025-07-29T15:20:31.06Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, + { url = "https://files.pythonhosted.org/packages/44/57/8db39bc5f98f042e0153b1de9fb88e1a409a33cda4dd7f723c2ed71e01f6/docutils-0.22-py3-none-any.whl", hash = "sha256:4ed966a0e96a0477d852f7af31bdcb3adc049fbb35ccba358c2ea8a03287615e", size = 630709, upload-time = "2025-07-29T15:20:28.335Z" }, ] [[package]] @@ -836,6 +971,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/f4/65b8a29adab331611259b86cf1d87a64f523fed52aba5d4bbdb2be2aed43/dodgy-0.2.1-py3-none-any.whl", hash = "sha256:51f54c0fd886fa3854387f354b19f429d38c04f984f38bc572558b703c0542a6", size = 5362, upload-time = "2019-12-31T16:44:58.264Z" }, ] +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +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" }, +] + [[package]] name = "dunamai" version = "1.25.0" @@ -911,11 +1057,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.18.0" +version = "3.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, ] [[package]] @@ -1030,30 +1176,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0", size = 30891, upload-time = "2023-03-28T06:22:42.576Z" }, ] -[[package]] -name = "gitdb" -version = "4.0.12" -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" } -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" }, -] - -[[package]] -name = "gitpython" -version = "3.1.44" -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" } -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" }, -] - [[package]] name = "glom" version = "22.1.0" @@ -1070,14 +1192,14 @@ wheels = [ [[package]] name = "googleapis-common-protos" -version = "1.70.0" +version = "1.59.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/9b/ea531afe585da044686ab13351c99dfbb2ca02b96c396874946d52d0e127/googleapis-common-protos-1.59.1.tar.gz", hash = "sha256:b35d530fe825fb4227857bc47ad84c33c809ac96f312e13182bdeaa2abe1178a", size = 118520, upload-time = "2023-06-12T21:07:00.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b7/bbaa556e9ff0580f408c64ccf4db0c1414eec79e7151d33a10bc209ffb6d/googleapis_common_protos-1.59.1-py2.py3-none-any.whl", hash = "sha256:0cbedb6fb68f1c07e18eb4c48256320777707e7d0c55063ae56c15db3224a61e", size = 224487, upload-time = "2023-06-12T21:06:58.546Z" }, ] [[package]] @@ -1091,37 +1213,75 @@ wheels = [ [[package]] name = "greenlet" -version = "3.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/92/bb85bd6e80148a4d2e0c59f7c0c2891029f8fd510183afc7d8d2feeed9b6/greenlet-3.2.3.tar.gz", hash = "sha256:8b0dd8ae4c0d6f5e54ee55ba935eeb3d735a9b58a8a1e5b5cbab64e01a39f365", size = 185752, upload-time = "2025-06-05T16:16:09.955Z" } -wheels = [ - { 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, upload-time = "2025-06-05T16:10:10.414Z" }, - { 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, upload-time = "2025-06-05T16:38:51.785Z" }, - { 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, upload-time = "2025-06-05T16:41:35.259Z" }, - { 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, upload-time = "2025-06-05T16:48:18.235Z" }, - { 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, upload-time = "2025-06-05T16:13:02.858Z" }, - { 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, upload-time = "2025-06-05T16:12:49.642Z" }, - { 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, upload-time = "2025-06-05T16:36:46.598Z" }, - { 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, upload-time = "2025-06-05T16:12:38.262Z" }, - { url = "https://files.pythonhosted.org/packages/66/77/d48fb441b5a71125bcac042fc5b1494c806ccb9a1432ecaa421e72157f77/greenlet-3.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:83a8761c75312361aa2b5b903b79da97f13f556164a7dd2d5448655425bd4c34", size = 297017, upload-time = "2025-06-05T16:25:05.225Z" }, - { url = "https://files.pythonhosted.org/packages/f3/94/ad0d435f7c48debe960c53b8f60fb41c2026b1d0fa4a99a1cb17c3461e09/greenlet-3.2.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:25ad29caed5783d4bd7a85c9251c651696164622494c00802a139c00d639242d", size = 271992, upload-time = "2025-06-05T16:11:23.467Z" }, - { url = "https://files.pythonhosted.org/packages/93/5d/7c27cf4d003d6e77749d299c7c8f5fd50b4f251647b5c2e97e1f20da0ab5/greenlet-3.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88cd97bf37fe24a6710ec6a3a7799f3f81d9cd33317dcf565ff9950c83f55e0b", size = 638820, upload-time = "2025-06-05T16:38:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/807e1e9be07a125bb4c169144937910bf59b9d2f6d931578e57f0bce0ae2/greenlet-3.2.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:baeedccca94880d2f5666b4fa16fc20ef50ba1ee353ee2d7092b383a243b0b0d", size = 653046, upload-time = "2025-06-05T16:41:36.343Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ab/158c1a4ea1068bdbc78dba5a3de57e4c7aeb4e7fa034320ea94c688bfb61/greenlet-3.2.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be52af4b6292baecfa0f397f3edb3c6092ce071b499dd6fe292c9ac9f2c8f264", size = 647701, upload-time = "2025-06-05T16:48:19.604Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0d/93729068259b550d6a0288da4ff72b86ed05626eaf1eb7c0d3466a2571de/greenlet-3.2.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0cc73378150b8b78b0c9fe2ce56e166695e67478550769536a6742dca3651688", size = 649747, upload-time = "2025-06-05T16:13:04.628Z" }, - { url = "https://files.pythonhosted.org/packages/f6/f6/c82ac1851c60851302d8581680573245c8fc300253fc1ff741ae74a6c24d/greenlet-3.2.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:706d016a03e78df129f68c4c9b4c4f963f7d73534e48a24f5f5a7101ed13dbbb", size = 605461, upload-time = "2025-06-05T16:12:50.792Z" }, - { url = "https://files.pythonhosted.org/packages/98/82/d022cf25ca39cf1200650fc58c52af32c90f80479c25d1cbf57980ec3065/greenlet-3.2.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:419e60f80709510c343c57b4bb5a339d8767bf9aef9b8ce43f4f143240f88b7c", size = 1121190, upload-time = "2025-06-05T16:36:48.59Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e1/25297f70717abe8104c20ecf7af0a5b82d2f5a980eb1ac79f65654799f9f/greenlet-3.2.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:93d48533fade144203816783373f27a97e4193177ebaaf0fc396db19e5d61163", size = 1149055, upload-time = "2025-06-05T16:12:40.457Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8f/8f9e56c5e82eb2c26e8cde787962e66494312dc8cb261c460e1f3a9c88bc/greenlet-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:7454d37c740bb27bdeddfc3f358f26956a07d5220818ceb467a483197d84f849", size = 297817, upload-time = "2025-06-05T16:29:49.244Z" }, - { url = "https://files.pythonhosted.org/packages/b1/cf/f5c0b23309070ae93de75c90d29300751a5aacefc0a3ed1b1d8edb28f08b/greenlet-3.2.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:500b8689aa9dd1ab26872a34084503aeddefcb438e2e7317b89b11eaea1901ad", size = 270732, upload-time = "2025-06-05T16:10:08.26Z" }, - { url = "https://files.pythonhosted.org/packages/48/ae/91a957ba60482d3fecf9be49bc3948f341d706b52ddb9d83a70d42abd498/greenlet-3.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a07d3472c2a93117af3b0136f246b2833fdc0b542d4a9799ae5f41c28323faef", size = 639033, upload-time = "2025-06-05T16:38:53.983Z" }, - { url = "https://files.pythonhosted.org/packages/6f/df/20ffa66dd5a7a7beffa6451bdb7400d66251374ab40b99981478c69a67a8/greenlet-3.2.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8704b3768d2f51150626962f4b9a9e4a17d2e37c8a8d9867bbd9fa4eb938d3b3", size = 652999, upload-time = "2025-06-05T16:41:37.89Z" }, - { url = "https://files.pythonhosted.org/packages/51/b4/ebb2c8cb41e521f1d72bf0465f2f9a2fd803f674a88db228887e6847077e/greenlet-3.2.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5035d77a27b7c62db6cf41cf786cfe2242644a7a337a0e155c80960598baab95", size = 647368, upload-time = "2025-06-05T16:48:21.467Z" }, - { url = "https://files.pythonhosted.org/packages/8e/6a/1e1b5aa10dced4ae876a322155705257748108b7fd2e4fae3f2a091fe81a/greenlet-3.2.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2d8aa5423cd4a396792f6d4580f88bdc6efcb9205891c9d40d20f6e670992efb", size = 650037, upload-time = "2025-06-05T16:13:06.402Z" }, - { url = "https://files.pythonhosted.org/packages/26/f2/ad51331a157c7015c675702e2d5230c243695c788f8f75feba1af32b3617/greenlet-3.2.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c724620a101f8170065d7dded3f962a2aea7a7dae133a009cada42847e04a7b", size = 608402, upload-time = "2025-06-05T16:12:51.91Z" }, - { url = "https://files.pythonhosted.org/packages/26/bc/862bd2083e6b3aff23300900a956f4ea9a4059de337f5c8734346b9b34fc/greenlet-3.2.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:873abe55f134c48e1f2a6f53f7d1419192a3d1a4e873bace00499a4e45ea6af0", size = 1119577, upload-time = "2025-06-05T16:36:49.787Z" }, - { url = "https://files.pythonhosted.org/packages/86/94/1fc0cc068cfde885170e01de40a619b00eaa8f2916bf3541744730ffb4c3/greenlet-3.2.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:024571bbce5f2c1cfff08bf3fbaa43bbc7444f580ae13b0099e95d0e6e67ed36", size = 1147121, upload-time = "2025-06-05T16:12:42.527Z" }, - { url = "https://files.pythonhosted.org/packages/27/1a/199f9587e8cb08a0658f9c30f3799244307614148ffe8b1e3aa22f324dea/greenlet-3.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5195fb1e75e592dd04ce79881c8a22becdfa3e6f500e7feb059b1e6fdd54d3e3", size = 297603, upload-time = "2025-06-05T16:20:12.651Z" }, +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, + { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, + { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, + { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, + { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, + { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, + { url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" }, + { url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, + { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, +] + +[[package]] +name = "grpcio" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b4/35feb8f7cab7239c5b94bd2db71abb3d6adb5f335ad8f131abb6060840b6/grpcio-1.74.0.tar.gz", hash = "sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1", size = 12756048, upload-time = "2025-07-24T18:54:23.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/77/b2f06db9f240a5abeddd23a0e49eae2b6ac54d85f0e5267784ce02269c3b/grpcio-1.74.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:69e1a8180868a2576f02356565f16635b99088da7df3d45aaa7e24e73a054e31", size = 5487368, upload-time = "2025-07-24T18:53:03.548Z" }, + { url = "https://files.pythonhosted.org/packages/48/99/0ac8678a819c28d9a370a663007581744a9f2a844e32f0fa95e1ddda5b9e/grpcio-1.74.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8efe72fde5500f47aca1ef59495cb59c885afe04ac89dd11d810f2de87d935d4", size = 10999804, upload-time = "2025-07-24T18:53:05.095Z" }, + { url = "https://files.pythonhosted.org/packages/45/c6/a2d586300d9e14ad72e8dc211c7aecb45fe9846a51e558c5bca0c9102c7f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a8f0302f9ac4e9923f98d8e243939a6fb627cd048f5cd38595c97e38020dffce", size = 5987667, upload-time = "2025-07-24T18:53:07.157Z" }, + { url = "https://files.pythonhosted.org/packages/c9/57/5f338bf56a7f22584e68d669632e521f0de460bb3749d54533fc3d0fca4f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f609a39f62a6f6f05c7512746798282546358a37ea93c1fcbadf8b2fed162e3", size = 6655612, upload-time = "2025-07-24T18:53:09.244Z" }, + { url = "https://files.pythonhosted.org/packages/82/ea/a4820c4c44c8b35b1903a6c72a5bdccec92d0840cf5c858c498c66786ba5/grpcio-1.74.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98e0b7434a7fa4e3e63f250456eaef52499fba5ae661c58cc5b5477d11e7182", size = 6219544, upload-time = "2025-07-24T18:53:11.221Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/0537630a921365928f5abb6d14c79ba4dcb3e662e0dbeede8af4138d9dcf/grpcio-1.74.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:662456c4513e298db6d7bd9c3b8df6f75f8752f0ba01fb653e252ed4a59b5a5d", size = 6334863, upload-time = "2025-07-24T18:53:12.925Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a6/85ca6cb9af3f13e1320d0a806658dca432ff88149d5972df1f7b51e87127/grpcio-1.74.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3d14e3c4d65e19d8430a4e28ceb71ace4728776fd6c3ce34016947474479683f", size = 7019320, upload-time = "2025-07-24T18:53:15.002Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a7/fe2beab970a1e25d2eff108b3cf4f7d9a53c185106377a3d1989216eba45/grpcio-1.74.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bf949792cee20d2078323a9b02bacbbae002b9e3b9e2433f2741c15bdeba1c4", size = 6514228, upload-time = "2025-07-24T18:53:16.999Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/2f9c945c8a248cebc3ccda1b7a1bf1775b9d7d59e444dbb18c0014e23da6/grpcio-1.74.0-cp311-cp311-win32.whl", hash = "sha256:55b453812fa7c7ce2f5c88be3018fb4a490519b6ce80788d5913f3f9d7da8c7b", size = 3817216, upload-time = "2025-07-24T18:53:20.564Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d1/a9cf9c94b55becda2199299a12b9feef0c79946b0d9d34c989de6d12d05d/grpcio-1.74.0-cp311-cp311-win_amd64.whl", hash = "sha256:86ad489db097141a907c559988c29718719aa3e13370d40e20506f11b4de0d11", size = 4495380, upload-time = "2025-07-24T18:53:22.058Z" }, + { url = "https://files.pythonhosted.org/packages/4c/5d/e504d5d5c4469823504f65687d6c8fb97b7f7bf0b34873b7598f1df24630/grpcio-1.74.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8533e6e9c5bd630ca98062e3a1326249e6ada07d05acf191a77bc33f8948f3d8", size = 5445551, upload-time = "2025-07-24T18:53:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/43/01/730e37056f96f2f6ce9f17999af1556df62ee8dab7fa48bceeaab5fd3008/grpcio-1.74.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2918948864fec2a11721d91568effffbe0a02b23ecd57f281391d986847982f6", size = 10979810, upload-time = "2025-07-24T18:53:25.349Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/09fd100473ea5c47083889ca47ffd356576173ec134312f6aa0e13111dee/grpcio-1.74.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:60d2d48b0580e70d2e1954d0d19fa3c2e60dd7cbed826aca104fff518310d1c5", size = 5941946, upload-time = "2025-07-24T18:53:27.387Z" }, + { url = "https://files.pythonhosted.org/packages/8a/99/12d2cca0a63c874c6d3d195629dcd85cdf5d6f98a30d8db44271f8a97b93/grpcio-1.74.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3601274bc0523f6dc07666c0e01682c94472402ac2fd1226fd96e079863bfa49", size = 6621763, upload-time = "2025-07-24T18:53:29.193Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2c/930b0e7a2f1029bbc193443c7bc4dc2a46fedb0203c8793dcd97081f1520/grpcio-1.74.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:176d60a5168d7948539def20b2a3adcce67d72454d9ae05969a2e73f3a0feee7", size = 6180664, upload-time = "2025-07-24T18:53:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/ff8a2442180ad0867717e670f5ec42bfd8d38b92158ad6bcd864e6d4b1ed/grpcio-1.74.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e759f9e8bc908aaae0412642afe5416c9f983a80499448fcc7fab8692ae044c3", size = 6301083, upload-time = "2025-07-24T18:53:32.454Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/b361d390451a37ca118e4ec7dccec690422e05bc85fba2ec72b06cefec9f/grpcio-1.74.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e7c4389771855a92934b2846bd807fc25a3dfa820fd912fe6bd8136026b2707", size = 6994132, upload-time = "2025-07-24T18:53:34.506Z" }, + { url = "https://files.pythonhosted.org/packages/3b/0c/3a5fa47d2437a44ced74141795ac0251bbddeae74bf81df3447edd767d27/grpcio-1.74.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cce634b10aeab37010449124814b05a62fb5f18928ca878f1bf4750d1f0c815b", size = 6489616, upload-time = "2025-07-24T18:53:36.217Z" }, + { url = "https://files.pythonhosted.org/packages/ae/95/ab64703b436d99dc5217228babc76047d60e9ad14df129e307b5fec81fd0/grpcio-1.74.0-cp312-cp312-win32.whl", hash = "sha256:885912559974df35d92219e2dc98f51a16a48395f37b92865ad45186f294096c", size = 3807083, upload-time = "2025-07-24T18:53:37.911Z" }, + { url = "https://files.pythonhosted.org/packages/84/59/900aa2445891fc47a33f7d2f76e00ca5d6ae6584b20d19af9c06fa09bf9a/grpcio-1.74.0-cp312-cp312-win_amd64.whl", hash = "sha256:42f8fee287427b94be63d916c90399ed310ed10aadbf9e2e5538b3e497d269bc", size = 4490123, upload-time = "2025-07-24T18:53:39.528Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d8/1004a5f468715221450e66b051c839c2ce9a985aa3ee427422061fcbb6aa/grpcio-1.74.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2bc2d7d8d184e2362b53905cb1708c84cb16354771c04b490485fa07ce3a1d89", size = 5449488, upload-time = "2025-07-24T18:53:41.174Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/33731a03f63740d7743dced423846c831d8e6da808fcd02821a4416df7fa/grpcio-1.74.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c14e803037e572c177ba54a3e090d6eb12efd795d49327c5ee2b3bddb836bf01", size = 10974059, upload-time = "2025-07-24T18:53:43.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c6/3d2c14d87771a421205bdca991467cfe473ee4c6a1231c1ede5248c62ab8/grpcio-1.74.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f6ec94f0e50eb8fa1744a731088b966427575e40c2944a980049798b127a687e", size = 5945647, upload-time = "2025-07-24T18:53:45.269Z" }, + { url = "https://files.pythonhosted.org/packages/c5/83/5a354c8aaff58594eef7fffebae41a0f8995a6258bbc6809b800c33d4c13/grpcio-1.74.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:566b9395b90cc3d0d0c6404bc8572c7c18786ede549cdb540ae27b58afe0fb91", size = 6626101, upload-time = "2025-07-24T18:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ca/4fdc7bf59bf6994aa45cbd4ef1055cd65e2884de6113dbd49f75498ddb08/grpcio-1.74.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1ea6176d7dfd5b941ea01c2ec34de9531ba494d541fe2057c904e601879f249", size = 6182562, upload-time = "2025-07-24T18:53:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/fd/48/2869e5b2c1922583686f7ae674937986807c2f676d08be70d0a541316270/grpcio-1.74.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:64229c1e9cea079420527fa8ac45d80fc1e8d3f94deaa35643c381fa8d98f362", size = 6303425, upload-time = "2025-07-24T18:53:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/bac93147b9a164f759497bc6913e74af1cb632c733c7af62c0336782bd38/grpcio-1.74.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:0f87bddd6e27fc776aacf7ebfec367b6d49cad0455123951e4488ea99d9b9b8f", size = 6996533, upload-time = "2025-07-24T18:53:52.747Z" }, + { url = "https://files.pythonhosted.org/packages/84/35/9f6b2503c1fd86d068b46818bbd7329db26a87cdd8c01e0d1a9abea1104c/grpcio-1.74.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3b03d8f2a07f0fea8c8f74deb59f8352b770e3900d143b3d1475effcb08eec20", size = 6491489, upload-time = "2025-07-24T18:53:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/75/33/a04e99be2a82c4cbc4039eb3a76f6c3632932b9d5d295221389d10ac9ca7/grpcio-1.74.0-cp313-cp313-win32.whl", hash = "sha256:b6a73b2ba83e663b2480a90b82fdae6a7aa6427f62bf43b29912c0cfd1aa2bfa", size = 3805811, upload-time = "2025-07-24T18:53:56.798Z" }, + { url = "https://files.pythonhosted.org/packages/34/80/de3eb55eb581815342d097214bed4c59e806b05f1b3110df03b2280d6dfd/grpcio-1.74.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd3c71aeee838299c5887230b8a1822795325ddfea635edd82954c1eaa831e24", size = 4489214, upload-time = "2025-07-24T18:53:59.771Z" }, ] [[package]] @@ -1188,12 +1348,44 @@ wheels = [ [[package]] name = "httpx-sse" -version = "0.4.0" +version = "0.4.1" 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/6e/fa/66bd985dd0b7c109a3bcb89272ee0bfb7e2b4d06309ad7b38ff866734b2a/httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e", size = 12998, upload-time = "2025-06-24T13:21:05.71Z" } 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/25/0a/6269e3473b09aed2dab8aa1a600c70f31f00ae1349bee30658f7e358a159/httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37", size = 8054, upload-time = "2025-06-24T13:21:04.772Z" }, +] + +[[package]] +name = "ibm-cos-sdk" +version = "2.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ibm-cos-sdk-core" }, + { name = "ibm-cos-sdk-s3transfer" }, + { name = "jmespath" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/98/b8/b99f17ece72d4bccd7e75539b9a294d0f73ace5c6c475d8f2631afd6f65b/ibm_cos_sdk-2.14.3.tar.gz", hash = "sha256:643b6f2aa1683adad7f432df23407d11ae5adb9d9ad01214115bee77dc64364a", size = 58831, upload-time = "2025-08-01T06:35:51.722Z" } + +[[package]] +name = "ibm-cos-sdk-core" +version = "2.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/45/80c23aa1e13175a9deefe43cbf8e853a3d3bfc8dfa8b6d6fe83e5785fe21/ibm_cos_sdk_core-2.14.3.tar.gz", hash = "sha256:85dee7790c92e8db69bf39dae4c02cac211e3c1d81bb86e64fa2d1e929674623", size = 1103637, upload-time = "2025-08-01T06:35:41.645Z" } + +[[package]] +name = "ibm-cos-sdk-s3transfer" +version = "2.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ibm-cos-sdk-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/ff/c9baf0997266d398ae08347951a2970e5e96ed6232ed0252f649f2b9a7eb/ibm_cos_sdk_s3transfer-2.14.3.tar.gz", hash = "sha256:2251ebfc4a46144401e431f4a5d9f04c262a0d6f95c88a8e71071da056e55f72", size = 139594, upload-time = "2025-08-01T06:35:46.403Z" } [[package]] name = "id" @@ -1209,11 +1401,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.12" +version = "2.6.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ca/ffbabe3635bb839aa36b3a893c91a9b0d368cb4d8073e03a12896970af82/identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32", size = 99243, upload-time = "2025-08-09T19:35:00.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ce/461b60a3ee109518c055953729bf9ed089a04db895d47e95444071dcdef2/identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b", size = 99153, upload-time = "2025-08-09T19:34:59.1Z" }, ] [[package]] @@ -1339,14 +1531,14 @@ wheels = [ [[package]] name = "jaraco-functools" -version = "4.1.0" +version = "4.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "more-itertools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159, upload-time = "2024-09-27T19:47:09.122Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/1c/831faaaa0f090b711c355c6d8b2abf277c72133aab472b6932b03322294c/jaraco_functools-4.2.1.tar.gz", hash = "sha256:be634abfccabce56fa3053f8c7ebe37b682683a4ee7793670ced17bab0087353", size = 19661, upload-time = "2025-06-21T19:22:03.201Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187, upload-time = "2024-09-27T19:47:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/179a20f832824514df39a90bb0e5372b314fea99f217f5ab942b10a8a4e8/jaraco_functools-4.2.1-py3-none-any.whl", hash = "sha256:590486285803805f4b1f99c60ca9e94ed348d4added84b74c7a12885561e524e", size = 10349, upload-time = "2025-06-21T19:22:02.039Z" }, ] [[package]] @@ -1383,6 +1575,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34", size = 18975, upload-time = "2022-06-30T14:08:49.571Z" }, ] +[[package]] +name = "jmespath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/2a/e867e8531cf3e36b41201936b7fa7ba7b5702dbef42922193f05c8976cd6/jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe", size = 25843, upload-time = "2022-06-17T18:00:12.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b4/b9b800c45527aadd64d5b442f9b932b00648617eb5d63d2c7a6587b7cafc/jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", size = 20256, upload-time = "2022-06-17T18:00:10.251Z" }, +] + [[package]] name = "jq" version = "1.10.0" @@ -1510,73 +1711,68 @@ wheels = [ [[package]] name = "license-expression" -version = "30.4.1" +version = "30.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boolean-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/6f/8709031ea6e0573e6075d24ea34507b0eb32f83f10e1420f2e34606bf0da/license_expression-30.4.1.tar.gz", hash = "sha256:9f02105f9e0fcecba6a85dfbbed7d94ea1c3a70cf23ddbfb5adf3438a6f6fce0", size = 177184, upload-time = "2025-01-14T05:11:39.967Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/84/8a89614b2e7eeeaf0a68a4046d6cfaea4544c8619ea02595ebeec9b2bae3/license_expression-30.4.1-py3-none-any.whl", hash = "sha256:679646bc3261a17690494a3e1cada446e5ee342dbd87dcfa4a0c24cc5dce13ee", size = 111457, upload-time = "2025-01-14T05:11:38.658Z" }, + { url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" }, ] [[package]] name = "lxml" -version = "5.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240, upload-time = "2025-04-23T01:45:18.566Z" }, - { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685, upload-time = "2025-04-23T01:45:21.387Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164, upload-time = "2025-04-23T01:45:23.849Z" }, - { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206, upload-time = "2025-04-23T01:45:26.361Z" }, - { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144, upload-time = "2025-04-23T01:45:28.939Z" }, - { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124, upload-time = "2025-04-23T01:45:31.361Z" }, - { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520, upload-time = "2025-04-23T01:45:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016, upload-time = "2025-04-23T01:45:36.7Z" }, - { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884, upload-time = "2025-04-23T01:45:39.291Z" }, - { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690, upload-time = "2025-04-23T01:45:42.386Z" }, - { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418, upload-time = "2025-04-23T01:45:46.051Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092, upload-time = "2025-04-23T01:45:48.943Z" }, - { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231, upload-time = "2025-04-23T01:45:51.481Z" }, - { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798, upload-time = "2025-04-23T01:45:54.146Z" }, - { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195, upload-time = "2025-04-23T01:45:56.685Z" }, - { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243, upload-time = "2025-04-23T01:45:58.863Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197, upload-time = "2025-04-23T01:46:01.096Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" }, - { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" }, - { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" }, - { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" }, - { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" }, - { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" }, - { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" }, - { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" }, - { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" }, - { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" }, - { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" }, - { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" }, - { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" }, - { url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" }, - { url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" }, - { url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" }, - { url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" }, - { url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" }, - { url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" }, - { url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" }, - { url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" }, - { url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" }, - { url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" }, - { url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" }, - { url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" }, - { url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" }, +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/23/828d4cc7da96c611ec0ce6147bbcea2fdbde023dc995a165afa512399bbf/lxml-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ee56288d0df919e4aac43b539dd0e34bb55d6a12a6562038e8d6f3ed07f9e36", size = 8438217, upload-time = "2025-06-26T16:25:34.349Z" }, + { url = "https://files.pythonhosted.org/packages/f1/33/5ac521212c5bcb097d573145d54b2b4a3c9766cda88af5a0e91f66037c6e/lxml-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8dd6dd0e9c1992613ccda2bcb74fc9d49159dbe0f0ca4753f37527749885c25", size = 4590317, upload-time = "2025-06-26T16:25:38.103Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2e/45b7ca8bee304c07f54933c37afe7dd4d39ff61ba2757f519dcc71bc5d44/lxml-6.0.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d7ae472f74afcc47320238b5dbfd363aba111a525943c8a34a1b657c6be934c3", size = 5221628, upload-time = "2025-06-26T16:25:40.878Z" }, + { url = "https://files.pythonhosted.org/packages/32/23/526d19f7eb2b85da1f62cffb2556f647b049ebe2a5aa8d4d41b1fb2c7d36/lxml-6.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5592401cdf3dc682194727c1ddaa8aa0f3ddc57ca64fd03226a430b955eab6f6", size = 4949429, upload-time = "2025-06-28T18:47:20.046Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cc/f6be27a5c656a43a5344e064d9ae004d4dcb1d3c9d4f323c8189ddfe4d13/lxml-6.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58ffd35bd5425c3c3b9692d078bf7ab851441434531a7e517c4984d5634cd65b", size = 5087909, upload-time = "2025-06-28T18:47:22.834Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e6/8ec91b5bfbe6972458bc105aeb42088e50e4b23777170404aab5dfb0c62d/lxml-6.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f720a14aa102a38907c6d5030e3d66b3b680c3e6f6bc95473931ea3c00c59967", size = 5031713, upload-time = "2025-06-26T16:25:43.226Z" }, + { url = "https://files.pythonhosted.org/packages/33/cf/05e78e613840a40e5be3e40d892c48ad3e475804db23d4bad751b8cadb9b/lxml-6.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a5e8d207311a0170aca0eb6b160af91adc29ec121832e4ac151a57743a1e1e", size = 5232417, upload-time = "2025-06-26T16:25:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/ac/8c/6b306b3e35c59d5f0b32e3b9b6b3b0739b32c0dc42a295415ba111e76495/lxml-6.0.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:2dd1cc3ea7e60bfb31ff32cafe07e24839df573a5e7c2d33304082a5019bcd58", size = 4681443, upload-time = "2025-06-26T16:25:48.837Z" }, + { url = "https://files.pythonhosted.org/packages/59/43/0bd96bece5f7eea14b7220476835a60d2b27f8e9ca99c175f37c085cb154/lxml-6.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cfcf84f1defed7e5798ef4f88aa25fcc52d279be731ce904789aa7ccfb7e8d2", size = 5074542, upload-time = "2025-06-26T16:25:51.65Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3d/32103036287a8ca012d8518071f8852c68f2b3bfe048cef2a0202eb05910/lxml-6.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a52a4704811e2623b0324a18d41ad4b9fabf43ce5ff99b14e40a520e2190c851", size = 4729471, upload-time = "2025-06-26T16:25:54.571Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a8/7be5d17df12d637d81854bd8648cd329f29640a61e9a72a3f77add4a311b/lxml-6.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c16304bba98f48a28ae10e32a8e75c349dd742c45156f297e16eeb1ba9287a1f", size = 5256285, upload-time = "2025-06-26T16:25:56.997Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d0/6cb96174c25e0d749932557c8d51d60c6e292c877b46fae616afa23ed31a/lxml-6.0.0-cp311-cp311-win32.whl", hash = "sha256:f8d19565ae3eb956d84da3ef367aa7def14a2735d05bd275cd54c0301f0d0d6c", size = 3612004, upload-time = "2025-06-26T16:25:59.11Z" }, + { url = "https://files.pythonhosted.org/packages/ca/77/6ad43b165dfc6dead001410adeb45e88597b25185f4479b7ca3b16a5808f/lxml-6.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b2d71cdefda9424adff9a3607ba5bbfc60ee972d73c21c7e3c19e71037574816", size = 4003470, upload-time = "2025-06-26T16:26:01.655Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bc/4c50ec0eb14f932a18efc34fc86ee936a66c0eb5f2fe065744a2da8a68b2/lxml-6.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:8a2e76efbf8772add72d002d67a4c3d0958638696f541734304c7f28217a9cab", size = 3682477, upload-time = "2025-06-26T16:26:03.808Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/d01d735c298d7e0ddcedf6f028bf556577e5ab4f4da45175ecd909c79378/lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108", size = 8429515, upload-time = "2025-06-26T16:26:06.776Z" }, + { url = "https://files.pythonhosted.org/packages/06/37/0e3eae3043d366b73da55a86274a590bae76dc45aa004b7042e6f97803b1/lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be", size = 4601387, upload-time = "2025-06-26T16:26:09.511Z" }, + { url = "https://files.pythonhosted.org/packages/a3/28/e1a9a881e6d6e29dda13d633885d13acb0058f65e95da67841c8dd02b4a8/lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab", size = 5228928, upload-time = "2025-06-26T16:26:12.337Z" }, + { url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289, upload-time = "2025-06-28T18:47:25.602Z" }, + { url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310, upload-time = "2025-06-28T18:47:28.136Z" }, + { url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457, upload-time = "2025-06-26T16:26:15.068Z" }, + { url = "https://files.pythonhosted.org/packages/99/b6/3a7971aa05b7be7dfebc7ab57262ec527775c2c3c5b2f43675cac0458cad/lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991", size = 5657016, upload-time = "2025-07-03T19:19:06.008Z" }, + { url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565, upload-time = "2025-06-26T16:26:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390, upload-time = "2025-06-26T16:26:20.292Z" }, + { url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103, upload-time = "2025-06-26T16:26:22.765Z" }, + { url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428, upload-time = "2025-06-26T16:26:26.461Z" }, + { url = "https://files.pythonhosted.org/packages/29/31/c0267d03b16954a85ed6b065116b621d37f559553d9339c7dcc4943a76f1/lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16", size = 5678523, upload-time = "2025-07-03T19:19:09.837Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290, upload-time = "2025-06-26T16:26:29.406Z" }, + { url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495, upload-time = "2025-06-26T16:26:31.588Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711, upload-time = "2025-06-26T16:26:33.723Z" }, + { url = "https://files.pythonhosted.org/packages/55/10/dc8e5290ae4c94bdc1a4c55865be7e1f31dfd857a88b21cbba68b5fea61b/lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb", size = 3674431, upload-time = "2025-06-26T16:26:35.959Z" }, + { url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" }, + { url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" }, + { url = "https://files.pythonhosted.org/packages/52/46/3572761efc1bd45fcafb44a63b3b0feeb5b3f0066886821e94b0254f9253/lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81", size = 4947559, upload-time = "2025-06-28T18:47:31.091Z" }, + { url = "https://files.pythonhosted.org/packages/94/8a/5e40de920e67c4f2eef9151097deb9b52d86c95762d8ee238134aff2125d/lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1", size = 5102143, upload-time = "2025-06-28T18:47:33.612Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/cf03b412f3763d4ca23b25e70c96a74cfece64cec3addf1c4ec639586b13/lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a", size = 5645469, upload-time = "2025-07-03T19:19:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" }, + { url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" }, + { url = "https://files.pythonhosted.org/packages/06/d4/fd216f3cd6625022c25b336c7570d11f4a43adbaf0a56106d3d496f727a7/lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f", size = 5662049, upload-time = "2025-07-03T19:19:16.409Z" }, + { url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" }, + { url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" }, + { url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" }, ] [[package]] @@ -1614,14 +1810,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "3.0.0" +version = "4.0.0" 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/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } 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/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] @@ -1719,7 +1915,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.12.4" +version = "1.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1734,9 +1930,9 @@ dependencies = [ { name = "starlette" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/88/f6cb7e7c260cd4b4ce375f2b1614b33ce401f63af0f49f7141a2e9bf0a45/mcp-1.12.4.tar.gz", hash = "sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5", size = 431148, upload-time = "2025-08-07T20:31:18.082Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/a8/564c094de5d6199f727f5d9f5672dbec3b00dfafd0f67bf52d995eaa5951/mcp-1.13.0.tar.gz", hash = "sha256:70452f56f74662a94eb72ac5feb93997b35995e389b3a3a574e078bed2aa9ab3", size = 434709, upload-time = "2025-08-14T15:03:58.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/68/316cbc54b7163fa22571dcf42c9cc46562aae0a021b974e0a8141e897200/mcp-1.12.4-py3-none-any.whl", hash = "sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789", size = 160145, upload-time = "2025-08-07T20:31:15.69Z" }, + { url = "https://files.pythonhosted.org/packages/8b/6b/46b8bcefc2ee9e2d2e8d2bd25f1c2512f5a879fac4619d716b194d6e7ccc/mcp-1.13.0-py3-none-any.whl", hash = "sha256:8b1a002ebe6e17e894ec74d1943cc09aa9d23cb931bf58d49ab2e9fa6bb17e4b", size = 160226, upload-time = "2025-08-14T15:03:56.641Z" }, ] [[package]] @@ -1784,6 +1980,9 @@ all = [ asyncpg = [ { name = "asyncpg" }, ] +chuck = [ + { name = "chuk-mcp-runtime" }, +] dev = [ { name = "aiohttp" }, { name = "argparse-manpage" }, @@ -1792,6 +1991,7 @@ dev = [ { name = "black" }, { name = "bump2version" }, { name = "check-manifest" }, + { name = "chuk-mcp-runtime" }, { name = "code2flow" }, { name = "cookiecutter" }, { name = "coverage" }, @@ -1856,6 +2056,7 @@ dev-all = [ { name = "black" }, { name = "bump2version" }, { name = "check-manifest" }, + { name = "chuk-mcp-runtime" }, { name = "code2flow" }, { name = "cookiecutter" }, { name = "coverage" }, @@ -1912,6 +2113,26 @@ dev-all = [ { name = "websockets" }, { name = "yamllint" }, ] +observability = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-sdk" }, +] +observability-all = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-jaeger" }, + { name = "opentelemetry-exporter-otlp" }, + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-zipkin" }, + { name = "opentelemetry-sdk" }, +] +observability-jaeger = [ + { name = "opentelemetry-exporter-jaeger" }, +] +observability-zipkin = [ + { name = "opentelemetry-exporter-zipkin" }, +] playwright = [ { name = "playwright" }, { name = "pytest-html" }, @@ -1938,10 +2159,12 @@ requires-dist = [ { name = "black", marker = "extra == 'dev'", specifier = ">=25.1.0" }, { name = "bump2version", marker = "extra == 'dev'", specifier = ">=1.0.1" }, { name = "check-manifest", marker = "extra == 'dev'", specifier = ">=0.50" }, + { name = "chuk-mcp-runtime", marker = "extra == 'chuck'", specifier = ">=0.6.5" }, + { name = "chuk-mcp-runtime", marker = "extra == 'dev'", specifier = ">=0.6.5" }, { name = "code2flow", marker = "extra == 'dev'", specifier = ">=2.5.1" }, { name = "cookiecutter", marker = "extra == 'dev'", specifier = ">=2.6.0" }, { name = "copier", specifier = ">=9.9.0" }, - { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.10.2" }, + { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.10.3" }, { name = "coverage-badge", marker = "extra == 'dev'", specifier = ">=1.1.2" }, { name = "cryptography", specifier = ">=45.0.6" }, { name = "darglint", marker = "extra == 'dev'", specifier = ">=1.8.1" }, @@ -1962,9 +2185,18 @@ requires-dist = [ { name = "jsonpath-ng", specifier = ">=1.7.0" }, { name = "jsonschema", specifier = ">=4.25.0" }, { name = "mcp", specifier = ">=1.12.4" }, + { name = "mcp-contextforge-gateway", extras = ["observability"], marker = "extra == 'observability-all'", specifier = ">=0.5.0" }, { name = "mcp-contextforge-gateway", extras = ["redis"], marker = "extra == 'all'", specifier = ">=0.5.0" }, { name = "mcp-contextforge-gateway", extras = ["redis", "dev"], marker = "extra == 'dev-all'", specifier = ">=0.5.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.17.1" }, + { name = "opentelemetry-api", marker = "extra == 'observability'", specifier = ">=1.25.0" }, + { name = "opentelemetry-exporter-jaeger", marker = "extra == 'observability-all'", specifier = ">=1.21.0" }, + { name = "opentelemetry-exporter-jaeger", marker = "extra == 'observability-jaeger'", specifier = ">=1.21.0" }, + { name = "opentelemetry-exporter-otlp", marker = "extra == 'observability'", specifier = ">=1.25.0" }, + { name = "opentelemetry-exporter-otlp-proto-grpc", marker = "extra == 'observability'", specifier = ">=1.25.0" }, + { name = "opentelemetry-exporter-zipkin", marker = "extra == 'observability-all'", specifier = ">=1.36.0" }, + { name = "opentelemetry-exporter-zipkin", marker = "extra == 'observability-zipkin'", specifier = ">=1.36.0" }, + { name = "opentelemetry-sdk", marker = "extra == 'observability'", specifier = ">=1.25.0" }, { name = "parse", specifier = ">=1.20.2" }, { name = "pexpect", marker = "extra == 'dev'", specifier = ">=4.9.0" }, { name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.9.0" }, @@ -1981,7 +2213,7 @@ requires-dist = [ { name = "pylint", marker = "extra == 'dev'", specifier = ">=3.3.8" }, { name = "pylint-pydantic", marker = "extra == 'dev'", specifier = ">=0.3.5" }, { name = "pyre-check", marker = "extra == 'dev'", specifier = ">=0.9.25" }, - { name = "pyrefly", marker = "extra == 'dev'", specifier = ">=0.27.2" }, + { name = "pyrefly", marker = "extra == 'dev'", specifier = ">=0.28.1" }, { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.403" }, { name = "pyroma", marker = "extra == 'dev'", specifier = ">=5.0" }, { name = "pyspelling", marker = "extra == 'dev'", specifier = ">=2.10" }, @@ -2008,7 +2240,7 @@ requires-dist = [ { name = "semgrep", marker = "extra == 'dev'", specifier = ">=1.131.0" }, { name = "settings-doc", marker = "extra == 'dev'", specifier = ">=4.3.2" }, { name = "snakeviz", marker = "extra == 'dev'", specifier = ">=2.2.2" }, - { name = "sqlalchemy", specifier = ">=2.0.42" }, + { name = "sqlalchemy", specifier = ">=2.0.43" }, { name = "sse-starlette", specifier = ">=3.0.2" }, { name = "starlette", specifier = ">=0.47.2" }, { name = "tomlcheck", marker = "extra == 'dev'", specifier = ">=0.2.3" }, @@ -2019,14 +2251,14 @@ requires-dist = [ { name = "typer", specifier = ">=0.16.0" }, { name = "types-tabulate", marker = "extra == 'dev'", specifier = ">=0.9.0.20241207" }, { name = "unimport", marker = "extra == 'dev'", specifier = ">=1.2.1" }, - { name = "uv", marker = "extra == 'dev'", specifier = ">=0.8.8" }, + { name = "uv", marker = "extra == 'dev'", specifier = ">=0.8.9" }, { name = "uvicorn", specifier = ">=0.35.0" }, { name = "vulture", marker = "extra == 'dev'", specifier = ">=2.14" }, { name = "websockets", marker = "extra == 'dev'", specifier = ">=15.0.1" }, { name = "yamllint", marker = "extra == 'dev'", specifier = ">=1.37.1" }, { name = "zeroconf", specifier = ">=0.147.0" }, ] -provides-extras = ["redis", "postgres", "alembic", "aiosqlite", "asyncpg", "dev", "playwright", "all", "dev-all"] +provides-extras = ["redis", "postgres", "alembic", "observability", "observability-jaeger", "observability-zipkin", "observability-all", "aiosqlite", "asyncpg", "chuck", "dev", "playwright", "all", "dev-all"] [[package]] name = "mdurl" @@ -2246,57 +2478,61 @@ wheels = [ [[package]] name = "nh3" -version = "0.2.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581, upload-time = "2025-02-25T13:38:44.619Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/81/b83775687fcf00e08ade6d4605f0be9c4584cb44c4973d9f27b7456a31c9/nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286", size = 1297678, upload-time = "2025-02-25T13:37:56.063Z" }, - { url = "https://files.pythonhosted.org/packages/22/ee/d0ad8fb4b5769f073b2df6807f69a5e57ca9cea504b78809921aef460d20/nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", size = 733774, upload-time = "2025-02-25T13:37:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/ea/76/b450141e2d384ede43fe53953552f1c6741a499a8c20955ad049555cabc8/nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", size = 760012, upload-time = "2025-02-25T13:38:01.017Z" }, - { url = "https://files.pythonhosted.org/packages/97/90/1182275db76cd8fbb1f6bf84c770107fafee0cb7da3e66e416bcb9633da2/nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", size = 923619, upload-time = "2025-02-25T13:38:02.617Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/269a7cfbec9693fad8d767c34a755c25ccb8d048fc1dfc7a7d86bc99375c/nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", size = 1000384, upload-time = "2025-02-25T13:38:04.402Z" }, - { url = "https://files.pythonhosted.org/packages/68/a9/48479dbf5f49ad93f0badd73fbb48b3d769189f04c6c69b0df261978b009/nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", size = 918908, upload-time = "2025-02-25T13:38:06.693Z" }, - { url = "https://files.pythonhosted.org/packages/d7/da/0279c118f8be2dc306e56819880b19a1cf2379472e3b79fc8eab44e267e3/nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", size = 909180, upload-time = "2025-02-25T13:38:10.941Z" }, - { url = "https://files.pythonhosted.org/packages/26/16/93309693f8abcb1088ae143a9c8dbcece9c8f7fb297d492d3918340c41f1/nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", size = 532747, upload-time = "2025-02-25T13:38:12.548Z" }, - { url = "https://files.pythonhosted.org/packages/a2/3a/96eb26c56cbb733c0b4a6a907fab8408ddf3ead5d1b065830a8f6a9c3557/nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", size = 528908, upload-time = "2025-02-25T13:38:14.059Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133, upload-time = "2025-02-25T13:38:16.601Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328, upload-time = "2025-02-25T13:38:18.972Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020, upload-time = "2025-02-25T13:38:20.571Z" }, - { url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878, upload-time = "2025-02-25T13:38:22.204Z" }, - { url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460, upload-time = "2025-02-25T13:38:25.951Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369, upload-time = "2025-02-25T13:38:28.174Z" }, - { url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036, upload-time = "2025-02-25T13:38:30.539Z" }, - { url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712, upload-time = "2025-02-25T13:38:32.992Z" }, - { url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559, upload-time = "2025-02-25T13:38:35.204Z" }, - { url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591, upload-time = "2025-02-25T13:38:37.099Z" }, - { url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670, upload-time = "2025-02-25T13:38:38.696Z" }, - { url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093, upload-time = "2025-02-25T13:38:40.249Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623, upload-time = "2025-02-25T13:38:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283, upload-time = "2025-02-25T13:38:43.355Z" }, +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/96cff0977357f60f06ec4368c4c7a7a26cccfe7c9fcd54f5378bf0428fd3/nh3-0.3.0.tar.gz", hash = "sha256:d8ba24cb31525492ea71b6aac11a4adac91d828aadeff7c4586541bf5dc34d2f", size = 19655, upload-time = "2025-07-17T14:43:37.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/11/340b7a551916a4b2b68c54799d710f86cf3838a4abaad8e74d35360343bb/nh3-0.3.0-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a537ece1bf513e5a88d8cff8a872e12fe8d0f42ef71dd15a5e7520fecd191bbb", size = 1427992, upload-time = "2025-07-17T14:43:06.848Z" }, + { url = "https://files.pythonhosted.org/packages/ad/7f/7c6b8358cf1222921747844ab0eef81129e9970b952fcb814df417159fb9/nh3-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c915060a2c8131bef6a29f78debc29ba40859b6dbe2362ef9e5fd44f11487c2", size = 798194, upload-time = "2025-07-17T14:43:08.263Z" }, + { url = "https://files.pythonhosted.org/packages/63/da/c5fd472b700ba37d2df630a9e0d8cc156033551ceb8b4c49cc8a5f606b68/nh3-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba0caa8aa184196daa6e574d997a33867d6d10234018012d35f86d46024a2a95", size = 837884, upload-time = "2025-07-17T14:43:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3c/cba7b26ccc0ef150c81646478aa32f9c9535234f54845603c838a1dc955c/nh3-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80fe20171c6da69c7978ecba33b638e951b85fb92059259edd285ff108b82a6d", size = 996365, upload-time = "2025-07-17T14:43:10.243Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ba/59e204d90727c25b253856e456ea61265ca810cda8ee802c35f3fadaab00/nh3-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e90883f9f85288f423c77b3f5a6f4486375636f25f793165112679a7b6363b35", size = 1071042, upload-time = "2025-07-17T14:43:11.57Z" }, + { url = "https://files.pythonhosted.org/packages/10/71/2fb1834c10fab6d9291d62c95192ea2f4c7518bd32ad6c46aab5d095cb87/nh3-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0649464ac8eee018644aacbc103874ccbfac80e3035643c3acaab4287e36e7f5", size = 995737, upload-time = "2025-07-17T14:43:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/33/c1/8f8ccc2492a000b6156dce68a43253fcff8b4ce70ab4216d08f90a2ac998/nh3-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1adeb1062a1c2974bc75b8d1ecb014c5fd4daf2df646bbe2831f7c23659793f9", size = 980552, upload-time = "2025-07-17T14:43:13.763Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d6/f1c6e091cbe8700401c736c2bc3980c46dca770a2cf6a3b48a175114058e/nh3-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:7275fdffaab10cc5801bf026e3c089d8de40a997afc9e41b981f7ac48c5aa7d5", size = 593618, upload-time = "2025-07-17T14:43:15.098Z" }, + { url = "https://files.pythonhosted.org/packages/23/1e/80a8c517655dd40bb13363fc4d9e66b2f13245763faab1a20f1df67165a7/nh3-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:423201bbdf3164a9e09aa01e540adbb94c9962cc177d5b1cbb385f5e1e79216e", size = 598948, upload-time = "2025-07-17T14:43:16.064Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e0/af86d2a974c87a4ba7f19bc3b44a8eaa3da480de264138fec82fe17b340b/nh3-0.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:16f8670201f7e8e0e05ed1a590eb84bfa51b01a69dd5caf1d3ea57733de6a52f", size = 580479, upload-time = "2025-07-17T14:43:17.038Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e0/cf1543e798ba86d838952e8be4cb8d18e22999be2a24b112a671f1c04fd6/nh3-0.3.0-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec6cfdd2e0399cb79ba4dcffb2332b94d9696c52272ff9d48a630c5dca5e325a", size = 1442218, upload-time = "2025-07-17T14:43:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/5c/86/a96b1453c107b815f9ab8fac5412407c33cc5c7580a4daf57aabeb41b774/nh3-0.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5e7185599f89b0e391e2f29cc12dc2e206167380cea49b33beda4891be2fe1", size = 823791, upload-time = "2025-07-17T14:43:19.721Z" }, + { url = "https://files.pythonhosted.org/packages/97/33/11e7273b663839626f714cb68f6eb49899da5a0d9b6bc47b41fe870259c2/nh3-0.3.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:389d93d59b8214d51c400fb5b07866c2a4f79e4e14b071ad66c92184fec3a392", size = 811143, upload-time = "2025-07-17T14:43:20.779Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1b/b15bd1ce201a1a610aeb44afd478d55ac018b4475920a3118ffd806e2483/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e9e6a7e4d38f7e8dda9edd1433af5170c597336c1a74b4693c5cb75ab2b30f2a", size = 1064661, upload-time = "2025-07-17T14:43:21.839Z" }, + { url = "https://files.pythonhosted.org/packages/8f/14/079670fb2e848c4ba2476c5a7a2d1319826053f4f0368f61fca9bb4227ae/nh3-0.3.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7852f038a054e0096dac12b8141191e02e93e0b4608c4b993ec7d4ffafea4e49", size = 997061, upload-time = "2025-07-17T14:43:23.179Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e5/ac7fc565f5d8bce7f979d1afd68e8cb415020d62fa6507133281c7d49f91/nh3-0.3.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af5aa8127f62bbf03d68f67a956627b1bd0469703a35b3dad28d0c1195e6c7fb", size = 924761, upload-time = "2025-07-17T14:43:24.23Z" }, + { url = "https://files.pythonhosted.org/packages/39/2c/6394301428b2017a9d5644af25f487fa557d06bc8a491769accec7524d9a/nh3-0.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f416c35efee3e6a6c9ab7716d9e57aa0a49981be915963a82697952cba1353e1", size = 803959, upload-time = "2025-07-17T14:43:26.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9a/344b9f9c4bd1c2413a397f38ee6a3d5db30f1a507d4976e046226f12b297/nh3-0.3.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37d3003d98dedca6cd762bf88f2e70b67f05100f6b949ffe540e189cc06887f9", size = 844073, upload-time = "2025-07-17T14:43:27.375Z" }, + { url = "https://files.pythonhosted.org/packages/66/3f/cd37f76c8ca277b02a84aa20d7bd60fbac85b4e2cbdae77cb759b22de58b/nh3-0.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:634e34e6162e0408e14fb61d5e69dbaea32f59e847cfcfa41b66100a6b796f62", size = 1000680, upload-time = "2025-07-17T14:43:28.452Z" }, + { url = "https://files.pythonhosted.org/packages/ee/db/7aa11b44bae4e7474feb1201d8dee04fabe5651c7cb51409ebda94a4ed67/nh3-0.3.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b0612ccf5de8a480cf08f047b08f9d3fecc12e63d2ee91769cb19d7290614c23", size = 1076613, upload-time = "2025-07-17T14:43:30.031Z" }, + { url = "https://files.pythonhosted.org/packages/97/03/03f79f7e5178eb1ad5083af84faff471e866801beb980cc72943a4397368/nh3-0.3.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c7a32a7f0d89f7d30cb8f4a84bdbd56d1eb88b78a2434534f62c71dac538c450", size = 1001418, upload-time = "2025-07-17T14:43:31.429Z" }, + { url = "https://files.pythonhosted.org/packages/ce/55/1974bcc16884a397ee699cebd3914e1f59be64ab305533347ca2d983756f/nh3-0.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3f1b4f8a264a0c86ea01da0d0c390fe295ea0bcacc52c2103aca286f6884f518", size = 986499, upload-time = "2025-07-17T14:43:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/76936ec021fe1f3270c03278b8af5f2079038116b5d0bfe8538ffe699d69/nh3-0.3.0-cp38-abi3-win32.whl", hash = "sha256:6d68fa277b4a3cf04e5c4b84dd0c6149ff7d56c12b3e3fab304c525b850f613d", size = 599000, upload-time = "2025-07-17T14:43:33.852Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/324b165d904dc1672eee5f5661c0a68d4bab5b59fbb07afb6d8d19a30b45/nh3-0.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:bae63772408fd63ad836ec569a7c8f444dd32863d0c67f6e0b25ebbd606afa95", size = 604530, upload-time = "2025-07-17T14:43:34.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/76/3165e84e5266d146d967a6cc784ff2fbf6ddd00985a55ec006b72bc39d5d/nh3-0.3.0-cp38-abi3-win_arm64.whl", hash = "sha256:d97d3efd61404af7e5721a0e74d81cdbfc6e5f97e11e731bb6d090e30a7b62b2", size = 585971, upload-time = "2025-07-17T14:43:35.936Z" }, ] [[package]] name = "ninja" -version = "1.11.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/d4/6b0324541018561c5e73e617bd16f20a4fc17d1179bb3b3520b6ca8beb7b/ninja-1.11.1.4.tar.gz", hash = "sha256:6aa39f6e894e0452e5b297327db00019383ae55d5d9c57c73b04f13bf79d438a", size = 201256, upload-time = "2025-03-22T06:46:43.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/b1/3a61b348936b62a386465b1937cd778fa3a5748582e26d832dbab844ff27/ninja-1.11.1.4-py3-none-macosx_10_9_universal2.whl", hash = "sha256:b33923c8da88e8da20b6053e38deb433f53656441614207e01d283ad02c5e8e7", size = 279071, upload-time = "2025-03-22T06:46:17.806Z" }, - { url = "https://files.pythonhosted.org/packages/12/42/4c94fdad51fcf1f039a156e97de9e4d564c2a8cc0303782d36f9bd893a4b/ninja-1.11.1.4-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cede0af00b58e27b31f2482ba83292a8e9171cdb9acc2c867a3b6e40b3353e43", size = 472026, upload-time = "2025-03-22T06:46:19.974Z" }, - { url = "https://files.pythonhosted.org/packages/eb/7a/455d2877fe6cf99886849c7f9755d897df32eaf3a0fba47b56e615f880f7/ninja-1.11.1.4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:096487995473320de7f65d622c3f1d16c3ad174797602218ca8c967f51ec38a0", size = 422814, upload-time = "2025-03-22T06:46:21.235Z" }, - { url = "https://files.pythonhosted.org/packages/e3/ad/fb6cca942528e25e8e0ab0f0cf98fe007319bf05cf69d726c564b815c4af/ninja-1.11.1.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3090d4488fadf6047d0d7a1db0c9643a8d391f0d94729554dbb89b5bdc769d7", size = 156965, upload-time = "2025-03-22T06:46:23.45Z" }, - { url = "https://files.pythonhosted.org/packages/a8/e7/d94a1b60031b115dd88526834b3da69eaacdc3c1a6769773ca8e2b1386b5/ninja-1.11.1.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecce44a00325a93631792974659cf253a815cc6da4ec96f89742925dfc295a0d", size = 179937, upload-time = "2025-03-22T06:46:24.728Z" }, - { url = "https://files.pythonhosted.org/packages/08/cc/e9316a28235409e9363794fc3d0b3083e48dd80d441006de66421e55f364/ninja-1.11.1.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c29bb66d2aa46a2409ab369ea804c730faec7652e8c22c1e428cc09216543e5", size = 157020, upload-time = "2025-03-22T06:46:26.046Z" }, - { url = "https://files.pythonhosted.org/packages/e3/30/389b22300541aa5f2e9dad322c4de2f84be4e32aa4e8babd9160d620b5f1/ninja-1.11.1.4-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:055f386fb550c2c9d6157e45e20a84d29c47968876b9c5794ae2aec46f952306", size = 130389, upload-time = "2025-03-22T06:46:27.174Z" }, - { url = "https://files.pythonhosted.org/packages/a9/10/e27f35cb92813aabbb7ae771b1685b45be1cc8a0798ce7d4bfd08d142b93/ninja-1.11.1.4-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:f6186d7607bb090c3be1e10c8a56b690be238f953616626f5032238c66e56867", size = 372435, upload-time = "2025-03-22T06:46:28.637Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/e3559619756739aae124c6abf7fe41f7e546ab1209cfbffb13137bff2d2e/ninja-1.11.1.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:cf4453679d15babc04ba023d68d091bb613091b67101c88f85d2171c6621c6eb", size = 419300, upload-time = "2025-03-22T06:46:30.392Z" }, - { url = "https://files.pythonhosted.org/packages/35/46/809e4e9572570991b8e6f88f3583807d017371ab4cb09171cbc72a7eb3e4/ninja-1.11.1.4-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:d4a6f159b08b0ac4aca5ee1572e3e402f969139e71d85d37c0e2872129098749", size = 420239, upload-time = "2025-03-22T06:46:32.442Z" }, - { url = "https://files.pythonhosted.org/packages/e6/64/5cb5710d15f844edf02ada577f8eddfdcd116f47eec15850f3371a3a4b33/ninja-1.11.1.4-py3-none-musllinux_1_1_s390x.whl", hash = "sha256:c3b96bd875f3ef1db782470e9e41d7508905a0986571f219d20ffed238befa15", size = 415986, upload-time = "2025-03-22T06:46:33.821Z" }, - { url = "https://files.pythonhosted.org/packages/95/b2/0e9ab1d926f423b12b09925f78afcc5e48b3c22e7121be3ddf6c35bf06a3/ninja-1.11.1.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:cf554e73f72c04deb04d0cf51f5fdb1903d9c9ca3d2344249c8ce3bd616ebc02", size = 379657, upload-time = "2025-03-22T06:46:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/c8/3e/fd6d330d0434168e7fe070d414b57dd99c4c133faa69c05b42a3cbdc6c13/ninja-1.11.1.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:cfdd09776436a1ff3c4a2558d3fc50a689fb9d7f1bdbc3e6f7b8c2991341ddb3", size = 454466, upload-time = "2025-03-22T06:46:37.413Z" }, - { url = "https://files.pythonhosted.org/packages/e6/df/a25f3ad0b1c59d1b90564096e4fd89a6ca30d562b1e942f23880c3000b89/ninja-1.11.1.4-py3-none-win32.whl", hash = "sha256:2ab67a41c90bea5ec4b795bab084bc0b3b3bb69d3cd21ca0294fc0fc15a111eb", size = 255931, upload-time = "2025-03-22T06:46:39.171Z" }, - { url = "https://files.pythonhosted.org/packages/5b/10/9b8fe9ac004847490cc7b54896124c01ce2d87d95dc60aabd0b8591addff/ninja-1.11.1.4-py3-none-win_amd64.whl", hash = "sha256:4617b3c12ff64b611a7d93fd9e378275512bb36eff8babff7c83f5116b4f8d66", size = 296461, upload-time = "2025-03-22T06:46:40.532Z" }, - { url = "https://files.pythonhosted.org/packages/b9/58/612a17593c2d117f96c7f6b7f1e6570246bddc4b1e808519403a1417f217/ninja-1.11.1.4-py3-none-win_arm64.whl", hash = "sha256:5713cf50c5be50084a8693308a63ecf9e55c3132a78a41ab1363a28b6caaaee1", size = 271441, upload-time = "2025-03-22T06:46:42.147Z" }, +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, + { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, + { url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" }, + { url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" }, ] [[package]] @@ -2321,6 +2557,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b2/4bc5e52c9a23df0ac17dbb23923e609a8269cd67000a712b4f5bcfae1490/opentelemetry_api-1.25.0-py3-none-any.whl", hash = "sha256:757fa1aa020a0f8fa139f8959e53dec2051cc26b832e76fa839a6d76ecefd737", size = 59910, upload-time = "2024-05-31T01:40:00.911Z" }, ] +[[package]] +name = "opentelemetry-exporter-jaeger" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-jaeger-proto-grpc" }, + { name = "opentelemetry-exporter-jaeger-thrift" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/52/d84ee33cb2a3b32f820bd1af035cdfe1fa03baa692aabb90bbe01812a235/opentelemetry_exporter_jaeger-1.21.0.tar.gz", hash = "sha256:e5650af9d09adad35add56b93d9856bd410644d25b0b5b87e6111467b9877dc2", size = 6150, upload-time = "2023-11-07T23:16:24.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/71/f45b52bde0cd0b316fd7ef034afa93d171c1de612923ea9e6be76dfe0d50/opentelemetry_exporter_jaeger-1.21.0-py3-none-any.whl", hash = "sha256:3d645eff7e31461d917be5c6635803ad7913cbb7662feed014ba2fe6e005b209", size = 7038, upload-time = "2023-11-07T23:15:48.48Z" }, +] + +[[package]] +name = "opentelemetry-exporter-jaeger-proto-grpc" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/c1/69b224098252b89a79e075db1be7001bb14095daff1c0348c3e83b466e85/opentelemetry_exporter_jaeger_proto_grpc-1.21.0.tar.gz", hash = "sha256:359ceb15b72e798881b4b7b2afdcbc189aef3a0508944856ac70d74968574ed7", size = 32224, upload-time = "2023-11-07T23:16:25.406Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/2e/d062a7a009138a40dd748add1cbe1c761eebfc791440bc7a631b53f8e5bd/opentelemetry_exporter_jaeger_proto_grpc-1.21.0-py3-none-any.whl", hash = "sha256:cf8ec4761c56d611e7bfb9538d4d85ea6a67cd1548f12deff83afaa5c447816e", size = 37548, upload-time = "2023-11-07T23:15:50.391Z" }, +] + +[[package]] +name = "opentelemetry-exporter-jaeger-thrift" +version = "1.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "thrift" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/ff/560d975ce09ec123dce1b7a97877bad1a5b2424c5c8e31e55169d4cd85e6/opentelemetry_exporter_jaeger_thrift-1.21.0.tar.gz", hash = "sha256:41119bc7e5602cec83dd7d7060f061ecbc91de231272e8f515b07ef9a4b6e41c", size = 27722, upload-time = "2023-11-07T23:16:26.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/2b/c6ff9604f5b8469e1fabe2b6ee50c36dadf0f572a430494d8dd5ce37d99f/opentelemetry_exporter_jaeger_thrift-1.21.0-py3-none-any.whl", hash = "sha256:4364b8dfa6965707c72c43d85942b1491982b7d44f0123d593513e8bedafa9e2", size = 35929, upload-time = "2023-11-07T23:15:52.341Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-otlp-proto-grpc" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/e5/49b7cd1a1647173eefbb7db5467163d5c4816413000555ee3f1cb80aafc3/opentelemetry_exporter_otlp-1.25.0.tar.gz", hash = "sha256:ce03199c1680a845f82e12c0a6a8f61036048c07ec7a0bd943142aca8fa6ced0", size = 6140, upload-time = "2024-05-31T01:40:40.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/fd/7d95c7320926a43b88e9d4350338d3dae3b5ac16dd344825194f01918f56/opentelemetry_exporter_otlp-1.25.0-py3-none-any.whl", hash = "sha256:d67a831757014a3bc3174e4cd629ae1493b7ba8d189e8a007003cacb9f1a6b60", size = 6996, upload-time = "2024-05-31T01:40:11.559Z" }, +] + [[package]] name = "opentelemetry-exporter-otlp-proto-common" version = "1.25.0" @@ -2333,6 +2624,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/02/74ac6619eec78c82a923324f916d3eccd2f2254cf4270b669e96b76bf717/opentelemetry_exporter_otlp_proto_common-1.25.0-py3-none-any.whl", hash = "sha256:15637b7d580c2675f70246563363775b4e6de947871e01d0f4e3881d1848d693", size = 17762, upload-time = "2024-05-31T01:40:13.172Z" }, ] +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecated" }, + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/76/94c72787f82130d1a24be64fad34eeaf7fa85cfe9d1740d096f577c5e57d/opentelemetry_exporter_otlp_proto_grpc-1.25.0.tar.gz", hash = "sha256:c0b1661415acec5af87625587efa1ccab68b873745ca0ee96b69bb1042087eac", size = 25278, upload-time = "2024-05-31T01:40:42.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/f3/e24294e7b3f6d2b9aafc97b9b82e214dfe9ffa152dfecbd897e7ffbf6844/opentelemetry_exporter_otlp_proto_grpc-1.25.0-py3-none-any.whl", hash = "sha256:3131028f0c0a155a64c430ca600fd658e8e37043cb13209f0109db5c1a3e4eb4", size = 18223, upload-time = "2024-05-31T01:40:14.844Z" }, +] + [[package]] name = "opentelemetry-exporter-otlp-proto-http" version = "1.25.0" @@ -2351,6 +2660,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/b9/a47734f7c5a45619d8c64c227f119092b4679b2c49d37116fda7c0fc4573/opentelemetry_exporter_otlp_proto_http-1.25.0-py3-none-any.whl", hash = "sha256:2eca686ee11b27acd28198b3ea5e5863a53d1266b91cda47c839d95d5e0541a6", size = 16790, upload-time = "2024-05-31T01:40:16.649Z" }, ] +[[package]] +name = "opentelemetry-exporter-zipkin" +version = "1.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-exporter-zipkin-json" }, + { name = "opentelemetry-exporter-zipkin-proto-http" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/d7/b39363de9bcd90e82615ddc5690e29440cd1684ebe66c97f2c1228814362/opentelemetry_exporter_zipkin-1.36.0.tar.gz", hash = "sha256:d5241422c90846bf850f7dfc6a44b35b0a26538030780a617c141efc20709145", size = 6042, upload-time = "2025-07-29T15:12:10.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/d7/dddf2b4343393a78caff9d2ad9ce2cf69d6408e0cf27afb9aceb672c21b6/opentelemetry_exporter_zipkin-1.36.0-py3-none-any.whl", hash = "sha256:6ff7d8350fc8ec1231346638bf92ce07087925eb1c64528dd99ce87b0bf1edff", size = 6977, upload-time = "2025-07-29T15:11:55.209Z" }, +] + +[[package]] +name = "opentelemetry-exporter-zipkin-json" +version = "1.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/3a55fc44cef7be9de3f116ff3715fe6eec8ff9b24da2e0aadd0345a99d45/opentelemetry_exporter_zipkin_json-1.36.0.tar.gz", hash = "sha256:8a15c9fb4621cf612ebd0e3ed6b6daf3b6d3a11f0c0bfa41ac6a72de04ff63df", size = 16558, upload-time = "2025-07-29T15:12:11.355Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/5d/d4529c987d5aafe074d078f3141a652da54fa6eedef45190402f7b170fc8/opentelemetry_exporter_zipkin_json-1.36.0-py3-none-any.whl", hash = "sha256:9f9013f7937e2db419ca7a93eb80611514d9c5d4b8022770a062f4c07f5df045", size = 16334, upload-time = "2025-07-29T15:11:56.411Z" }, +] + +[[package]] +name = "opentelemetry-exporter-zipkin-proto-http" +version = "1.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-zipkin-json" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/20/bd3912b1b1a98028c24fa2bbf01305d055efcb97e071ce991a3e88acd4c6/opentelemetry_exporter_zipkin_proto_http-1.36.0.tar.gz", hash = "sha256:3edd6a8b4b7dbe2f5f0687cdac0bd4e9edce0085430d4f4d3fa5f0dcffd192fa", size = 18226, upload-time = "2025-07-29T15:12:11.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2d/7daf0f4c137a0891aa946afab40001c6f1fbb498f2e763ee71bd049522fa/opentelemetry_exporter_zipkin_proto_http-1.36.0-py3-none-any.whl", hash = "sha256:6cf3ba77fb8bb26ff7c5e3a4c7cfd0cf7642316d3ef655d46b90c732244d6ed8", size = 15907, upload-time = "2025-07-29T15:11:57.247Z" }, +] + [[package]] name = "opentelemetry-instrumentation" version = "0.46b0" @@ -2441,11 +2793,11 @@ wheels = [ [[package]] name = "packageurl-python" -version = "0.17.1" +version = "0.17.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/b6/d28c4fa7535530879e7d64176f7ff081fb6308b50cac8e30f038a89e8fdd/packageurl_python-0.17.1.tar.gz", hash = "sha256:5db592a990b60bc02446033c50fb1803a26c5124cd72c5a2cd1b8ea1ae741969", size = 41312, upload-time = "2025-06-06T13:13:59.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/f0/de0ac00a4484c0d87b71e3d9985518278d89797fa725e90abd3453bccb42/packageurl_python-0.17.5.tar.gz", hash = "sha256:a7be3f3ba70d705f738ace9bf6124f31920245a49fa69d4b416da7037dd2de61", size = 43832, upload-time = "2025-08-06T14:08:20.235Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/ba/9c01f96eca2d1833fa787e695208d57a7a07f6531517040e523509ced368/packageurl_python-0.17.1-py3-none-any.whl", hash = "sha256:59b0862ae0b216994f847e05b4c6e870e0d16e1ddd706feefb19d79810f22cbd", size = 28748, upload-time = "2025-06-06T13:13:58.631Z" }, + { url = "https://files.pythonhosted.org/packages/be/78/9dbb7d2ef240d20caf6f79c0f66866737c9d0959601fd783ff635d1d019d/packageurl_python-0.17.5-py3-none-any.whl", hash = "sha256:f0e55452ab37b5c192c443de1458e3f3b4d8ac27f747df6e8c48adeab081d321", size = 30544, upload-time = "2025-08-06T14:08:19.055Z" }, ] [[package]] @@ -2486,14 +2838,14 @@ wheels = [ [[package]] name = "pbr" -version = "6.1.1" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/d2/510cc0d218e753ba62a1bc1434651db3cd797a9716a0a66cc714cb4f0935/pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b", size = 125702, upload-time = "2025-02-04T14:28:06.514Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/88/baf6b45d064271f19fefac7def6a030a893f912f430de0024dd595ced61f/pbr-7.0.0.tar.gz", hash = "sha256:cf4127298723dafbce3afd13775ccf3885be5d3c8435751b867f9a6a10b71a39", size = 129146, upload-time = "2025-08-13T09:16:41.654Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/ac/684d71315abc7b1214d59304e23a982472967f6bf4bde5a98f1503f648dc/pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76", size = 108997, upload-time = "2025-02-04T14:28:03.168Z" }, + { url = "https://files.pythonhosted.org/packages/d6/98/120c3e21bf3fc0ef397a3906465ee9f5c76996c52811e65455eadc12d68a/pbr-7.0.0-py2.py3-none-any.whl", hash = "sha256:b447e63a2bc04fd975fc0480b8d5ebf979179e2c0ae203bf1eff9ea20073bc38", size = 125109, upload-time = "2025-08-13T09:16:40.269Z" }, ] [[package]] @@ -2528,11 +2880,11 @@ wheels = [ [[package]] name = "pip" -version = "25.1.1" +version = "25.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/59/de/241caa0ca606f2ec5fe0c1f4261b0465df78d786a38da693864a116c37f4/pip-25.1.1.tar.gz", hash = "sha256:3de45d411d308d5054c2168185d8da7f9a2cd753dbac8acbfa88a8909ecd9077", size = 1940155, upload-time = "2025-05-02T15:14:02.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/16/650289cd3f43d5a2fadfd98c68bd1e1e7f2550a1a5326768cddfbcedb2c5/pip-25.2.tar.gz", hash = "sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2", size = 1840021, upload-time = "2025-07-30T21:50:15.401Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/a2/d40fb2460e883eca5199c62cfc2463fd261f760556ae6290f88488c362c0/pip-25.1.1-py3-none-any.whl", hash = "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af", size = 1825227, upload-time = "2025-05-02T15:13:59.102Z" }, + { url = "https://files.pythonhosted.org/packages/b7/3f/945ef7ab14dc4f9d7f40288d2df998d1837ee0888ec3659c813487572faa/pip-25.2-py3-none-any.whl", hash = "sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717", size = 1752557, upload-time = "2025-07-30T21:50:13.323Z" }, ] [[package]] @@ -2766,11 +3118,10 @@ wheels = [ [[package]] name = "prospector" -version = "1.17.2" +version = "1.17.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dodgy" }, - { name = "gitpython" }, { name = "mccabe" }, { name = "packaging" }, { name = "pep8-naming" }, @@ -2785,9 +3136,9 @@ dependencies = [ { name = "setoptconf-tmp" }, { name = "toml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/f7/ffdef5487dd651fac632eec5a5cd0df3008d16a9e901bceedaddb8720f57/prospector-1.17.2.tar.gz", hash = "sha256:44a92f5027bfbac4cc00b2598a773bf2a546067f5a7a6a525e7a1e96a2635a52", size = 71016, upload-time = "2025-06-23T15:31:39.805Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/89/180c37306d39adea1137160e626d55a621bc4d4a325386adae1f37f97416/prospector-1.17.3.tar.gz", hash = "sha256:c4df723012c45fc70f5b71f5eee318dbccaf903c2012c8ecf3ec89f60e5cb822", size = 71052, upload-time = "2025-08-13T11:19:11.555Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/12/0e6f5bac93560d88b0a6a50a5d30b2e2b3cfb6d10d413af21275214e272c/prospector-1.17.2-py3-none-any.whl", hash = "sha256:b99206e7f214c3823cff155cd458f507d45beae86b2dbcead2260eda570ed8db", size = 96171, upload-time = "2025-06-23T15:31:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/36/93/e400ea04527e304beb52fda165000fa3997e2b6f7fc8bcedea4528e8447f/prospector-1.17.3-py3-none-any.whl", hash = "sha256:e3698bba95f86ef50d626df33b29272cf6462061aca2b19161186c4405a6a402", size = 96195, upload-time = "2025-08-13T11:19:10.25Z" }, ] [package.optional-dependencies] @@ -2802,16 +3153,11 @@ with-everything = [ [[package]] name = "protobuf" -version = "4.25.8" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/01/34c8d2b6354906d728703cb9d546a0e534de479e25f1b581e4094c4a85cc/protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd", size = 380920, upload-time = "2025-05-28T14:22:25.153Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/5b/e3d951e34f8356e5feecacd12a8e3b258a1da6d9a03ad1770f28925f29bc/protobuf-3.20.3.tar.gz", hash = "sha256:2e3427429c9cffebf259491be0af70189607f365c2f41c7c3764af6f337105f2", size = 216768, upload-time = "2022-09-29T22:39:47.592Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/ff/05f34305fe6b85bbfbecbc559d423a5985605cad5eda4f47eae9e9c9c5c5/protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0", size = 392745, upload-time = "2025-05-28T14:22:10.524Z" }, - { url = "https://files.pythonhosted.org/packages/08/35/8b8a8405c564caf4ba835b1fdf554da869954712b26d8f2a98c0e434469b/protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9", size = 413736, upload-time = "2025-05-28T14:22:13.156Z" }, - { url = "https://files.pythonhosted.org/packages/28/d7/ab27049a035b258dab43445eb6ec84a26277b16105b277cbe0a7698bdc6c/protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f", size = 394537, upload-time = "2025-05-28T14:22:14.768Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6d/a4a198b61808dd3d1ee187082ccc21499bc949d639feb948961b48be9a7e/protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7", size = 294005, upload-time = "2025-05-28T14:22:16.052Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c6/c9deaa6e789b6fc41b88ccbdfe7a42d2b82663248b715f55aa77fbc00724/protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0", size = 294924, upload-time = "2025-05-28T14:22:17.105Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c1/6aece0ab5209981a70cd186f164c133fdba2f51e124ff92b73de7fd24d78/protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59", size = 156757, upload-time = "2025-05-28T14:22:24.135Z" }, + { url = "https://files.pythonhosted.org/packages/8d/14/619e24a4c70df2901e1f4dbc50a6291eb63a759172558df326347dce1f0d/protobuf-3.20.3-py2.py3-none-any.whl", hash = "sha256:a7ca6d488aa8ff7f329d4c545b2dbad8ac31464f1d8b1c87ad1346717731e4db", size = 162128, upload-time = "2022-09-29T22:39:44.547Z" }, ] [[package]] @@ -2892,14 +3238,14 @@ wheels = [ [[package]] name = "py-serializable" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "defusedxml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/75/813967eae0542776314c6def33feac687642a193b9d5591c20684b2eafd8/py_serializable-2.0.0.tar.gz", hash = "sha256:e9e6491dd7d29c31daf1050232b57f9657f9e8a43b867cca1ff204752cf420a5", size = 51617, upload-time = "2025-02-09T13:41:55.768Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/0e/8601d2331dea0825f3be769688b48a95f387a83c918cca6a8c9cee4b9eb7/py_serializable-2.0.0-py3-none-any.whl", hash = "sha256:1721e4c0368adeec965c183168da4b912024702f19e15e13f8577098b9a4f8fe", size = 22824, upload-time = "2025-02-09T13:41:54.236Z" }, + { url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" }, ] [[package]] @@ -3070,11 +3416,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" 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/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } 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/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] @@ -3130,14 +3476,14 @@ wheels = [ [[package]] name = "pylint-plugin-utils" -version = "0.8.2" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pylint" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/d2/3b9728910bc69232ec38d8fb7053c03c887bfe7e6e170649b683dd351750/pylint_plugin_utils-0.8.2.tar.gz", hash = "sha256:d3cebf68a38ba3fba23a873809155562571386d4c1b03e5b4c4cc26c3eee93e4", size = 10674, upload-time = "2023-05-20T04:42:16.154Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/85/24eaf5d0d078fc8799ae6d89faf326d6e4d27d862fc9a710a52ab07b7bb5/pylint_plugin_utils-0.9.0.tar.gz", hash = "sha256:5468d763878a18d5cc4db46eaffdda14313b043c962a263a7d78151b90132055", size = 10474, upload-time = "2025-06-24T07:14:00.534Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/ee/49d11aee31061bcc1d2726bd8334a2883ddcdbde7d7744ed6b3bd11704ed/pylint_plugin_utils-0.8.2-py3-none-any.whl", hash = "sha256:ae11664737aa2effbf26f973a9e0b6779ab7106ec0adc5fe104b0907ca04e507", size = 11171, upload-time = "2023-05-20T04:42:13.65Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c9/a3b871b0b590c49e38884af6dab58ab9711053bd5c39b8899b72e367b9f6/pylint_plugin_utils-0.9.0-py3-none-any.whl", hash = "sha256:16e9b84e5326ba893a319a0323fcc8b4bcc9c71fc654fcabba0605596c673818", size = 11129, upload-time = "2025-06-24T07:13:58.993Z" }, ] [[package]] @@ -3221,18 +3567,18 @@ wheels = [ [[package]] name = "pyrefly" -version = "0.28.0" +version = "0.28.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/9f/861d01445e02e0e668619f3bc7d0f2fa2524dedd328a40072afb71845876/pyrefly-0.28.0.tar.gz", hash = "sha256:6b1431e86d35c434ef286058fe2ea8ef2e968f70a706dc6296904a4850940ed5", size = 1213997, upload-time = "2025-08-11T17:34:34.199Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/36/55f1503e8722ae60e7db9040c2562a69cd1ac1b698188b34b56ece9461e9/pyrefly-0.28.1.tar.gz", hash = "sha256:9ebc67e4a2e3d33c78f1962e7b2a16cd9b4415ce22fcf7a290b741ed9f3b7535", size = 1220266, upload-time = "2025-08-12T05:31:01.835Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/54/84e8cb476e143e5766432e9b1c7f2207928766472454720e11c36ccd2eba/pyrefly-0.28.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2a37dae101147fcd10e82fd7769f3658abd60fbc335d09820a4e0d78e0ab691a", size = 6423862, upload-time = "2025-08-11T17:34:20.321Z" }, - { url = "https://files.pythonhosted.org/packages/ea/51/1d0979e3bac99549bb1323279039536e63a7d1d6a035f00ed19a60ad5e79/pyrefly-0.28.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0931bc81650c1209310cbac576eba554fbd318ea9eaeae097181e9db07196197", size = 5993928, upload-time = "2025-08-11T17:34:22.219Z" }, - { url = "https://files.pythonhosted.org/packages/d7/c9/a62dca991532a3e9bc02f16c7e8d5dc0cbae70ea21fbc9520ac38fe3ace5/pyrefly-0.28.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eeb470ed84d0e0ff63c5fb7573120ac822b7dfb9cf51c9a9b6bd75eceb64728", size = 6213273, upload-time = "2025-08-11T17:34:24.013Z" }, - { url = "https://files.pythonhosted.org/packages/6e/96/e0b05163e2eaef1ee74be3e5fce9bc4a43e53a355eab5382fb589f0df57f/pyrefly-0.28.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a80b8213d4dc4c3d139dbdbac64c2a4324f876f3ed79f76cec66223b7394afc3", size = 6983753, upload-time = "2025-08-11T17:34:25.836Z" }, - { url = "https://files.pythonhosted.org/packages/99/ff/6e95277b489d38fbb2345f68bdc392988817ca892f71d98995095947d056/pyrefly-0.28.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bda267113d4dcb284818ddf5a64236fdcb6ddf28343493f8162efdcaf104929", size = 6687047, upload-time = "2025-08-11T17:34:27.465Z" }, - { url = "https://files.pythonhosted.org/packages/36/55/a839dcfdc8e119d8120a8a71737dc3a4c6b2bdbcaa69db0413a1723afce4/pyrefly-0.28.0-py3-none-win32.whl", hash = "sha256:cb2cd5867510a990683172b05b73dff7a3822ea1e41bb5bb3ce194f2763b4a0c", size = 6206654, upload-time = "2025-08-11T17:34:29.202Z" }, - { url = "https://files.pythonhosted.org/packages/0b/a9/d0d5af3446e6306926931cc83f0a3442c1a558160665dec5fc8527fd65c8/pyrefly-0.28.0-py3-none-win_amd64.whl", hash = "sha256:22dfcb5be34ddc08c43d45cca4c13487bfe63bc2eba9fd039bb0c4410f29c6e4", size = 6619285, upload-time = "2025-08-11T17:34:31.067Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0d/80d1f0310aaa0224e3e453721036beb49f0fa740d6a09eabf73008f30ab6/pyrefly-0.28.0-py3-none-win_arm64.whl", hash = "sha256:c9e900b2a2e784665fd270fdafc241b77b9cfc5a8ec78166656d750c2fde9c58", size = 6256284, upload-time = "2025-08-11T17:34:32.613Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b6/e5857aa7225f6ca65f9c3f21a710b0942b62d7f37ee065321bfc03be8d87/pyrefly-0.28.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a4abd5218f43c25c00571fc498f85892b434d2361882a9e38ca7bb0ccb949bff", size = 6434495, upload-time = "2025-08-12T05:30:43.81Z" }, + { url = "https://files.pythonhosted.org/packages/a7/53/4a8810ede4c049856ea890f6d4093a7204e63fa4fdea750697b458ef49ee/pyrefly-0.28.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7b0ef6859ceca146f41be152e3bc783248cac7425f904a67bb9d3b130210e03c", size = 6006320, upload-time = "2025-08-12T05:30:46.259Z" }, + { url = "https://files.pythonhosted.org/packages/ed/66/9a0a90de127d4c1a186c49e8ca6ee94498c0c53e289e42d8a1baac5278b0/pyrefly-0.28.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36ac5fa3dcf83d51f9a7524a391f3614e8227fa4d22f4c053437f91e763d1fa6", size = 6222713, upload-time = "2025-08-12T05:30:49.416Z" }, + { url = "https://files.pythonhosted.org/packages/b0/52/d214d8df6fa24b256835b7a9e7d31b8a2fc161fd72fbc7b38d4f0f2302e6/pyrefly-0.28.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:428e8a174891a14d9e7364e20073162d66c7a0e2575dc5433e2f8228a0fe94ca", size = 7003024, upload-time = "2025-08-12T05:30:51.304Z" }, + { url = "https://files.pythonhosted.org/packages/de/db/9fdbc4b73348bc1c3c4899051c1fbd614824ceb63e838b399f81a5786f23/pyrefly-0.28.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a5f66c43fc2e4676307b539d1ed085744a5625c579365cfc889cb9faf9ef8d0", size = 6695834, upload-time = "2025-08-12T05:30:53.501Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fb/a3bf2c416735068f86342bf155bb30bf791a2ace87cc99eec1889f8a1845/pyrefly-0.28.1-py3-none-win32.whl", hash = "sha256:ccf2e7d1253de03940953aeb8746c189435899620d113cb05114e8d2175892e4", size = 6209107, upload-time = "2025-08-12T05:30:55.659Z" }, + { url = "https://files.pythonhosted.org/packages/dd/bc/d75894cf78b9c961e76c9a13e79216ff9081d7ecd9190d2148ffa72a7e99/pyrefly-0.28.1-py3-none-win_amd64.whl", hash = "sha256:cb973dc1fc3c128f9d674f943eca9eea6d4ed272a329836efc9d6e5c16ebe12a", size = 6630549, upload-time = "2025-08-12T05:30:58.289Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8d/63a69a61aea24e4580e25f2a6bf5bb27bea30f06d34c4de0f3d414bf11ec/pyrefly-0.28.1-py3-none-win_arm64.whl", hash = "sha256:b3aa87f12555dda76b60aa101466ad5fde54a53f20c5112b02ea2eaaf0d6bfe9", size = 6258641, upload-time = "2025-08-12T05:31:00.195Z" }, ] [[package]] @@ -3505,11 +3851,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.1.0" +version = "1.1.1" 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/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } 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/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, ] [[package]] @@ -3594,18 +3940,18 @@ wheels = [ [[package]] name = "pywin32" -version = "310" +version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload-time = "2025-03-17T00:55:53.124Z" }, - { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload-time = "2025-03-17T00:55:55.203Z" }, - { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload-time = "2025-03-17T00:55:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239, upload-time = "2025-03-17T00:55:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839, upload-time = "2025-03-17T00:56:00.8Z" }, - { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470, upload-time = "2025-03-17T00:56:02.601Z" }, - { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384, upload-time = "2025-03-17T00:56:04.383Z" }, - { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039, upload-time = "2025-03-17T00:56:06.207Z" }, - { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload-time = "2025-03-17T00:56:07.819Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, ] [[package]] @@ -3782,76 +4128,81 @@ wheels = [ [[package]] name = "rpds-py" -version = "0.25.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/a6/60184b7fc00dd3ca80ac635dd5b8577d444c57e8e8742cecabfacb829921/rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3", size = 27304, upload-time = "2025-05-21T12:46:12.502Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/e1/df13fe3ddbbea43567e07437f097863b20c99318ae1f58a0fe389f763738/rpds_py-0.25.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5f048bbf18b1f9120685c6d6bb70cc1a52c8cc11bdd04e643d28d3be0baf666d", size = 373341, upload-time = "2025-05-21T12:43:02.978Z" }, - { url = "https://files.pythonhosted.org/packages/7a/58/deef4d30fcbcbfef3b6d82d17c64490d5c94585a2310544ce8e2d3024f83/rpds_py-0.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fbb0dbba559959fcb5d0735a0f87cdbca9e95dac87982e9b95c0f8f7ad10255", size = 359111, upload-time = "2025-05-21T12:43:05.128Z" }, - { url = "https://files.pythonhosted.org/packages/bb/7e/39f1f4431b03e96ebaf159e29a0f82a77259d8f38b2dd474721eb3a8ac9b/rpds_py-0.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4ca54b9cf9d80b4016a67a0193ebe0bcf29f6b0a96f09db942087e294d3d4c2", size = 386112, upload-time = "2025-05-21T12:43:07.13Z" }, - { url = "https://files.pythonhosted.org/packages/db/e7/847068a48d63aec2ae695a1646089620b3b03f8ccf9f02c122ebaf778f3c/rpds_py-0.25.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ee3e26eb83d39b886d2cb6e06ea701bba82ef30a0de044d34626ede51ec98b0", size = 400362, upload-time = "2025-05-21T12:43:08.693Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3d/9441d5db4343d0cee759a7ab4d67420a476cebb032081763de934719727b/rpds_py-0.25.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89706d0683c73a26f76a5315d893c051324d771196ae8b13e6ffa1ffaf5e574f", size = 522214, upload-time = "2025-05-21T12:43:10.694Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/2cc5b30d95f9f1a432c79c7a2f65d85e52812a8f6cbf8768724571710786/rpds_py-0.25.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2013ee878c76269c7b557a9a9c042335d732e89d482606990b70a839635feb7", size = 411491, upload-time = "2025-05-21T12:43:12.739Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6c/44695c1f035077a017dd472b6a3253553780837af2fac9b6ac25f6a5cb4d/rpds_py-0.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45e484db65e5380804afbec784522de84fa95e6bb92ef1bd3325d33d13efaebd", size = 386978, upload-time = "2025-05-21T12:43:14.25Z" }, - { url = "https://files.pythonhosted.org/packages/b1/74/b4357090bb1096db5392157b4e7ed8bb2417dc7799200fcbaee633a032c9/rpds_py-0.25.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:48d64155d02127c249695abb87d39f0faf410733428d499867606be138161d65", size = 420662, upload-time = "2025-05-21T12:43:15.8Z" }, - { url = "https://files.pythonhosted.org/packages/26/dd/8cadbebf47b96e59dfe8b35868e5c38a42272699324e95ed522da09d3a40/rpds_py-0.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:048893e902132fd6548a2e661fb38bf4896a89eea95ac5816cf443524a85556f", size = 563385, upload-time = "2025-05-21T12:43:17.78Z" }, - { url = "https://files.pythonhosted.org/packages/c3/ea/92960bb7f0e7a57a5ab233662f12152085c7dc0d5468534c65991a3d48c9/rpds_py-0.25.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0317177b1e8691ab5879f4f33f4b6dc55ad3b344399e23df2e499de7b10a548d", size = 592047, upload-time = "2025-05-21T12:43:19.457Z" }, - { url = "https://files.pythonhosted.org/packages/61/ad/71aabc93df0d05dabcb4b0c749277881f8e74548582d96aa1bf24379493a/rpds_py-0.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bffcf57826d77a4151962bf1701374e0fc87f536e56ec46f1abdd6a903354042", size = 557863, upload-time = "2025-05-21T12:43:21.69Z" }, - { url = "https://files.pythonhosted.org/packages/93/0f/89df0067c41f122b90b76f3660028a466eb287cbe38efec3ea70e637ca78/rpds_py-0.25.1-cp311-cp311-win32.whl", hash = "sha256:cda776f1967cb304816173b30994faaf2fd5bcb37e73118a47964a02c348e1bc", size = 219627, upload-time = "2025-05-21T12:43:23.311Z" }, - { url = "https://files.pythonhosted.org/packages/7c/8d/93b1a4c1baa903d0229374d9e7aa3466d751f1d65e268c52e6039c6e338e/rpds_py-0.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:dc3c1ff0abc91444cd20ec643d0f805df9a3661fcacf9c95000329f3ddf268a4", size = 231603, upload-time = "2025-05-21T12:43:25.145Z" }, - { url = "https://files.pythonhosted.org/packages/cb/11/392605e5247bead2f23e6888e77229fbd714ac241ebbebb39a1e822c8815/rpds_py-0.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:5a3ddb74b0985c4387719fc536faced33cadf2172769540c62e2a94b7b9be1c4", size = 223967, upload-time = "2025-05-21T12:43:26.566Z" }, - { url = "https://files.pythonhosted.org/packages/7f/81/28ab0408391b1dc57393653b6a0cf2014cc282cc2909e4615e63e58262be/rpds_py-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5ffe453cde61f73fea9430223c81d29e2fbf412a6073951102146c84e19e34c", size = 364647, upload-time = "2025-05-21T12:43:28.559Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9a/7797f04cad0d5e56310e1238434f71fc6939d0bc517192a18bb99a72a95f/rpds_py-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:115874ae5e2fdcfc16b2aedc95b5eef4aebe91b28e7e21951eda8a5dc0d3461b", size = 350454, upload-time = "2025-05-21T12:43:30.615Z" }, - { url = "https://files.pythonhosted.org/packages/69/3c/93d2ef941b04898011e5d6eaa56a1acf46a3b4c9f4b3ad1bbcbafa0bee1f/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a714bf6e5e81b0e570d01f56e0c89c6375101b8463999ead3a93a5d2a4af91fa", size = 389665, upload-time = "2025-05-21T12:43:32.629Z" }, - { url = "https://files.pythonhosted.org/packages/c1/57/ad0e31e928751dde8903a11102559628d24173428a0f85e25e187defb2c1/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35634369325906bcd01577da4c19e3b9541a15e99f31e91a02d010816b49bfda", size = 403873, upload-time = "2025-05-21T12:43:34.576Z" }, - { url = "https://files.pythonhosted.org/packages/16/ad/c0c652fa9bba778b4f54980a02962748479dc09632e1fd34e5282cf2556c/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cb2b3ddc16710548801c6fcc0cfcdeeff9dafbc983f77265877793f2660309", size = 525866, upload-time = "2025-05-21T12:43:36.123Z" }, - { url = "https://files.pythonhosted.org/packages/2a/39/3e1839bc527e6fcf48d5fec4770070f872cdee6c6fbc9b259932f4e88a38/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ceca1cf097ed77e1a51f1dbc8d174d10cb5931c188a4505ff9f3e119dfe519b", size = 416886, upload-time = "2025-05-21T12:43:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/7a/95/dd6b91cd4560da41df9d7030a038298a67d24f8ca38e150562644c829c48/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2cd1a4b0c2b8c5e31ffff50d09f39906fe351389ba143c195566056c13a7ea", size = 390666, upload-time = "2025-05-21T12:43:40.065Z" }, - { url = "https://files.pythonhosted.org/packages/64/48/1be88a820e7494ce0a15c2d390ccb7c52212370badabf128e6a7bb4cb802/rpds_py-0.25.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de336a4b164c9188cb23f3703adb74a7623ab32d20090d0e9bf499a2203ad65", size = 425109, upload-time = "2025-05-21T12:43:42.263Z" }, - { url = "https://files.pythonhosted.org/packages/cf/07/3e2a17927ef6d7720b9949ec1b37d1e963b829ad0387f7af18d923d5cfa5/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9fca84a15333e925dd59ce01da0ffe2ffe0d6e5d29a9eeba2148916d1824948c", size = 567244, upload-time = "2025-05-21T12:43:43.846Z" }, - { url = "https://files.pythonhosted.org/packages/d2/e5/76cf010998deccc4f95305d827847e2eae9c568099c06b405cf96384762b/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88ec04afe0c59fa64e2f6ea0dd9657e04fc83e38de90f6de201954b4d4eb59bd", size = 596023, upload-time = "2025-05-21T12:43:45.932Z" }, - { url = "https://files.pythonhosted.org/packages/52/9a/df55efd84403736ba37a5a6377b70aad0fd1cb469a9109ee8a1e21299a1c/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8bd2f19e312ce3e1d2c635618e8a8d8132892bb746a7cf74780a489f0f6cdcb", size = 561634, upload-time = "2025-05-21T12:43:48.263Z" }, - { url = "https://files.pythonhosted.org/packages/ab/aa/dc3620dd8db84454aaf9374bd318f1aa02578bba5e567f5bf6b79492aca4/rpds_py-0.25.1-cp312-cp312-win32.whl", hash = "sha256:e5e2f7280d8d0d3ef06f3ec1b4fd598d386cc6f0721e54f09109a8132182fbfe", size = 222713, upload-time = "2025-05-21T12:43:49.897Z" }, - { url = "https://files.pythonhosted.org/packages/a3/7f/7cef485269a50ed5b4e9bae145f512d2a111ca638ae70cc101f661b4defd/rpds_py-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:db58483f71c5db67d643857404da360dce3573031586034b7d59f245144cc192", size = 235280, upload-time = "2025-05-21T12:43:51.893Z" }, - { url = "https://files.pythonhosted.org/packages/99/f2/c2d64f6564f32af913bf5f3f7ae41c7c263c5ae4c4e8f1a17af8af66cd46/rpds_py-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:6d50841c425d16faf3206ddbba44c21aa3310a0cebc3c1cdfc3e3f4f9f6f5728", size = 225399, upload-time = "2025-05-21T12:43:53.351Z" }, - { url = "https://files.pythonhosted.org/packages/2b/da/323848a2b62abe6a0fec16ebe199dc6889c5d0a332458da8985b2980dffe/rpds_py-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:659d87430a8c8c704d52d094f5ba6fa72ef13b4d385b7e542a08fc240cb4a559", size = 364498, upload-time = "2025-05-21T12:43:54.841Z" }, - { url = "https://files.pythonhosted.org/packages/1f/b4/4d3820f731c80fd0cd823b3e95b9963fec681ae45ba35b5281a42382c67d/rpds_py-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68f6f060f0bbdfb0245267da014d3a6da9be127fe3e8cc4a68c6f833f8a23bb1", size = 350083, upload-time = "2025-05-21T12:43:56.428Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b1/3a8ee1c9d480e8493619a437dec685d005f706b69253286f50f498cbdbcf/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083a9513a33e0b92cf6e7a6366036c6bb43ea595332c1ab5c8ae329e4bcc0a9c", size = 389023, upload-time = "2025-05-21T12:43:57.995Z" }, - { url = "https://files.pythonhosted.org/packages/3b/31/17293edcfc934dc62c3bf74a0cb449ecd549531f956b72287203e6880b87/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:816568614ecb22b18a010c7a12559c19f6fe993526af88e95a76d5a60b8b75fb", size = 403283, upload-time = "2025-05-21T12:43:59.546Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ca/e0f0bc1a75a8925024f343258c8ecbd8828f8997ea2ac71e02f67b6f5299/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c6564c0947a7f52e4792983f8e6cf9bac140438ebf81f527a21d944f2fd0a40", size = 524634, upload-time = "2025-05-21T12:44:01.087Z" }, - { url = "https://files.pythonhosted.org/packages/3e/03/5d0be919037178fff33a6672ffc0afa04ea1cfcb61afd4119d1b5280ff0f/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c4a128527fe415d73cf1f70a9a688d06130d5810be69f3b553bf7b45e8acf79", size = 416233, upload-time = "2025-05-21T12:44:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325", size = 390375, upload-time = "2025-05-21T12:44:04.162Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ac/a87f339f0e066b9535074a9f403b9313fd3892d4a164d5d5f5875ac9f29f/rpds_py-0.25.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d74ec9bc0e2feb81d3f16946b005748119c0f52a153f6db6a29e8cd68636f295", size = 424537, upload-time = "2025-05-21T12:44:06.175Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8f/8d5c1567eaf8c8afe98a838dd24de5013ce6e8f53a01bd47fe8bb06b5533/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3af5b4cc10fa41e5bc64e5c198a1b2d2864337f8fcbb9a67e747e34002ce812b", size = 566425, upload-time = "2025-05-21T12:44:08.242Z" }, - { url = "https://files.pythonhosted.org/packages/95/33/03016a6be5663b389c8ab0bbbcca68d9e96af14faeff0a04affcb587e776/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79dc317a5f1c51fd9c6a0c4f48209c6b8526d0524a6904fc1076476e79b00f98", size = 595197, upload-time = "2025-05-21T12:44:10.449Z" }, - { url = "https://files.pythonhosted.org/packages/33/8d/da9f4d3e208c82fda311bff0cf0a19579afceb77cf456e46c559a1c075ba/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1521031351865e0181bc585147624d66b3b00a84109b57fcb7a779c3ec3772cd", size = 561244, upload-time = "2025-05-21T12:44:12.387Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b3/39d5dcf7c5f742ecd6dbc88f6f84ae54184b92f5f387a4053be2107b17f1/rpds_py-0.25.1-cp313-cp313-win32.whl", hash = "sha256:5d473be2b13600b93a5675d78f59e63b51b1ba2d0476893415dfbb5477e65b31", size = 222254, upload-time = "2025-05-21T12:44:14.261Z" }, - { url = "https://files.pythonhosted.org/packages/5f/19/2d6772c8eeb8302c5f834e6d0dfd83935a884e7c5ce16340c7eaf89ce925/rpds_py-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7b74e92a3b212390bdce1d93da9f6488c3878c1d434c5e751cbc202c5e09500", size = 234741, upload-time = "2025-05-21T12:44:16.236Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/145ada26cfaf86018d0eb304fe55eafdd4f0b6b84530246bb4a7c4fb5c4b/rpds_py-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:dd326a81afe332ede08eb39ab75b301d5676802cdffd3a8f287a5f0b694dc3f5", size = 224830, upload-time = "2025-05-21T12:44:17.749Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ca/d435844829c384fd2c22754ff65889c5c556a675d2ed9eb0e148435c6690/rpds_py-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a58d1ed49a94d4183483a3ce0af22f20318d4a1434acee255d683ad90bf78129", size = 359668, upload-time = "2025-05-21T12:44:19.322Z" }, - { url = "https://files.pythonhosted.org/packages/1f/01/b056f21db3a09f89410d493d2f6614d87bb162499f98b649d1dbd2a81988/rpds_py-0.25.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f251bf23deb8332823aef1da169d5d89fa84c89f67bdfb566c49dea1fccfd50d", size = 345649, upload-time = "2025-05-21T12:44:20.962Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0f/e0d00dc991e3d40e03ca36383b44995126c36b3eafa0ccbbd19664709c88/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd586bfa270c1103ece2109314dd423df1fa3d9719928b5d09e4840cec0d72", size = 384776, upload-time = "2025-05-21T12:44:22.516Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a2/59374837f105f2ca79bde3c3cd1065b2f8c01678900924949f6392eab66d/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d273f136e912aa101a9274c3145dcbddbe4bac560e77e6d5b3c9f6e0ed06d34", size = 395131, upload-time = "2025-05-21T12:44:24.147Z" }, - { url = "https://files.pythonhosted.org/packages/9c/dc/48e8d84887627a0fe0bac53f0b4631e90976fd5d35fff8be66b8e4f3916b/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666fa7b1bd0a3810a7f18f6d3a25ccd8866291fbbc3c9b912b917a6715874bb9", size = 520942, upload-time = "2025-05-21T12:44:25.915Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f5/ee056966aeae401913d37befeeab57a4a43a4f00099e0a20297f17b8f00c/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:921954d7fbf3fccc7de8f717799304b14b6d9a45bbeec5a8d7408ccbf531faf5", size = 411330, upload-time = "2025-05-21T12:44:27.638Z" }, - { url = "https://files.pythonhosted.org/packages/ab/74/b2cffb46a097cefe5d17f94ede7a174184b9d158a0aeb195f39f2c0361e8/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d86373ff19ca0441ebeb696ef64cb58b8b5cbacffcda5a0ec2f3911732a194", size = 387339, upload-time = "2025-05-21T12:44:29.292Z" }, - { url = "https://files.pythonhosted.org/packages/7f/9a/0ff0b375dcb5161c2b7054e7d0b7575f1680127505945f5cabaac890bc07/rpds_py-0.25.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8980cde3bb8575e7c956a530f2c217c1d6aac453474bf3ea0f9c89868b531b6", size = 418077, upload-time = "2025-05-21T12:44:30.877Z" }, - { url = "https://files.pythonhosted.org/packages/0d/a1/fda629bf20d6b698ae84c7c840cfb0e9e4200f664fc96e1f456f00e4ad6e/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8eb8c84ecea987a2523e057c0d950bcb3f789696c0499290b8d7b3107a719d78", size = 562441, upload-time = "2025-05-21T12:44:32.541Z" }, - { url = "https://files.pythonhosted.org/packages/20/15/ce4b5257f654132f326f4acd87268e1006cc071e2c59794c5bdf4bebbb51/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e43a005671a9ed5a650f3bc39e4dbccd6d4326b24fb5ea8be5f3a43a6f576c72", size = 590750, upload-time = "2025-05-21T12:44:34.557Z" }, - { url = "https://files.pythonhosted.org/packages/fb/ab/e04bf58a8d375aeedb5268edcc835c6a660ebf79d4384d8e0889439448b0/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58f77c60956501a4a627749a6dcb78dac522f249dd96b5c9f1c6af29bfacfb66", size = 558891, upload-time = "2025-05-21T12:44:37.358Z" }, - { url = "https://files.pythonhosted.org/packages/90/82/cb8c6028a6ef6cd2b7991e2e4ced01c854b6236ecf51e81b64b569c43d73/rpds_py-0.25.1-cp313-cp313t-win32.whl", hash = "sha256:2cb9e5b5e26fc02c8a4345048cd9998c2aca7c2712bd1b36da0c72ee969a3523", size = 218718, upload-time = "2025-05-21T12:44:38.969Z" }, - { url = "https://files.pythonhosted.org/packages/b6/97/5a4b59697111c89477d20ba8a44df9ca16b41e737fa569d5ae8bff99e650/rpds_py-0.25.1-cp313-cp313t-win_amd64.whl", hash = "sha256:401ca1c4a20cc0510d3435d89c069fe0a9ae2ee6495135ac46bdd49ec0495763", size = 232218, upload-time = "2025-05-21T12:44:40.512Z" }, - { url = "https://files.pythonhosted.org/packages/49/74/48f3df0715a585cbf5d34919c9c757a4c92c1a9eba059f2d334e72471f70/rpds_py-0.25.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee86d81551ec68a5c25373c5643d343150cc54672b5e9a0cafc93c1870a53954", size = 374208, upload-time = "2025-05-21T12:45:26.306Z" }, - { url = "https://files.pythonhosted.org/packages/55/b0/9b01bb11ce01ec03d05e627249cc2c06039d6aa24ea5a22a39c312167c10/rpds_py-0.25.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89c24300cd4a8e4a51e55c31a8ff3918e6651b241ee8876a42cc2b2a078533ba", size = 359262, upload-time = "2025-05-21T12:45:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/a9/eb/5395621618f723ebd5116c53282052943a726dba111b49cd2071f785b665/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:771c16060ff4e79584dc48902a91ba79fd93eade3aa3a12d6d2a4aadaf7d542b", size = 387366, upload-time = "2025-05-21T12:45:30.42Z" }, - { url = "https://files.pythonhosted.org/packages/68/73/3d51442bdb246db619d75039a50ea1cf8b5b4ee250c3e5cd5c3af5981cd4/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:785ffacd0ee61c3e60bdfde93baa6d7c10d86f15655bd706c89da08068dc5038", size = 400759, upload-time = "2025-05-21T12:45:32.516Z" }, - { url = "https://files.pythonhosted.org/packages/b7/4c/3a32d5955d7e6cb117314597bc0f2224efc798428318b13073efe306512a/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a40046a529cc15cef88ac5ab589f83f739e2d332cb4d7399072242400ed68c9", size = 523128, upload-time = "2025-05-21T12:45:34.396Z" }, - { url = "https://files.pythonhosted.org/packages/be/95/1ffccd3b0bb901ae60b1dd4b1be2ab98bb4eb834cd9b15199888f5702f7b/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:85fc223d9c76cabe5d0bff82214459189720dc135db45f9f66aa7cffbf9ff6c1", size = 411597, upload-time = "2025-05-21T12:45:36.164Z" }, - { url = "https://files.pythonhosted.org/packages/ef/6d/6e6cd310180689db8b0d2de7f7d1eabf3fb013f239e156ae0d5a1a85c27f/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b0be9965f93c222fb9b4cc254235b3b2b215796c03ef5ee64f995b1b69af0762", size = 388053, upload-time = "2025-05-21T12:45:38.45Z" }, - { url = "https://files.pythonhosted.org/packages/4a/87/ec4186b1fe6365ced6fa470960e68fc7804bafbe7c0cf5a36237aa240efa/rpds_py-0.25.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8378fa4a940f3fb509c081e06cb7f7f2adae8cf46ef258b0e0ed7519facd573e", size = 421821, upload-time = "2025-05-21T12:45:40.732Z" }, - { url = "https://files.pythonhosted.org/packages/7a/60/84f821f6bf4e0e710acc5039d91f8f594fae0d93fc368704920d8971680d/rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:33358883a4490287e67a2c391dfaea4d9359860281db3292b6886bf0be3d8692", size = 564534, upload-time = "2025-05-21T12:45:42.672Z" }, - { url = "https://files.pythonhosted.org/packages/41/3a/bc654eb15d3b38f9330fe0f545016ba154d89cdabc6177b0295910cd0ebe/rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1d1fadd539298e70cac2f2cb36f5b8a65f742b9b9f1014dd4ea1f7785e2470bf", size = 592674, upload-time = "2025-05-21T12:45:44.533Z" }, - { url = "https://files.pythonhosted.org/packages/2e/ba/31239736f29e4dfc7a58a45955c5db852864c306131fd6320aea214d5437/rpds_py-0.25.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a46c2fb2545e21181445515960006e85d22025bd2fe6db23e76daec6eb689fe", size = 558781, upload-time = "2025-05-21T12:45:46.281Z" }, +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/d9/991a0dee12d9fc53ed027e26a26a64b151d77252ac477e22666b9688bc16/rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f", size = 27420, upload-time = "2025-08-07T08:26:39.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/c1/49d515434c1752e40f5e35b985260cf27af052593378580a2f139a5be6b8/rpds_py-0.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dbc2ab5d10544eb485baa76c63c501303b716a5c405ff2469a1d8ceffaabf622", size = 371577, upload-time = "2025-08-07T08:23:25.379Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/bf2715b2fee5087fa13b752b5fd573f1a93e4134c74d275f709e38e54fe7/rpds_py-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ec85994f96a58cf7ed288caa344b7fe31fd1d503bdf13d7331ead5f70ab60d5", size = 354959, upload-time = "2025-08-07T08:23:26.767Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5c/e7762808c746dd19733a81373c10da43926f6a6adcf4920a21119697a60a/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:190d7285cd3bb6d31d37a0534d7359c1ee191eb194c511c301f32a4afa5a1dd4", size = 381485, upload-time = "2025-08-07T08:23:27.869Z" }, + { url = "https://files.pythonhosted.org/packages/40/51/0d308eb0b558309ca0598bcba4243f52c4cd20e15fe991b5bd75824f2e61/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10d92fb6d7fd827e44055fcd932ad93dac6a11e832d51534d77b97d1d85400f", size = 396816, upload-time = "2025-08-07T08:23:29.424Z" }, + { url = "https://files.pythonhosted.org/packages/5c/aa/2d585ec911d78f66458b2c91252134ca0c7c70f687a72c87283173dc0c96/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd2c1d27ebfe6a015cfa2005b7fe8c52d5019f7bbdd801bc6f7499aab9ae739e", size = 514950, upload-time = "2025-08-07T08:23:30.576Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ef/aced551cc1148179557aed84343073adadf252c91265263ee6203458a186/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4790c9d5dd565ddb3e9f656092f57268951398cef52e364c405ed3112dc7c7c1", size = 402132, upload-time = "2025-08-07T08:23:32.428Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ac/cf644803d8d417653fe2b3604186861d62ea6afaef1b2284045741baef17/rpds_py-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4300e15e7d03660f04be84a125d1bdd0e6b2f674bc0723bc0fd0122f1a4585dc", size = 383660, upload-time = "2025-08-07T08:23:33.829Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ec/caf47c55ce02b76cbaeeb2d3b36a73da9ca2e14324e3d75cf72b59dcdac5/rpds_py-0.27.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:59195dc244fc183209cf8a93406889cadde47dfd2f0a6b137783aa9c56d67c85", size = 401730, upload-time = "2025-08-07T08:23:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/0b/71/c1f355afdcd5b99ffc253422aa4bdcb04ccf1491dcd1bda3688a0c07fd61/rpds_py-0.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fae4a01ef8c4cb2bbe92ef2063149596907dc4a881a8d26743b3f6b304713171", size = 416122, upload-time = "2025-08-07T08:23:36.062Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/f4b5b1eda724ed0e04d2b26d8911cdc131451a7ee4c4c020a1387e5c6ded/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3dc8d4ede2dbae6c0fc2b6c958bf51ce9fd7e9b40c0f5b8835c3fde44f5807d", size = 558771, upload-time = "2025-08-07T08:23:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/93/c0/5f8b834db2289ab48d5cffbecbb75e35410103a77ac0b8da36bf9544ec1c/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3782fb753aa825b4ccabc04292e07897e2fd941448eabf666856c5530277626", size = 587876, upload-time = "2025-08-07T08:23:38.662Z" }, + { url = "https://files.pythonhosted.org/packages/d2/dd/1a1df02ab8eb970115cff2ae31a6f73916609b900dc86961dc382b8c2e5e/rpds_py-0.27.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:887ab1f12b0d227e9260558a4a2320024b20102207ada65c43e1ffc4546df72e", size = 554359, upload-time = "2025-08-07T08:23:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/95a014ab0d51ab6e3bebbdb476a42d992d2bbf9c489d24cff9fda998e925/rpds_py-0.27.0-cp311-cp311-win32.whl", hash = "sha256:5d6790ff400254137b81b8053b34417e2c46921e302d655181d55ea46df58cf7", size = 218084, upload-time = "2025-08-07T08:23:41.086Z" }, + { url = "https://files.pythonhosted.org/packages/49/78/f8d5b71ec65a0376b0de31efcbb5528ce17a9b7fdd19c3763303ccfdedec/rpds_py-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:e24d8031a2c62f34853756d9208eeafa6b940a1efcbfe36e8f57d99d52bb7261", size = 230085, upload-time = "2025-08-07T08:23:42.143Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/84429745184091e06b4cc70f8597408e314c2d2f7f5e13249af9ffab9e3d/rpds_py-0.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:08680820d23df1df0a0260f714d12966bc6c42d02e8055a91d61e03f0c47dda0", size = 222112, upload-time = "2025-08-07T08:23:43.233Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/e67309ca1ac993fa1888a0d9b2f5ccc1f67196ace32e76c9f8e1dbbbd50c/rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4", size = 362611, upload-time = "2025-08-07T08:23:44.773Z" }, + { url = "https://files.pythonhosted.org/packages/93/2e/28c2fb84aa7aa5d75933d1862d0f7de6198ea22dfd9a0cca06e8a4e7509e/rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b", size = 347680, upload-time = "2025-08-07T08:23:46.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/3e/9834b4c8f4f5fe936b479e623832468aa4bd6beb8d014fecaee9eac6cdb1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e", size = 384600, upload-time = "2025-08-07T08:23:48Z" }, + { url = "https://files.pythonhosted.org/packages/19/78/744123c7b38865a965cd9e6f691fde7ef989a00a256fa8bf15b75240d12f/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34", size = 400697, upload-time = "2025-08-07T08:23:49.407Z" }, + { url = "https://files.pythonhosted.org/packages/32/97/3c3d32fe7daee0a1f1a678b6d4dfb8c4dcf88197fa2441f9da7cb54a8466/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8", size = 517781, upload-time = "2025-08-07T08:23:50.557Z" }, + { url = "https://files.pythonhosted.org/packages/b2/be/28f0e3e733680aa13ecec1212fc0f585928a206292f14f89c0b8a684cad1/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726", size = 406449, upload-time = "2025-08-07T08:23:51.732Z" }, + { url = "https://files.pythonhosted.org/packages/95/ae/5d15c83e337c082d0367053baeb40bfba683f42459f6ebff63a2fd7e5518/rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e", size = 386150, upload-time = "2025-08-07T08:23:52.822Z" }, + { url = "https://files.pythonhosted.org/packages/bf/65/944e95f95d5931112829e040912b25a77b2e7ed913ea5fe5746aa5c1ce75/rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3", size = 406100, upload-time = "2025-08-07T08:23:54.339Z" }, + { url = "https://files.pythonhosted.org/packages/21/a4/1664b83fae02894533cd11dc0b9f91d673797c2185b7be0f7496107ed6c5/rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e", size = 421345, upload-time = "2025-08-07T08:23:55.832Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/b7303941c2b0823bfb34c71378249f8beedce57301f400acb04bb345d025/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f", size = 561891, upload-time = "2025-08-07T08:23:56.951Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c8/48623d64d4a5a028fa99576c768a6159db49ab907230edddc0b8468b998b/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03", size = 591756, upload-time = "2025-08-07T08:23:58.146Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/18f62617e8e61cc66334c9fb44b1ad7baae3438662098efbc55fb3fda453/rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374", size = 557088, upload-time = "2025-08-07T08:23:59.6Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4c/e84c3a276e2496a93d245516be6b49e20499aa8ca1c94d59fada0d79addc/rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97", size = 221926, upload-time = "2025-08-07T08:24:00.695Z" }, + { url = "https://files.pythonhosted.org/packages/83/89/9d0fbcef64340db0605eb0a0044f258076f3ae0a3b108983b2c614d96212/rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5", size = 233235, upload-time = "2025-08-07T08:24:01.846Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b0/e177aa9f39cbab060f96de4a09df77d494f0279604dc2f509263e21b05f9/rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9", size = 223315, upload-time = "2025-08-07T08:24:03.337Z" }, + { url = "https://files.pythonhosted.org/packages/81/d2/dfdfd42565a923b9e5a29f93501664f5b984a802967d48d49200ad71be36/rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff", size = 362133, upload-time = "2025-08-07T08:24:04.508Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/0a2e2460c4b66021d349ce9f6331df1d6c75d7eea90df9785d333a49df04/rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367", size = 347128, upload-time = "2025-08-07T08:24:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/35/8d/7d1e4390dfe09d4213b3175a3f5a817514355cb3524593380733204f20b9/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185", size = 384027, upload-time = "2025-08-07T08:24:06.841Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/78499d1a62172891c8cd45de737b2a4b84a414b6ad8315ab3ac4945a5b61/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc", size = 399973, upload-time = "2025-08-07T08:24:08.143Z" }, + { url = "https://files.pythonhosted.org/packages/10/a1/1c67c1d8cc889107b19570bb01f75cf49852068e95e6aee80d22915406fc/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe", size = 515295, upload-time = "2025-08-07T08:24:09.711Z" }, + { url = "https://files.pythonhosted.org/packages/df/27/700ec88e748436b6c7c4a2262d66e80f8c21ab585d5e98c45e02f13f21c0/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9", size = 406737, upload-time = "2025-08-07T08:24:11.182Z" }, + { url = "https://files.pythonhosted.org/packages/33/cc/6b0ee8f0ba3f2df2daac1beda17fde5cf10897a7d466f252bd184ef20162/rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c", size = 385898, upload-time = "2025-08-07T08:24:12.798Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7e/c927b37d7d33c0a0ebf249cc268dc2fcec52864c1b6309ecb960497f2285/rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295", size = 405785, upload-time = "2025-08-07T08:24:14.906Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d2/8ed50746d909dcf402af3fa58b83d5a590ed43e07251d6b08fad1a535ba6/rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43", size = 419760, upload-time = "2025-08-07T08:24:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/d3/60/2b2071aee781cb3bd49f94d5d35686990b925e9b9f3e3d149235a6f5d5c1/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432", size = 561201, upload-time = "2025-08-07T08:24:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/98/1f/27b67304272521aaea02be293fecedce13fa351a4e41cdb9290576fc6d81/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b", size = 591021, upload-time = "2025-08-07T08:24:18.999Z" }, + { url = "https://files.pythonhosted.org/packages/db/9b/a2fadf823164dd085b1f894be6443b0762a54a7af6f36e98e8fcda69ee50/rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d", size = 556368, upload-time = "2025-08-07T08:24:20.54Z" }, + { url = "https://files.pythonhosted.org/packages/24/f3/6d135d46a129cda2e3e6d4c5e91e2cc26ea0428c6cf152763f3f10b6dd05/rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd", size = 221236, upload-time = "2025-08-07T08:24:22.144Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/65d7494f5448ecc755b545d78b188440f81da98b50ea0447ab5ebfdf9bd6/rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2", size = 232634, upload-time = "2025-08-07T08:24:23.642Z" }, + { url = "https://files.pythonhosted.org/packages/70/d9/23852410fadab2abb611733933401de42a1964ce6600a3badae35fbd573e/rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac", size = 222783, upload-time = "2025-08-07T08:24:25.098Z" }, + { url = "https://files.pythonhosted.org/packages/15/75/03447917f78512b34463f4ef11066516067099a0c466545655503bed0c77/rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774", size = 359154, upload-time = "2025-08-07T08:24:26.249Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fc/4dac4fa756451f2122ddaf136e2c6aeb758dc6fdbe9ccc4bc95c98451d50/rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b", size = 343909, upload-time = "2025-08-07T08:24:27.405Z" }, + { url = "https://files.pythonhosted.org/packages/7b/81/723c1ed8e6f57ed9d8c0c07578747a2d3d554aaefc1ab89f4e42cfeefa07/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd", size = 379340, upload-time = "2025-08-07T08:24:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/98/16/7e3740413de71818ce1997df82ba5f94bae9fff90c0a578c0e24658e6201/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb", size = 391655, upload-time = "2025-08-07T08:24:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/e0/63/2a9f510e124d80660f60ecce07953f3f2d5f0b96192c1365443859b9c87f/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433", size = 513017, upload-time = "2025-08-07T08:24:31.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4e/cf6ff311d09776c53ea1b4f2e6700b9d43bb4e99551006817ade4bbd6f78/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615", size = 402058, upload-time = "2025-08-07T08:24:32.613Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/5e36096d474cb10f2a2d68b22af60a3bc4164fd8db15078769a568d9d3ac/rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8", size = 383474, upload-time = "2025-08-07T08:24:33.767Z" }, + { url = "https://files.pythonhosted.org/packages/db/a2/3dff02805b06058760b5eaa6d8cb8db3eb3e46c9e452453ad5fc5b5ad9fe/rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858", size = 400067, upload-time = "2025-08-07T08:24:35.021Z" }, + { url = "https://files.pythonhosted.org/packages/67/87/eed7369b0b265518e21ea836456a4ed4a6744c8c12422ce05bce760bb3cf/rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5", size = 412085, upload-time = "2025-08-07T08:24:36.267Z" }, + { url = "https://files.pythonhosted.org/packages/8b/48/f50b2ab2fbb422fbb389fe296e70b7a6b5ea31b263ada5c61377e710a924/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9", size = 555928, upload-time = "2025-08-07T08:24:37.573Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/b18eb51045d06887666c3560cd4bbb6819127b43d758f5adb82b5f56f7d1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79", size = 585527, upload-time = "2025-08-07T08:24:39.391Z" }, + { url = "https://files.pythonhosted.org/packages/be/03/a3dd6470fc76499959b00ae56295b76b4bdf7c6ffc60d62006b1217567e1/rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c", size = 554211, upload-time = "2025-08-07T08:24:40.6Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d1/ee5fd1be395a07423ac4ca0bcc05280bf95db2b155d03adefeb47d5ebf7e/rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23", size = 216624, upload-time = "2025-08-07T08:24:42.204Z" }, + { url = "https://files.pythonhosted.org/packages/1c/94/4814c4c858833bf46706f87349c37ca45e154da7dbbec9ff09f1abeb08cc/rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1", size = 230007, upload-time = "2025-08-07T08:24:43.329Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/72ab5b911fdcc48058359b0e786e5363e3fde885156116026f1a2ba9a5b5/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6491658dd2569f05860bad645569145c8626ac231877b0fb2d5f9bcb7054089", size = 371658, upload-time = "2025-08-07T08:26:02.369Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4b/90ff04b4da055db53d8fea57640d8d5d55456343a1ec9a866c0ecfe10fd1/rpds_py-0.27.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec77545d188f8bdd29d42bccb9191682a46fb2e655e3d1fb446d47c55ac3b8d", size = 355529, upload-time = "2025-08-07T08:26:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/a4/be/527491fb1afcd86fc5ce5812eb37bc70428ee017d77fee20de18155c3937/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a4aebf8ca02bbb90a9b3e7a463bbf3bee02ab1c446840ca07b1695a68ce424", size = 382822, upload-time = "2025-08-07T08:26:05.52Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a5/dcdb8725ce11e6d0913e6fcf782a13f4b8a517e8acc70946031830b98441/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44524b96481a4c9b8e6c46d6afe43fa1fb485c261e359fbe32b63ff60e3884d8", size = 397233, upload-time = "2025-08-07T08:26:07.179Z" }, + { url = "https://files.pythonhosted.org/packages/33/f9/0947920d1927e9f144660590cc38cadb0795d78fe0d9aae0ef71c1513b7c/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45d04a73c54b6a5fd2bab91a4b5bc8b426949586e61340e212a8484919183859", size = 514892, upload-time = "2025-08-07T08:26:08.622Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ed/d1343398c1417c68f8daa1afce56ef6ce5cc587daaf98e29347b00a80ff2/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:343cf24de9ed6c728abefc5d5c851d5de06497caa7ac37e5e65dd572921ed1b5", size = 402733, upload-time = "2025-08-07T08:26:10.433Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0b/646f55442cd14014fb64d143428f25667a100f82092c90087b9ea7101c74/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aed8118ae20515974650d08eb724150dc2e20c2814bcc307089569995e88a14", size = 384447, upload-time = "2025-08-07T08:26:11.847Z" }, + { url = "https://files.pythonhosted.org/packages/4b/15/0596ef7529828e33a6c81ecf5013d1dd33a511a3e0be0561f83079cda227/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:af9d4fd79ee1cc8e7caf693ee02737daabfc0fcf2773ca0a4735b356c8ad6f7c", size = 402502, upload-time = "2025-08-07T08:26:13.537Z" }, + { url = "https://files.pythonhosted.org/packages/c3/8d/986af3c42f8454a6cafff8729d99fb178ae9b08a9816325ac7a8fa57c0c0/rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f0396e894bd1e66c74ecbc08b4f6a03dc331140942c4b1d345dd131b68574a60", size = 416651, upload-time = "2025-08-07T08:26:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9a/b4ec3629b7b447e896eec574469159b5b60b7781d3711c914748bf32de05/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:59714ab0a5af25d723d8e9816638faf7f4254234decb7d212715c1aa71eee7be", size = 559460, upload-time = "2025-08-07T08:26:16.295Z" }, + { url = "https://files.pythonhosted.org/packages/61/63/d1e127b40c3e4733b3a6f26ae7a063cdf2bc1caa5272c89075425c7d397a/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:88051c3b7d5325409f433c5a40328fcb0685fc04e5db49ff936e910901d10114", size = 588072, upload-time = "2025-08-07T08:26:17.776Z" }, + { url = "https://files.pythonhosted.org/packages/04/7e/8ffc71a8f6833d9c9fb999f5b0ee736b8b159fd66968e05c7afc2dbcd57e/rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:181bc29e59e5e5e6e9d63b143ff4d5191224d355e246b5a48c88ce6b35c4e466", size = 555083, upload-time = "2025-08-07T08:26:19.301Z" }, ] [[package]] @@ -3903,27 +4254,40 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, - { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, - { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, - { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, - { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, - { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, - { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, - { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, - { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, - { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, - { url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" }, - { url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" }, - { url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" }, +version = "0.12.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" }, + { url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" }, + { url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" }, + { url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" }, + { url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" }, + { url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785, upload-time = "2025-08-14T16:08:48.062Z" }, + { url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654, upload-time = "2025-08-14T16:08:50.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" }, +] + +[[package]] +name = "s3transfer" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/05/d52bf1e65044b4e5e27d4e63e8d1579dbdec54fce685908ae09bc3720030/s3transfer-0.13.1.tar.gz", hash = "sha256:c3fdba22ba1bd367922f27ec8032d6a1cf5f10c934fb5d68cf60fd5a23d936cf", size = 150589, upload-time = "2025-07-18T19:22:42.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/4f/d073e09df851cfa251ef7840007d04db3293a0482ce607d2b993926089be/s3transfer-0.13.1-py3-none-any.whl", hash = "sha256:a981aa7429be23fe6dfc13e80e4020057cbab622b08c0315288758d67cabc724", size = 85308, upload-time = "2025-07-18T19:22:40.947Z" }, ] [[package]] @@ -3941,7 +4305,7 @@ wheels = [ [[package]] name = "semgrep" -version = "1.131.0" +version = "1.132.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -3967,13 +4331,13 @@ dependencies = [ { name = "urllib3" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4d/2b/25a016b731474eeb687baeb367662287fe2f4fb3d75ce84e099e7f0c3be7/semgrep-1.131.0.tar.gz", hash = "sha256:a56edd14610c44d74eb535447b7b7b938396077b42fbac176b0a7a7af7efff07", size = 42016368, upload-time = "2025-07-31T21:10:46.744Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/35/2ba92de542799ff386e01bb09ccbe8fe1829111144c4dc1432d0ca5afbb2/semgrep-1.132.1.tar.gz", hash = "sha256:bc1e64134062924879090af08285bb79c265217c6b59b6a8d3e7c6f5f9cb5f07", size = 41814139, upload-time = "2025-08-15T00:31:00.826Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/bd/44a027208d85cac5d1130439ed3ecfedd55fe88231169c5e839b52d92ba1/semgrep-1.131.0-cp39.cp310.cp311.py39.py310.py311-none-macosx_10_14_x86_64.whl", hash = "sha256:160498fcc086998c438340e3ff439b38753fa2091d8c97c37568d4ce156a0a4e", size = 34859330, upload-time = "2025-07-31T21:10:27.165Z" }, - { url = "https://files.pythonhosted.org/packages/80/4e/004cf9bd7301f2c9cb5b7e844d1398ba994b3261641bcfc57c7edf0b4289/semgrep-1.131.0-cp39.cp310.cp311.py39.py310.py311-none-macosx_11_0_arm64.whl", hash = "sha256:69a9dab95626a6d017e2e51cbac8909d3c5c7a99ba1879f5b5915df5342e37a8", size = 39757782, upload-time = "2025-07-31T21:10:31.607Z" }, - { url = "https://files.pythonhosted.org/packages/6d/60/72cd60d880fc835f7e9721b7a0623b5fab731ea9b1f12829eb91be6f070a/semgrep-1.131.0-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46b20a7fbc12cd069260402bdfcb72e93934361e94cdceb1e1ca3bd4c28f9857", size = 52121458, upload-time = "2025-07-31T21:10:35.088Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/d0f4ed63a86bed7ce96792198106d5b33c7e2b5f34c78abd5c33772a2bb4/semgrep-1.131.0-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2d11da8d7ca6f22d59fe131ac238dc387002e9197950877b6b8a27f5ba2692", size = 48266980, upload-time = "2025-07-31T21:10:39.394Z" }, - { url = "https://files.pythonhosted.org/packages/82/8c/6f62d97501220d54e75a74c1caee164b74bc2f56abcc2e83cf1dd5d83c71/semgrep-1.131.0-cp39.cp310.cp311.py39.py310.py311-none-win_amd64.whl", hash = "sha256:c7ff0ffb30fe6b6101d040c1881f9c4ac9efb91655ee5d27c50d180a840be566", size = 42603915, upload-time = "2025-07-31T21:10:43.39Z" }, + { url = "https://files.pythonhosted.org/packages/7e/97/4fce00cc2cea09b763328a5c31f3f3d5a6af535bc5d12edc69b2ad6f7a13/semgrep-1.132.1-cp39.cp310.cp311.py39.py310.py311-none-macosx_10_14_x86_64.whl", hash = "sha256:6c21c3afa2b7e7cf183badd42e1ed3ad81ee2ea096bbc9fd09448486ec6b3778", size = 34658790, upload-time = "2025-08-15T00:30:44.26Z" }, + { url = "https://files.pythonhosted.org/packages/75/ac/1a4864005c5f0e87eaf90fd234acd6fa10941630182666e48c34d49e887f/semgrep-1.132.1-cp39.cp310.cp311.py39.py310.py311-none-macosx_11_0_arm64.whl", hash = "sha256:0f4da44e0b112100642467521f7ec495145ec4d47a1a67addc7a8332999aeaac", size = 39555857, upload-time = "2025-08-15T00:30:48.006Z" }, + { url = "https://files.pythonhosted.org/packages/06/c7/9b093abb3d4ca6de0dc0c7cf3dd9ac892926f1e465ff21f0aef0b5533f1d/semgrep-1.132.1-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5043e7fca95db5a8fef39ef3fdebf8638189164983a123752c2ee93495ec3aef", size = 51878376, upload-time = "2025-08-15T00:30:50.712Z" }, + { url = "https://files.pythonhosted.org/packages/7c/60/bab04dd9b41e06e38a03e2b1c92f57eb63b2a89a13c32d5afeac57068d8f/semgrep-1.132.1-cp39.cp310.cp311.py39.py310.py311-none-musllinux_1_0_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e06349068c2ceacbad2961dbaf3cbe1c242bc4d4b58c5891c82b4babda48ed2e", size = 48056583, upload-time = "2025-08-15T00:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/c1/89/fff1eff4e1ff89311878e6da665b5911eaa808ab4fbc9e8a090e5b26f680/semgrep-1.132.1-cp39.cp310.cp311.py39.py310.py311-none-win_amd64.whl", hash = "sha256:0950aae6695f71b8c11e52faec3bc92aabb343d6b76b8295430a138449742e4b", size = 42404839, upload-time = "2025-08-15T00:30:58.059Z" }, ] [[package]] @@ -4036,15 +4400,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[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" } -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" }, -] - [[package]] name = "snakeviz" version = "2.2.2" @@ -4218,6 +4573,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, ] +[[package]] +name = "thrift" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/c2/db648cc10dd7d15560f2eafd92a27cd280811924696e0b4a87175fb28c94/thrift-0.22.0.tar.gz", hash = "sha256:42e8276afbd5f54fe1d364858b6877bc5e5a4a5ed69f6a005b94ca4918fe1466", size = 62303, upload-time = "2025-05-23T20:49:33.309Z" } + [[package]] name = "tokenize-rt" version = "6.2.0" @@ -4277,21 +4638,21 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.1" +version = "6.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload-time = "2025-05-22T18:15:38.788Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload-time = "2025-05-22T18:15:20.862Z" }, - { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload-time = "2025-05-22T18:15:22.591Z" }, - { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload-time = "2025-05-22T18:15:24.027Z" }, - { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload-time = "2025-05-22T18:15:25.735Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload-time = "2025-05-22T18:15:27.499Z" }, - { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload-time = "2025-05-22T18:15:29.299Z" }, - { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload-time = "2025-05-22T18:15:31.038Z" }, - { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload-time = "2025-05-22T18:15:32.426Z" }, - { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload-time = "2025-05-22T18:15:34.205Z" }, - { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload-time = "2025-05-22T18:15:36.1Z" }, - { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" }, + { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, + { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, ] [[package]] @@ -4316,16 +4677,16 @@ wheels = [ [[package]] name = "tox-uv" -version = "1.27.0" +version = "1.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "tox" }, { name = "uv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/01/3bf884009509b7eef169bf3628e4eb470be7be269486cdc4ed411215e74a/tox_uv-1.27.0.tar.gz", hash = "sha256:93e432728c51f8659830dc6f5096528dc7726d7524ea7cb60f69ab00dbdd7fc8", size = 22253, upload-time = "2025-08-07T15:32:27.788Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/9a/f4b675ebcbd623854129891e87045f80c1d8e91b2957496f1fe6e463f291/tox_uv-1.28.0.tar.gz", hash = "sha256:a06ff909f73232b2b7965de19090d887b12b44e44eb0843b2c07266d2957ade2", size = 23265, upload-time = "2025-08-14T17:53:07.909Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/60/eb/669ff92885ec3fd497f9f1eaa33d4b98bad3f3c665258f0f607142f72c0c/tox_uv-1.27.0-py3-none-any.whl", hash = "sha256:479ca855db5ad555818498181a6fbaf6979624a1eae84ef63031279cfd00cbf5", size = 16880, upload-time = "2025-08-07T15:32:26.357Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ac/b32555d190c4440b8d2779d4a19439e5fbd5a3950f7e5a17ead7c7d30cad/tox_uv-1.28.0-py3-none-any.whl", hash = "sha256:3fbe13fa6eb6961df5512e63fc4a5cc0c8d264872674ee09164649f441839053", size = 17225, upload-time = "2025-08-14T17:53:06.299Z" }, ] [[package]] @@ -4347,11 +4708,11 @@ wheels = [ [[package]] name = "trove-classifiers" -version = "2025.5.9.12" +version = "2025.8.6.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/38/04/1cd43f72c241fedcf0d9a18d0783953ee301eac9e5d9db1df0f0f089d9af/trove_classifiers-2025.5.9.12.tar.gz", hash = "sha256:7ca7c8a7a76e2cd314468c677c69d12cc2357711fcab4a60f87994c1589e5cb5", size = 16940, upload-time = "2025-05-09T12:04:48.829Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/21/707af14daa638b0df15b5d5700349e0abdd3e5140069f9ab6e0ccb922806/trove_classifiers-2025.8.6.13.tar.gz", hash = "sha256:5a0abad839d2ed810f213ab133d555d267124ddea29f1d8a50d6eca12a50ae6e", size = 16932, upload-time = "2025-08-06T13:26:26.479Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/ef/c6deb083748be3bcad6f471b6ae983950c161890bf5ae1b2af80cc56c530/trove_classifiers-2025.5.9.12-py3-none-any.whl", hash = "sha256:e381c05537adac78881c8fa345fd0e9970159f4e4a04fcc42cfd3129cca640ce", size = 14119, upload-time = "2025-05-09T12:04:46.38Z" }, + { url = "https://files.pythonhosted.org/packages/d5/44/323a87d78f04d5329092aada803af3612dd004a64b69ba8b13046601a8c9/trove_classifiers-2025.8.6.13-py3-none-any.whl", hash = "sha256:c4e7fc83012770d80b3ae95816111c32b085716374dccee0d3fbf5c235495f9f", size = 14121, upload-time = "2025-08-06T13:26:25.063Z" }, ] [[package]] @@ -4376,27 +4737,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.1a17" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/59/f29cf1adc5c5dd6e739e08138dfa2435d3210841c6b6aa4d5bee7203cabf/ty-0.0.1a17.tar.gz", hash = "sha256:8bd0c5722c630b46a136ffc8f273f47d46cf00d9df2b0c72f1bfd28d1908a7c2", size = 4037064, upload-time = "2025-08-06T12:13:55.862Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/76/1275e4b02a74dbff40408c608dcccb758245173bd794618dadcb092e384f/ty-0.0.1a17-py3-none-linux_armv6l.whl", hash = "sha256:c16b109f05ab34f084b98b9da84c795a23780c9a2f44c237464e52efc5f97139", size = 7970950, upload-time = "2025-08-06T12:13:24.031Z" }, - { url = "https://files.pythonhosted.org/packages/df/15/10947e3a0993b02acb563fa958fb9334937615195dbe6efd17c889d1925d/ty-0.0.1a17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:59e2db756b2b727723ebee58c2e00f172e00a60083a49c8522a19e81887dbc71", size = 8117684, upload-time = "2025-08-06T12:13:26.035Z" }, - { url = "https://files.pythonhosted.org/packages/c0/73/1f982b361b0f161dad3181739f6dc010252e17d5eb8eea625d88f03deb23/ty-0.0.1a17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:79b6d76d64f86414d482f08e09433bedd3e489a1973fa1226b457d4935b592a3", size = 7721774, upload-time = "2025-08-06T12:13:27.528Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fc/a8837d4c1e395730157b16f379f4204035bb75e3fc815a1238c02bab2655/ty-0.0.1a17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdbc144e7d7a5c8dc102715870bf211b51efe581e952a933a0fcba2df9d6ac8d", size = 7841709, upload-time = "2025-08-06T12:13:29.726Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ae/1f69a9aa9f3092c7c1e3bf8e8d2d3db4a7a03108432fc02af24e313c8deb/ty-0.0.1a17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:677388fbe5b9e75764dd8b520eff9c3273f9749ece5425eb34e6fa1359430e3b", size = 7811651, upload-time = "2025-08-06T12:13:31.448Z" }, - { url = "https://files.pythonhosted.org/packages/f9/d8/561283da06dd8f7da44543af9e4a7fde1716f1fe174dde073f78ea291b35/ty-0.0.1a17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdaa4baee9f559ee5bb36b66ad0635e2e4308c6937e8e655a4d4ae1bcf736ad0", size = 8702850, upload-time = "2025-08-06T12:13:34.484Z" }, - { url = "https://files.pythonhosted.org/packages/8a/10/da263a67fea576027b65d78a7d2a55d9829aa22b17e0e10d4201b8d6bd8c/ty-0.0.1a17-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a0637f80301e7a961b87df2e01db22e8f764cd46d371a73b9a968e1894c334ab", size = 9188621, upload-time = "2025-08-06T12:13:36.206Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/e0d2c55df43ecf1bd5f11859fd9f8dd8536643ce1433aec6b8c8bf3b2865/ty-0.0.1a17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:749622726587e758fbbb530d1ab293b7489476cd5502c3ac5da5d6b042bb6a1b", size = 8795061, upload-time = "2025-08-06T12:13:37.99Z" }, - { url = "https://files.pythonhosted.org/packages/2d/3c/ad62544ad7982cb4029f7901953ede9a27c7f6a3afef207fef6a4c6ebfce/ty-0.0.1a17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f1cb91c4b79127193829ab2d1bda79a86ab782fcbdf2988b5a3af37a44a7ae2", size = 8643000, upload-time = "2025-08-06T12:13:40.075Z" }, - { url = "https://files.pythonhosted.org/packages/c5/cb/029bf9f24bb5c5c7c4b139d1f131b19530303fcdd8141607a8d065a87f74/ty-0.0.1a17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7310e783c54f4f9b2380e050b2e992ccbebcf7e73748689f1d8408199cc5a14e", size = 8432265, upload-time = "2025-08-06T12:13:41.631Z" }, - { url = "https://files.pythonhosted.org/packages/ee/6c/b4c7ba46218953a822f17813ed2d86238a04ca7937de286841a2c18ff857/ty-0.0.1a17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:96681e474600811066bf42e3e2cfac7603b7ca1da065fb4f852f94f3df9c944a", size = 7730711, upload-time = "2025-08-06T12:13:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/74/fd/f3aa541e1b7e1d0becf9f28e44e986ae5eb556f827a5312011026938d197/ty-0.0.1a17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0ddc72485af203a70267c522ff6ab6cf648ea0a065a7fa542e9e9552c83cdaee", size = 7836927, upload-time = "2025-08-06T12:13:44.614Z" }, - { url = "https://files.pythonhosted.org/packages/94/06/7d8b4b52af385a20705cc063a7f9c144aae3b6aaef165ad2fcc029c9f755/ty-0.0.1a17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7b05f97cc5149b01cb979b9b6b2d773055fb482e490d7169d7c3a213de07ade5", size = 8304523, upload-time = "2025-08-06T12:13:45.983Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a6/e14d4600339a6654e9ccc90ad9662a116f9544e0afb8d0abf1c99d6a2c2d/ty-0.0.1a17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8f80d9b5bc8681fe072ede10d4052035ec1f54c194532932e6c4230a2a5526e5", size = 8492400, upload-time = "2025-08-06T12:13:47.406Z" }, - { url = "https://files.pythonhosted.org/packages/ab/90/19dac956ab9f1ad04b4d1a38df856b44f237b3eda5af5b76a29439e64165/ty-0.0.1a17-py3-none-win32.whl", hash = "sha256:c3ba585145c4a019cb31001a1d0bd606e01fe01b285a20188a42eba165dddd50", size = 7617341, upload-time = "2025-08-06T12:13:49.093Z" }, - { url = "https://files.pythonhosted.org/packages/f5/94/08e2b3f6bc0af97abcd3fcc8ea28797a627296613256ae37e98043c871ca/ty-0.0.1a17-py3-none-win_amd64.whl", hash = "sha256:7d00b569ebd4635c58840d2ed9e1d2d8b36f496619c0bc0c8d1777767786b508", size = 8230727, upload-time = "2025-08-06T12:13:50.705Z" }, - { url = "https://files.pythonhosted.org/packages/98/c6/207bbc2f3bb71df4b1aeabe8e9c31a1cd22c72aff0ab9c1a832b9ae54f6e/ty-0.0.1a17-py3-none-win_arm64.whl", hash = "sha256:636eacc1dceaf09325415a70a03cd57eae53e5c7f281813aaa943a698a45cddb", size = 7782847, upload-time = "2025-08-06T12:13:54.243Z" }, +version = "0.0.1a18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/17/6f74949ef1686a8933fab8d11b0e6c2925235b02d513bd0aea49ab717609/ty-0.0.1a18.tar.gz", hash = "sha256:f213e6d8ab3b6abaeb76aa66c6c5c9ad04d7d21a80b4111422286e4beff6d294", size = 4079761, upload-time = "2025-08-14T10:32:18.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/6b/9b6480d9cff51a8d1346950194363ac269f883771e4f53bd3cb55bd79b93/ty-0.0.1a18-py3-none-linux_armv6l.whl", hash = "sha256:f1fe54d817294f18e29a164ad8c460c7cdf60251ef392733a8afcfb6db5e933b", size = 8107401, upload-time = "2025-08-14T10:31:44.691Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1e/a02beb69386416da8b44b12b9cd32c55ac6f72361f70048d811c7efc2dff/ty-0.0.1a18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb40403157f2c7fd81d15c6745932daa486cd7954c2721e5dc22c0a5d0415add", size = 8304302, upload-time = "2025-08-14T10:31:46.819Z" }, + { url = "https://files.pythonhosted.org/packages/d0/00/421bad42f962dd29cf16c7d941a10a39dc1f9d7906e186a098c75cb0a6aa/ty-0.0.1a18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07f310f21416e50d726f68fb213266576fab18a977bd76bc57e852551427b5a1", size = 7883646, upload-time = "2025-08-14T10:31:49.449Z" }, + { url = "https://files.pythonhosted.org/packages/92/f1/e7069f8914e2b700709567f24e19221623e35e0c21fa474a5c568ce9485a/ty-0.0.1a18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdbd9bbe7a44f467c0f21171aada32f7a05a5aecc4e20c9a53ae888916589014", size = 8020426, upload-time = "2025-08-14T10:31:51.259Z" }, + { url = "https://files.pythonhosted.org/packages/68/d5/81ef03cf47554f6b080ab1d549275b7411631a412feab34da20c21c0c17d/ty-0.0.1a18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ab463dc1a51f294b01b8b6fe869893a441df05590ac1edd4c76f154d1fc0b5a", size = 7933541, upload-time = "2025-08-14T10:31:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/c131f9774ed8d8e048786e8005d3f70ffe09b536fab33b60b98b67f17fe5/ty-0.0.1a18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a6256b657ce45cd9b0173f1b8f858ff5ff1fef9314b1e167fcf53eb0c84d166", size = 8870017, upload-time = "2025-08-14T10:31:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/3e/23/f75594959c952a63e94f6c97d830fb0975ff877def40b7016d448eaaf2a2/ty-0.0.1a18-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0ed83cf7f4cc7ea9a40aed1cc0e19b6770b14d7b22fdbb9b65a68eb8ec11fe12", size = 9376017, upload-time = "2025-08-14T10:31:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ff/7c/3e843c781e2149731ce3b9a50a52ab98113041170901acb1f57bf78f142e/ty-0.0.1a18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4b3b7462bee0bc78b73124a19e47ca507ef609425310c3fa34bdf3257671618", size = 8990092, upload-time = "2025-08-14T10:31:59.023Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/7ac61ab4211d11d9596a541ab964d602e14126f124ec0b2c3a1300344642/ty-0.0.1a18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b0d83180dca1daf96fc46f2728cb5feadc867390b6d63e74aa9068eb3c85c13", size = 8776514, upload-time = "2025-08-14T10:32:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/38/cd/16c04b8b93cb563990d27e65bf2def173909b7743b12df716974f492ba0b/ty-0.0.1a18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5b4f8cd8fe3d1fe4e4b338570b8aee48889f5294a01d25d1b7a232a75b9fae", size = 8585311, upload-time = "2025-08-14T10:32:03.296Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/56a4c49b68c3d37c35c76af377b7e55a4ead95680deef4929b539a4dae5f/ty-0.0.1a18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b64181d14e3d6b179860471cbe9e95a35d6d17919e73db6bef34b130de8f7c38", size = 7900949, upload-time = "2025-08-14T10:32:05.323Z" }, + { url = "https://files.pythonhosted.org/packages/88/0a/a4de919c09b8c1d5421069631f8f2d59e4db61c7aeef43cb4f342cb93d2e/ty-0.0.1a18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:53cb0a37b007ed48eb104ea7e776ba84080a3c85ad118eea84e3f8d10644f9f6", size = 7956204, upload-time = "2025-08-14T10:32:08.056Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/cb6cf1f1b6524a1c07173a27d58a9075fbf5c776f072b72448880ea8e419/ty-0.0.1a18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:205f55224d6441ba36082fb9c0690088feba2b0d02dc8ebc11006afddd73a2fb", size = 8452269, upload-time = "2025-08-14T10:32:09.574Z" }, + { url = "https://files.pythonhosted.org/packages/9f/75/7152152a249613ccda535152d683a46a4e781737663a76154082a9eef2dd/ty-0.0.1a18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:121032a844008e7f0e29590eadaa12ea4df2aa9c27c37f06943181c81025c920", size = 8650870, upload-time = "2025-08-14T10:32:11.37Z" }, + { url = "https://files.pythonhosted.org/packages/36/c0/f181a6cd2a6d6208f342950e241950e23dfa6218195469a304dae4fdf7fb/ty-0.0.1a18-py3-none-win32.whl", hash = "sha256:4ff87364366ce98d70eca7993a604e60f762b27156fc63a597aeb4f05e56be1c", size = 7789583, upload-time = "2025-08-14T10:32:13.115Z" }, + { url = "https://files.pythonhosted.org/packages/78/04/577495cd64b7e8ecd63fa3bb8c07cba11772f092b2a54c602b41acdabcf6/ty-0.0.1a18-py3-none-win_amd64.whl", hash = "sha256:596562ac1b67a3c4da2a895b5601a61babe9953b6fc3e6c66a6f6e1a2c2c5d60", size = 8422373, upload-time = "2025-08-14T10:32:14.653Z" }, + { url = "https://files.pythonhosted.org/packages/88/28/e7a9ec9195f3cfd76b95f1d01e7edecfe3be750c6f0f9cd3658625bedf07/ty-0.0.1a18-py3-none-win_arm64.whl", hash = "sha256:a557af5cf678485c77914da298621de751e86964d8d3237ee2229f5fce27ab35", size = 7968662, upload-time = "2025-08-14T10:32:16.386Z" }, ] [[package]] @@ -4444,11 +4805,11 @@ wheels = [ [[package]] name = "types-python-dateutil" -version = "2.9.0.20250516" +version = "2.9.0.20250809" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/88/d65ed807393285204ab6e2801e5d11fbbea811adcaa979a2ed3b67a5ef41/types_python_dateutil-2.9.0.20250516.tar.gz", hash = "sha256:13e80d6c9c47df23ad773d54b2826bd52dbbb41be87c3f339381c1700ad21ee5", size = 13943, upload-time = "2025-05-16T03:06:58.385Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/53/07dac71db45fb6b3c71c2fd29a87cada2239eac7ecfb318e6ebc7da00a3b/types_python_dateutil-2.9.0.20250809.tar.gz", hash = "sha256:69cbf8d15ef7a75c3801d65d63466e46ac25a0baa678d89d0a137fc31a608cc1", size = 15820, upload-time = "2025-08-09T03:14:14.109Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/3f/b0e8db149896005adc938a1e7f371d6d7e9eca4053a29b108978ed15e0c2/types_python_dateutil-2.9.0.20250516-py3-none-any.whl", hash = "sha256:2b2b3f57f9c6a61fba26a9c0ffb9ea5681c9b83e69cd897c6b5f668d9c0cab93", size = 14356, upload-time = "2025-05-16T03:06:57.249Z" }, + { url = "https://files.pythonhosted.org/packages/43/5e/67312e679f612218d07fcdbd14017e6d571ce240a5ba1ad734f15a8523cc/types_python_dateutil-2.9.0.20250809-py3-none-any.whl", hash = "sha256:768890cac4f2d7fd9e0feb6f3217fce2abbfdfc0cadd38d11fba325a815e4b9f", size = 17707, upload-time = "2025-08-09T03:14:13.314Z" }, ] [[package]] @@ -4462,11 +4823,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.14.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, ] [[package]] @@ -4519,28 +4880,28 @@ wheels = [ [[package]] name = "uv" -version = "0.8.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/d0/4cd8ac2c7938da78c8e9ca791205f80e74b0f5a680f2a2d50323d54961d0/uv-0.8.8.tar.gz", hash = "sha256:6880e96cd994e53445d364206ddb4b2fff89fd2fbc74a74bef4a6f86384b07d9", size = 3477036, upload-time = "2025-08-09T00:26:00.883Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/d5/49e188db80f3d8b1969bdbcb8a5468a3796827f15d773241204f206a9ff6/uv-0.8.8-py3-none-linux_armv6l.whl", hash = "sha256:fcdbee030de120478db1a4bb3e3bbf04eec572527ea9107ecf064a808259b6c9", size = 18470316, upload-time = "2025-08-09T00:25:11.956Z" }, - { url = "https://files.pythonhosted.org/packages/01/50/add1afadccd141d0d72b54e5146f8181fcc6efd1567a17c5b1edec444010/uv-0.8.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:461e8fb83931755cf0596bf1b8ccbfe02765e81a0d392c495c07685d6b6591f9", size = 18468770, upload-time = "2025-08-09T00:25:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ac/3c6dc8781d37ef9854f412322caffac2978dd3fa1bf806f7daebcfebf2be/uv-0.8.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:58056e5ccebb0a1aad27bd89d0ccc5b65c086d5a7f6b0ac16a9dde030b63cf14", size = 17200419, upload-time = "2025-08-09T00:25:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/a1/9e/c30ea1f634673d234999985984afbe96c3d2a4381986e36df0bb46c0f21b/uv-0.8.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:5b4c56a620137f562e1d7b09eac6c9d4adeb876aefc51be27973257fcb426c9d", size = 17779351, upload-time = "2025-08-09T00:25:20.891Z" }, - { url = "https://files.pythonhosted.org/packages/2f/89/f2885c6e97a265b4b18050df6285f56c81b603a867a63fcd8f2caa04d95c/uv-0.8.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5fc33adb91c4e3db550648aa30c2b97e8e4d8b8842ead7784a9e76dae3cb14dc", size = 18139292, upload-time = "2025-08-09T00:25:23.352Z" }, - { url = "https://files.pythonhosted.org/packages/38/5f/98dad16987919e7dc02f2566026a263ea6307bf57e8de0008dde4717d9cf/uv-0.8.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19a82d6738d3aa58e6646b9d6c343d103abf0c4caf97a68d16a8cab55282e4be", size = 18932468, upload-time = "2025-08-09T00:25:25.691Z" }, - { url = "https://files.pythonhosted.org/packages/56/99/52d0d9f53cc5df11b1a459e743bd7b2f4660d49f125a63640eb85ce993e0/uv-0.8.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9dce4de70098cb5b98feea9ef0b8f7db5d6b9deea003a926bc044a793872d719", size = 20251614, upload-time = "2025-08-09T00:25:28.122Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/0698099a905b4a07b8fa9d6838e0680de707216ccf003433ca1b4afff224/uv-0.8.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1038324c178d2d7407a4005c4c3294cbad6a02368ba5a85242308de62a6f4e12", size = 19916222, upload-time = "2025-08-09T00:25:30.732Z" }, - { url = "https://files.pythonhosted.org/packages/7f/29/8384e0f3f3536ef376d94b7ab177753179906a6c2f5bab893e3fb9525b45/uv-0.8.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3bd016beea3935f9148b3d2482e3d60dee36f0260f9e99d4f57acfd978c1142a", size = 19238516, upload-time = "2025-08-09T00:25:33.637Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f1/6c107deccd6e66eb1c46776d8cef4ca9274aac73cec1b14453fe85e18a54/uv-0.8.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0a2b5ebc96aba2b0bf54283d2906b40f32949298cbc6ec48648097ddeac5c5d", size = 19232295, upload-time = "2025-08-09T00:25:37.154Z" }, - { url = "https://files.pythonhosted.org/packages/c5/96/9f5e935cd970102c67ce2a753ac721665fb4477c262e86afa0ab385cefff/uv-0.8.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e529dc0a1be5e896d299e4eae4599fa68909f8cb3e6c5ee1a46f66c9048e3334", size = 18046917, upload-time = "2025-08-09T00:25:39.72Z" }, - { url = "https://files.pythonhosted.org/packages/32/75/97f371add0a02e5e37156ac0fea908ab4a1160fdf716d0e6c257b6767122/uv-0.8.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5d58d986c3b6a9ce0fb48cd48b3aee6cb1b1057f928d598432e75a4fcaa370f4", size = 18949133, upload-time = "2025-08-09T00:25:42.139Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/ea988ae9d8c5531454ea6904290e229624c9ea830a5c37b91ec74ebde9a4/uv-0.8.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:e117e1230559058fd286292dd5839e8e82d1aaf05763bf4a496e91fe07b69fa1", size = 18080018, upload-time = "2025-08-09T00:25:44.645Z" }, - { url = "https://files.pythonhosted.org/packages/ff/14/3b16af331b79ae826d00a73e98f26f7f660dabedc0f82acb99069601b355/uv-0.8.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:372934fd94193c98dec59bd379cf39e73f906ae6162cbfb66686f32afd75fa0f", size = 18437896, upload-time = "2025-08-09T00:25:49.162Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b6/c866684da5571dbf42e9a60b6587a62adc8a2eb592f07411d3b29cb09871/uv-0.8.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:9330c924faa9df00a5e78b54561ecf4e5eac1211066f027620dbe85bd6f479ce", size = 19341221, upload-time = "2025-08-09T00:25:51.444Z" }, - { url = "https://files.pythonhosted.org/packages/49/ea/55a0eff462b2ec5a6327dd87c401c53306406c830fa8f2cabd2af79dd97f/uv-0.8.8-py3-none-win32.whl", hash = "sha256:65113735aa3427d3897e2f537da1331d1391735c6eecb9b820da6a15fd2f6738", size = 18244601, upload-time = "2025-08-09T00:25:53.696Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c0/f56ddb1b2276405618e3d2522018c962c010fc71f97f385d01b7e1dcd8df/uv-0.8.8-py3-none-win_amd64.whl", hash = "sha256:66189ca0b4051396aa19a6f036351477656073d0fd01618051faca699e1b3cdc", size = 20233481, upload-time = "2025-08-09T00:25:56.247Z" }, - { url = "https://files.pythonhosted.org/packages/ac/1a/70dc4c730c19f3af40be9450b98b801e03cd6d16609743013f7258f69a29/uv-0.8.8-py3-none-win_arm64.whl", hash = "sha256:1d829486e88ebbf7895306ff09a8b6014d3af7a18e27d751979ee37bf3a27832", size = 18786215, upload-time = "2025-08-09T00:25:58.941Z" }, +version = "0.8.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/c1/765112567045a2219979d1a7038e4a2afbddd0637446556b089e77252528/uv-0.8.11.tar.gz", hash = "sha256:d98105244b895c6026e9f3d86f200b70039d39a5f4866022fae664ed935530f3", size = 3504312, upload-time = "2025-08-14T19:48:18.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/2f/6703896c45d29b44e5954bb283f00616387cef7ae80188226dac87aff93d/uv-0.8.11-py3-none-linux_armv6l.whl", hash = "sha256:1be7cbc874980dc3e5e0c40fdb3787013a35cce64485f7685fc4b0ee550f7c0c", size = 18497046, upload-time = "2025-08-14T19:47:28.18Z" }, + { url = "https://files.pythonhosted.org/packages/61/fe/3ae518ea5a6c2e4fd3d0174486c841bd85e676b3971d9553445ab57319d9/uv-0.8.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:84c888cc7b3310aada6058ce964d9b48d4f7801add6f1236548adeb262c637bf", size = 18573000, upload-time = "2025-08-14T19:47:32.156Z" }, + { url = "https://files.pythonhosted.org/packages/00/21/6a1cd01103aec916fdf2daa034e3a179a6b835b25db89f4f5e43117ac68c/uv-0.8.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3e46395c7f2c7e52bf63f29f3fc1c6b357b011285d1df37d8af9c6f6f7cad36f", size = 17205164, upload-time = "2025-08-14T19:47:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b2/8a9e00d6e5c41a231f59f75c15b04626f7d4561364475962894a31b01fee/uv-0.8.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:d9d35783ac8600cd8e95e9afd007aa281edf3125803c570a4b3246138e2a304d", size = 17822163, upload-time = "2025-08-14T19:47:37.111Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/230f1ed3cbeae61d10ac8acc3d63b38a81c728161e7671fe3516aec72c76/uv-0.8.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce267b13f498cebb9690c06461b727718bd11624679ddebb0a3998efe6b80ad7", size = 18152038, upload-time = "2025-08-14T19:47:39.951Z" }, + { url = "https://files.pythonhosted.org/packages/95/be/7fd436adedd79c9afad14722135029010a972e17b05312795a976bc08854/uv-0.8.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c03aec1ad898642ae427b763cf5e5f90a678b91254f733ae08d01d15acd3672b", size = 18991855, upload-time = "2025-08-14T19:47:42.664Z" }, + { url = "https://files.pythonhosted.org/packages/80/4e/2cca1be92fc3cdfddb5f2fa8d5650098948f357774cbe51810aaa5968da0/uv-0.8.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:83aa9c8b0085949542674301268e2b7b541f1108dc95664dedf50fffd1578f97", size = 20248085, upload-time = "2025-08-14T19:47:45.489Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/c4a5bbccfa45d8573d22da0d753329e572e72cd70796720dc0bc5c74e5c5/uv-0.8.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e9506b3febbce3559290cb10cd1c84dbed32bc4f4b1062bc2fe4f093aa42aea", size = 19961250, upload-time = "2025-08-14T19:47:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f1/c1f9e59110fce261ee67cff854b4f95cae39a523d2a076c7566a704ebbe6/uv-0.8.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba7bb038f0a263accefde1db68ecba7a756c85e6bcc25af161acef2711d6da19", size = 19314178, upload-time = "2025-08-14T19:47:50.469Z" }, + { url = "https://files.pythonhosted.org/packages/fc/47/c398c3a9657a6f8c3a7b1938ae0b7061c4087e1fbb00f83a7a4f79005752/uv-0.8.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36eb184758f18347547045a3aa7cc87c98a75c773e437c8a85878eb004a31c2e", size = 19314121, upload-time = "2025-08-14T19:47:54.17Z" }, + { url = "https://files.pythonhosted.org/packages/69/04/7ff94b68c33b93e89ec9920724b2a6d3992051584afd3410bf2604d2b93c/uv-0.8.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0a7fcbe71cc5402b7c3d4c381f9b970a455d8ccc2a43ee2ce5ac2b617ec0534c", size = 18105431, upload-time = "2025-08-14T19:47:56.844Z" }, + { url = "https://files.pythonhosted.org/packages/09/5a/aee6041cd0c9ab1c56da61ba1e9ac30b4ea7c1c85471e19cb0cc1e415c0a/uv-0.8.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0da2c794dead209e660cb7df143ea9756c118ffa5874859e8a28a79101b5c760", size = 18984052, upload-time = "2025-08-14T19:47:59.927Z" }, + { url = "https://files.pythonhosted.org/packages/05/cd/7b9926b676a3807312bfb91662813305b305c5218a05a9b651763b28267e/uv-0.8.11-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:0a95dc944d62db4ca282f7415c2d3c0fa3ead9e245a47d845515f5ddbd5a80ef", size = 18109344, upload-time = "2025-08-14T19:48:02.607Z" }, + { url = "https://files.pythonhosted.org/packages/82/19/1e90e45fd84c4f5512dc9c8ad0ac3a4792677725047d3e7299f9dae41406/uv-0.8.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0cd14f319e18a7b278238f0d87b18180282ec4d44d023f8b3ed2c8c091a14277", size = 18493945, upload-time = "2025-08-14T19:48:05.112Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/e6b784ede573d3f1ba6fafe70dd317b4543146a6c2ca88a5f56923518552/uv-0.8.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:261d19395a211f980d1ebc861356cf73ba23ceece2392c0b36ade38f89fd16a6", size = 19398023, upload-time = "2025-08-14T19:48:07.993Z" }, + { url = "https://files.pythonhosted.org/packages/65/5f/fd61ebec95bb5854c860d5268bc8ecbbca881465340f1e86302cacdd8234/uv-0.8.11-py3-none-win32.whl", hash = "sha256:0b922061f7b5915f224df23a849b6e1bfcace2e6b9fc0ee128868447873edb22", size = 18308608, upload-time = "2025-08-14T19:48:10.847Z" }, + { url = "https://files.pythonhosted.org/packages/bb/57/84358ea67cee7ec029ed0d51e801a64c5929b7d647ae31cd5e5aea0c6f61/uv-0.8.11-py3-none-win_amd64.whl", hash = "sha256:fe01737f3ddd533903f31236219c29e09063541f17a060403acc51906ce0cfe8", size = 20214609, upload-time = "2025-08-14T19:48:13.368Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/069a75703693d3297d95657957ea00d2f035896066f00a5692fbdce76d36/uv-0.8.11-py3-none-win_arm64.whl", hash = "sha256:cf3454d3407a5cac0d661b6033e3197643d0a6b5bb0e00869f6877ff7af907c9", size = 18878482, upload-time = "2025-08-14T19:48:15.743Z" }, ] [[package]] @@ -4558,16 +4919,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.31.2" +version = "20.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, ] [[package]] @@ -4653,55 +5014,41 @@ wheels = [ [[package]] name = "wrapt" -version = "1.17.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" }, - { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" }, - { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" }, - { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" }, - { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" }, - { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" }, - { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" }, - { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" }, - { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" }, - { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" }, - { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" }, - { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" }, - { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" }, - { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" }, - { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" }, - { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" }, - { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" }, - { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" }, - { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" }, - { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" }, - { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" }, - { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" }, - { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" }, - { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" }, - { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" }, - { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" }, - { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" }, - { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" }, - { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" }, - { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" }, - { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" }, - { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" }, - { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" }, - { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, +version = "1.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, + { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, + { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] [[package]] From 371a7d1beb8c4eb3cd348d25a9814a1424c7a9d9 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Fri, 15 Aug 2025 22:38:57 -0400 Subject: [PATCH 31/65] fix: plugin config variable Signed-off-by: Frederico Araujo --- .../framework/external/mcp/test_client_stdio.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py index aabddd1d5..e1f59dee7 100644 --- a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py @@ -15,7 +15,7 @@ @pytest.mark.asyncio async def test_client_load_stdio(): - os.environ["CFMCP_PLUGIN_CONFIG"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" + os.environ["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" os.environ["PYTHONPATH"] = "." config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml") print(config) @@ -35,11 +35,11 @@ async def test_client_load_stdio(): assert config.priority == 100 assert config.kind == "external" await plugin.shutdown() - del os.environ["CFMCP_PLUGIN_CONFIG"] + del os.environ["PLUGINS_CONFIG_PATH"] del os.environ["PYTHONPATH"] async def test_client_load_stdio_overrides(): - os.environ["CFMCP_PLUGIN_CONFIG"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" + os.environ["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" os.environ["PYTHONPATH"] = "." config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml") print(config) @@ -61,12 +61,12 @@ async def test_client_load_stdio_overrides(): assert config.hooks[1] == "prompt_post_fetch" assert config.kind == "external" await plugin.shutdown() - del os.environ["CFMCP_PLUGIN_CONFIG"] + del os.environ["PLUGINS_CONFIG_PATH"] del os.environ["PYTHONPATH"] @pytest.mark.asyncio async def test_client_load_stdio_post_prompt(): - os.environ["CFMCP_PLUGIN_CONFIG"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_single_plugin.yaml" + os.environ["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_single_plugin.yaml" os.environ["PYTHONPATH"] = "." config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml") print(config) @@ -75,7 +75,7 @@ async def test_client_load_stdio_post_prompt(): plugin = await loader.load_and_instantiate_plugin(config.plugins[0]) print(plugin) prompt = PromptPrehookPayload(name="test_prompt", args = {"user": "What a crapshow!"}) - context = PluginContext(request_id="1", server_id="2") + context = PluginContext(request_id="1", server_id="2") result = await plugin.prompt_pre_fetch(prompt, context) assert result.modified_payload.args["user"] == "What a yikesshow!" config = plugin.config @@ -94,5 +94,5 @@ async def test_client_load_stdio_post_prompt(): assert result.modified_payload.result.messages[0].content.text == "What the yikes?" await plugin.shutdown() await loader.shutdown() - del os.environ["CFMCP_PLUGIN_CONFIG"] + del os.environ["PLUGINS_CONFIG_PATH"] del os.environ["PYTHONPATH"] \ No newline at end of file From 54f312c0c8472aa21bc207a87d32033b0d0acb90 Mon Sep 17 00:00:00 2001 From: Teryl Taylor Date: Sun, 17 Aug 2025 12:04:07 -0600 Subject: [PATCH 32/65] fix(tests): fixed doctests for plugins. Signed-off-by: Teryl Taylor --- .../plugins/framework/external/__init__.py | 8 +++++ mcpgateway/plugins/framework/loader/plugin.py | 10 +++++- mcpgateway/plugins/framework/manager.py | 12 +++---- mcpgateway/plugins/framework/models.py | 36 +++++++++---------- mcpgateway/plugins/framework/utils.py | 36 +++++++++---------- mcpgateway/services/tool_service.py | 3 +- tests/unit/mcpgateway/plugins/__init__.py | 8 +++++ .../mcpgateway/plugins/framework/__init__.py | 8 +++++ .../plugins/framework/external/__init__.py | 8 +++++ .../framework/external/mcp/__init__.py | 8 +++++ .../plugins/framework/loader/__init__.py | 8 +++++ .../mcpgateway/plugins/plugins/__init__.py | 8 +++++ .../plugins/plugins/pii_filter/__init__.py | 8 +++++ .../plugins/resource_filter/__init__.py | 8 +++++ tests/unit/mcpgateway/utils/__init__.py | 8 +++++ 15 files changed, 133 insertions(+), 44 deletions(-) create mode 100644 mcpgateway/plugins/framework/external/__init__.py create mode 100644 tests/unit/mcpgateway/plugins/__init__.py create mode 100644 tests/unit/mcpgateway/plugins/framework/__init__.py create mode 100644 tests/unit/mcpgateway/plugins/framework/external/__init__.py create mode 100644 tests/unit/mcpgateway/plugins/framework/external/mcp/__init__.py create mode 100644 tests/unit/mcpgateway/plugins/framework/loader/__init__.py create mode 100644 tests/unit/mcpgateway/plugins/plugins/__init__.py create mode 100644 tests/unit/mcpgateway/plugins/plugins/pii_filter/__init__.py create mode 100644 tests/unit/mcpgateway/plugins/plugins/resource_filter/__init__.py create mode 100644 tests/unit/mcpgateway/utils/__init__.py diff --git a/mcpgateway/plugins/framework/external/__init__.py b/mcpgateway/plugins/framework/external/__init__.py new file mode 100644 index 000000000..e1ac9d70d --- /dev/null +++ b/mcpgateway/plugins/framework/external/__init__.py @@ -0,0 +1,8 @@ +"""External plugin which connects to a remote server. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +Module that contains plugin client/server code to serve external plugins. +""" diff --git a/mcpgateway/plugins/framework/loader/plugin.py b/mcpgateway/plugins/framework/loader/plugin.py index 56f31e7d9..3803d1b9f 100644 --- a/mcpgateway/plugins/framework/loader/plugin.py +++ b/mcpgateway/plugins/framework/loader/plugin.py @@ -97,6 +97,14 @@ async def load_and_instantiate_plugin(self, config: PluginConfig) -> Plugin | No return None async def shutdown(self) -> None: - """Shutdown and cleanup plugin loader.""" + """Shutdown and cleanup plugin loader. + + Examples: + >>> import asyncio + >>> loader = PluginLoader() + >>> asyncio.run(loader.shutdown()) + >>> loader._plugin_types + {} + """ if self._plugin_types: self._plugin_types.clear() diff --git a/mcpgateway/plugins/framework/manager.py b/mcpgateway/plugins/framework/manager.py index a11d1222d..193305243 100644 --- a/mcpgateway/plugins/framework/manager.py +++ b/mcpgateway/plugins/framework/manager.py @@ -290,7 +290,7 @@ async def pre_prompt_fetch(plugin: PluginRef, payload: PromptPrehookPayload, con >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = PromptPrehookPayload(name="test", args={"key": "value"}) - >>> context = PluginContext(GlobalContext(request_id="123")) + >>> context = PluginContext(request_id="123") >>> # In async context: >>> # result = await pre_prompt_fetch(plugin_ref, payload, context) """ @@ -316,7 +316,7 @@ async def post_prompt_fetch(plugin: PluginRef, payload: PromptPosthookPayload, c >>> # plugin_ref = PluginRef(my_plugin) >>> result = PromptResult(messages=[]) >>> payload = PromptPosthookPayload(name="test", result=result) - >>> context = PluginContext(GlobalContext(request_id="123")) + >>> context = PluginContext(request_id="123") >>> # In async context: >>> # result = await post_prompt_fetch(plugin_ref, payload, context) """ @@ -340,7 +340,7 @@ async def pre_tool_invoke(plugin: PluginRef, payload: ToolPreInvokePayload, cont >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = ToolPreInvokePayload(name="calculator", args={"operation": "add", "a": 5, "b": 3}) - >>> context = PluginContext(GlobalContext(request_id="123")) + >>> context = PluginContext(request_id="123") >>> # In async context: >>> # result = await pre_tool_invoke(plugin_ref, payload, context) """ @@ -364,7 +364,7 @@ async def post_tool_invoke(plugin: PluginRef, payload: ToolPostInvokePayload, co >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = ToolPostInvokePayload(name="calculator", result={"result": 8, "status": "success"}) - >>> context = PluginContext(GlobalContext(request_id="123")) + >>> context = PluginContext(request_id="123") >>> # In async context: >>> # result = await post_tool_invoke(plugin_ref, payload, context) """ @@ -388,7 +388,7 @@ async def pre_resource_fetch(plugin: PluginRef, payload: ResourcePreFetchPayload >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = ResourcePreFetchPayload(uri="file:///data.txt", metadata={"cache": True}) - >>> context = PluginContext(GlobalContext(request_id="123")) + >>> context = PluginContext(request_id="123") >>> # In async context: >>> # result = await pre_resource_fetch(plugin_ref, payload, context) """ @@ -414,7 +414,7 @@ async def post_resource_fetch(plugin: PluginRef, payload: ResourcePostFetchPaylo >>> # plugin_ref = PluginRef(my_plugin) >>> content = ResourceContent(type="resource", uri="file:///data.txt", text="Data") >>> payload = ResourcePostFetchPayload(uri="file:///data.txt", content=content) - >>> context = PluginContext(GlobalContext(request_id="123")) + >>> context = PluginContext(request_id="123") >>> # In async context: >>> # result = await post_resource_fetch(plugin_ref, payload, context) """ diff --git a/mcpgateway/plugins/framework/models.py b/mcpgateway/plugins/framework/models.py index e10728b66..1403a24b2 100644 --- a/mcpgateway/plugins/framework/models.py +++ b/mcpgateway/plugins/framework/models.py @@ -486,15 +486,15 @@ class PromptPrehookPayload(BaseModel): args (dic[str,str]): The prompt template arguments. Examples: - >>> payload = PromptPrehookPayload("test_prompt", {"user": "alice"}) + >>> payload = PromptPrehookPayload(name="test_prompt", args={"user": "alice"}) >>> payload.name 'test_prompt' >>> payload.args {'user': 'alice'} - >>> payload2 = PromptPrehookPayload("empty", None) + >>> payload2 = PromptPrehookPayload(name="empty") >>> payload2.args {} - >>> p = PromptPrehookPayload("greeting", {"name": "Bob", "time": "morning"}) + >>> p = PromptPrehookPayload(name="greeting", args={"name": "Bob", "time": "morning"}) >>> p.name 'greeting' >>> p.args["name"] @@ -516,7 +516,7 @@ class PromptPosthookPayload(BaseModel): >>> from mcpgateway.models import PromptResult, Message, TextContent >>> msg = Message(role="user", content=TextContent(type="text", text="Hello World")) >>> result = PromptResult(messages=[msg]) - >>> payload = PromptPosthookPayload("greeting", result) + >>> payload = PromptPosthookPayload(name="greeting", result=result) >>> payload.name 'greeting' >>> payload.result.messages[0].content.text @@ -524,7 +524,7 @@ class PromptPosthookPayload(BaseModel): >>> from mcpgateway.models import PromptResult, Message, TextContent >>> msg = Message(role="assistant", content=TextContent(type="text", text="Test output")) >>> r = PromptResult(messages=[msg]) - >>> p = PromptPosthookPayload("test", r) + >>> p = PromptPosthookPayload(name="test", result=r) >>> p.name 'test' """ @@ -583,15 +583,15 @@ class ToolPreInvokePayload(BaseModel): args: The tool arguments for invocation. Examples: - >>> payload = ToolPreInvokePayload("test_tool", {"input": "data"}) + >>> payload = ToolPreInvokePayload(name="test_tool", args={"input": "data"}) >>> payload.name 'test_tool' >>> payload.args {'input': 'data'} - >>> payload2 = ToolPreInvokePayload("empty", None) + >>> payload2 = ToolPreInvokePayload(name="empty") >>> payload2.args {} - >>> p = ToolPreInvokePayload("calculator", {"operation": "add", "a": 5, "b": 3}) + >>> p = ToolPreInvokePayload(name="calculator", args={"operation": "add", "a": 5, "b": 3}) >>> p.name 'calculator' >>> p.args["operation"] @@ -611,12 +611,12 @@ class ToolPostInvokePayload(BaseModel): result: The tool invocation result. Examples: - >>> payload = ToolPostInvokePayload("calculator", {"result": 8, "status": "success"}) + >>> payload = ToolPostInvokePayload(name="calculator", result={"result": 8, "status": "success"}) >>> payload.name 'calculator' >>> payload.result {'result': 8, 'status': 'success'} - >>> p = ToolPostInvokePayload("analyzer", {"confidence": 0.95, "sentiment": "positive"}) + >>> p = ToolPostInvokePayload(name="analyzer", result={"confidence": 0.95, "sentiment": "positive"}) >>> p.name 'analyzer' >>> p.result["confidence"] @@ -641,17 +641,17 @@ class GlobalContext(BaseModel): server_id (str): server ID. Examples: - >>> ctx = GlobalContext("req-123") + >>> ctx = GlobalContext(request_id="req-123") >>> ctx.request_id 'req-123' >>> ctx.user is None True - >>> ctx2 = GlobalContext("req-456", user="alice", tenant_id="tenant1") + >>> ctx2 = GlobalContext(request_id="req-456", user="alice", tenant_id="tenant1") >>> ctx2.user 'alice' >>> ctx2.tenant_id 'tenant1' - >>> c = GlobalContext("123", server_id="srv1") + >>> c = GlobalContext(request_id="123", server_id="srv1") >>> c.request_id '123' >>> c.server_id @@ -713,13 +713,13 @@ class ResourcePreFetchPayload(BaseModel): metadata: Optional metadata for the resource request. Examples: - >>> payload = ResourcePreFetchPayload("file:///data.txt") + >>> payload = ResourcePreFetchPayload(uri="file:///data.txt") >>> payload.uri 'file:///data.txt' - >>> payload2 = ResourcePreFetchPayload("http://api/data", {"Accept": "application/json"}) + >>> payload2 = ResourcePreFetchPayload(uri="http://api/data", metadata={"Accept": "application/json"}) >>> payload2.metadata {'Accept': 'application/json'} - >>> p = ResourcePreFetchPayload("file:///docs/readme.md", {"version": "1.0"}) + >>> p = ResourcePreFetchPayload(uri="file:///docs/readme.md", metadata={"version": "1.0"}) >>> p.uri 'file:///docs/readme.md' >>> p.metadata["version"] @@ -741,14 +741,14 @@ class ResourcePostFetchPayload(BaseModel): >>> from mcpgateway.models import ResourceContent >>> content = ResourceContent(type="resource", uri="file:///data.txt", ... text="Hello World") - >>> payload = ResourcePostFetchPayload("file:///data.txt", content) + >>> payload = ResourcePostFetchPayload(uri="file:///data.txt", content=content) >>> payload.uri 'file:///data.txt' >>> payload.content.text 'Hello World' >>> from mcpgateway.models import ResourceContent >>> resource_content = ResourceContent(type="resource", uri="test://resource", text="Test data") - >>> p = ResourcePostFetchPayload("test://resource", resource_content) + >>> p = ResourcePostFetchPayload(uri="test://resource", content=resource_content) >>> p.uri 'test://resource' """ diff --git a/mcpgateway/plugins/framework/utils.py b/mcpgateway/plugins/framework/utils.py index 37d76c30c..8dd93663e 100644 --- a/mcpgateway/plugins/framework/utils.py +++ b/mcpgateway/plugins/framework/utils.py @@ -86,14 +86,14 @@ def matches(condition: PluginCondition, context: GlobalContext) -> bool: >>> from mcpgateway.plugins.framework.models import PluginCondition >>> from mcpgateway.plugins.framework.models import GlobalContext >>> cond = PluginCondition(server_ids={"srv1", "srv2"}) - >>> ctx = GlobalContext("req1", server_id="srv1") + >>> ctx = GlobalContext(request_id="req1", server_id="srv1") >>> matches(cond, ctx) True - >>> ctx2 = GlobalContext("req2", server_id="srv3") + >>> ctx2 = GlobalContext(request_id="req2", server_id="srv3") >>> matches(cond, ctx2) False >>> cond2 = PluginCondition(user_patterns=["admin"]) - >>> ctx3 = GlobalContext("req3", user="admin_user") + >>> ctx3 = GlobalContext(request_id="req3", user="admin_user") >>> matches(cond2, ctx3) True """ @@ -126,12 +126,12 @@ def pre_prompt_matches(payload: PromptPrehookPayload, conditions: list[PluginCon Examples: >>> from mcpgateway.plugins.framework.models import PluginCondition >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload, GlobalContext - >>> payload = PromptPrehookPayload("greeting", {}) + >>> payload = PromptPrehookPayload(name="greeting", args={}) >>> cond = PluginCondition(prompts={"greeting"}) - >>> ctx = GlobalContext("req1") + >>> ctx = GlobalContext(request_id="req1") >>> pre_prompt_matches(payload, [cond], ctx) True - >>> payload2 = PromptPrehookPayload("other", {}) + >>> payload2 = PromptPrehookPayload(name="other", args={}) >>> pre_prompt_matches(payload2, [cond], ctx) False """ @@ -188,12 +188,12 @@ def pre_tool_matches(payload: ToolPreInvokePayload, conditions: list[PluginCondi Examples: >>> from mcpgateway.plugins.framework.models import PluginCondition >>> from mcpgateway.plugins.framework.models import ToolPreInvokePayload, GlobalContext - >>> payload = ToolPreInvokePayload("calculator", {}) + >>> payload = ToolPreInvokePayload(name="calculator", args={}) >>> cond = PluginCondition(tools={"calculator"}) - >>> ctx = GlobalContext("req1") + >>> ctx = GlobalContext(request_id="req1") >>> pre_tool_matches(payload, [cond], ctx) True - >>> payload2 = ToolPreInvokePayload("other", {}) + >>> payload2 = ToolPreInvokePayload(name="other", args={}) >>> pre_tool_matches(payload2, [cond], ctx) False """ @@ -225,12 +225,12 @@ def post_tool_matches(payload: ToolPostInvokePayload, conditions: list[PluginCon Examples: >>> from mcpgateway.plugins.framework.models import PluginCondition >>> from mcpgateway.plugins.framework.models import ToolPostInvokePayload, GlobalContext - >>> payload = ToolPostInvokePayload("calculator", {"result": 8}) + >>> payload = ToolPostInvokePayload(name="calculator", result={"result": 8}) >>> cond = PluginCondition(tools={"calculator"}) - >>> ctx = GlobalContext("req1") + >>> ctx = GlobalContext(request_id="req1") >>> post_tool_matches(payload, [cond], ctx) True - >>> payload2 = ToolPostInvokePayload("other", {"result": 8}) + >>> payload2 = ToolPostInvokePayload(name="other", result={"result": 8}) >>> post_tool_matches(payload2, [cond], ctx) False """ @@ -262,12 +262,12 @@ def pre_resource_matches(payload: ResourcePreFetchPayload, conditions: list[Plug Examples: >>> from mcpgateway.plugins.framework.models import PluginCondition >>> from mcpgateway.plugins.framework.models import ResourcePreFetchPayload, GlobalContext - >>> payload = ResourcePreFetchPayload("file:///data.txt") + >>> payload = ResourcePreFetchPayload(uri="file:///data.txt") >>> cond = PluginCondition(resources={"file:///data.txt"}) - >>> ctx = GlobalContext("req1") + >>> ctx = GlobalContext(request_id="req1") >>> pre_resource_matches(payload, [cond], ctx) True - >>> payload2 = ResourcePreFetchPayload("http://api/other") + >>> payload2 = ResourcePreFetchPayload(uri="http://api/other") >>> pre_resource_matches(payload2, [cond], ctx) False """ @@ -301,12 +301,12 @@ def post_resource_matches(payload: ResourcePostFetchPayload, conditions: list[Pl >>> from mcpgateway.plugins.framework.models import ResourcePostFetchPayload, GlobalContext >>> from mcpgateway.models import ResourceContent >>> content = ResourceContent(type="resource", uri="file:///data.txt", text="Test") - >>> payload = ResourcePostFetchPayload("file:///data.txt", content) + >>> payload = ResourcePostFetchPayload(uri="file:///data.txt", content=content) >>> cond = PluginCondition(resources={"file:///data.txt"}) - >>> ctx = GlobalContext("req1") + >>> ctx = GlobalContext(request_id="req1") >>> post_resource_matches(payload, [cond], ctx) True - >>> payload2 = ResourcePostFetchPayload("http://api/other", content) + >>> payload2 = ResourcePostFetchPayload(uri="http://api/other", content=content) >>> post_resource_matches(payload2, [cond], ctx) False """ diff --git a/mcpgateway/services/tool_service.py b/mcpgateway/services/tool_service.py index be04fe1d9..79b9fa284 100644 --- a/mcpgateway/services/tool_service.py +++ b/mcpgateway/services/tool_service.py @@ -679,7 +679,8 @@ async def invoke_tool(self, db: Session, name: str, arguments: Dict[str, Any], r context_table = None request_id = uuid.uuid4().hex # Use gateway_id if available, otherwise use a generic server identifier - server_id = getattr(tool, "gateway_id", None) or "unknown" + gateway_id = getattr(tool, "gateway_id", "unknown") + server_id = gateway_id if isinstance(gateway_id, str) else "unknown" global_context = GlobalContext(request_id=request_id, server_id=server_id, tenant_id=None) if self._plugin_manager: diff --git a/tests/unit/mcpgateway/plugins/__init__.py b/tests/unit/mcpgateway/plugins/__init__.py new file mode 100644 index 000000000..63f957fa7 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +""" \ No newline at end of file diff --git a/tests/unit/mcpgateway/plugins/framework/__init__.py b/tests/unit/mcpgateway/plugins/framework/__init__.py new file mode 100644 index 000000000..63f957fa7 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +""" \ No newline at end of file diff --git a/tests/unit/mcpgateway/plugins/framework/external/__init__.py b/tests/unit/mcpgateway/plugins/framework/external/__init__.py new file mode 100644 index 000000000..63f957fa7 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/external/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +""" \ No newline at end of file diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/__init__.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/__init__.py new file mode 100644 index 000000000..63f957fa7 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +""" \ No newline at end of file diff --git a/tests/unit/mcpgateway/plugins/framework/loader/__init__.py b/tests/unit/mcpgateway/plugins/framework/loader/__init__.py new file mode 100644 index 000000000..63f957fa7 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/loader/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +""" \ No newline at end of file diff --git a/tests/unit/mcpgateway/plugins/plugins/__init__.py b/tests/unit/mcpgateway/plugins/plugins/__init__.py new file mode 100644 index 000000000..63f957fa7 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/plugins/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +""" \ No newline at end of file diff --git a/tests/unit/mcpgateway/plugins/plugins/pii_filter/__init__.py b/tests/unit/mcpgateway/plugins/plugins/pii_filter/__init__.py new file mode 100644 index 000000000..63f957fa7 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/plugins/pii_filter/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +""" \ No newline at end of file diff --git a/tests/unit/mcpgateway/plugins/plugins/resource_filter/__init__.py b/tests/unit/mcpgateway/plugins/plugins/resource_filter/__init__.py new file mode 100644 index 000000000..63f957fa7 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/plugins/resource_filter/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +""" \ No newline at end of file diff --git a/tests/unit/mcpgateway/utils/__init__.py b/tests/unit/mcpgateway/utils/__init__.py new file mode 100644 index 000000000..63f957fa7 --- /dev/null +++ b/tests/unit/mcpgateway/utils/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +""" \ No newline at end of file From 86ee2f0d54eb49e52a47b7858e6fc302e8ffbaf7 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 17 Aug 2025 18:53:26 -0400 Subject: [PATCH 33/65] refactor: external plugin server and plugin external API Signed-off-by: Frederico Araujo --- copier.yml | 4 +- mcpgateway/config.py | 4 +- mcpgateway/main.py | 2 +- mcpgateway/plugins/__init__.py | 12 -- mcpgateway/plugins/framework/__init__.py | 65 ++++++- mcpgateway/plugins/framework/base.py | 8 +- .../framework/external/mcp/server/__init__.py | 14 ++ .../mcp/{server.py => server/runtime.py} | 117 +++--------- .../framework/external/mcp/server/server.py | 167 ++++++++++++++++++ mcpgateway/plugins/tools/__init__.py | 7 +- mcpgateway/plugins/tools/cli.py | 96 ++++++---- mcpgateway/services/prompt_service.py | 2 +- mcpgateway/services/resource_service.py | 3 +- mcpgateway/services/tool_service.py | 2 +- mcpgateway/validators.py | 4 +- plugin_templates/external/run-server.sh | 2 +- .../plugin.py.jinja | 4 +- plugins/deny_filter/deny.py | 7 +- plugins/install.yaml | 3 + plugins/pii_filter/pii_filter.py | 7 +- plugins/regex_filter/search_replace.py | 4 +- plugins/resource_filter/resource_filter.py | 5 +- .../configs/valid_stdio_external_plugin.yaml | 2 +- ...valid_stdio_external_plugin_overrides.yaml | 2 +- .../valid_stdio_external_plugin_regex.yaml | 2 +- 25 files changed, 365 insertions(+), 180 deletions(-) create mode 100644 mcpgateway/plugins/framework/external/mcp/server/__init__.py rename mcpgateway/plugins/framework/external/mcp/{server.py => server/runtime.py} (60%) create mode 100644 mcpgateway/plugins/framework/external/mcp/server/server.py create mode 100644 plugins/install.yaml diff --git a/copier.yml b/copier.yml index 7f9a7148d..aea09c8e3 100644 --- a/copier.yml +++ b/copier.yml @@ -9,7 +9,8 @@ template_type: plugin_name: type: str - help: What is the plugin name? (e.g., MyFilter) + help: What is the plugin name? + default: MyFilter version: type: str @@ -29,3 +30,4 @@ email: description: type: str help: Write a plugin description + default: A filter plugin diff --git a/mcpgateway/config.py b/mcpgateway/config.py index 2a5865ba6..4629b3cdd 100644 --- a/mcpgateway/config.py +++ b/mcpgateway/config.py @@ -663,7 +663,9 @@ def validate_database(self) -> None: db_dir.mkdir(parents=True) # Validation patterns for safe display (configurable) - validation_dangerous_html_pattern: str = r"<(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)\b|" + validation_dangerous_html_pattern: str = ( + r"<(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)\b|" + ) validation_dangerous_js_pattern: str = r"(?i)(?:^|\s|[\"'`<>=])(javascript:|vbscript:|data:\s*[^,]*[;\s]*(javascript|vbscript)|\bon[a-z]+\s*=|<\s*script\b)" diff --git a/mcpgateway/main.py b/mcpgateway/main.py index b37c540c6..cd0277564 100644 --- a/mcpgateway/main.py +++ b/mcpgateway/main.py @@ -63,7 +63,7 @@ from mcpgateway.middleware.security_headers import SecurityHeadersMiddleware from mcpgateway.models import InitializeResult, ListResourceTemplatesResult, LogLevel, ResourceContent, Root from mcpgateway.observability import init_telemetry -from mcpgateway.plugins import PluginManager, PluginViolationError +from mcpgateway.plugins.framework import PluginManager, PluginViolationError from mcpgateway.routers.well_known import router as well_known_router from mcpgateway.schemas import ( GatewayCreate, diff --git a/mcpgateway/plugins/__init__.py b/mcpgateway/plugins/__init__.py index 7a5234fdb..994b3d0b3 100644 --- a/mcpgateway/plugins/__init__.py +++ b/mcpgateway/plugins/__init__.py @@ -4,16 +4,4 @@ Copyright 2025 SPDX-License-Identifier: Apache-2.0 Authors: Fred Araujo - -Exposes core MCP Gateway plugin components: -- Context -- Manager -- Payloads -- Models """ - -from mcpgateway.plugins.framework.manager import PluginManager -from mcpgateway.plugins.framework.models import GlobalContext, PluginViolation, PromptPosthookPayload, PromptPrehookPayload, ToolPostInvokePayload, ToolPreInvokePayload -from mcpgateway.plugins.framework.errors import PluginViolationError, PluginError - -__all__ = ["GlobalContext", "PluginError", "PluginManager", "PluginViolation", "PluginViolationError", "PromptPosthookPayload", "PromptPrehookPayload", "ToolPostInvokePayload", "ToolPreInvokePayload"] diff --git a/mcpgateway/plugins/framework/__init__.py b/mcpgateway/plugins/framework/__init__.py index 5726b6d21..e521d8af2 100644 --- a/mcpgateway/plugins/framework/__init__.py +++ b/mcpgateway/plugins/framework/__init__.py @@ -1,19 +1,72 @@ # -*- coding: utf-8 -*- -"""Plugin framework package. +"""Services Package. Copyright 2025 SPDX-License-Identifier: Apache-2.0 Authors: Fred Araujo -Exposes plugin framework components: -- Plugin (base class) -- PluginManager (loader) +Exposes core MCP Gateway plugin components: +- Context +- Manager +- Payloads - Models +- ExternalPluginServer """ +# First-Party from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.external.mcp.server import run_plugin_mcp_server +from mcpgateway.plugins.framework.errors import PluginError, PluginViolationError +from mcpgateway.plugins.framework.external.mcp.server import ExternalPluginServer from mcpgateway.plugins.framework.loader.config import ConfigLoader from mcpgateway.plugins.framework.manager import PluginManager +from mcpgateway.plugins.framework.models import ( + GlobalContext, + HookType, + PluginConfig, + PluginContext, + PluginErrorModel, + PluginMode, + PluginResult, + PluginViolation, + PromptPosthookPayload, + PromptPosthookResult, + PromptPrehookPayload, + PromptPrehookResult, + ResourcePostFetchPayload, + ResourcePostFetchResult, + ResourcePreFetchPayload, + ResourcePreFetchResult, + ToolPostInvokePayload, + ToolPostInvokeResult, + ToolPreInvokePayload, + ToolPreInvokeResult, +) -__all__ = ["ConfigLoader", "Plugin", "PluginManager", "run_plugin_mcp_server"] +__all__ = [ + "ConfigLoader", + "ExternalPluginServer", + "GlobalContext", + "HookType", + "Plugin", + "PluginConfig", + "PluginContext", + "PluginError", + "PluginErrorModel", + "PluginManager", + "PluginMode", + "PluginResult", + "PluginViolation", + "PluginViolationError", + "PromptPosthookPayload", + "PromptPosthookResult", + "PromptPrehookPayload", + "PromptPrehookResult", + "ResourcePostFetchPayload", + "ResourcePostFetchResult", + "ResourcePreFetchPayload", + "ResourcePreFetchResult", + "ToolPostInvokePayload", + "ToolPostInvokeResult", + "ToolPreInvokePayload", + "ToolPreInvokeResult", +] diff --git a/mcpgateway/plugins/framework/base.py b/mcpgateway/plugins/framework/base.py index b0a05740d..b44ae8883 100644 --- a/mcpgateway/plugins/framework/base.py +++ b/mcpgateway/plugins/framework/base.py @@ -42,12 +42,12 @@ class Plugin: """Base plugin object for pre/post processing of inputs and outputs at various locations throughout the server. Examples: - >>> from mcpgateway.plugins.framework.models import PluginConfig, HookType, PluginMode + >>> from mcpgateway.plugins.framework import PluginConfig, HookType, PluginMode >>> config = PluginConfig( ... name="test_plugin", ... description="Test plugin", ... author="test", - ... kind="mcpgateway.plugins.framework.base.Plugin", + ... kind="mcpgateway.plugins.framework.Plugin", ... version="1.0.0", ... hooks=[HookType.PROMPT_PRE_FETCH], ... tags=["test"], @@ -72,7 +72,7 @@ def __init__(self, config: PluginConfig) -> None: config: The plugin configuration Examples: - >>> from mcpgateway.plugins.framework.models import PluginConfig, HookType + >>> from mcpgateway.plugins.framework import PluginConfig, HookType >>> config = PluginConfig( ... name="simple_plugin", ... description="Simple test", @@ -258,7 +258,7 @@ class PluginRef: """Plugin reference which contains a uuid. Examples: - >>> from mcpgateway.plugins.framework.models import PluginConfig, HookType, PluginMode + >>> from mcpgateway.plugins.framework import PluginConfig, HookType, PluginMode >>> config = PluginConfig( ... name="ref_test", ... description="Reference test", diff --git a/mcpgateway/plugins/framework/external/mcp/server/__init__.py b/mcpgateway/plugins/framework/external/mcp/server/__init__.py new file mode 100644 index 000000000..ffaadab79 --- /dev/null +++ b/mcpgateway/plugins/framework/external/mcp/server/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +"""External plugins package. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Fred Araujo + +Exposes external plugin components: +- server +""" + +from mcpgateway.plugins.framework.external.mcp.server.server import ExternalPluginServer + +__all__ = ["ExternalPluginServer"] diff --git a/mcpgateway/plugins/framework/external/mcp/server.py b/mcpgateway/plugins/framework/external/mcp/server/runtime.py similarity index 60% rename from mcpgateway/plugins/framework/external/mcp/server.py rename to mcpgateway/plugins/framework/external/mcp/server/runtime.py index 827ec7c46..2d87abd94 100644 --- a/mcpgateway/plugins/framework/external/mcp/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server/runtime.py @@ -1,34 +1,17 @@ -# -*- coding: utf-8 -*- -"""Plugin MCP Server. - -Copyright 2025 -SPDX-License-Identifier: Apache-2.0 -Authors: Teryl Taylor - Fred Araujo - -Module that contains plugin MCP server code to serve external plugins. -""" - # Standard import asyncio import logging -import os -from typing import Any, Callable, Dict, Type, TypeVar +from typing import Any, Dict # Third-Party from chuk_mcp_runtime.common.mcp_tool_decorator import mcp_tool from chuk_mcp_runtime.entry import main_async -from pydantic import BaseModel # First-Party -from mcpgateway.plugins.framework import Plugin -from mcpgateway.plugins.framework.errors import convert_exception_to_error -from mcpgateway.plugins.framework.loader.config import ConfigLoader -from mcpgateway.plugins.framework.manager import DEFAULT_PLUGIN_TIMEOUT, PluginManager -from mcpgateway.plugins.framework.models import ( +from mcpgateway.plugins.framework import ( + ExternalPluginServer, + Plugin, PluginContext, - PluginErrorModel, - PluginResult, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, @@ -43,23 +26,9 @@ ToolPreInvokeResult, ) -P = TypeVar("P", bound=BaseModel) - logger = logging.getLogger(__name__) -config_file = os.environ.get("PLUGINS_CONFIG_PATH", os.path.join(".", "resources", "plugins", "config.yaml")) -global_plugin_manager = None - - -async def initialize() -> None: - """Initialize the plugin manager with configured plugins.""" - try: - global global_plugin_manager - global_plugin_manager = PluginManager(config_file) - await global_plugin_manager.initialize() - except Exception: - logger.exception("Could not initialize external plugin server.") - raise +server = None @mcp_tool(name="get_plugin_configs", description="Get the plugin configurations installed on the server") @@ -69,11 +38,7 @@ async def get_plugin_configs() -> list[dict]: Returns: A list of plugin configurations. """ - config = ConfigLoader.load_config(config_file, use_jinja=False) - plugins: list[dict] = [] - for plug in config.plugins: - plugins.append(plug.model_dump()) - return plugins + return await server.get_plugin_configs() @mcp_tool(name="get_plugin_config", description="Get the plugin configuration installed on the server given a plugin name") @@ -86,45 +51,7 @@ async def get_plugin_config(name: str) -> dict: Returns: A list of plugin configurations. """ - config = ConfigLoader.load_config(config_file, use_jinja=False) - for plug in config.plugins: - if plug.name.lower() == name.lower(): - return plug.model_dump() - return None - - -async def _invoke_hook( - payload_model: Type[P], hook_function: Callable[[Plugin], Callable[[P, PluginContext], PluginResult]], plugin_name: str, payload: Dict[str, Any], context: Dict[str, Any] -) -> dict: - """Invoke a plugin hook. - - Args: - payload_model: The type of the payload accepted for the hook. - hook_function: The hook function to be invoked. - plugin_name: The name of the plugin to execute. - payload: The prompt name and arguments to be analyzed. - context: The contextual and state information required for the execution of the hook. - - Raises: - ValueError: If unable to retrieve a plugin. - - Returns: - The transformed or filtered response from the plugin hook. - """ - plugin_timeout = global_plugin_manager.config.plugin_settings.plugin_timeout if global_plugin_manager.config else DEFAULT_PLUGIN_TIMEOUT - plugin = global_plugin_manager.get_plugin(plugin_name) - try: - if plugin: - _payload = payload_model.model_validate(payload) - _context = PluginContext.model_validate(context) - result = await asyncio.wait_for(hook_function(plugin, _payload, _context), plugin_timeout) - return result.model_dump() - raise ValueError(f"Unable to retrieve plugin {plugin_name} to execute.") - except asyncio.TimeoutError: - return PluginErrorModel(message=f"Plugin {plugin_name} timed out from execution after {plugin_timeout} seconds.", plugin_name=plugin_name).model_dump() - except Exception as ex: - logger.exception(ex) - return convert_exception_to_error(ex, plugin_name=plugin_name).model_dump() + return await server.get_plugin_config(name) @mcp_tool(name="prompt_pre_fetch", description="Execute prompt prefetch hook for a plugin") @@ -146,7 +73,7 @@ async def prompt_pre_fetch(plugin_name: str, payload: Dict[str, Any], context: D def prompt_pre_fetch_func(plugin: Plugin, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: return plugin.prompt_pre_fetch(payload, context) - return await _invoke_hook(PromptPrehookPayload, prompt_pre_fetch_func, plugin_name, payload, context) + return await server.invoke_hook(PromptPrehookPayload, prompt_pre_fetch_func, plugin_name, payload, context) @mcp_tool(name="prompt_post_fetch", description="Execute prompt postfetch hook for a plugin") @@ -168,7 +95,7 @@ async def prompt_post_fetch(plugin_name: str, payload: Dict[str, Any], context: def prompt_post_fetch_func(plugin: Plugin, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult: return plugin.prompt_post_fetch(payload, context) - return await _invoke_hook(PromptPosthookPayload, prompt_post_fetch_func, plugin_name, payload, context) + return await server.invoke_hook(PromptPosthookPayload, prompt_post_fetch_func, plugin_name, payload, context) @mcp_tool(name="tool_pre_invoke", description="Execute tool pre-invoke hook for a plugin") @@ -190,7 +117,7 @@ async def tool_pre_invoke(plugin_name: str, payload: Dict[str, Any], context: Di def tool_pre_invoke_func(plugin: Plugin, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: return plugin.tool_pre_invoke(payload, context) - return await _invoke_hook(ToolPreInvokePayload, tool_pre_invoke_func, plugin_name, payload, context) + return await server.invoke_hook(ToolPreInvokePayload, tool_pre_invoke_func, plugin_name, payload, context) @mcp_tool(name="tool_post_invoke", description="Execute tool post-invoke hook for a plugin") @@ -212,7 +139,7 @@ async def tool_post_invoke(plugin_name: str, payload: Dict[str, Any], context: D def tool_post_invoke_func(plugin: Plugin, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: return plugin.tool_post_invoke(payload, context) - return await _invoke_hook(ToolPostInvokePayload, tool_post_invoke_func, plugin_name, payload, context) + return await server.invoke_hook(ToolPostInvokePayload, tool_post_invoke_func, plugin_name, payload, context) @mcp_tool(name="resource_pre_fetch", description="Execute resource prefetch hook for a plugin") @@ -234,7 +161,7 @@ async def resource_pre_fetch(plugin_name: str, payload: Dict[str, Any], context: def resource_pre_fetch_func(plugin: Plugin, payload: ResourcePreFetchPayload, context: PluginContext) -> ResourcePreFetchResult: return plugin.resource_pre_fetch(payload, context) - return await _invoke_hook(ResourcePreFetchPayload, resource_pre_fetch_func, plugin_name, payload, context) + return await server.invoke_hook(ResourcePreFetchPayload, resource_pre_fetch_func, plugin_name, payload, context) @mcp_tool(name="resource_post_fetch", description="Execute resource postfetch hook for a plugin") @@ -256,15 +183,23 @@ async def resource_post_fetch(plugin_name: str, payload: Dict[str, Any], context def resource_post_fetch_func(plugin: Plugin, payload: ResourcePostFetchPayload, context: PluginContext) -> ResourcePostFetchResult: return plugin.resource_post_fetch(payload, context) - return await _invoke_hook(ResourcePostFetchPayload, resource_post_fetch_func, plugin_name, payload, context) + return await server.invoke_hook(ResourcePostFetchPayload, resource_post_fetch_func, plugin_name, payload, context) -async def run_plugin_mcp_server(): - """Initialize plugin manager and run mcp server.""" - await initialize() - await main_async() +async def run(): + """Run the external plugin server.""" + global server + server = ExternalPluginServer() + if await server.initialize(): + try: + await main_async() + except Exception: + logger.exception("Caught error while executing plugin server") + raise + finally: + await server.shutdown() if __name__ == "__main__": # pragma: no cover - executed only when run directly # launch - asyncio.run(run_plugin_mcp_server()) + asyncio.run(run()) diff --git a/mcpgateway/plugins/framework/external/mcp/server/server.py b/mcpgateway/plugins/framework/external/mcp/server/server.py new file mode 100644 index 000000000..329195b3b --- /dev/null +++ b/mcpgateway/plugins/framework/external/mcp/server/server.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +"""Plugin MCP Server. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + Fred Araujo + +Module that contains plugin MCP server code to serve external plugins. +""" + +# Standard +import asyncio +import logging +import os +from typing import Any, Callable, Dict, Type, TypeVar + +# Third-Party +from pydantic import BaseModel + +# First-Party +from mcpgateway.plugins.framework import Plugin +from mcpgateway.plugins.framework.errors import convert_exception_to_error +from mcpgateway.plugins.framework.loader.config import ConfigLoader +from mcpgateway.plugins.framework.manager import DEFAULT_PLUGIN_TIMEOUT, PluginManager +from mcpgateway.plugins.framework.models import ( + PluginContext, + PluginErrorModel, + PluginResult, +) + +P = TypeVar("P", bound=BaseModel) + +logger = logging.getLogger(__name__) + +# config_file = os.environ.get("PLUGINS_CONFIG_PATH", os.path.join(".", "resources", "plugins", "config.yaml")) +# global_plugin_manager = None + + +class ExternalPluginServer: + + def __init__(self, config_path: str | None = None) -> None: + """Create an external plugin server. + + Args: + config_path: The configuration file path for loading plugins. + If set, this attribute overrides the value in PLUGINS_CONFIG_PATH. + + Returns: + An external plugin server instance. + + Examples: + >>> server = ExternalPluginServer(config_path="./tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") + >>> server is not None + True + """ + self._config_path = config_path or os.environ.get("PLUGINS_CONFIG_PATH", os.path.join(".", "resources", "plugins", "config.yaml")) + self._config = ConfigLoader.load_config(self._config_path, use_jinja=False) + self._plugin_manager = PluginManager(self._config_path) + + async def get_plugin_configs(self) -> list[dict]: + """Return a list of plugin configurations for plugins currently installed on the MCP server. + + Returns: + A list of plugin configurations. + + Examples: + >>> import asyncio + >>> server = ExternalPluginServer(config_path="./tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") + >>> plugins = asyncio.run(server.get_plugin_configs()) + >>> len(plugins) > 0 + True + """ + plugins: list[dict] = [] + for plug in self._config.plugins: + plugins.append(plug.model_dump()) + return plugins + + async def get_plugin_config(self, name: str) -> dict: + """Return a plugin configuration give a plugin name. + + Args: + name: The name of the plugin of which to return the plugin configuration. + + Returns: + A list of plugin configurations. + + Examples: + >>> import asyncio + >>> server = ExternalPluginServer(config_path="./tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") + >>> c = asyncio.run(server.get_plugin_config(name = "DenyListPlugin")) + >>> c is not None + True + >>> c["name"] == "DenyListPlugin" + True + """ + for plug in self._config.plugins: + if plug.name.lower() == name.lower(): + return plug.model_dump() + return None + + async def invoke_hook( + self, payload_model: Type[P], hook_function: Callable[[Plugin], Callable[[P, PluginContext], PluginResult]], plugin_name: str, payload: Dict[str, Any], context: Dict[str, Any] + ) -> dict: + """Invoke a plugin hook. + + Args: + payload_model: The type of the payload accepted for the hook. + hook_function: The hook function to be invoked. + plugin_name: The name of the plugin to execute. + payload: The prompt name and arguments to be analyzed. + context: The contextual and state information required for the execution of the hook. + + Raises: + ValueError: If unable to retrieve a plugin. + + Returns: + The transformed or filtered response from the plugin hook. + + Examples: + >>> import asyncio + >>> import os + >>> os.environ["PYTHONPATH"] = "." + >>> from mcpgateway.plugins.framework import PromptPrehookPayload, PluginContext, PromptPrehookResult + >>> server = ExternalPluginServer(config_path="./tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") + >>> def prompt_pre_fetch_func(plugin: Plugin, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: + ... return plugin.prompt_pre_fetch(payload, context) + >>> payload = PromptPrehookPayload(name="test_prompt", args={"user": "This is so innovative"}) + >>> context = PluginContext(request_id="1", server_id="2") + >>> initialized = asyncio.run(server.initialize()) + >>> initialized + True + >>> result = asyncio.run(server.invoke_hook(PromptPrehookPayload, prompt_pre_fetch_func, "DenyListPlugin", payload.model_dump(), context.model_dump())) + >>> result is not None + True + >>> result["continue_processing"] + False + """ + global_plugin_manager = PluginManager() + plugin_timeout = global_plugin_manager.config.plugin_settings.plugin_timeout if global_plugin_manager.config else DEFAULT_PLUGIN_TIMEOUT + plugin = global_plugin_manager.get_plugin(plugin_name) + try: + if plugin: + _payload = payload_model.model_validate(payload) + _context = PluginContext.model_validate(context) + result = await asyncio.wait_for(hook_function(plugin, _payload, _context), plugin_timeout) + return result.model_dump() + raise ValueError(f"Unable to retrieve plugin {plugin_name} to execute.") + except asyncio.TimeoutError: + return PluginErrorModel(message=f"Plugin {plugin_name} timed out from execution after {plugin_timeout} seconds.", plugin_name=plugin_name).model_dump() + except Exception as ex: + logger.exception(ex) + return convert_exception_to_error(ex, plugin_name=plugin_name).model_dump() + + async def initialize(self) -> bool: + await self._plugin_manager.initialize() + return self._plugin_manager.initialized + + async def shutdown(self) -> None: + if self._plugin_manager.initialized: + await self._plugin_manager.shutdown() + + +if __name__ == "__main__": # pragma: no cover - executed only when run directly + # launch + server = ExternalPluginServer() + asyncio.run(server.run()) diff --git a/mcpgateway/plugins/tools/__init__.py b/mcpgateway/plugins/tools/__init__.py index a88d13091..951339f8a 100644 --- a/mcpgateway/plugins/tools/__init__.py +++ b/mcpgateway/plugins/tools/__init__.py @@ -1,12 +1,7 @@ # -*- coding: utf-8 -*- -"""Validation Package. +"""Plugin tools package. Copyright 2025 SPDX-License-Identifier: Apache-2.0 Authors: Fred Araujo - -Provides CLI tools for the MCP Gateway plugin system, including: -- bootstrap -- install -- package """ diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index 99498c04e..47aec052e 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -26,7 +26,6 @@ """ # Standard -import os from pathlib import Path import subprocess from typing import Optional @@ -35,11 +34,9 @@ from copier import Worker import typer from typing_extensions import Annotated -import yaml # First-Party from mcpgateway.config import settings -from mcpgateway.plugins.tools.models import InstallManifest # --------------------------------------------------------------------------- # Configuration defaults @@ -79,6 +76,11 @@ def git_user_name() -> str: Returns: The git user name configured in the user's environment. + + Examples: + >>> user_name = git_user_name() + >>> isinstance(user_name, str) + True """ try: res = subprocess.run(["git", "config", "user.name"], stdout=subprocess.PIPE) @@ -92,6 +94,11 @@ def git_user_email() -> str: Returns: The git user email configured in the user's environment. + + Examples: + >>> user_name = git_user_email() + >>> isinstance(user_name, str) + True """ try: res = subprocess.run(["git", "config", "user.email"], stdout=subprocess.PIPE) @@ -111,6 +118,15 @@ def bootstrap( answers_file: Optional[Annotated[typer.FileText, typer.Option("--answers_file", "-a", help="The answers file to be used for bootstrapping.")]] = None, defaults: Annotated[bool, typer.Option("--defaults", help="Bootstrap with defaults.")] = False, ): + """Boostrap a new plugin project from a template. + + Args: + destination: The directory in which to bootstrap the plugin project. + template_url: The URL to the plugins copier template. + vcs_ref: The version control system tag/branch/commit to use for the template. + answers-file: The copier answers file that can be used to skip interactive mode. + defaults: Bootstrap with defaults. + """ with Worker( src_path=template_url, dst_path=destination, @@ -122,39 +138,53 @@ def bootstrap( worker.run_copy() -@app.command(help="Installs plugins into a Python environment.") -def install( - install_manifest: Annotated[typer.FileText, typer.Option("--install_manifest", "-i", help="The install manifest describing which plugins to install.")] = DEFAULT_INSTALL_MANIFEST, - installer: Annotated[str, typer.Option("--installer", "-c", help="The install command to install plugins.")] = DEFAULT_INSTALLER, -): - typer.echo(f"Installing plugin packages from {install_manifest.name}") - data = yaml.safe_load(install_manifest) - manifest = InstallManifest.model_validate(data) - for pkg in manifest.packages: - typer.echo(f"Installing plugin package {pkg.package} from {pkg.repository}") - repository = os.path.expandvars(pkg.repository) - cmd = installer.split(" ") - if pkg.extras: - cmd.append(f"{pkg.package}[{','.join(pkg.extras)}]@{repository}") - else: - cmd.append(f"{pkg.package}@{repository}") - subprocess.run(cmd) - - -@app.command(help="Builds an MCP server to serve plugins as tools") -def package( - image_tag: Annotated[str, typer.Option("--image_tag", "-t", help="The container image tag to generated container.")] = DEFAULT_IMAGE_TAG, - containerfile: Annotated[Path, typer.Option("--containerfile", "-c", help="The Dockerfile used to build the container.")] = DEFAULT_CONTAINERFILE_PATH, - builder: Annotated[str, typer.Option("--builder", "-b", help="The container builder, compatible with docker build.")] = DEFAULT_IMAGE_BUILDER, - build_context: Annotated[Path, typer.Option("--build_context", "-p", help="The container builder context, specified as a path.")] = DEFAULT_BUILD_CONTEXT, -): - typer.echo("Building MCP server image") - cmd = builder.split(" ") - cmd.extend(["-f", containerfile, "-t", image_tag, build_context]) - subprocess.run(cmd) +# @app.command(help="Installs plugins into a Python environment.") +# def install( +# install_manifest: Annotated[typer.FileText, typer.Option("--install_manifest", "-i", help="The install manifest describing which plugins to install.")] = DEFAULT_INSTALL_MANIFEST, +# installer: Annotated[str, typer.Option("--installer", "-c", help="The install command to install plugins.")] = DEFAULT_INSTALLER, +# ): +# typer.echo(f"Installing plugin packages from {install_manifest.name}") +# data = yaml.safe_load(install_manifest) +# manifest = InstallManifest.model_validate(data) +# for pkg in manifest.packages: +# typer.echo(f"Installing plugin package {pkg.package} from {pkg.repository}") +# repository = os.path.expandvars(pkg.repository) +# cmd = installer.split(" ") +# if pkg.extras: +# cmd.append(f"{pkg.package}[{','.join(pkg.extras)}]@{repository}") +# else: +# cmd.append(f"{pkg.package}@{repository}") +# subprocess.run(cmd) + + +# @app.command(help="Builds an MCP server to serve plugins as tools") +# def package( +# image_tag: Annotated[str, typer.Option("--image_tag", "-t", help="The container image tag to generated container.")] = DEFAULT_IMAGE_TAG, +# containerfile: Annotated[Path, typer.Option("--containerfile", "-c", help="The Dockerfile used to build the container.")] = DEFAULT_CONTAINERFILE_PATH, +# builder: Annotated[str, typer.Option("--builder", "-b", help="The container builder, compatible with docker build.")] = DEFAULT_IMAGE_BUILDER, +# build_context: Annotated[Path, typer.Option("--build_context", "-p", help="The container builder context, specified as a path.")] = DEFAULT_BUILD_CONTEXT, +# ): +# typer.echo("Building MCP server image") +# cmd = builder.split(" ") +# cmd.extend(["-f", containerfile, "-t", image_tag, build_context]) +# subprocess.run(cmd) def main() -> None: # noqa: D401 - imperative mood is fine here + """Entry point for the *mcpplugins* console script. + + Processes command line arguments, handles version requests, and forwards + all other arguments to Uvicorn with sensible defaults injected. + + Environment Variables: + PLUGINS_CLI_COMPLETION: Enable auto-completion for plugins CLI (default: false) + PLUGINS_CLI_MARKUP_MODE: Set markup mode for plugins CLI (default: rich) + Valid options: + rich: use rich markup + markdown: allow markdown in help strings + disabled: disable markup + If unset (commented out), uses "rich" if rich is detected, otherwise disables it. + """ app() diff --git a/mcpgateway/services/prompt_service.py b/mcpgateway/services/prompt_service.py index 9db0d2cbb..5bbdd90b1 100644 --- a/mcpgateway/services/prompt_service.py +++ b/mcpgateway/services/prompt_service.py @@ -34,7 +34,7 @@ from mcpgateway.db import PromptMetric, server_prompt_association from mcpgateway.models import Message, PromptResult, Role, TextContent from mcpgateway.observability import create_span -from mcpgateway.plugins import GlobalContext, PluginManager, PluginViolationError, PromptPosthookPayload, PromptPrehookPayload +from mcpgateway.plugins.framework import GlobalContext, PluginManager, PluginViolationError, PromptPosthookPayload, PromptPrehookPayload from mcpgateway.schemas import PromptCreate, PromptRead, PromptUpdate, TopPerformer from mcpgateway.services.logging_service import LoggingService from mcpgateway.utils.metrics_common import build_top_performers diff --git a/mcpgateway/services/resource_service.py b/mcpgateway/services/resource_service.py index d28ec8453..d3aa91173 100644 --- a/mcpgateway/services/resource_service.py +++ b/mcpgateway/services/resource_service.py @@ -54,8 +54,7 @@ # Plugin support imports (conditional) try: # First-Party - from mcpgateway.plugins.framework.manager import PluginManager - from mcpgateway.plugins.framework.models import GlobalContext, ResourcePostFetchPayload, ResourcePreFetchPayload + from mcpgateway.plugins.framework import GlobalContext, PluginManager, ResourcePostFetchPayload, ResourcePreFetchPayload PLUGINS_AVAILABLE = True except ImportError: diff --git a/mcpgateway/services/tool_service.py b/mcpgateway/services/tool_service.py index 79b9fa284..738c4a372 100644 --- a/mcpgateway/services/tool_service.py +++ b/mcpgateway/services/tool_service.py @@ -41,7 +41,7 @@ from mcpgateway.db import ToolMetric from mcpgateway.models import TextContent, ToolResult from mcpgateway.observability import create_span -from mcpgateway.plugins import GlobalContext, PluginManager, PluginViolationError, ToolPostInvokePayload, ToolPreInvokePayload +from mcpgateway.plugins.framework import GlobalContext, PluginManager, PluginViolationError, ToolPostInvokePayload, ToolPreInvokePayload from mcpgateway.schemas import ToolCreate, ToolRead, ToolUpdate, TopPerformer from mcpgateway.services.logging_service import LoggingService from mcpgateway.utils.create_slug import slugify diff --git a/mcpgateway/validators.py b/mcpgateway/validators.py index 1e9a84d57..344b62b47 100644 --- a/mcpgateway/validators.py +++ b/mcpgateway/validators.py @@ -63,7 +63,9 @@ class SecurityValidator: """Configurable validation with MCP-compliant limits""" # Configurable patterns (from settings) - DANGEROUS_HTML_PATTERN = settings.validation_dangerous_html_pattern # Default: '<(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)\b|' + DANGEROUS_HTML_PATTERN = ( + settings.validation_dangerous_html_pattern + ) # Default: '<(script|iframe|object|embed|link|meta|base|form|img|svg|video|audio|source|track|area|map|canvas|applet|frame|frameset|html|head|body|style)\b|' DANGEROUS_JS_PATTERN = settings.validation_dangerous_js_pattern # Default: javascript:|vbscript:|on\w+\s*=|data:.*script ALLOWED_URL_SCHEMES = settings.validation_allowed_url_schemes # Default: ["http://", "https://", "ws://", "wss://"] diff --git a/plugin_templates/external/run-server.sh b/plugin_templates/external/run-server.sh index 57959dea5..88c3ed4a1 100755 --- a/plugin_templates/external/run-server.sh +++ b/plugin_templates/external/run-server.sh @@ -24,7 +24,7 @@ set -euo pipefail # Determine the absolute path of the API server script #โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ if [[ -z "${API_SERVER_SCRIPT:-}" ]]; then - API_SERVER_SCRIPT="$(python -c 'import mcpgateway.plugins.framework.external.mcp.server as server; print(server.__file__)')" + API_SERVER_SCRIPT="$(python -c 'import mcpgateway.plugins.framework.external.mcp.server.runtime as server; print(server.__file__)')" echo "โœ“ API server script path auto-detected: ${API_SERVER_SCRIPT}" else echo "โœ“ Using provided API server script path: ${API_SERVER_SCRIPT}" diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin.py.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin.py.jinja index d42a2dca7..483008278 100644 --- a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin.py.jinja +++ b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin.py.jinja @@ -8,8 +8,8 @@ This module loads configurations for plugins. """ # First-Party -from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.models import ( +from mcpgateway.plugins.framework import ( + Plugin, PluginConfig, PluginContext, PromptPosthookPayload, diff --git a/plugins/deny_filter/deny.py b/plugins/deny_filter/deny.py index 550c31e29..c89aa0d69 100644 --- a/plugins/deny_filter/deny.py +++ b/plugins/deny_filter/deny.py @@ -7,15 +7,12 @@ This module loads configurations for plugins. """ -# Standard -import logging - # Third-Party from pydantic import BaseModel # First-Party -from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.models import ( +from mcpgateway.plugins.framework import ( + Plugin, PluginConfig, PluginContext, PluginViolation, diff --git a/plugins/install.yaml b/plugins/install.yaml new file mode 100644 index 000000000..eb96dde69 --- /dev/null +++ b/plugins/install.yaml @@ -0,0 +1,3 @@ +packages: + - package: myplugin + repository: https://github.com/araujof/my-plugin.git diff --git a/plugins/pii_filter/pii_filter.py b/plugins/pii_filter/pii_filter.py index 18f9014af..a7d7c1fc8 100644 --- a/plugins/pii_filter/pii_filter.py +++ b/plugins/pii_filter/pii_filter.py @@ -12,15 +12,14 @@ # Standard import re from enum import Enum -from typing import Any, Optional, Pattern, Dict, List, Tuple -import logging +from typing import Any, Pattern, Dict, List, Tuple # Third-Party from pydantic import BaseModel, Field # First-Party -from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.models import ( +from mcpgateway.plugins.framework import ( + Plugin, PluginConfig, PluginContext, PluginViolation, diff --git a/plugins/regex_filter/search_replace.py b/plugins/regex_filter/search_replace.py index 1fe885a64..12c34849f 100644 --- a/plugins/regex_filter/search_replace.py +++ b/plugins/regex_filter/search_replace.py @@ -14,8 +14,8 @@ from pydantic import BaseModel # First-Party -from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.models import ( +from mcpgateway.plugins.framework import ( + Plugin, PluginConfig, PluginContext, PromptPosthookPayload, diff --git a/plugins/resource_filter/resource_filter.py b/plugins/resource_filter/resource_filter.py index 0c598bcf8..7d118e78e 100644 --- a/plugins/resource_filter/resource_filter.py +++ b/plugins/resource_filter/resource_filter.py @@ -14,11 +14,10 @@ """ import re -from typing import Any, Optional from urllib.parse import urlparse -from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.models import ( +from mcpgateway.plugins.framework import ( + Plugin, PluginConfig, PluginContext, PluginMode, diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml index 500da605e..5a622d16d 100644 --- a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml @@ -5,7 +5,7 @@ plugins: kind: "external" mcp: proto: STDIO - script: mcpgateway/plugins/framework/external/mcp/server.py + script: mcpgateway/plugins/framework/external/mcp/server/runtime.py # Plugin directories to scan plugin_dirs: diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml index 294da3838..31cf4fe99 100644 --- a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml @@ -8,7 +8,7 @@ plugins: hooks: ["prompt_pre_fetch", "prompt_post_fetch"] mcp: proto: STDIO - script: mcpgateway/plugins/framework/external/mcp/server.py + script: mcpgateway/plugins/framework/external/mcp/server/runtime.py # Plugin directories to scan plugin_dirs: diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml index 375c5fb0b..200b75555 100644 --- a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml @@ -5,7 +5,7 @@ plugins: kind: "external" mcp: proto: STDIO - script: mcpgateway/plugins/framework/external/mcp/server.py + script: mcpgateway/plugins/framework/external/mcp/server/runtime.py # Plugin directories to scan plugin_dirs: From a8630c7c69a4c8ea2179ca689392396c684a1954 Mon Sep 17 00:00:00 2001 From: Teryl Taylor Date: Sun, 17 Aug 2025 18:08:40 -0600 Subject: [PATCH 34/65] docs(plugins): removed subpackages from examples Signed-off-by: Teryl Taylor --- mcpgateway/plugins/framework/__init__.py | 2 ++ mcpgateway/plugins/framework/base.py | 2 +- mcpgateway/plugins/framework/manager.py | 38 ++++++++++++------------ mcpgateway/plugins/framework/models.py | 2 +- mcpgateway/plugins/framework/registry.py | 3 +- mcpgateway/plugins/framework/utils.py | 18 ++++------- 6 files changed, 30 insertions(+), 35 deletions(-) diff --git a/mcpgateway/plugins/framework/__init__.py b/mcpgateway/plugins/framework/__init__.py index e521d8af2..df92f73c4 100644 --- a/mcpgateway/plugins/framework/__init__.py +++ b/mcpgateway/plugins/framework/__init__.py @@ -22,6 +22,7 @@ from mcpgateway.plugins.framework.models import ( GlobalContext, HookType, + PluginCondition, PluginConfig, PluginContext, PluginErrorModel, @@ -48,6 +49,7 @@ "GlobalContext", "HookType", "Plugin", + "PluginCondition", "PluginConfig", "PluginContext", "PluginError", diff --git a/mcpgateway/plugins/framework/base.py b/mcpgateway/plugins/framework/base.py index b44ae8883..9fe4bcc6b 100644 --- a/mcpgateway/plugins/framework/base.py +++ b/mcpgateway/plugins/framework/base.py @@ -291,7 +291,7 @@ def __init__(self, plugin: Plugin): plugin: The plugin to reference. Examples: - >>> from mcpgateway.plugins.framework.models import PluginConfig, HookType + >>> from mcpgateway.plugins.framework import PluginConfig, HookType >>> config = PluginConfig( ... name="plugin_ref", ... description="Test", diff --git a/mcpgateway/plugins/framework/manager.py b/mcpgateway/plugins/framework/manager.py index 193305243..08e33c99a 100644 --- a/mcpgateway/plugins/framework/manager.py +++ b/mcpgateway/plugins/framework/manager.py @@ -99,7 +99,7 @@ class PluginExecutor(Generic[T]): - Metadata aggregation from multiple plugins Examples: - >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload + >>> from mcpgateway.plugins.framework import PromptPrehookPayload >>> executor = PluginExecutor[PromptPrehookPayload]() >>> # In async context: >>> # result, contexts = await executor.execute( @@ -148,7 +148,7 @@ async def execute( Examples: >>> # Execute plugins with timeout protection - >>> from mcpgateway.plugins.framework.models import HookType + >>> from mcpgateway.plugins.framework import HookType >>> executor = PluginExecutor(timeout=30) >>> # Assuming you have a registry instance: >>> # plugins = registry.get_plugins_for_hook(HookType.PROMPT_PRE_FETCH) @@ -285,8 +285,8 @@ async def pre_prompt_fetch(plugin: PluginRef, payload: PromptPrehookPayload, con The result of the plugin execution. Examples: - >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.base import PluginRef + >>> from mcpgateway.plugins.framework import Plugin, PromptPrehookPayload, PluginContext, GlobalContext >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = PromptPrehookPayload(name="test", args={"key": "value"}) @@ -309,8 +309,8 @@ async def post_prompt_fetch(plugin: PluginRef, payload: PromptPosthookPayload, c The result of the plugin execution. Examples: - >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.models import PromptPosthookPayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.base import PluginRef + >>> from mcpgateway.plugins.framework import Plugin, PromptPosthookPayload, PluginContext, GlobalContext >>> from mcpgateway.models import PromptResult >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) @@ -335,8 +335,8 @@ async def pre_tool_invoke(plugin: PluginRef, payload: ToolPreInvokePayload, cont The result of the plugin execution. Examples: - >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.models import ToolPreInvokePayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.base import PluginRef + >>> from mcpgateway.plugins.framework import Plugin, ToolPreInvokePayload, PluginContext, GlobalContext >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = ToolPreInvokePayload(name="calculator", args={"operation": "add", "a": 5, "b": 3}) @@ -359,8 +359,8 @@ async def post_tool_invoke(plugin: PluginRef, payload: ToolPostInvokePayload, co The result of the plugin execution. Examples: - >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.models import ToolPostInvokePayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.base import PluginRef + >>> from mcpgateway.plugins.framework import Plugin, ToolPostInvokePayload, PluginContext, GlobalContext >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = ToolPostInvokePayload(name="calculator", result={"result": 8, "status": "success"}) @@ -383,8 +383,8 @@ async def pre_resource_fetch(plugin: PluginRef, payload: ResourcePreFetchPayload ResourcePreFetchResult with processing status. Examples: - >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.models import ResourcePreFetchPayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.base import PluginRef + >>> from mcpgateway.plugins.framework import Plugin, ResourcePreFetchPayload, PluginContext, GlobalContext >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) >>> payload = ResourcePreFetchPayload(uri="file:///data.txt", metadata={"cache": True}) @@ -407,8 +407,8 @@ async def post_resource_fetch(plugin: PluginRef, payload: ResourcePostFetchPaylo ResourcePostFetchResult with processing status. Examples: - >>> from mcpgateway.plugins.framework.base import Plugin, PluginRef - >>> from mcpgateway.plugins.framework.models import ResourcePostFetchPayload, PluginContext, GlobalContext + >>> from mcpgateway.plugins.framework.base import PluginRef + >>> from mcpgateway.plugins.framework import Plugin, ResourcePostFetchPayload, PluginContext, GlobalContext >>> from mcpgateway.models import ResourceContent >>> # Assuming you have a plugin instance: >>> # plugin_ref = PluginRef(my_plugin) @@ -444,7 +444,7 @@ class PluginManager: >>> # print(f"Loaded {manager.plugin_count} plugins") >>> >>> # Execute prompt hooks - >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload, GlobalContext + >>> from mcpgateway.plugins.framework import PromptPrehookPayload, GlobalContext >>> payload = PromptPrehookPayload(name="test", args={}) >>> context = GlobalContext(request_id="req-123") >>> # In async context: @@ -662,7 +662,7 @@ async def prompt_pre_fetch( >>> # In async context: >>> # await manager.initialize() >>> - >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload, GlobalContext + >>> from mcpgateway.plugins.framework import PromptPrehookPayload, GlobalContext >>> payload = PromptPrehookPayload( ... name="greeting", ... args={"user": "Alice"} @@ -714,7 +714,7 @@ async def prompt_post_fetch( Examples: >>> # Continuing from prompt_pre_fetch example >>> from mcpgateway.models import PromptResult, Message, TextContent, Role - >>> from mcpgateway.plugins.framework.models import PromptPosthookPayload, GlobalContext + >>> from mcpgateway.plugins.framework import PromptPosthookPayload, GlobalContext >>> >>> # Create a proper Message with TextContent >>> message = Message( @@ -779,7 +779,7 @@ async def tool_pre_invoke( >>> # In async context: >>> # await manager.initialize() >>> - >>> from mcpgateway.plugins.framework.models import ToolPreInvokePayload, GlobalContext + >>> from mcpgateway.plugins.framework import ToolPreInvokePayload, GlobalContext >>> payload = ToolPreInvokePayload( ... name="calculator", ... args={"operation": "add", "a": 5, "b": 3} @@ -830,7 +830,7 @@ async def tool_post_invoke( Examples: >>> # Continuing from tool_pre_invoke example - >>> from mcpgateway.plugins.framework.models import ToolPostInvokePayload, GlobalContext + >>> from mcpgateway.plugins.framework import ToolPostInvokePayload, GlobalContext >>> >>> post_payload = ToolPostInvokePayload( ... name="calculator", diff --git a/mcpgateway/plugins/framework/models.py b/mcpgateway/plugins/framework/models.py index 1403a24b2..03d09241b 100644 --- a/mcpgateway/plugins/framework/models.py +++ b/mcpgateway/plugins/framework/models.py @@ -548,7 +548,7 @@ class PluginResult(BaseModel, Generic[T]): True >>> result.metadata {} - >>> from mcpgateway.plugins.framework.models import PluginViolation + >>> from mcpgateway.plugins.framework import PluginViolation >>> violation = PluginViolation( ... reason="Test", description="Test desc", code="TEST", details={} ... ) diff --git a/mcpgateway/plugins/framework/registry.py b/mcpgateway/plugins/framework/registry.py index 9c5683056..6bf205f2c 100644 --- a/mcpgateway/plugins/framework/registry.py +++ b/mcpgateway/plugins/framework/registry.py @@ -25,8 +25,7 @@ class PluginInstanceRegistry: """Registry for managing loaded plugins. Examples: - >>> from mcpgateway.plugins.framework.base import Plugin - >>> from mcpgateway.plugins.framework.models import PluginConfig, HookType + >>> from mcpgateway.plugins.framework import Plugin, PluginConfig, HookType >>> registry = PluginInstanceRegistry() >>> config = PluginConfig( ... name="test", diff --git a/mcpgateway/plugins/framework/utils.py b/mcpgateway/plugins/framework/utils.py index 8dd93663e..6921455a0 100644 --- a/mcpgateway/plugins/framework/utils.py +++ b/mcpgateway/plugins/framework/utils.py @@ -83,8 +83,7 @@ def matches(condition: PluginCondition, context: GlobalContext) -> bool: True if the plugin matches criteria. Examples: - >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.models import GlobalContext + >>> from mcpgateway.plugins.framework import GlobalContext, PluginCondition >>> cond = PluginCondition(server_ids={"srv1", "srv2"}) >>> ctx = GlobalContext(request_id="req1", server_id="srv1") >>> matches(cond, ctx) @@ -124,8 +123,7 @@ def pre_prompt_matches(payload: PromptPrehookPayload, conditions: list[PluginCon True if the plugin matches criteria. Examples: - >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.models import PromptPrehookPayload, GlobalContext + >>> from mcpgateway.plugins.framework import PluginCondition, PromptPrehookPayload, GlobalContext >>> payload = PromptPrehookPayload(name="greeting", args={}) >>> cond = PluginCondition(prompts={"greeting"}) >>> ctx = GlobalContext(request_id="req1") @@ -186,8 +184,7 @@ def pre_tool_matches(payload: ToolPreInvokePayload, conditions: list[PluginCondi True if the plugin matches criteria. Examples: - >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.models import ToolPreInvokePayload, GlobalContext + >>> from mcpgateway.plugins.framework import PluginCondition, ToolPreInvokePayload, GlobalContext >>> payload = ToolPreInvokePayload(name="calculator", args={}) >>> cond = PluginCondition(tools={"calculator"}) >>> ctx = GlobalContext(request_id="req1") @@ -223,8 +220,7 @@ def post_tool_matches(payload: ToolPostInvokePayload, conditions: list[PluginCon True if the plugin matches criteria. Examples: - >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.models import ToolPostInvokePayload, GlobalContext + >>> from mcpgateway.plugins.framework import PluginCondition, ToolPostInvokePayload, GlobalContext >>> payload = ToolPostInvokePayload(name="calculator", result={"result": 8}) >>> cond = PluginCondition(tools={"calculator"}) >>> ctx = GlobalContext(request_id="req1") @@ -260,8 +256,7 @@ def pre_resource_matches(payload: ResourcePreFetchPayload, conditions: list[Plug True if the plugin matches criteria. Examples: - >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.models import ResourcePreFetchPayload, GlobalContext + >>> from mcpgateway.plugins.framework import PluginCondition, ResourcePreFetchPayload, GlobalContext >>> payload = ResourcePreFetchPayload(uri="file:///data.txt") >>> cond = PluginCondition(resources={"file:///data.txt"}) >>> ctx = GlobalContext(request_id="req1") @@ -297,8 +292,7 @@ def post_resource_matches(payload: ResourcePostFetchPayload, conditions: list[Pl True if the plugin matches criteria. Examples: - >>> from mcpgateway.plugins.framework.models import PluginCondition - >>> from mcpgateway.plugins.framework.models import ResourcePostFetchPayload, GlobalContext + >>> from mcpgateway.plugins.framework import PluginCondition, ResourcePostFetchPayload, GlobalContext >>> from mcpgateway.models import ResourceContent >>> content = ResourceContent(type="resource", uri="file:///data.txt", text="Test") >>> payload = ResourcePostFetchPayload(uri="file:///data.txt", content=content) From 9afade4e6bae9fb23d70d6511b8e23626e4ee6c7 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 17 Aug 2025 20:28:04 -0400 Subject: [PATCH 35/65] docs: update plugin docs to use public framework API Signed-off-by: Frederico Araujo --- docs/docs/using/plugins/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/docs/using/plugins/index.md b/docs/docs/using/plugins/index.md index de69a4d0b..69afb4af3 100644 --- a/docs/docs/using/plugins/index.md +++ b/docs/docs/using/plugins/index.md @@ -209,9 +209,9 @@ Planned hooks (not yet implemented): ### Plugin Structure ```python -from mcpgateway.plugins.framework.base import Plugin -from mcpgateway.plugins.framework.models import PluginConfig -from mcpgateway.plugins.framework.plugin_types import ( +from mcpgateway.plugins.framework import ( + Plugin, + PluginConfig, PluginContext, PromptPrehookPayload, PromptPrehookResult, @@ -520,7 +520,7 @@ default_config: ```python # plugins/my_plugin/plugin.py -from mcpgateway.plugins.framework.base import Plugin +from mcpgateway.plugins.framework import Plugin class MyPlugin(Plugin): # Implementation here @@ -544,7 +544,7 @@ plugins: # tests/test_my_plugin.py import pytest from plugins.my_plugin.plugin import MyPlugin -from mcpgateway.plugins.framework.models import PluginConfig +from mcpgateway.plugins.framework import PluginConfig @pytest.mark.asyncio async def test_my_plugin(): From 3053810295192fed0c728cb9ac14dc548f7ac3c8 Mon Sep 17 00:00:00 2001 From: Teryl Taylor Date: Sun, 17 Aug 2025 18:35:31 -0600 Subject: [PATCH 36/65] fix(plugin): added resource payloads to base plugin. Signed-off-by: Teryl Taylor --- mcpgateway/plugins/framework/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mcpgateway/plugins/framework/base.py b/mcpgateway/plugins/framework/base.py index 9fe4bcc6b..99b6c172e 100644 --- a/mcpgateway/plugins/framework/base.py +++ b/mcpgateway/plugins/framework/base.py @@ -30,7 +30,10 @@ PromptPosthookResult, PromptPrehookPayload, PromptPrehookResult, + ResourcePreFetchPayload, + ResourcePostFetchPayload, ResourcePreFetchResult, + ResourcePostFetchResult, ToolPostInvokePayload, ToolPostInvokeResult, ToolPreInvokePayload, @@ -218,7 +221,7 @@ async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: Plugin """ ) - async def resource_pre_fetch(self, payload, context) -> ResourcePreFetchResult: + async def resource_pre_fetch(self, payload: ResourcePreFetchPayload, context: PluginContext) -> ResourcePreFetchResult: """Plugin hook run before a resource is fetched. Args: @@ -234,7 +237,7 @@ async def resource_pre_fetch(self, payload, context) -> ResourcePreFetchResult: """ ) - async def resource_post_fetch(self, payload, context) -> ResourcePreFetchResult: + async def resource_post_fetch(self, payload: ResourcePostFetchPayload, context: PluginContext) -> ResourcePostFetchResult: """Plugin hook run after a resource is fetched. Args: From 3ea75955d71ac98c472335f6e481bc2e5d24f04c Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 17 Aug 2025 21:46:12 -0400 Subject: [PATCH 37/65] feat: udpate test templates Signed-off-by: Frederico Araujo --- mcpgateway/plugins/framework/__init__.py | 2 + plugin_templates/external/tests/test_all | 72 +++++++++++++++++++ ...ce(' ', '_').replace('-', '_') }}.py.jinja | 36 ++++++---- 3 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 plugin_templates/external/tests/test_all diff --git a/mcpgateway/plugins/framework/__init__.py b/mcpgateway/plugins/framework/__init__.py index df92f73c4..796e24271 100644 --- a/mcpgateway/plugins/framework/__init__.py +++ b/mcpgateway/plugins/framework/__init__.py @@ -33,6 +33,7 @@ PromptPosthookResult, PromptPrehookPayload, PromptPrehookResult, + PromptResult, ResourcePostFetchPayload, ResourcePostFetchResult, ResourcePreFetchPayload, @@ -63,6 +64,7 @@ "PromptPosthookResult", "PromptPrehookPayload", "PromptPrehookResult", + "PromptResult", "ResourcePostFetchPayload", "ResourcePostFetchResult", "ResourcePreFetchPayload", diff --git a/plugin_templates/external/tests/test_all b/plugin_templates/external/tests/test_all new file mode 100644 index 000000000..f09a259bd --- /dev/null +++ b/plugin_templates/external/tests/test_all @@ -0,0 +1,72 @@ +"""Tests for plugin.""" + +# Third-Party +import pytest + +# First-Party +from mcpgateway.models import Message, PromptResult, Role, TextContent +from mcpgateway.plugins.framework import ( + PluginManager, + GlobalContext, + PromptPrehookPayload, + PromptPosthookPayload, + PromptResult, + ToolPreInvokePayload, + ToolPostInvokePayload, +) + + +@pytest.fixture(scope="module", autouse=True) +@pytest.mark.asyncio +async def plugin_manager(): + """Initialize plugin manager.""" + plugin_manager = PluginManager("./resources/config/config.yaml") + await plugin_manager.initialize() + yield plugin_manager + await plugin_manager.shutdown() + + +@pytest.mark.asyncio +async def test_prompt_pre_hook(plugin_manager: PluginManager): + """Test prompt pre hook across all registered plugins.""" + # Customize payload for testing + payload = PromptPrehookPayload(name="test_prompt", args={"arg0": "This is an argument"}) + global_context = GlobalContext(request_id="1") + result, _ = await plugin_manager.prompt_pre_fetch(payload, global_context) + # Assert expected behaviors + assert result.continue_processing + + +@pytest.mark.asyncio +async def test_prompt_post_hook(plugin_manager: PluginManager): + """Test prompt post hook across all registered plugins.""" + # Customize payload for testing + message = Message(content=TextContent(type="text", text=result.modified_payload.args["user"]), role=Role.USER) + prompt_result = PromptResult(messages=[message]) + payload = PromptPosthookPayload(name="test_prompt", result=prompt_result) + global_context = GlobalContext(request_id="1") + result, _ = await plugin_manager.prompt_post_fetch(payload, global_context) + # Assert expected behaviors + assert result.continue_processing + + +@pytest.mark.asyncio +async def test_tool_pre_hook(plugin_manager: PluginManager): + """Test tool pre hook across all registered plugins.""" + # Customize payload for testing + payload = ToolPreInvokePayload(name="test_prompt", args={"arg0": "This is an argument"}) + global_context = GlobalContext(request_id="1") + result, _ = await plugin_manager.tool_pre_invoke(payload, global_context) + # Assert expected behaviors + assert result.continue_processing + + +@pytest.mark.asyncio +async def test_tool_post_hook(plugin_manager: PluginManager): + """Test tool post hook across all registered plugins.""" + # Customize payload for testing + payload = ToolPostInvokePayload(name="test_tool", result={"output0": "output value"}) + global_context = GlobalContext(request_id="1") + result, _ = await plugin_manager.tool_post_invoke(payload, global_context) + # Assert expected behaviors + assert result.continue_processing diff --git a/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja b/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja index d78b81dcb..fa8118e32 100644 --- a/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja +++ b/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja @@ -4,22 +4,28 @@ import pytest # First-Party -from mcpgateway.plugins.framework.manager import PluginManager +from {{plugin_name.lower().replace(' ', '_').replace('-', '_')}}.plugin import {{plugin_name}} +from mcpgateway.plugins.framework import ( + PluginConfig, + PluginContext, + PromptPrehookPayload, + ) -@pytest.fixture(scope="module", autouse=True) -def plugin(): - """Loads plugin loader and skill.""" - plugin = PluginManager("./resources/config/config.yaml") - await plugin.initialize() - yield plugin - await plugin.shutdown() +@pytest.mark.asyncio +async def test_{{plugin_name.lower().replace(' ', '_').replace('-', '_')}}(): + """Test plugin prompt prefetch hook.""" + config = PluginConfig( + name="test", + kind="{{plugin_name.lower().replace(' ', '_').replace('-', '_')}}.{{plugin_name}}", + hooks=["prompt_pre_fetch"], + config={"setting_one": "test_value"} + ) + plugin = {{plugin_name}}(config) -def test_prompt_pre_hook(plugin): - """Implement me.""" - # payload = PromptPrehookPayload(...) - # global_context = GlobalContext(request_id="...") - # result, contexts = await plugin.prompt_pre_fetch(payload, global_context) - # assert result.modified_payload ... - assert True + # Test your plugin logic + payload = PromptPrehookPayload(name="test_prompt", args={"arg0": "This is an argument"}) + context = PluginContext(request_id="1", server_id="2") + result = await plugin.prompt_pre_fetch(payload, context) + assert result.continue_processing From 9c0efa54da87d91b2decfa7d7fbce0a6f34ec40c Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 17 Aug 2025 21:56:09 -0400 Subject: [PATCH 38/65] feat: update test templates Signed-off-by: Frederico Araujo --- plugin_templates/external/tests/{test_all => test_all.py} | 0 ...me.lower().replace(' ', '_').replace('-', '_') }}.py.jinja | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename plugin_templates/external/tests/{test_all => test_all.py} (100%) diff --git a/plugin_templates/external/tests/test_all b/plugin_templates/external/tests/test_all.py similarity index 100% rename from plugin_templates/external/tests/test_all rename to plugin_templates/external/tests/test_all.py diff --git a/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja b/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja index fa8118e32..7f16a4756 100644 --- a/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja +++ b/plugin_templates/external/tests/test_{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}.py.jinja @@ -9,7 +9,7 @@ from mcpgateway.plugins.framework import ( PluginConfig, PluginContext, PromptPrehookPayload, - ) +) @pytest.mark.asyncio @@ -19,7 +19,7 @@ async def test_{{plugin_name.lower().replace(' ', '_').replace('-', '_')}}(): name="test", kind="{{plugin_name.lower().replace(' ', '_').replace('-', '_')}}.{{plugin_name}}", hooks=["prompt_pre_fetch"], - config={"setting_one": "test_value"} + config={"setting_one": "test_value"}, ) plugin = {{plugin_name}}(config) From 290b4d123993a62dbf941167ef9045f8d062170e Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 17 Aug 2025 22:00:17 -0400 Subject: [PATCH 39/65] feat: update plugin template Signed-off-by: Frederico Araujo --- .../plugin.py.jinja | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin.py.jinja b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin.py.jinja index 483008278..cdd8f3e80 100644 --- a/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin.py.jinja +++ b/plugin_templates/external/{{ plugin_name.lower().replace(' ', '_').replace('-', '_') }}/plugin.py.jinja @@ -51,7 +51,7 @@ class {{ class_name }}(Plugin): Returns: The result of the plugin's analysis, including whether the prompt can proceed. """ - pass + return PromptPrehookResult(continue_processing=True) async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult: """Plugin hook run after a prompt is rendered. @@ -63,7 +63,7 @@ class {{ class_name }}(Plugin): Returns: The result of the plugin's analysis, including whether the prompt can proceed. """ - pass + return PromptPosthookResult(continue_processing=True) async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: """Plugin hook run before a tool is invoked. @@ -75,7 +75,7 @@ class {{ class_name }}(Plugin): Returns: The result of the plugin's analysis, including whether the tool can proceed. """ - pass + return ToolPreInvokeResult(continue_processing=True) async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: """Plugin hook run after a tool is invoked. @@ -87,4 +87,4 @@ class {{ class_name }}(Plugin): Returns: The result of the plugin's analysis, including whether the tool result should proceed. """ - pass + return ToolPostInvokeResult(continue_processing=True) From 09c50821882663917ec24983df254ca2dad752ac Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 17 Aug 2025 22:28:21 -0400 Subject: [PATCH 40/65] feat: update plugin template Signed-off-by: Frederico Araujo --- plugin_templates/external/tests/__init__.py | 0 plugin_templates/external/tests/pytest.ini | 2 +- plugin_templates/external/tests/test_all.py | 14 +++++++------- 3 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 plugin_templates/external/tests/__init__.py diff --git a/plugin_templates/external/tests/__init__.py b/plugin_templates/external/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/plugin_templates/external/tests/pytest.ini b/plugin_templates/external/tests/pytest.ini index d08a0a52f..d8d63180b 100644 --- a/plugin_templates/external/tests/pytest.ini +++ b/plugin_templates/external/tests/pytest.ini @@ -8,6 +8,6 @@ log_format = %(asctime)s [%(module)s] [%(levelname)s] %(message)s log_date_format = %Y-%m-%d %H:%M:%S addopts = --cov --cov-report term-missing env_files = .env -pythonpath = src +pythonpath = . src filterwarnings = ignore::DeprecationWarning:pydantic.* \ No newline at end of file diff --git a/plugin_templates/external/tests/test_all.py b/plugin_templates/external/tests/test_all.py index f09a259bd..0c190be75 100644 --- a/plugin_templates/external/tests/test_all.py +++ b/plugin_templates/external/tests/test_all.py @@ -1,6 +1,7 @@ -"""Tests for plugin.""" +"""Tests for registered plugins.""" # Third-Party +import asyncio import pytest # First-Party @@ -17,13 +18,12 @@ @pytest.fixture(scope="module", autouse=True) -@pytest.mark.asyncio -async def plugin_manager(): +def plugin_manager(): """Initialize plugin manager.""" - plugin_manager = PluginManager("./resources/config/config.yaml") - await plugin_manager.initialize() + plugin_manager = PluginManager("./resources/plugins/config.yaml") + asyncio.run(plugin_manager.initialize()) yield plugin_manager - await plugin_manager.shutdown() + asyncio.run(plugin_manager.shutdown()) @pytest.mark.asyncio @@ -41,7 +41,7 @@ async def test_prompt_pre_hook(plugin_manager: PluginManager): async def test_prompt_post_hook(plugin_manager: PluginManager): """Test prompt post hook across all registered plugins.""" # Customize payload for testing - message = Message(content=TextContent(type="text", text=result.modified_payload.args["user"]), role=Role.USER) + message = Message(content=TextContent(type="text", text="prompt"), role=Role.USER) prompt_result = PromptResult(messages=[message]) payload = PromptPosthookPayload(name="test_prompt", result=prompt_result) global_context = GlobalContext(request_id="1") From 6f8990112da20dcddd20f0aeb3a89c8fcc7da7d1 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 17 Aug 2025 22:50:28 -0400 Subject: [PATCH 41/65] feat: update tempalte makefile Signed-off-by: Frederico Araujo --- plugin_templates/external/Makefile.jinja | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/plugin_templates/external/Makefile.jinja b/plugin_templates/external/Makefile.jinja index 7d40e9148..9bc7613ae 100644 --- a/plugin_templates/external/Makefile.jinja +++ b/plugin_templates/external/Makefile.jinja @@ -10,8 +10,8 @@ PROJECT_NAME = {{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }} TARGET ?= {{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }} # Virtual-environment variables -VENVS_DIR := $(HOME)/.venv -VENV_DIR := $(VENVS_DIR)/$(PROJECT_NAME) +VENVS_DIR ?= $(HOME)/.venv +VENV_DIR ?= $(VENVS_DIR)/$(PROJECT_NAME) # ============================================================================= # Linters @@ -295,11 +295,24 @@ show-runtime: # Targets # ============================================================================= +.PHONY: venv +venv: + @rm -Rf "$(VENV_DIR)" + @test -d "$(VENVS_DIR)" || mkdir -p "$(VENVS_DIR)" + @python3 -m venv "$(VENV_DIR)" + @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m pip install --upgrade pip setuptools pdm uv" + @echo -e "โœ… Virtual env created.\n๐Ÿ’ก Enter it with:\n . $(VENV_DIR)/bin/activate\n" + .PHONY: install -install: +install: venv $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`))) @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m uv pip install ." +.PHONY: install-editable +install-editable: venv + $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`))) + @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m uv pip install -e ." + .PHONY: uninstall uninstall: pip uninstall $(PACKAGE_NAME) @@ -413,6 +426,7 @@ help: @echo "" @echo "The following are the valid targets for this Makefile:" @echo "...install Install package from sources" + @echo "...install-editable Install package from sources in editabled mode" @echo "...uninstall Uninstall package" @echo "...dist Clean-build wheel *and* sdist into ./dist" @echo "...wheel Build wheel only" From e80d4e36100f2182ada9ebddb3d9a55bdf4e33b4 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 17 Aug 2025 22:50:44 -0400 Subject: [PATCH 42/65] feat: add template for native plugin Signed-off-by: Frederico Araujo --- plugin_templates/native/__init__.py.jinja | 7 ++ plugin_templates/native/config.yaml.jinja | 34 +++++++ .../native/plugin-manifest.yaml.jinja | 9 ++ plugin_templates/native/plugin.py.jinja | 90 +++++++++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 plugin_templates/native/__init__.py.jinja create mode 100644 plugin_templates/native/config.yaml.jinja create mode 100644 plugin_templates/native/plugin-manifest.yaml.jinja create mode 100644 plugin_templates/native/plugin.py.jinja diff --git a/plugin_templates/native/__init__.py.jinja b/plugin_templates/native/__init__.py.jinja new file mode 100644 index 000000000..e8a186349 --- /dev/null +++ b/plugin_templates/native/__init__.py.jinja @@ -0,0 +1,7 @@ +"""MCP Gateway {{plugin_name}} Plugin - {{description}}. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: {{author}} + +""" diff --git a/plugin_templates/native/config.yaml.jinja b/plugin_templates/native/config.yaml.jinja new file mode 100644 index 000000000..ba5c19abe --- /dev/null +++ b/plugin_templates/native/config.yaml.jinja @@ -0,0 +1,34 @@ +plugins: + - name: "{{ plugin_name }}" + {% set class_parts = plugin_name.replace(' ', '_').replace('-','_').split('_') -%} + {% if class_parts|length > 1 -%} + {% set class_name = class_parts|map('capitalize')|join -%} + {% else -%} + {% set class_name = class_parts|join -%} + {% endif -%} + kind: "{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}.plugin.{{ class_name }}" + description: "{{ description }}" + version: "{{ version }}" + author: "{{ author }}" + hooks: ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke"] + tags: ["plugin"] + mode: "enforce" # enforce | permissive | disabled + priority: 150 + conditions: + # Apply to specific tools/servers + - server_ids: [] # Apply to all servers + tenant_ids: [] # Apply to all tenants + config: + # Plugin config dict passed to the plugin constructor + +# Plugin directories to scan +plugin_dirs: + - "{{ plugin_name|lower|replace(' ', '_')|replace('-', '_') }}" + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/plugin_templates/native/plugin-manifest.yaml.jinja b/plugin_templates/native/plugin-manifest.yaml.jinja new file mode 100644 index 000000000..5cc75c018 --- /dev/null +++ b/plugin_templates/native/plugin-manifest.yaml.jinja @@ -0,0 +1,9 @@ +description: "{{description}}" +author: "{{author}}" +version: "{{version}}" +available_hooks: + - "prompt_pre_hook" + - "prompt_post_hook" + - "tool_pre_hook" + - "tool_post_hook" +default_configs: diff --git a/plugin_templates/native/plugin.py.jinja b/plugin_templates/native/plugin.py.jinja new file mode 100644 index 000000000..cdd8f3e80 --- /dev/null +++ b/plugin_templates/native/plugin.py.jinja @@ -0,0 +1,90 @@ +"""{{ description }}. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: {{ author }} + +This module loads configurations for plugins. +""" + +# First-Party +from mcpgateway.plugins.framework import ( + Plugin, + PluginConfig, + PluginContext, + PromptPosthookPayload, + PromptPosthookResult, + PromptPrehookPayload, + PromptPrehookResult, + ToolPostInvokePayload, + ToolPostInvokeResult, + ToolPreInvokePayload, + ToolPreInvokeResult, +) + + +{% set class_parts = plugin_name.replace(' ', '_').replace('-','_').split('_') -%} +{% if class_parts|length > 1 -%} +{% set class_name = class_parts|map('capitalize')|join -%} +{% else -%} +{% set class_name = class_parts|join -%} +{% endif -%} +class {{ class_name }}(Plugin): + """{{ description }}.""" + + def __init__(self, config: PluginConfig): + """Entry init block for plugin. + + Args: + logger: logger that the skill can make use of + config: the skill configuration + """ + super().__init__(config) + + async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: + """The plugin hook run before a prompt is retrieved and rendered. + + Args: + payload: The prompt payload to be analyzed. + context: contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the prompt can proceed. + """ + return PromptPrehookResult(continue_processing=True) + + async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult: + """Plugin hook run after a prompt is rendered. + + Args: + payload: The prompt payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the prompt can proceed. + """ + return PromptPosthookResult(continue_processing=True) + + async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: + """Plugin hook run before a tool is invoked. + + Args: + payload: The tool payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the tool can proceed. + """ + return ToolPreInvokeResult(continue_processing=True) + + async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: + """Plugin hook run after a tool is invoked. + + Args: + payload: The tool result payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the tool result should proceed. + """ + return ToolPostInvokeResult(continue_processing=True) From 8cbd10e11d75b3c92db0b0d92e8c72b701345900 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 17 Aug 2025 22:51:40 -0400 Subject: [PATCH 43/65] feat: add readme for native template Signed-off-by: Frederico Araujo --- plugin_templates/native/README.md.jinja | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 plugin_templates/native/README.md.jinja diff --git a/plugin_templates/native/README.md.jinja b/plugin_templates/native/README.md.jinja new file mode 100644 index 000000000..dcdc59a79 --- /dev/null +++ b/plugin_templates/native/README.md.jinja @@ -0,0 +1,30 @@ +# {{plugin_name}} for Context Forge MCP Gateway + +{{description}}. + + +## Installation + + +## Setting up the development environment + + +## Testing + +Test modules are created under the `tests` directory. + +To run all tests, use the following command: + +``` +make test +``` + +**Note:** To enable logging, set `log_cli = true` in `tests/pytest.ini`. + +## Code Linting + +Before checking in any code for the project, please lint the code. This can be done using: + +``` +make lint-fix +``` From a4111a32d60794808172630fe5136461e2542b8e Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Sun, 17 Aug 2025 23:02:41 -0400 Subject: [PATCH 44/65] feat: force boostrap to be a subcommnand Signed-off-by: Frederico Araujo --- mcpgateway/plugins/framework/base.py | 4 ++-- mcpgateway/plugins/tools/cli.py | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/mcpgateway/plugins/framework/base.py b/mcpgateway/plugins/framework/base.py index 99b6c172e..78d01ffb4 100644 --- a/mcpgateway/plugins/framework/base.py +++ b/mcpgateway/plugins/framework/base.py @@ -30,10 +30,10 @@ PromptPosthookResult, PromptPrehookPayload, PromptPrehookResult, - ResourcePreFetchPayload, ResourcePostFetchPayload, - ResourcePreFetchResult, ResourcePostFetchResult, + ResourcePreFetchPayload, + ResourcePreFetchResult, ToolPostInvokePayload, ToolPostInvokeResult, ToolPreInvokePayload, diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index 47aec052e..e0f744f20 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -138,6 +138,12 @@ def bootstrap( worker.run_copy() +@app.callback() +def callback(): + """This function exists to force 'bootstrap' to be a subcommand.""" + pass + + # @app.command(help="Installs plugins into a Python environment.") # def install( # install_manifest: Annotated[typer.FileText, typer.Option("--install_manifest", "-i", help="The install manifest describing which plugins to install.")] = DEFAULT_INSTALL_MANIFEST, @@ -157,7 +163,7 @@ def bootstrap( # subprocess.run(cmd) -# @app.command(help="Builds an MCP server to serve plugins as tools") +# @app.command(help="Builds an MCP server to serve plugins as tools.") # def package( # image_tag: Annotated[str, typer.Option("--image_tag", "-t", help="The container image tag to generated container.")] = DEFAULT_IMAGE_TAG, # containerfile: Annotated[Path, typer.Option("--containerfile", "-c", help="The Dockerfile used to build the container.")] = DEFAULT_CONTAINERFILE_PATH, From a9d522eb75ea175fcad8f8aac6e6b2099bb2fff5 Mon Sep 17 00:00:00 2001 From: Teryl Taylor Date: Sun, 17 Aug 2025 23:30:24 -0600 Subject: [PATCH 45/65] tests(plugin): added http streamable and error tests. Signed-off-by: Teryl Taylor --- mcpgateway/plugins/framework/__init__.py | 2 + .../plugins/framework/external/mcp/client.py | 4 +- plugins/resources/server/config-http.yaml | 2 +- ...lid_strhttp_external_plugin_overrides.yaml | 25 ++++ .../valid_strhttp_external_plugin_regex.yaml | 22 +++ .../external/mcp/test_client_stdio.py | 67 +++++++-- .../mcp/test_client_streamable_http.py | 128 ++++++++++++++++++ .../plugins/framework/test_errors.py | 22 +++ 8 files changed, 260 insertions(+), 12 deletions(-) create mode 100644 tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_overrides.yaml create mode 100644 tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_regex.yaml create mode 100644 tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py create mode 100644 tests/unit/mcpgateway/plugins/framework/test_errors.py diff --git a/mcpgateway/plugins/framework/__init__.py b/mcpgateway/plugins/framework/__init__.py index 796e24271..beb1323c8 100644 --- a/mcpgateway/plugins/framework/__init__.py +++ b/mcpgateway/plugins/framework/__init__.py @@ -18,6 +18,7 @@ from mcpgateway.plugins.framework.errors import PluginError, PluginViolationError from mcpgateway.plugins.framework.external.mcp.server import ExternalPluginServer from mcpgateway.plugins.framework.loader.config import ConfigLoader +from mcpgateway.plugins.framework.loader.plugin import PluginLoader from mcpgateway.plugins.framework.manager import PluginManager from mcpgateway.plugins.framework.models import ( GlobalContext, @@ -55,6 +56,7 @@ "PluginContext", "PluginError", "PluginErrorModel", + "PluginLoader", "PluginManager", "PluginMode", "PluginResult", diff --git a/mcpgateway/plugins/framework/external/mcp/client.py b/mcpgateway/plugins/framework/external/mcp/client.py index 4fbfa0c7e..3c1c6b202 100644 --- a/mcpgateway/plugins/framework/external/mcp/client.py +++ b/mcpgateway/plugins/framework/external/mcp/client.py @@ -94,7 +94,9 @@ async def __connect_to_stdio_server(self, server_script_path: str) -> None: if not (is_python): raise ValueError("Server script must be a .py file") - server_params = StdioServerParameters(command=PYTHON, args=[server_script_path], env=os.environ) + current_env = os.environ.copy() + + server_params = StdioServerParameters(command=PYTHON, args=[server_script_path], env=current_env) stdio_transport = await self._exit_stack.enter_async_context(stdio_client(server_params)) self._stdio, self._write = stdio_transport diff --git a/plugins/resources/server/config-http.yaml b/plugins/resources/server/config-http.yaml index 23d3922b6..99cc0472e 100644 --- a/plugins/resources/server/config-http.yaml +++ b/plugins/resources/server/config-http.yaml @@ -3,7 +3,7 @@ server: streamable-http: host: "127.0.0.1" - port: 3000 + port: 3001 mcp_path: "/mcp" json_response: true stateless: true diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_overrides.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_overrides.yaml new file mode 100644 index 000000000..e82f7fe87 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_overrides.yaml @@ -0,0 +1,25 @@ +# plugins/config.yaml - Main plugin configuration file + +plugins: + - name: "DenyListPlugin" + kind: "external" + description: "a different configuration." + priority: 150 + hooks: ["prompt_pre_fetch", "prompt_post_fetch"] + mcp: + proto: STREAMABLEHTTP + url: http://127.0.0.1:3001/mcp + +# Plugin directories to scan +plugin_dirs: + - "plugins/native" # Built-in plugins + - "plugins/custom" # Custom organization plugins + - "/etc/mcpgateway/plugins" # System-wide plugins + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_regex.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_regex.yaml new file mode 100644 index 000000000..53ba731df --- /dev/null +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_regex.yaml @@ -0,0 +1,22 @@ +# plugins/config.yaml - Main plugin configuration file + +plugins: + - name: "ReplaceBadWordsPlugin" + kind: "external" + mcp: + proto: STREAMABLEHTTP + url: http://127.0.0.1:3001/mcp + +# Plugin directories to scan +plugin_dirs: + - "plugins/native" # Built-in plugins + - "plugins/custom" # Custom organization plugins + - "/etc/mcpgateway/plugins" # System-wide plugins + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py index e1f59dee7..8048a322b 100644 --- a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py @@ -4,13 +4,18 @@ Copyright 2025 SPDX-License-Identifier: Apache-2.0 """ +from contextlib import AsyncExitStack +import json import os +import sys +from typing import Optional import pytest +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client from mcpgateway.models import Message, PromptResult, Role, TextContent -from mcpgateway.plugins.framework.loader.config import ConfigLoader -from mcpgateway.plugins.framework.loader.plugin import PluginLoader -from mcpgateway.plugins.framework.models import PluginContext, PromptPrehookPayload, PromptPosthookPayload +from mcpgateway.plugins.framework import ConfigLoader, PluginConfig, PluginLoader, PluginContext, PromptPrehookPayload, PromptPosthookPayload +from plugins.regex_filter.search_replace import SearchReplaceConfig @pytest.mark.asyncio @@ -18,11 +23,9 @@ async def test_client_load_stdio(): os.environ["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" os.environ["PYTHONPATH"] = "." config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin.yaml") - print(config) loader = PluginLoader() plugin = await loader.load_and_instantiate_plugin(config.plugins[0]) - print(plugin) prompt = PromptPrehookPayload(name="test_prompt", args = {"text": "That was innovative!"}) result = await plugin.prompt_pre_fetch(prompt, PluginContext(request_id="1", server_id="2")) assert result.violation @@ -38,15 +41,14 @@ async def test_client_load_stdio(): del os.environ["PLUGINS_CONFIG_PATH"] del os.environ["PYTHONPATH"] +@pytest.mark.asyncio async def test_client_load_stdio_overrides(): os.environ["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" os.environ["PYTHONPATH"] = "." config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_overrides.yaml") - print(config) loader = PluginLoader() plugin = await loader.load_and_instantiate_plugin(config.plugins[0]) - print(plugin) prompt = PromptPrehookPayload(name="test_prompt", args = {"text": "That was innovative!"}) result = await plugin.prompt_pre_fetch(prompt, PluginContext(request_id="1", server_id="2")) assert result.violation @@ -69,11 +71,9 @@ async def test_client_load_stdio_post_prompt(): os.environ["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_single_plugin.yaml" os.environ["PYTHONPATH"] = "." config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_regex.yaml") - print(config) loader = PluginLoader() plugin = await loader.load_and_instantiate_plugin(config.plugins[0]) - print(plugin) prompt = PromptPrehookPayload(name="test_prompt", args = {"user": "What a crapshow!"}) context = PluginContext(request_id="1", server_id="2") result = await plugin.prompt_pre_fetch(prompt, context) @@ -95,4 +95,51 @@ async def test_client_load_stdio_post_prompt(): await plugin.shutdown() await loader.shutdown() del os.environ["PLUGINS_CONFIG_PATH"] - del os.environ["PYTHONPATH"] \ No newline at end of file + del os.environ["PYTHONPATH"] + +@pytest.mark.asyncio +async def test_client_get_plugin_configs(): + session: Optional[ClientSession] = None + exit_stack = AsyncExitStack() + current_env = os.environ.copy() + current_env["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins.yaml" + current_env["PYTHONPATH"] = "." + server_params = StdioServerParameters(command=sys.executable, args=["mcpgateway/plugins/framework/external/mcp/server/runtime.py"], env=current_env) + + stdio_transport = await exit_stack.enter_async_context(stdio_client(server_params)) + stdio, write = stdio_transport + session = await exit_stack.enter_async_context(ClientSession(stdio, write)) + + await session.initialize() + all_configs = [] + configs = await session.call_tool("get_plugin_configs", {}) + for content in configs.content: + confs = json.loads(content.text) + for c in confs: + plugconfig = PluginConfig.model_validate(c) + all_configs.append(plugconfig) + await exit_stack.aclose() + assert all_configs[0].name == "SynonymsPlugin" + assert all_configs[0].kind == "plugins.regex_filter.search_replace.SearchReplacePlugin" + assert all_configs[0].description == "A plugin for finding and replacing synonyms." + assert all_configs[0].version == "0.1" + assert all_configs[0].author == "MCP Context Forge Team" + assert all_configs[0].hooks[0] == "prompt_pre_fetch" + assert all_configs[0].hooks[1] == "prompt_post_fetch" + assert all_configs[0].config + srconfig = SearchReplaceConfig.model_validate(all_configs[0].config) + assert len(srconfig.words) == 2 + assert srconfig.words[0].search == "happy" + assert srconfig.words[0].replace == "gleeful" + assert all_configs[1].name == "ReplaceBadWordsPlugin" + assert all_configs[1].kind == "plugins.regex_filter.search_replace.SearchReplacePlugin" + assert all_configs[1].description == "A plugin for finding and replacing words." + assert all_configs[1].version == "0.1" + assert all_configs[1].author == "MCP Context Forge Team" + assert all_configs[1].hooks[0] == "prompt_pre_fetch" + assert all_configs[1].hooks[1] == "prompt_post_fetch" + assert all_configs[1].config + srconfig = SearchReplaceConfig.model_validate(all_configs[1].config) + assert srconfig.words[0].search == "crap" + assert srconfig.words[0].replace == "crud" + assert len(all_configs) == 2 diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py new file mode 100644 index 000000000..06e5b14db --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py @@ -0,0 +1,128 @@ +""" +Tests for external client on streamable http. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +""" +import os +import subprocess +import sys +import time + +import pytest + +from mcpgateway.models import Message, PromptResult, Role, TextContent +from mcpgateway.plugins.framework import ConfigLoader, PluginLoader, PluginContext, PromptPrehookPayload, PromptPosthookPayload + +@pytest.mark.asyncio +async def test_client_load_streamable_http(): + + current_env = os.environ.copy() + current_env["CHUK_MCP_CONFIG_PATH"] = "plugins/resources/server/config-http.yaml" + current_env["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_single_plugin.yaml" + current_env["PYTHONPATH"] = "." + # Start the server as a subprocess + server_proc = subprocess.Popen([sys.executable, "mcpgateway/plugins/framework/external/mcp/server/runtime.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=current_env) + time.sleep(2) # Give the server time to start + + assert not server_proc.poll(), "Server failed to start" + + + config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_regex.yaml") + + loader = PluginLoader() + plugin = await loader.load_and_instantiate_plugin(config.plugins[0]) + prompt = PromptPrehookPayload(name="test_prompt", args = {"user": "What a crapshow!"}) + context = PluginContext(request_id="1", server_id="2") + result = await plugin.prompt_pre_fetch(prompt, context) + assert result.modified_payload.args["user"] == "What a yikesshow!" + config = plugin.config + assert config.name == "ReplaceBadWordsPlugin" + assert config.description == "A plugin for finding and replacing words." + assert config.priority == 150 + assert config.kind == "external" + message = Message(content=TextContent(type="text", text="What the crud?"), role=Role.USER) + prompt_result = PromptResult(messages=[message]) + + payload_result = PromptPosthookPayload(name="test_prompt", result=prompt_result) + + result = await plugin.prompt_post_fetch(payload_result, context=context) + assert len(result.modified_payload.result.messages) == 1 + assert result.modified_payload.result.messages[0].content.text == "What the yikes?" + await plugin.shutdown() + await loader.shutdown() + server_proc.terminate() + server_proc.wait() # Wait for the process to fully terminate + +@pytest.mark.asyncio +async def test_client_load_strhttp_overrides(): + current_env = os.environ.copy() + current_env["CHUK_MCP_CONFIG_PATH"] = "plugins/resources/server/config-http.yaml" + current_env["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" + current_env["PYTHONPATH"] = "." + # Start the server as a subprocess + server_proc = subprocess.Popen([sys.executable, "mcpgateway/plugins/framework/external/mcp/server/runtime.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=current_env) + time.sleep(2) # Give the server time to start + + assert not server_proc.poll(), "Server failed to start" + + config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_overrides.yaml") + + loader = PluginLoader() + plugin = await loader.load_and_instantiate_plugin(config.plugins[0]) + prompt = PromptPrehookPayload(name="test_prompt", args = {"text": "That was innovative!"}) + result = await plugin.prompt_pre_fetch(prompt, PluginContext(request_id="1", server_id="2")) + assert result.violation + assert result.violation.reason == "Prompt not allowed" + assert result.violation.description == "A deny word was found in the prompt" + assert result.violation.code == "deny" + config = plugin.config + assert config.name == "DenyListPlugin" + assert config.description == "a different configuration." + assert config.priority == 150 + assert config.hooks[0] == "prompt_pre_fetch" + assert config.hooks[1] == "prompt_post_fetch" + assert config.kind == "external" + await plugin.shutdown() + await loader.shutdown() + server_proc.terminate() + server_proc.wait() # Wait for the process to fully terminate + +@pytest.mark.asyncio +async def test_client_load_strhttp_post_prompt(): + current_env = os.environ.copy() + current_env["CHUK_MCP_CONFIG_PATH"] = "plugins/resources/server/config-http.yaml" + current_env["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" + current_env["PYTHONPATH"] = "." + # Start the server as a subprocess + server_proc = subprocess.Popen([sys.executable, "mcpgateway/plugins/framework/external/mcp/server/runtime.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=current_env) + time.sleep(2) # Give the server time to start + + assert not server_proc.poll(), "Server failed to start" + + config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_regex.yaml") + + loader = PluginLoader() + plugin = await loader.load_and_instantiate_plugin(config.plugins[0]) + prompt = PromptPrehookPayload(name="test_prompt", args = {"user": "What a crapshow!"}) + context = PluginContext(request_id="1", server_id="2") + result = await plugin.prompt_pre_fetch(prompt, context) + assert result.modified_payload.args["user"] == "What a yikesshow!" + config = plugin.config + assert config.name == "ReplaceBadWordsPlugin" + assert config.description == "A plugin for finding and replacing words." + assert config.priority == 150 + assert config.kind == "external" + + message = Message(content=TextContent(type="text", text="What the crud?"), role=Role.USER) + prompt_result = PromptResult(messages=[message]) + + payload_result = PromptPosthookPayload(name="test_prompt", result=prompt_result) + + result = await plugin.prompt_post_fetch(payload_result, context=context) + assert len(result.modified_payload.result.messages) == 1 + assert result.modified_payload.result.messages[0].content.text == "What the yikes?" + await plugin.shutdown() + await loader.shutdown() + server_proc.terminate() + server_proc.wait() # Wait for the process to fully terminate diff --git a/tests/unit/mcpgateway/plugins/framework/test_errors.py b/tests/unit/mcpgateway/plugins/framework/test_errors.py new file mode 100644 index 000000000..58326451e --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/test_errors.py @@ -0,0 +1,22 @@ +""" +Tests for errors module. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +""" + +import pytest +from mcpgateway.plugins.framework.errors import convert_exception_to_error, PluginError + + +@pytest.mark.asyncio +async def test_convert_exception_to_error(): + error_model = convert_exception_to_error(ValueError("This is some error."), "SomePluginName") + assert error_model.message == "ValueError('This is some error.')" + assert error_model.plugin_name == "SomePluginName" + + plugin_error = PluginError(error_model) + + assert plugin_error.error.message == "ValueError('This is some error.')" + assert plugin_error.error.plugin_name == "SomePluginName" + From f2045e5bfe72321b9352972f2428e129f65410e6 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 01:43:06 -0400 Subject: [PATCH 46/65] tests: add tests for plugins CLI Signed-off-by: Frederico Araujo --- mcpgateway/plugins/tools/cli.py | 2 + .../unit/mcpgateway/plugins/tools/__init__.py | 0 .../unit/mcpgateway/plugins/tools/test_cli.py | 37 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 tests/unit/mcpgateway/plugins/tools/__init__.py create mode 100644 tests/unit/mcpgateway/plugins/tools/test_cli.py diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index e0f744f20..ceec22192 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -117,6 +117,7 @@ def bootstrap( vcs_ref: Annotated[str, typer.Option("--vcs_ref", "-r", help="The version control system tag/branch/commit to use for the template.")] = DEFAULT_VCS_REF, answers_file: Optional[Annotated[typer.FileText, typer.Option("--answers_file", "-a", help="The answers file to be used for bootstrapping.")]] = None, defaults: Annotated[bool, typer.Option("--defaults", help="Bootstrap with defaults.")] = False, + dry_run: Annotated[bool, typer.Option("--dry_run", help="Run but do not make any changes.")] = False, ): """Boostrap a new plugin project from a template. @@ -134,6 +135,7 @@ def bootstrap( defaults=defaults, vcs_ref=vcs_ref, data={"default_author_name": git_user_name(), "default_author_email": git_user_email()}, + pretend=dry_run, ) as worker: worker.run_copy() diff --git a/tests/unit/mcpgateway/plugins/tools/__init__.py b/tests/unit/mcpgateway/plugins/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/mcpgateway/plugins/tools/test_cli.py b/tests/unit/mcpgateway/plugins/tools/test_cli.py new file mode 100644 index 000000000..aa63317c8 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/tools/test_cli.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +"""Tests for the mcpplugins CLI module (plugins/tools/cli.py). + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Fred Araujo + +""" + +# Future +from __future__ import annotations + +# Third-Party +import pytest +from typer.testing import CliRunner + +# First-Party +import mcpgateway.plugins.tools.cli as cli + + +@pytest.fixture(scope="module", autouse=True) +def runner(): + runner = CliRunner() + yield runner + + +def test_bootrap_command_help(runner: CliRunner): + """Boostrapping help.""" + raw = ["bootstrap", "--help"] + result = runner.invoke(cli.app, raw) + assert "Creates a new plugin project from template" in result.stdout + +def test_bootrap_command_dry_run(runner: CliRunner): + """Boostrapping dry run.""" + raw = ["bootstrap", "--destination", "/tmp/myplugin", "--template_url", ".", "--defaults", "--dry_run"] + result = runner.invoke(cli.app, raw) + assert result.exit_code == 0 From 416b969c07874cb7acc404e62ca3714bb80ad5f6 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 02:13:55 -0400 Subject: [PATCH 47/65] fix: deprecation warning Signed-off-by: Frederico Araujo --- mcpgateway/plugins/tools/cli.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index ceec22192..f8c786b11 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -31,7 +31,7 @@ from typing import Optional # Third-Party -from copier import Worker +from copier import run_copy import typer from typing_extensions import Annotated @@ -128,7 +128,7 @@ def bootstrap( answers-file: The copier answers file that can be used to skip interactive mode. defaults: Bootstrap with defaults. """ - with Worker( + run_copy( src_path=template_url, dst_path=destination, answers_file=answers_file, @@ -136,8 +136,7 @@ def bootstrap( vcs_ref=vcs_ref, data={"default_author_name": git_user_name(), "default_author_email": git_user_email()}, pretend=dry_run, - ) as worker: - worker.run_copy() + ) @app.callback() From 8874503598d7182e14068807506ee8dbcd66eaed Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 02:19:34 -0400 Subject: [PATCH 48/65] tests: add CLI tests Signed-off-by: Frederico Araujo --- tests/unit/mcpgateway/plugins/fixtures/install.yaml | 3 +++ tests/unit/mcpgateway/plugins/tools/test_cli.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 tests/unit/mcpgateway/plugins/fixtures/install.yaml diff --git a/tests/unit/mcpgateway/plugins/fixtures/install.yaml b/tests/unit/mcpgateway/plugins/fixtures/install.yaml new file mode 100644 index 000000000..eb96dde69 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/fixtures/install.yaml @@ -0,0 +1,3 @@ +packages: + - package: myplugin + repository: https://github.com/araujof/my-plugin.git diff --git a/tests/unit/mcpgateway/plugins/tools/test_cli.py b/tests/unit/mcpgateway/plugins/tools/test_cli.py index aa63317c8..b0a5116c8 100644 --- a/tests/unit/mcpgateway/plugins/tools/test_cli.py +++ b/tests/unit/mcpgateway/plugins/tools/test_cli.py @@ -10,12 +10,16 @@ # Future from __future__ import annotations +# Standard +import yaml + # Third-Party import pytest from typer.testing import CliRunner # First-Party import mcpgateway.plugins.tools.cli as cli +from mcpgateway.plugins.tools.models import InstallManifest @pytest.fixture(scope="module", autouse=True) @@ -35,3 +39,11 @@ def test_bootrap_command_dry_run(runner: CliRunner): raw = ["bootstrap", "--destination", "/tmp/myplugin", "--template_url", ".", "--defaults", "--dry_run"] result = runner.invoke(cli.app, raw) assert result.exit_code == 0 + +def test_install_manifest(): + """Test install manifest.""" + with open("./tests/unit/mcpgateway/plugins/fixtures/install.yaml") as f: + data = yaml.safe_load(f) + manifest = InstallManifest.model_validate(data) + assert manifest + assert len(manifest.packages) > 0 \ No newline at end of file From 457fdb55e6f87c78feb95b73dc5024e21103d984 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 02:34:26 -0400 Subject: [PATCH 49/65] tests: update plugin cli Signed-off-by: Frederico Araujo --- .coveragerc | 3 +++ mcpgateway/plugins/tools/cli.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index b6bb6fa2d..5e84e1a2c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,5 +1,8 @@ [run] branch = True +omit = + mcpgateway/alembic/* + */__init__.py [report] exclude_lines = diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index f8c786b11..7b1b2c2e9 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -140,7 +140,7 @@ def bootstrap( @app.callback() -def callback(): +def callback(): # pragma: no cover """This function exists to force 'bootstrap' to be a subcommand.""" pass From 429010bc210c9a056d28d7cf1ab9d1fc3cbe5c5c Mon Sep 17 00:00:00 2001 From: Teryl Taylor Date: Mon, 18 Aug 2025 00:40:25 -0600 Subject: [PATCH 50/65] tests(plugins): added client hook tests for external plugins. Signed-off-by: Teryl Taylor --- .../plugins/framework/external/mcp/client.py | 86 +++++++++++++++- .../valid_single_plugin_passthrough.yaml | 30 ++++++ ...lid_stdio_external_plugin_passthrough.yaml | 22 +++++ .../plugins/fixtures/plugins/__init__.py | 8 ++ .../plugins/fixtures/plugins/passthrough.py | 99 +++++++++++++++++++ .../external/mcp/test_client_stdio.py | 65 +++++++++++- 6 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 tests/unit/mcpgateway/plugins/fixtures/configs/valid_single_plugin_passthrough.yaml create mode 100644 tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_passthrough.yaml create mode 100644 tests/unit/mcpgateway/plugins/fixtures/plugins/__init__.py create mode 100644 tests/unit/mcpgateway/plugins/fixtures/plugins/passthrough.py diff --git a/mcpgateway/plugins/framework/external/mcp/client.py b/mcpgateway/plugins/framework/external/mcp/client.py index 3c1c6b202..ec8509725 100644 --- a/mcpgateway/plugins/framework/external/mcp/client.py +++ b/mcpgateway/plugins/framework/external/mcp/client.py @@ -32,7 +32,23 @@ PYTHON, PYTHON_SUFFIX, ) -from mcpgateway.plugins.framework.models import HookType, PluginConfig, PluginContext, PromptPosthookPayload, PromptPosthookResult, PromptPrehookPayload, PromptPrehookResult +from mcpgateway.plugins.framework.models import ( + HookType, + PluginConfig, + PluginContext, + PromptPosthookPayload, + PromptPosthookResult, + PromptPrehookPayload, + PromptPrehookResult, + ResourcePostFetchPayload, + ResourcePostFetchResult, + ResourcePreFetchPayload, + ResourcePreFetchResult, + ToolPostInvokePayload, + ToolPostInvokeResult, + ToolPreInvokePayload, + ToolPreInvokeResult, +) from mcpgateway.schemas import TransportType logger = logging.getLogger(__name__) @@ -162,6 +178,74 @@ async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: Plugi return PromptPosthookResult.model_validate(res) return PromptPosthookResult(continue_processing=True) + async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: + """Plugin hook run before a tool is invoked. + + Args: + payload: The tool payload to be analyzed. + context: contextual information about the hook call. Including why it was called. + + Returns: + The tool prehook with name and arguments as modified or blocked by the plugin. + """ + + result = await self._session.call_tool(HookType.TOOL_PRE_INVOKE, {PLUGIN_NAME: self.name, PAYLOAD: payload, CONTEXT: context}) + for content in result.content: + res = json.loads(content.text) + return ToolPreInvokeResult.model_validate(res) + return ToolPreInvokeResult(continue_processing=True) + + async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: + """Plugin hook run after a tool is invoked. + + Args: + payload: The tool payload to be analyzed. + context: contextual information about the hook call. Including why it was called. + + Returns: + The tool posthook with name and arguments as modified or blocked by the plugin. + """ + + result = await self._session.call_tool(HookType.TOOL_POST_INVOKE, {PLUGIN_NAME: self.name, PAYLOAD: payload, CONTEXT: context}) + for content in result.content: + res = json.loads(content.text) + return ToolPostInvokeResult.model_validate(res) + return ToolPostInvokeResult(continue_processing=True) + + async def resource_pre_fetch(self, payload: ResourcePreFetchPayload, context: PluginContext) -> ResourcePreFetchResult: + """Plugin hook run before a resource is fetched. + + Args: + payload: The resource payload to be analyzed. + context: contextual information about the hook call. Including why it was called. + + Returns: + The resource prehook with name and arguments as modified or blocked by the plugin. + """ + + result = await self._session.call_tool(HookType.RESOURCE_PRE_FETCH, {PLUGIN_NAME: self.name, PAYLOAD: payload, CONTEXT: context}) + for content in result.content: + res = json.loads(content.text) + return ResourcePreFetchResult.model_validate(res) + return ResourcePreFetchResult(continue_processing=True) + + async def resource_post_fetch(self, payload: ResourcePostFetchPayload, context: PluginContext) -> ResourcePostFetchResult: + """Plugin hook run after a resource is fetched. + + Args: + payload: The resource payload to be analyzed. + context: contextual information about the hook call. Including why it was called. + + Returns: + The resource posthook with name and arguments as modified or blocked by the plugin. + """ + + result = await self._session.call_tool(HookType.RESOURCE_POST_FETCH, {PLUGIN_NAME: self.name, PAYLOAD: payload, CONTEXT: context}) + for content in result.content: + res = json.loads(content.text) + return ResourcePostFetchResult.model_validate(res) + return ResourcePostFetchResult(continue_processing=True) + async def __get_plugin_config(self) -> PluginConfig | None: """Retrieve plugin configuration for the current plugin on the remote MCP server. diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_single_plugin_passthrough.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_single_plugin_passthrough.yaml new file mode 100644 index 000000000..effa3777a --- /dev/null +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_single_plugin_passthrough.yaml @@ -0,0 +1,30 @@ +plugins: + # Self-contained Search Replace Plugin + - name: "PassThroughPlugin" + kind: "tests.unit.mcpgateway.plugins.fixtures.plugins.passthrough.PassThroughPlugin" + description: "A simple passthrough plugin." + version: "0.1" + author: "MCP Context Forge Team" + hooks: ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke", "resource_pre_fetch", "resource_post_fetch"] + tags: ["plugin", "passthrough"] + mode: "enforce" # enforce | permissive | disabled + priority: 150 + conditions: + # Apply to specific tools/servers + - prompts: [] + server_ids: [] # Apply to all servers + tenant_ids: [] # Apply to all tenants + +# Plugin directories to scan +plugin_dirs: + - "plugins/native" # Built-in plugins + - "plugins/custom" # Custom organization plugins + - "/etc/mcpgateway/plugins" # System-wide plugins + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_passthrough.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_passthrough.yaml new file mode 100644 index 000000000..8686fecd8 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_passthrough.yaml @@ -0,0 +1,22 @@ +# plugins/config.yaml - Main plugin configuration file + +plugins: + - name: "PassThroughPlugin" + kind: "external" + mcp: + proto: STDIO + script: mcpgateway/plugins/framework/external/mcp/server/runtime.py + +# Plugin directories to scan +plugin_dirs: + - "plugins/native" # Built-in plugins + - "plugins/custom" # Custom organization plugins + - "/etc/mcpgateway/plugins" # System-wide plugins + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 diff --git a/tests/unit/mcpgateway/plugins/fixtures/plugins/__init__.py b/tests/unit/mcpgateway/plugins/fixtures/plugins/__init__.py new file mode 100644 index 000000000..63f957fa7 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/fixtures/plugins/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +""" \ No newline at end of file diff --git a/tests/unit/mcpgateway/plugins/fixtures/plugins/passthrough.py b/tests/unit/mcpgateway/plugins/fixtures/plugins/passthrough.py new file mode 100644 index 000000000..2f437a1e3 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/fixtures/plugins/passthrough.py @@ -0,0 +1,99 @@ +""" +Passthrough plugin. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +""" + + +from mcpgateway.plugins.framework import ( + Plugin, + PluginContext, + PromptPosthookPayload, + PromptPosthookResult, + PromptPrehookPayload, + PromptPrehookResult, + ResourcePostFetchPayload, + ResourcePostFetchResult, + ResourcePreFetchPayload, + ResourcePreFetchResult, + ToolPostInvokePayload, + ToolPostInvokeResult, + ToolPreInvokePayload, + ToolPreInvokeResult, +) + +class PassThroughPlugin(Plugin): + """A simple pass through plugin.""" + + async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: + """The plugin hook run before a prompt is retrieved and rendered. + + Args: + payload: The prompt payload to be analyzed. + context: contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the prompt can proceed. + """ + return PromptPrehookResult(continue_processing=True) + + async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult: + """Plugin hook run after a prompt is rendered. + + Args: + payload: The prompt payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the prompt can proceed. + """ + return PromptPosthookResult(continue_processing=True) + + async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: + """Plugin hook run before a tool is invoked. + + Args: + payload: The tool payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the tool can proceed. + """ + return ToolPreInvokeResult(continue_processing=True) + + async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: + """Plugin hook run after a tool is invoked. + + Args: + payload: The tool result payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the tool result should proceed. + """ + return ToolPostInvokeResult(continue_processing=True) + + async def resource_post_fetch(self, payload: ResourcePostFetchPayload, context: PluginContext) -> ResourcePostFetchResult: + """Plugin hook run after a resource was fetched. + + Args: + payload: The resource result payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the resource result should proceed. + """ + return ResourcePostFetchResult(continue_processing=True) + + async def resource_pre_fetch(self, payload: ResourcePreFetchPayload, context: PluginContext) -> ResourcePreFetchResult: + """Plugin hook run before a resource was fetched. + + Args: + payload: The resource result payload to be analyzed. + context: Contextual information about the hook call. + + Returns: + The result of the plugin's analysis, including whether the resource result should proceed. + """ + return ResourcePreFetchResult(continue_processing=True) diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py index 8048a322b..54a32f93d 100644 --- a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py @@ -13,8 +13,21 @@ from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client -from mcpgateway.models import Message, PromptResult, Role, TextContent -from mcpgateway.plugins.framework import ConfigLoader, PluginConfig, PluginLoader, PluginContext, PromptPrehookPayload, PromptPosthookPayload +from mcpgateway.models import Message, PromptResult, ResourceContent, Role, TextContent +from mcpgateway.plugins.framework import ( + ConfigLoader, + GlobalContext, + PluginConfig, + PluginLoader, + PluginManager, + PluginContext, + PromptPrehookPayload, + PromptPosthookPayload, + ResourcePostFetchPayload, + ResourcePreFetchPayload, + ToolPostInvokePayload, + ToolPreInvokePayload, +) from plugins.regex_filter.search_replace import SearchReplaceConfig @@ -143,3 +156,51 @@ async def test_client_get_plugin_configs(): assert srconfig.words[0].search == "crap" assert srconfig.words[0].replace == "crud" assert len(all_configs) == 2 + +@pytest.mark.asyncio +async def test_hooks(): + os.environ["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_single_plugin_passthrough.yaml" + os.environ["PYTHONPATH"] = "." + pm = PluginManager() + if pm.initialized: + await pm.shutdown() + plugin_manager = PluginManager(config="tests/unit/mcpgateway/plugins/fixtures/configs/valid_stdio_external_plugin_passthrough.yaml") + await plugin_manager.initialize() + payload = PromptPrehookPayload(name="test_prompt", args={"arg0": "This is a crap argument"}) + global_context = GlobalContext(request_id="1") + result, _ = await plugin_manager.prompt_pre_fetch(payload, global_context) + # Assert expected behaviors + assert result.continue_processing + """Test prompt post hook across all registered plugins.""" + # Customize payload for testing + message = Message(content=TextContent(type="text", text="prompt"), role=Role.USER) + prompt_result = PromptResult(messages=[message]) + payload = PromptPosthookPayload(name="test_prompt", result=prompt_result) + result, _ = await plugin_manager.prompt_post_fetch(payload, global_context) + # Assert expected behaviors + assert result.continue_processing + """Test tool pre hook across all registered plugins.""" + # Customize payload for testing + payload = ToolPreInvokePayload(name="test_prompt", args={"arg0": "This is an argument"}) + result, _ = await plugin_manager.tool_pre_invoke(payload, global_context) + # Assert expected behaviors + assert result.continue_processing + """Test tool post hook across all registered plugins.""" + # Customize payload for testing + payload = ToolPostInvokePayload(name="test_tool", result={"output0": "output value"}) + result, _ = await plugin_manager.tool_post_invoke(payload, global_context) + # Assert expected behaviors + assert result.continue_processing + + payload = ResourcePreFetchPayload(uri="file:///data.txt") + result, _ = await plugin_manager.resource_pre_fetch(payload, global_context) + # Assert expected behaviors + assert result.continue_processing + + content = ResourceContent(type="resource", uri="file:///data.txt", + text="Hello World") + payload = ResourcePostFetchPayload(uri="file:///data.txt", content=content) + result, _ = await plugin_manager.resource_post_fetch(payload, global_context) + # Assert expected behaviors + assert result.continue_processing + await plugin_manager.shutdown() From e8c8793aa20acfb659aecb0ab778bf4ac13e65ee Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 02:49:09 -0400 Subject: [PATCH 51/65] chore: update template readmes Signed-off-by: Frederico Araujo --- plugin_templates/external/README.md.jinja | 31 +++++++++++++++++++++-- plugin_templates/native/README.md.jinja | 26 +++---------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/plugin_templates/external/README.md.jinja b/plugin_templates/external/README.md.jinja index dcdc59a79..a7002efae 100644 --- a/plugin_templates/external/README.md.jinja +++ b/plugin_templates/external/README.md.jinja @@ -5,9 +5,14 @@ ## Installation +```bash +make install +``` ## Setting up the development environment +1. Copy .env.template .env +2. Enable plugins in `.env` ## Testing @@ -15,7 +20,7 @@ Test modules are created under the `tests` directory. To run all tests, use the following command: -``` +```bash make test ``` @@ -25,6 +30,28 @@ make test Before checking in any code for the project, please lint the code. This can be done using: -``` +```bash make lint-fix ``` + +## Runtime (server) + +This project uses [chuck-mcp-runtime](https://github.com/chrishayuk/chuk-mcp-runtime) to run external plugins as a standardized MCP server. + +To build the container image: + +```bash +make build +``` + +To run the container: + +```bash +make start +``` + +To stop the container: + +```bash +make stop +``` \ No newline at end of file diff --git a/plugin_templates/native/README.md.jinja b/plugin_templates/native/README.md.jinja index dcdc59a79..5a6e6ede6 100644 --- a/plugin_templates/native/README.md.jinja +++ b/plugin_templates/native/README.md.jinja @@ -5,26 +5,6 @@ ## Installation - -## Setting up the development environment - - -## Testing - -Test modules are created under the `tests` directory. - -To run all tests, use the following command: - -``` -make test -``` - -**Note:** To enable logging, set `log_cli = true` in `tests/pytest.ini`. - -## Code Linting - -Before checking in any code for the project, please lint the code. This can be done using: - -``` -make lint-fix -``` +1. Copy .env.example .env +2. Enable plugins in `.env` +3. Add the plugin configuration to `plugins/config.yaml`: From 34c8a1405d901715b005e59149770879a1859dfb Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 03:09:22 -0400 Subject: [PATCH 52/65] fix: lint docstrings in cli Signed-off-by: Frederico Araujo --- mcpgateway/plugins/tools/cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index 7b1b2c2e9..4a5467e0c 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -125,8 +125,9 @@ def bootstrap( destination: The directory in which to bootstrap the plugin project. template_url: The URL to the plugins copier template. vcs_ref: The version control system tag/branch/commit to use for the template. - answers-file: The copier answers file that can be used to skip interactive mode. + answers_file: The copier answers file that can be used to skip interactive mode. defaults: Bootstrap with defaults. + dry_run: Run but do not make any changes. """ run_copy( src_path=template_url, From baa3fa4789d6f1620cbe9bbd64d1dfb85b651dc9 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 03:17:56 -0400 Subject: [PATCH 53/65] chore: fix lint errors in docstrings Signed-off-by: Frederico Araujo --- mcpgateway/plugins/framework/external/mcp/server/runtime.py | 6 +++++- mcpgateway/plugins/framework/external/mcp/server/server.py | 3 --- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/mcpgateway/plugins/framework/external/mcp/server/runtime.py b/mcpgateway/plugins/framework/external/mcp/server/runtime.py index 2d87abd94..eb3c1e83d 100644 --- a/mcpgateway/plugins/framework/external/mcp/server/runtime.py +++ b/mcpgateway/plugins/framework/external/mcp/server/runtime.py @@ -187,7 +187,11 @@ def resource_post_fetch_func(plugin: Plugin, payload: ResourcePostFetchPayload, async def run(): - """Run the external plugin server.""" + """Run the external plugin server. + + Raises: + Exception: if unnable to run the plugin server. + """ global server server = ExternalPluginServer() if await server.initialize(): diff --git a/mcpgateway/plugins/framework/external/mcp/server/server.py b/mcpgateway/plugins/framework/external/mcp/server/server.py index 329195b3b..5c13f69e3 100644 --- a/mcpgateway/plugins/framework/external/mcp/server/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server/server.py @@ -46,9 +46,6 @@ def __init__(self, config_path: str | None = None) -> None: config_path: The configuration file path for loading plugins. If set, this attribute overrides the value in PLUGINS_CONFIG_PATH. - Returns: - An external plugin server instance. - Examples: >>> server = ExternalPluginServer(config_path="./tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") >>> server is not None From 09a79c70c854be394ce06dfedf1c8c8598914a5b Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 03:24:57 -0400 Subject: [PATCH 54/65] chore: fix lint errors Signed-off-by: Frederico Araujo --- .../plugins/framework/external/mcp/server/server.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mcpgateway/plugins/framework/external/mcp/server/server.py b/mcpgateway/plugins/framework/external/mcp/server/server.py index 5c13f69e3..160f0087d 100644 --- a/mcpgateway/plugins/framework/external/mcp/server/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server/server.py @@ -150,15 +150,15 @@ async def invoke_hook( return convert_exception_to_error(ex, plugin_name=plugin_name).model_dump() async def initialize(self) -> bool: + """Initialize the plugin server. + + Returns: + A boolean indicating the intialization status of the server. + """ await self._plugin_manager.initialize() return self._plugin_manager.initialized async def shutdown(self) -> None: + """Shutdow the plugin server.""" if self._plugin_manager.initialized: await self._plugin_manager.shutdown() - - -if __name__ == "__main__": # pragma: no cover - executed only when run directly - # launch - server = ExternalPluginServer() - asyncio.run(server.run()) From 79cf12c98f8b07b1c9e1bcace7083e7ef28e3bb8 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 04:29:40 -0400 Subject: [PATCH 55/65] tests: add external plugin server tests Signed-off-by: Frederico Araujo --- .../valid_multiple_plugins_filter.yaml | 32 +++++ .../framework/external/mcp/server/__init__.py | 8 ++ .../external/mcp/server/test_runtime.py | 116 ++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 tests/unit/mcpgateway/plugins/framework/external/mcp/server/__init__.py create mode 100644 tests/unit/mcpgateway/plugins/framework/external/mcp/server/test_runtime.py diff --git a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml index 7e6258e3a..d75ca8d90 100644 --- a/tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml +++ b/tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml @@ -40,6 +40,38 @@ plugins: replace: crud - search: crud replace: yikes + - name: "ResourceFilterExample" + kind: "plugins.resource_filter.resource_filter.ResourceFilterPlugin" + description: "Demonstrates resource pre/post fetch hooks for filtering and validation" + version: "1.0.0" + author: "MCP Gateway Team" + hooks: ["resource_pre_fetch", "resource_post_fetch"] + tags: ["resource", "filter", "security", "example"] + mode: "enforce" # Block resources that violate rules + priority: 200 + conditions: [] # Apply to all resources + config: + # Maximum content size in bytes (1MB) + max_content_size: 1048576 + # Allowed protocols (removing file for testing) + allowed_protocols: + - test + - time + - timezone + - http + - https + # Blocked domains (examples) + blocked_domains: + - malicious.example.com + - untrusted-site.net + # Content filters to redact sensitive data + content_filters: + - pattern: "password\\s*[:=]\\s*\\S+" + replacement: "password: [REDACTED]" + - pattern: "api[_-]?key\\s*[:=]\\s*\\S+" + replacement: "api_key: [REDACTED]" + - pattern: "secret\\s*[:=]\\s*\\S+" + replacement: "secret: [REDACTED]" # Plugin directories to scan diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/server/__init__.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/server/__init__.py new file mode 100644 index 000000000..63f957fa7 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/server/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +""" + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Teryl Taylor + +""" \ No newline at end of file diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/server/test_runtime.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/server/test_runtime.py new file mode 100644 index 000000000..473166a91 --- /dev/null +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/server/test_runtime.py @@ -0,0 +1,116 @@ +""" +Tests for external client on stdio. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Fred Araujo +""" + +# Standard +import asyncio + +# Third-Party +import pytest + +# First-Party +from mcpgateway.models import Message, PromptResult, Role, TextContent +from mcpgateway.plugins.framework import ( + PluginContext, + PromptPosthookPayload, + PromptPrehookPayload, + ResourcePostFetchPayload, + ResourcePreFetchPayload, + ToolPostInvokePayload, + ToolPostInvokeResult, + ToolPreInvokePayload, +) +from mcpgateway.plugins.framework.external.mcp.server import ExternalPluginServer +import mcpgateway.plugins.framework.external.mcp.server.runtime as runtime + + +@pytest.fixture(scope="module", autouse=True) +def server(): + server = ExternalPluginServer(config_path="./tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml") + asyncio.run(server.initialize()) + yield server + asyncio.run(server.shutdown()) + + +@pytest.mark.asyncio +async def test_get_plugin_configs(monkeypatch, server): + monkeypatch.setattr(runtime, "server", server) + configs = await runtime.get_plugin_configs() + assert len(configs) > 0 + + +@pytest.mark.asyncio +async def test_get_plugin_config(monkeypatch, server): + monkeypatch.setattr(runtime, "server", server) + config = await runtime.get_plugin_config(name="DenyListPlugin") + assert config["name"] == "DenyListPlugin" + + +@pytest.mark.asyncio +async def test_prompt_pre_fetch(monkeypatch, server): + monkeypatch.setattr(runtime, "server", server) + payload = PromptPrehookPayload(name="test_prompt", args={"user": "This is so innovative"}) + context = PluginContext(request_id="1", server_id="2") + result = await runtime.prompt_pre_fetch("DenyListPlugin", payload=payload, context=context) + assert result + assert not result["continue_processing"] + + +@pytest.mark.asyncio +async def test_prompt_post_fetch(monkeypatch, server): + monkeypatch.setattr(runtime, "server", server) + message = Message(content=TextContent(type="text", text="crap prompt"), role=Role.USER) + prompt_result = PromptResult(messages=[message]) + payload = PromptPosthookPayload(name="test_prompt", result=prompt_result) + context = PluginContext(request_id="1", server_id="2") + result = await runtime.prompt_post_fetch("ReplaceBadWordsPlugin", payload=payload, context=context) + assert result + assert result["continue_processing"] + assert "crap" not in result["modified_payload"] + + +@pytest.mark.asyncio +async def test_tool_pre_invoke(monkeypatch, server): + monkeypatch.setattr(runtime, "server", server) + payload = ToolPreInvokePayload(name="test_tool", args={"arg0": "Good argument"}) + context = PluginContext(request_id="1", server_id="2") + result = await runtime.tool_pre_invoke("ReplaceBadWordsPlugin", payload=payload, context=context) + assert result + assert result["continue_processing"] + + +@pytest.mark.asyncio +async def test_tool_post_invoke(monkeypatch, server): + monkeypatch.setattr(runtime, "server", server) + message = Message(content=TextContent(type="text", text="crap result"), role=Role.USER) + prompt_result = ToolPostInvokeResult(messages=[message]) + payload = ToolPostInvokePayload(name="test_prompt", result=prompt_result) + context = PluginContext(request_id="1", server_id="2") + result = await runtime.tool_post_invoke("ReplaceBadWordsPlugin", payload=payload, context=context) + assert result + assert result["continue_processing"] + assert "crap" not in result["modified_payload"] + + +@pytest.mark.asyncio +async def test_resource_pre_fetch(monkeypatch, server): + monkeypatch.setattr(runtime, "server", server) + payload = ResourcePreFetchPayload(uri="resource", metadata={"arg0": "Good argument"}) + context = PluginContext(request_id="1", server_id="2") + result = await runtime.resource_pre_fetch("ResourceFilterExample", payload=payload, context=context) + assert result + assert not result["continue_processing"] + + +@pytest.mark.asyncio +async def test_tool_post_invoke(monkeypatch, server): + monkeypatch.setattr(runtime, "server", server) + payload = ResourcePostFetchPayload(uri="resource", content="content") + context = PluginContext(request_id="1", server_id="2") + result = await runtime.resource_post_fetch("ResourceFilterExample", payload=payload, context=context) + assert result + assert result["continue_processing"] From 3f89f92bc3df3f7e8b641ffa3f6957194742a9ee Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 04:34:37 -0400 Subject: [PATCH 56/65] chore: cleanup Signed-off-by: Frederico Araujo --- mcpgateway/plugins/framework/external/mcp/server/runtime.py | 2 +- mcpgateway/plugins/framework/external/mcp/server/server.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/mcpgateway/plugins/framework/external/mcp/server/runtime.py b/mcpgateway/plugins/framework/external/mcp/server/runtime.py index eb3c1e83d..f067ca998 100644 --- a/mcpgateway/plugins/framework/external/mcp/server/runtime.py +++ b/mcpgateway/plugins/framework/external/mcp/server/runtime.py @@ -186,7 +186,7 @@ def resource_post_fetch_func(plugin: Plugin, payload: ResourcePostFetchPayload, return await server.invoke_hook(ResourcePostFetchPayload, resource_post_fetch_func, plugin_name, payload, context) -async def run(): +async def run(): # pragma: no cover - executed only when run directly """Run the external plugin server. Raises: diff --git a/mcpgateway/plugins/framework/external/mcp/server/server.py b/mcpgateway/plugins/framework/external/mcp/server/server.py index 160f0087d..e91ee8a83 100644 --- a/mcpgateway/plugins/framework/external/mcp/server/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server/server.py @@ -33,9 +33,6 @@ logger = logging.getLogger(__name__) -# config_file = os.environ.get("PLUGINS_CONFIG_PATH", os.path.join(".", "resources", "plugins", "config.yaml")) -# global_plugin_manager = None - class ExternalPluginServer: From 73025dc30884f62ea4f16b666fb65ae1a8ba7848 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 04:51:11 -0400 Subject: [PATCH 57/65] chore: add missing docstrings Signed-off-by: Frederico Araujo --- .../framework/external/mcp/server/runtime.py | 70 +++++++++++++++++-- .../framework/external/mcp/server/server.py | 1 + 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/mcpgateway/plugins/framework/external/mcp/server/runtime.py b/mcpgateway/plugins/framework/external/mcp/server/runtime.py index f067ca998..0f714d17a 100644 --- a/mcpgateway/plugins/framework/external/mcp/server/runtime.py +++ b/mcpgateway/plugins/framework/external/mcp/server/runtime.py @@ -1,3 +1,11 @@ +# -*- coding: utf-8 -*- +"""Runtime MCP server for external plugins. + +Copyright 2025 +SPDX-License-Identifier: Apache-2.0 +Authors: Fred Araujo +""" + # Standard import asyncio import logging @@ -71,6 +79,15 @@ async def prompt_pre_fetch(plugin_name: str, payload: Dict[str, Any], context: D """ def prompt_pre_fetch_func(plugin: Plugin, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: + """Wrapper function for hook. + + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. + + Returns: + The transformed or filtered response from the plugin hook. + """ return plugin.prompt_pre_fetch(payload, context) return await server.invoke_hook(PromptPrehookPayload, prompt_pre_fetch_func, plugin_name, payload, context) @@ -93,6 +110,15 @@ async def prompt_post_fetch(plugin_name: str, payload: Dict[str, Any], context: """ def prompt_post_fetch_func(plugin: Plugin, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult: + """Wrapper function for hook. + + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. + + Returns: + The transformed or filtered response from the plugin hook. + """ return plugin.prompt_post_fetch(payload, context) return await server.invoke_hook(PromptPosthookPayload, prompt_post_fetch_func, plugin_name, payload, context) @@ -115,6 +141,15 @@ async def tool_pre_invoke(plugin_name: str, payload: Dict[str, Any], context: Di """ def tool_pre_invoke_func(plugin: Plugin, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: + """Wrapper function for hook. + + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. + + Returns: + The transformed or filtered response from the plugin hook. + """ return plugin.tool_pre_invoke(payload, context) return await server.invoke_hook(ToolPreInvokePayload, tool_pre_invoke_func, plugin_name, payload, context) @@ -137,6 +172,15 @@ async def tool_post_invoke(plugin_name: str, payload: Dict[str, Any], context: D """ def tool_post_invoke_func(plugin: Plugin, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: + """Wrapper function for hook. + + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. + + Returns: + The transformed or filtered response from the plugin hook. + """ return plugin.tool_post_invoke(payload, context) return await server.invoke_hook(ToolPostInvokePayload, tool_post_invoke_func, plugin_name, payload, context) @@ -158,7 +202,16 @@ async def resource_pre_fetch(plugin_name: str, payload: Dict[str, Any], context: The transformed or filtered response from the plugin hook. """ - def resource_pre_fetch_func(plugin: Plugin, payload: ResourcePreFetchPayload, context: PluginContext) -> ResourcePreFetchResult: + def resource_pre_fetch_func(plugin: Plugin, payload: ResourcePreFetchPayload, context: PluginContext) -> ResourcePreFetchResult: # pragma: no cover + """Wrapper function for hook. + + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. + + Returns: + The transformed or filtered response from the plugin hook. + """ return plugin.resource_pre_fetch(payload, context) return await server.invoke_hook(ResourcePreFetchPayload, resource_pre_fetch_func, plugin_name, payload, context) @@ -180,13 +233,22 @@ async def resource_post_fetch(plugin_name: str, payload: Dict[str, Any], context The result of the plugin execution. """ - def resource_post_fetch_func(plugin: Plugin, payload: ResourcePostFetchPayload, context: PluginContext) -> ResourcePostFetchResult: + def resource_post_fetch_func(plugin: Plugin, payload: ResourcePostFetchPayload, context: PluginContext) -> ResourcePostFetchResult: # pragma: no cover + """Wrapper function for hook. + + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. + + Returns: + The transformed or filtered response from the plugin hook. + """ return plugin.resource_post_fetch(payload, context) return await server.invoke_hook(ResourcePostFetchPayload, resource_post_fetch_func, plugin_name, payload, context) -async def run(): # pragma: no cover - executed only when run directly +async def run(): # pragma: no cover """Run the external plugin server. Raises: @@ -204,6 +266,6 @@ async def run(): # pragma: no cover - executed only when run directly await server.shutdown() -if __name__ == "__main__": # pragma: no cover - executed only when run directly +if __name__ == "__main__": # pragma: no cover # launch asyncio.run(run()) diff --git a/mcpgateway/plugins/framework/external/mcp/server/server.py b/mcpgateway/plugins/framework/external/mcp/server/server.py index e91ee8a83..2dcfe4302 100644 --- a/mcpgateway/plugins/framework/external/mcp/server/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server/server.py @@ -35,6 +35,7 @@ class ExternalPluginServer: + """External plugin server, providing methods for invoking plugin hooks.""" def __init__(self, config_path: str | None = None) -> None: """Create an external plugin server. From 82c476013e14ded83a3dd2742a9705468ee59588 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 04:56:29 -0400 Subject: [PATCH 58/65] chore: add missing docstrings Signed-off-by: Frederico Araujo --- .../framework/external/mcp/server/runtime.py | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/mcpgateway/plugins/framework/external/mcp/server/runtime.py b/mcpgateway/plugins/framework/external/mcp/server/runtime.py index 0f714d17a..87f8d6f22 100644 --- a/mcpgateway/plugins/framework/external/mcp/server/runtime.py +++ b/mcpgateway/plugins/framework/external/mcp/server/runtime.py @@ -81,9 +81,10 @@ async def prompt_pre_fetch(plugin_name: str, payload: Dict[str, Any], context: D def prompt_pre_fetch_func(plugin: Plugin, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult: """Wrapper function for hook. - plugin: The plugin instance. - payload: The tool name and arguments to be analyzed. - context: the contextual and state information required for the execution of the hook. + Args: + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. Returns: The transformed or filtered response from the plugin hook. @@ -112,9 +113,10 @@ async def prompt_post_fetch(plugin_name: str, payload: Dict[str, Any], context: def prompt_post_fetch_func(plugin: Plugin, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult: """Wrapper function for hook. - plugin: The plugin instance. - payload: The tool name and arguments to be analyzed. - context: the contextual and state information required for the execution of the hook. + Args: + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. Returns: The transformed or filtered response from the plugin hook. @@ -143,9 +145,10 @@ async def tool_pre_invoke(plugin_name: str, payload: Dict[str, Any], context: Di def tool_pre_invoke_func(plugin: Plugin, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult: """Wrapper function for hook. - plugin: The plugin instance. - payload: The tool name and arguments to be analyzed. - context: the contextual and state information required for the execution of the hook. + Args: + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. Returns: The transformed or filtered response from the plugin hook. @@ -174,9 +177,10 @@ async def tool_post_invoke(plugin_name: str, payload: Dict[str, Any], context: D def tool_post_invoke_func(plugin: Plugin, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult: """Wrapper function for hook. - plugin: The plugin instance. - payload: The tool name and arguments to be analyzed. - context: the contextual and state information required for the execution of the hook. + Args: + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. Returns: The transformed or filtered response from the plugin hook. @@ -205,9 +209,10 @@ async def resource_pre_fetch(plugin_name: str, payload: Dict[str, Any], context: def resource_pre_fetch_func(plugin: Plugin, payload: ResourcePreFetchPayload, context: PluginContext) -> ResourcePreFetchResult: # pragma: no cover """Wrapper function for hook. - plugin: The plugin instance. - payload: The tool name and arguments to be analyzed. - context: the contextual and state information required for the execution of the hook. + Args: + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. Returns: The transformed or filtered response from the plugin hook. @@ -236,9 +241,10 @@ async def resource_post_fetch(plugin_name: str, payload: Dict[str, Any], context def resource_post_fetch_func(plugin: Plugin, payload: ResourcePostFetchPayload, context: PluginContext) -> ResourcePostFetchResult: # pragma: no cover """Wrapper function for hook. - plugin: The plugin instance. - payload: The tool name and arguments to be analyzed. - context: the contextual and state information required for the execution of the hook. + Args: + plugin: The plugin instance. + payload: The tool name and arguments to be analyzed. + context: the contextual and state information required for the execution of the hook. Returns: The transformed or filtered response from the plugin hook. From 04ad7939a22b2f2a62a99d17a44ae6ab7d746c6a Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 09:29:40 -0400 Subject: [PATCH 59/65] tests: fix cli dryrun test Signed-off-by: Frederico Araujo --- mcpgateway/plugins/tools/cli.py | 40 ++++++++++++++----- .../unit/mcpgateway/plugins/tools/test_cli.py | 2 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index 4a5467e0c..8146fbb10 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -26,7 +26,9 @@ """ # Standard +import logging from pathlib import Path +import shutil import subprocess from typing import Optional @@ -38,6 +40,8 @@ # First-Party from mcpgateway.config import settings +logger = logging.getLogger(__name__) + # --------------------------------------------------------------------------- # Configuration defaults # --------------------------------------------------------------------------- @@ -71,6 +75,18 @@ # --------------------------------------------------------------------------- +def command_exists(command_name): + """Check if a given command-line utility exists and is executable. + + Args: + command_name: The name of the command to check (e.g., "ls", "git"). + + Returns: + True if the command exists and is executable, False otherwise. + """ + return shutil.which(command_name) is not None + + def git_user_name() -> str: """Return the current git user name from the environment. @@ -129,15 +145,21 @@ def bootstrap( defaults: Bootstrap with defaults. dry_run: Run but do not make any changes. """ - run_copy( - src_path=template_url, - dst_path=destination, - answers_file=answers_file, - defaults=defaults, - vcs_ref=vcs_ref, - data={"default_author_name": git_user_name(), "default_author_email": git_user_email()}, - pretend=dry_run, - ) + try: + if command_exists("git"): + run_copy( + src_path=template_url, + dst_path=destination, + answers_file=answers_file, + defaults=defaults, + vcs_ref=vcs_ref, + data={"default_author_name": git_user_name(), "default_author_email": git_user_email()}, + pretend=dry_run, + ) + else: + logger.warning("A git client was not found in the environment to copy remote template.") + except Exception: + logger.exception("An error was caught while copying template.") @app.callback() diff --git a/tests/unit/mcpgateway/plugins/tools/test_cli.py b/tests/unit/mcpgateway/plugins/tools/test_cli.py index b0a5116c8..9663bc9df 100644 --- a/tests/unit/mcpgateway/plugins/tools/test_cli.py +++ b/tests/unit/mcpgateway/plugins/tools/test_cli.py @@ -34,7 +34,7 @@ def test_bootrap_command_help(runner: CliRunner): result = runner.invoke(cli.app, raw) assert "Creates a new plugin project from template" in result.stdout -def test_bootrap_command_dry_run(runner: CliRunner): +def test_bootstrap_command_dry_run(runner: CliRunner): """Boostrapping dry run.""" raw = ["bootstrap", "--destination", "/tmp/myplugin", "--template_url", ".", "--defaults", "--dry_run"] result = runner.invoke(cli.app, raw) From 5bdb7c43591b94488efe58501563b20387786e5f Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 10:20:09 -0400 Subject: [PATCH 60/65] chore: fix lint issues Signed-off-by: Frederico Araujo --- mcpgateway/plugins/tools/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index 8146fbb10..9b00774f8 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -79,10 +79,10 @@ def command_exists(command_name): """Check if a given command-line utility exists and is executable. Args: - command_name: The name of the command to check (e.g., "ls", "git"). + command_name: The name of the command to check (e.g., "ls", "git"). Returns: - True if the command exists and is executable, False otherwise. + True if the command exists and is executable, False otherwise. """ return shutil.which(command_name) is not None From 6fe7e2b563f8aca7b67c2084a8ccd47aadfd7956 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 12:38:56 -0400 Subject: [PATCH 61/65] tests: fix teardown of client http tests Signed-off-by: Frederico Araujo --- .../mcp/test_client_streamable_http.py | 68 +++++++++++++------ 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py index 06e5b14db..b42512791 100644 --- a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py @@ -14,20 +14,27 @@ from mcpgateway.models import Message, PromptResult, Role, TextContent from mcpgateway.plugins.framework import ConfigLoader, PluginLoader, PluginContext, PromptPrehookPayload, PromptPosthookPayload -@pytest.mark.asyncio -async def test_client_load_streamable_http(): - +@pytest.fixture(autouse=True) +def server_proc(): current_env = os.environ.copy() current_env["CHUK_MCP_CONFIG_PATH"] = "plugins/resources/server/config-http.yaml" current_env["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_single_plugin.yaml" current_env["PYTHONPATH"] = "." # Start the server as a subprocess - server_proc = subprocess.Popen([sys.executable, "mcpgateway/plugins/framework/external/mcp/server/runtime.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=current_env) - time.sleep(2) # Give the server time to start + try: + with subprocess.Popen([sys.executable, "mcpgateway/plugins/framework/external/mcp/server/runtime.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=current_env) as server_proc: + time.sleep(2) # Give the server time to start + yield server_proc + server_proc.terminate() + server_proc.wait(timeout=3) # Wait for the subprocess to complete + except subprocess.TimeoutExpired: + server_proc.kill() # Force kill if timeout occurs + server_proc.wait(timeout=3) +@pytest.mark.asyncio +async def test_client_load_streamable_http(server_proc): assert not server_proc.poll(), "Server failed to start" - config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_regex.yaml") loader = PluginLoader() @@ -54,17 +61,27 @@ async def test_client_load_streamable_http(): server_proc.terminate() server_proc.wait() # Wait for the process to fully terminate -@pytest.mark.asyncio -async def test_client_load_strhttp_overrides(): +@pytest.fixture(autouse=True) +def server_proc1(): current_env = os.environ.copy() current_env["CHUK_MCP_CONFIG_PATH"] = "plugins/resources/server/config-http.yaml" current_env["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" current_env["PYTHONPATH"] = "." # Start the server as a subprocess - server_proc = subprocess.Popen([sys.executable, "mcpgateway/plugins/framework/external/mcp/server/runtime.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=current_env) - time.sleep(2) # Give the server time to start - - assert not server_proc.poll(), "Server failed to start" + try: + with subprocess.Popen([sys.executable, "mcpgateway/plugins/framework/external/mcp/server/runtime.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=current_env) as server_proc: + time.sleep(2) # Give the server time to start + yield server_proc + server_proc.terminate() + server_proc.wait(timeout=3) # Wait for the subprocess to complete + except subprocess.TimeoutExpired: + server_proc.kill() # Force kill if timeout occurs + server_proc.wait(timeout=3) + +@pytest.mark.skip(reason="Flaky, need to debug.") +@pytest.mark.asyncio +async def test_client_load_strhttp_overrides(server_proc1): + assert not server_proc1.poll(), "Server failed to start" config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_overrides.yaml") @@ -85,20 +102,29 @@ async def test_client_load_strhttp_overrides(): assert config.kind == "external" await plugin.shutdown() await loader.shutdown() - server_proc.terminate() - server_proc.wait() # Wait for the process to fully terminate + server_proc1.terminate() + server_proc1.wait() # Wait for the process to fully terminate -@pytest.mark.asyncio -async def test_client_load_strhttp_post_prompt(): +@pytest.fixture(autouse=True) +def server_proc2(): current_env = os.environ.copy() current_env["CHUK_MCP_CONFIG_PATH"] = "plugins/resources/server/config-http.yaml" current_env["PLUGINS_CONFIG_PATH"] = "tests/unit/mcpgateway/plugins/fixtures/configs/valid_multiple_plugins_filter.yaml" current_env["PYTHONPATH"] = "." # Start the server as a subprocess - server_proc = subprocess.Popen([sys.executable, "mcpgateway/plugins/framework/external/mcp/server/runtime.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=current_env) - time.sleep(2) # Give the server time to start + try: + with subprocess.Popen([sys.executable, "mcpgateway/plugins/framework/external/mcp/server/runtime.py"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=current_env) as server_proc: + time.sleep(2) # Give the server time to start + yield server_proc + server_proc.terminate() + server_proc.wait(timeout=3) # Wait for the subprocess to complete + except subprocess.TimeoutExpired: + server_proc.kill() # Force kill if timeout occurs + server_proc.wait(timeout=3) - assert not server_proc.poll(), "Server failed to start" +@pytest.mark.asyncio +async def test_client_load_strhttp_post_prompt(server_proc2): + assert not server_proc2.poll(), "Server failed to start" config = ConfigLoader.load_config("tests/unit/mcpgateway/plugins/fixtures/configs/valid_strhttp_external_plugin_regex.yaml") @@ -124,5 +150,5 @@ async def test_client_load_strhttp_post_prompt(): assert result.modified_payload.result.messages[0].content.text == "What the yikes?" await plugin.shutdown() await loader.shutdown() - server_proc.terminate() - server_proc.wait() # Wait for the process to fully terminate + server_proc2.terminate() + server_proc2.wait() # Wait for the process to fully terminate From a9e2011d3dcd13102d0960f28c23bc217b51d953 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 12:55:44 -0400 Subject: [PATCH 62/65] tests: skipping flaky tests Signed-off-by: Frederico Araujo --- .../framework/external/mcp/test_client_streamable_http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py index b42512791..503389b52 100644 --- a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py @@ -31,6 +31,7 @@ def server_proc(): server_proc.kill() # Force kill if timeout occurs server_proc.wait(timeout=3) +@pytest.mark.skip(reason="Flaky, fails on Python 3.12, need to debug.") @pytest.mark.asyncio async def test_client_load_streamable_http(server_proc): assert not server_proc.poll(), "Server failed to start" @@ -122,6 +123,7 @@ def server_proc2(): server_proc.kill() # Force kill if timeout occurs server_proc.wait(timeout=3) +@pytest.mark.skip(reason="Flaky, fails on Python 3.12, need to debug.") @pytest.mark.asyncio async def test_client_load_strhttp_post_prompt(server_proc2): assert not server_proc2.poll(), "Server failed to start" From 094ead218d1ceed01bce1c15d760426a788a11e2 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 15:14:18 -0400 Subject: [PATCH 63/65] docs: plugin lifecycle tools Signed-off-by: Frederico Araujo --- docs/docs/index.md | 2682 +++++++++++++++++++++++++++----- docs/docs/using/plugins/.pages | 1 + 2 files changed, 2284 insertions(+), 399 deletions(-) diff --git a/docs/docs/index.md b/docs/docs/index.md index 5494dd4aa..c5e80d262 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1,429 +1,2313 @@ +# MCP Gateway + +> Model Context Protocol gateway & proxy - unify REST, MCP, and A2A with federation, virtual servers, retries, security, and an optional admin UI. + +![](docs/docs/images/contextforge-banner.png) + + +[![Build Python Package](https://github.com/IBM/mcp-context-forge/actions/workflows/python-package.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/python-package.yml)  +[![CodeQL](https://github.com/IBM/mcp-context-forge/actions/workflows/codeql.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/codeql.yml)  +[![Bandit Security](https://github.com/IBM/mcp-context-forge/actions/workflows/bandit.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/bandit.yml)  +[![Dependency Review](https://github.com/IBM/mcp-context-forge/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/dependency-review.yml)  +[![Tests & Coverage](https://github.com/IBM/mcp-context-forge/actions/workflows/pytest.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/pytest.yml)  +[![Lint & Static Analysis](https://github.com/IBM/mcp-context-forge/actions/workflows/lint.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/lint.yml) + + +[![Secure Docker Build](https://github.com/IBM/mcp-context-forge/actions/workflows/docker-image.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/docker-image.yml)  +[![Deploy to IBM Code Engine](https://github.com/IBM/mcp-context-forge/actions/workflows/ibm-cloud-code-engine.yml/badge.svg)](https://github.com/IBM/mcp-context-forge/actions/workflows/ibm-cloud-code-engine.yml) + + +[![Async](https://img.shields.io/badge/async-await-green.svg)](https://docs.python.org/3/library/asyncio.html) +[![License](https://img.shields.io/github/license/ibm/mcp-context-forge)](LICENSE)  +[![PyPI](https://img.shields.io/pypi/v/mcp-contextforge-gateway)](https://pypi.org/project/mcp-contextforge-gateway/)  +[![Docker Image](https://img.shields.io/badge/docker-ghcr.io%2Fibm%2Fmcp--context--forge-blue)](https://github.com/ibm/mcp-context-forge/pkgs/container/mcp-context-forge)  + + +ContextForge MCP Gateway is a feature-rich gateway, proxy and MCP Registry that federates MCP and REST services - unifying discovery, auth, rate-limiting, observability, virtual servers, multi-transport protocols, and an optional Admin UI into one clean endpoint for your AI clients. It runs as a fully compliant MCP server, deployable via PyPI or Docker, and scales to multi-cluster environments on Kubernetes with Redis-backed federation and caching. + +![MCP Gateway](https://ibm.github.io/mcp-context-forge/images/mcpgateway.gif) --- -classification: -status: draft -owner: Mihai Criveti + + +## Table of Contents + +* 1. [Table of Contents](#table-of-contents) +* 2. [๐Ÿš€ Overview & Goals](#-overview--goals) +* 3. [Quick Start - PyPI](#quick-start---pypi) + * 3.1. [1 - Install & run (copy-paste friendly)](#1---install--run-copy-paste-friendly) +* 4. [Quick Start - Containers](#quick-start---containers) + * 4.1. [๐Ÿณ Docker](#-docker) + * 4.1.1. [1 - Minimum viable run](#1---minimum-viable-run) + * 4.1.2. [2 - Persist the SQLite database](#2---persist-the-sqlite-database) + * 4.1.3. [3 - Local tool discovery (host network)](#3---local-tool-discovery-host-network) + * 4.2. [๐Ÿฆญ Podman (rootless-friendly)](#-podman-rootless-friendly) + * 4.2.1. [1 - Basic run](#1---basic-run) + * 4.2.2. [2 - Persist SQLite](#2---persist-sqlite) + * 4.2.3. [3 - Host networking (rootless)](#3---host-networking-rootless) +* 5. [Testing `mcpgateway.wrapper` by hand](#testing-mcpgatewaywrapper-by-hand) + * 5.1. [๐Ÿงฉ Running from an MCP Client (`mcpgateway.wrapper`)](#-running-from-an-mcp-client-mcpgatewaywrapper) + * 5.1.1. [1 - Install `uv` (`uvx` is an alias it provides)](#1---install-uv-uvx-is-an-alias-it-provides) + * 5.1.2. [2 - Create an on-the-spot venv & run the wrapper](#2---create-an-on-the-spot-venv--run-the-wrapper) + * 5.1.3. [Claude Desktop JSON (runs through **uvx**)](#claude-desktop-json-runs-through-uvx) + * 5.2. [๐Ÿš€ Using with Claude Desktop (or any GUI MCP client)](#-using-with-claude-desktop-or-any-gui-mcp-client) +* 6. [๐Ÿš€ Quick Start: VS Code Dev Container](#-quick-start-vs-code-dev-container) + * 6.1. [1 - Clone & Open](#1---clone--open) + * 6.2. [2 - First-Time Build (Automatic)](#2---first-time-build-automatic) +* 7. [Quick Start (manual install)](#quick-start-manual-install) + * 7.1. [Prerequisites](#prerequisites) + * 7.2. [One-liner (dev)](#one-liner-dev) + * 7.3. [Containerized (self-signed TLS)](#containerized-self-signed-tls) + * 7.4. [Smoke-test the API](#smoke-test-the-api) +* 8. [Installation](#installation) + * 8.1. [Via Make](#via-make) + * 8.2. [UV (alternative)](#uv-alternative) + * 8.3. [pip (alternative)](#pip-alternative) + * 8.4. [Optional (PostgreSQL adapter)](#optional-postgresql-adapter) + * 8.4.1. [Quick Postgres container](#quick-postgres-container) +* 9. [Configuration (`.env` or env vars)](#configuration-env-or-env-vars) + * 9.1. [Basic](#basic) + * 9.2. [Authentication](#authentication) + * 9.3. [UI Features](#ui-features) + * 9.4. [Security](#security) + * 9.5. [Logging](#logging) + * 9.6. [Transport](#transport) + * 9.7. [Federation](#federation) + * 9.8. [Resources](#resources) + * 9.9. [Tools](#tools) + * 9.10. [Prompts](#prompts) + * 9.11. [Health Checks](#health-checks) + * 9.12. [Database](#database) + * 9.13. [Cache Backend](#cache-backend) + * 9.14. [Development](#development) +* 10. [Running](#running) + * 10.1. [Makefile](#makefile) + * 10.2. [Script helper](#script-helper) + * 10.3. [Manual (Uvicorn)](#manual-uvicorn) +* 11. [Authentication examples](#authentication-examples) +* 12. [โ˜๏ธ AWS / Azure / OpenShift](#๏ธ-aws--azure--openshift) +* 13. [โ˜๏ธ IBM Cloud Code Engine Deployment](#๏ธ-ibm-cloud-code-engine-deployment) + * 13.1. [๐Ÿ”ง Prerequisites](#-prerequisites-1) + * 13.2. [๐Ÿ“ฆ Environment Variables](#-environment-variables) + * 13.3. [๐Ÿš€ Make Targets](#-make-targets) + * 13.4. [๐Ÿ“ Example Workflow](#-example-workflow) +* 14. [API Endpoints](#api-endpoints) +* 15. [Testing](#testing) +* 16. [Project Structure](#project-structure) +* 17. [API Documentation](#api-documentation) +* 18. [Makefile targets](#makefile-targets) +* 19. [๐Ÿ” Troubleshooting](#-troubleshooting) + * 19.1. [Diagnose the listener](#diagnose-the-listener) + * 19.2. [Why localhost fails on Windows](#why-localhost-fails-on-windows) + * 19.2.1. [Fix (Podman rootless)](#fix-podman-rootless) + * 19.2.2. [Fix (Docker Desktop > 4.19)](#fix-docker-desktop--419) +* 20. [Contributing](#contributing) +* 21. [Changelog](#changelog) +* 22. [License](#license) +* 23. [Core Authors and Maintainers](#core-authors-and-maintainers) +* 24. [Star History and Project Activity](#star-history-and-project-activity) + + + + + + +## ๐Ÿš€ Overview & Goals + +**ContextForge MCP Gateway** is a gateway, registry, and proxy that sits in front of any [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server or REST API-exposing a unified endpoint for all your AI clients. + +**โš ๏ธ Caution**: The current release (0.5.0) is considered alpha / early beta. It is not production-ready and should only be used for local development, testing, or experimentation. Features, APIs, and behaviors are subject to change without notice. **Do not** deploy in production environments without thorough security review, validation and additional security mechanisms. Many of the features required for secure, large-scale, or multi-tenant production deployments are still on the [project roadmap](https://ibm.github.io/mcp-context-forge/architecture/roadmap/) - which is itself evolving. + +It currently supports: + +* Federation across multiple MCP and REST services +* Virtualization of legacy APIs as MCP-compliant tools and servers +* Transport over HTTP, JSON-RPC, WebSocket, SSE (with configurable keepalive), stdio and streamable-HTTP +* An Admin UI for real-time management, configuration, and log monitoring +* Built-in auth, retries, and rate-limiting +* **OpenTelemetry observability** with Phoenix, Jaeger, Zipkin, and other OTLP backends +* Scalable deployments via Docker or PyPI, Redis-backed caching, and multi-cluster federation + +![MCP Gateway Architecture](https://ibm.github.io/mcp-context-forge/images/mcpgateway.svg) + +For a list of upcoming features, check out the [ContextForge MCP Gateway Roadmap](https://ibm.github.io/mcp-context-forge/architecture/roadmap/) + +**โš ๏ธ Important**: MCP Gateway is not a standalone product - it is an open source component with **NO OFFICIAL SUPPORT** from IBM or its affiliates that can be integrated into your own solution architecture. If you choose to use it, you are responsible for evaluating its fit, securing the deployment, and managing its lifecycle. See [SECURITY.md](./SECURITY.md) for more details. + --- -# MCP Gateway +
+๐Ÿ”Œ Gateway Layer with Protocol Flexibility + +* Sits in front of any MCP server or REST API +* Lets you choose your MCP protocol version (e.g., `2025-03-26`) +* Exposes a single, unified interface for diverse backends + +
+ +
+๐ŸŒ Federation of Peer Gateways (MCP Registry) + +* Auto-discovers or configures peer gateways (via mDNS or manual) +* Performs health checks and merges remote registries transparently +* Supports Redis-backed syncing and fail-over + +
+ +
+๐Ÿงฉ Virtualization of REST/gRPC Services + +* Wraps non-MCP services as virtual MCP servers +* Registers tools, prompts, and resources with minimal configuration + +
+ +
+๐Ÿ” REST-to-MCP Tool Adapter + +* Adapts REST APIs into tools with: + + * Automatic JSON Schema extraction + * Support for headers, tokens, and custom auth + * Retry, timeout, and rate-limit policies + +
+ +
+๐Ÿง  Unified Registries + +* **Prompts**: Jinja2 templates, multimodal support, rollback/versioning +* **Resources**: URI-based access, MIME detection, caching, SSE updates +* **Tools**: Native or adapted, with input validation and concurrency controls + +
+ +
+๐Ÿ“ˆ Admin UI, Observability & Dev Experience + +* Admin UI built with HTMX + Alpine.js +* Real-time log viewer with filtering, search, and export capabilities +* Auth: Basic, JWT, or custom schemes +* Structured logs, health endpoints, metrics +* 400+ tests, Makefile targets, live reload, pre-commit hooks + +
+ +
+๐Ÿ” OpenTelemetry Observability + +* **Vendor-agnostic tracing** with OpenTelemetry (OTLP) protocol support +* **Multiple backend support**: Phoenix (LLM-focused), Jaeger, Zipkin, Tempo, DataDog, New Relic +* **Distributed tracing** across federated gateways and services +* **Automatic instrumentation** of tools, prompts, resources, and gateway operations +* **LLM-specific metrics**: Token usage, costs, model performance +* **Zero-overhead when disabled** with graceful degradation +* **Easy configuration** via environment variables + +Quick start with Phoenix (LLM observability): +```bash +# Start Phoenix +docker run -p 6006:6006 -p 4317:4317 arizephoenix/phoenix:latest + +# Configure gateway +export OTEL_ENABLE_OBSERVABILITY=true +export OTEL_TRACES_EXPORTER=otlp +export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 + +# Run gateway - traces automatically sent to Phoenix +mcpgateway +``` + +See [Observability Documentation](https://ibm.github.io/mcp-context-forge/manage/observability/) for detailed setup with other backends. + +
+ +--- + +## Quick Start - PyPI + +MCP Gateway is published on [PyPI](https://pypi.org/project/mcp-contextforge-gateway/) as `mcp-contextforge-gateway`. + +--- + +**TLDR;**: +(single command using [uv](https://docs.astral.sh/uv/)) + +```bash +BASIC_AUTH_PASSWORD=pass \ +MCPGATEWAY_UI_ENABLED=true \ +MCPGATEWAY_ADMIN_API_ENABLED=true \ +uvx --from mcp-contextforge-gateway mcpgateway --host 0.0.0.0 --port 4444 +``` + +
+๐Ÿ“‹ Prerequisites + +* **Python โ‰ฅ 3.10** (3.11 recommended) +* **curl + jq** - only for the last smoke-test step + +
+ +### 1 - Install & run (copy-paste friendly) + +```bash +# 1๏ธโƒฃ Isolated env + install from pypi +mkdir mcpgateway && cd mcpgateway +python3 -m venv .venv && source .venv/bin/activate +pip install --upgrade pip +pip install mcp-contextforge-gateway + +# 2๏ธโƒฃ Launch on all interfaces with custom creds & secret key +# Enable the Admin API endpoints (true/false) - disabled by default +export MCPGATEWAY_UI_ENABLED=true +export MCPGATEWAY_ADMIN_API_ENABLED=true + +BASIC_AUTH_PASSWORD=pass JWT_SECRET_KEY=my-test-key \ + mcpgateway --host 0.0.0.0 --port 4444 & # admin/pass + +# 3๏ธโƒฃ Generate a bearer token & smoke-test the API +export MCPGATEWAY_BEARER_TOKEN=$(python3 -m mcpgateway.utils.create_jwt_token \ + --username admin --exp 10080 --secret my-test-key) + +curl -s -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + http://127.0.0.1:4444/version | jq +``` + +
+Windows (PowerShell) quick-start + +```powershell +# 1๏ธโƒฃ Isolated env + install from PyPI +mkdir mcpgateway ; cd mcpgateway +python3 -m venv .venv ; .\.venv\Scripts\Activate.ps1 +pip install --upgrade pip +pip install mcp-contextforge-gateway + +# 2๏ธโƒฃ Environment variables (session-only) +$Env:MCPGATEWAY_UI_ENABLED = "true" +$Env:MCPGATEWAY_ADMIN_API_ENABLED = "true" +$Env:BASIC_AUTH_PASSWORD = "changeme" # admin/changeme +$Env:JWT_SECRET_KEY = "my-test-key" + +# 3๏ธโƒฃ Launch the gateway +mcpgateway.exe --host 0.0.0.0 --port 4444 + +# Optional: background it +# Start-Process -FilePath "mcpgateway.exe" -ArgumentList "--host 0.0.0.0 --port 4444" + +# 4๏ธโƒฃ Bearer token and smoke-test +$Env:MCPGATEWAY_BEARER_TOKEN = python3 -m mcpgateway.utils.create_jwt_token ` + --username admin --exp 10080 --secret my-test-key + +curl -s -H "Authorization: Bearer $Env:MCPGATEWAY_BEARER_TOKEN" ` + http://127.0.0.1:4444/version | jq +``` + +
+ +
+More configuration + +Copy [.env.example](.env.example) to `.env` and tweak any of the settings (or use them as env variables). + +
+ +
+๐Ÿš€ End-to-end demo (register a local MCP server) + +```bash +# 1๏ธโƒฃ Spin up the sample GO MCP time server using mcpgateway.translate & docker +python3 -m mcpgateway.translate \ + --stdio "docker run --rm -i -p 8888:8080 ghcr.io/ibm/fast-time-server:latest -transport=stdio" \ + --expose-sse \ + --port 8003 + +# Or using the official mcp-server-git using uvx: +pip install uv # to install uvx, if not already installed +python3 -m mcpgateway.translate --stdio "uvx mcp-server-git" --expose-sse --port 9000 + +# Alternative: running the local binary +# cd mcp-servers/go/fast-time-server; make build +# python3 -m mcpgateway.translate --stdio "./dist/fast-time-server -transport=stdio" --expose-sse --port 8002 + +# NEW: Expose via multiple protocols simultaneously! +python3 -m mcpgateway.translate \ + --stdio "uvx mcp-server-git" \ + --expose-sse \ + --expose-streamable-http \ + --port 9000 +# Now accessible via both /sse (SSE) and /mcp (streamable HTTP) endpoints + +# 2๏ธโƒฃ Register it with the gateway +curl -s -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"fast_time","url":"http://localhost:9000/sse"}' \ + http://localhost:4444/gateways + +# 3๏ธโƒฃ Verify tool catalog +curl -s -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/tools | jq + +# 4๏ธโƒฃ Create a *virtual server* bundling those tools. Use the ID of tools from the tool catalog (Step #3) and pass them in the associatedTools list. +curl -s -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"time_server","description":"Fast time tools","associatedTools":[]}' \ + http://localhost:4444/servers | jq + +# Example curl +curl -s -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" + -H "Content-Type: application/json" + -d '{"name":"time_server","description":"Fast time tools","associatedTools":["6018ca46d32a4ac6b4c054c13a1726a2"]}' \ + http://localhost:4444/servers | jq + +# 5๏ธโƒฃ List servers (should now include the UUID of the newly created virtual server) +curl -s -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/servers | jq + +# 6๏ธโƒฃ Client SSE endpoint. Inspect it interactively with the MCP Inspector CLI (or use any MCP client) +npx -y @modelcontextprotocol/inspector +# Transport Type: SSE, URL: http://localhost:4444/servers/UUID_OF_SERVER_1/sse, Header Name: "Authorization", Bearer Token +``` + +
+ +
+๐Ÿ–ง Using the stdio wrapper (mcpgateway-wrapper) + +```bash +export MCP_AUTH_TOKEN=$MCPGATEWAY_BEARER_TOKEN +export MCP_SERVER_CATALOG_URLS=http://localhost:4444/servers/UUID_OF_SERVER_1 +python3 -m mcpgateway.wrapper # Ctrl-C to exit +``` + +You can also run it with `uv` or inside Docker/Podman - see the *Containers* section above. -A flexible FastAPI-based gateway and router for **Model Context Protocol (MCP)** with support for virtual servers. It acts as a unified interface for tools, resources, prompts, virtual servers, and federated gateways - all accessible via rich multi-transport APIs and an interactive web-based Admin UI. - - - - - - - - Agentic Core - - - -
-
-
- โšก ContextForge MCP Gateway Use Case Overview -
- -
- -
-
-
๐Ÿค– Agent Frameworks
-
-
๐Ÿ”— Langchain
-
๐Ÿ“Š Langgraph
-
๐Ÿ‘ฅ crew.ai
-
๐Ÿ”„ Autogen
-
๐Ÿ PydanticAI
-
๐Ÿค— Huggingface Smol
-
๐Ÿ Agent Bee
-
-
- -
-
๐Ÿ’ป Visual Studio Code
-
-
๐Ÿค– GitHub Copilot
-
๐Ÿ”ง Cline
-
โžก๏ธ Continue
-
-
- -
-
๐Ÿ”ง Other Clients
-
-
๐ŸŒ OpenWebUI
-
โŒจ๏ธ MCP-CLI
-
-
-
- - -
-
-
๐ŸŒ MCP Gateway
-
-
๐Ÿ“š MCP Registry
-
๐Ÿ–ฅ๏ธ Virtual Servers
-
๐Ÿ” Authorization
-
๐Ÿ”‘ Authentication
-
-
๐Ÿ”„ Protocol Conversion โ†’ any to any
-
(stdio, SSE, Streamable HTTP, JSON-RPC, REST)
-
-
๐Ÿ“Š Observability
-
โฑ๏ธ Rate Limiting
-
๐Ÿ”€ HA / Routing
-
๐Ÿ’š Healthchecks
-
๐Ÿ› ๏ธ API / UI / CLI
-
-
- -
-
๐Ÿ”Œ Plugin Framework
-
-
๐Ÿ”’ PII Filtering
-
๐Ÿ›ก๏ธ XSS Filtering
-
๐Ÿ“‹ Open Policy Agent
-
-
-
- - -
-
-
๐Ÿ”Œ MCP Servers
-
-
๐Ÿ™ GitHub
-
๐Ÿ“‹ Jira
-
๐ŸŽซ ServiceNow
-
๐ŸŽญ Playwright
-
๐ŸŽจ Figma
-
๐Ÿ“… Monday
-
๐Ÿ“ฆ Box
-
๐ŸŒ Internet Search
-
-
- -
-
๐Ÿ”— REST APIs
-
-
๐ŸŒ External Services
-
โ˜๏ธ Cloud Providers
-
๐Ÿ“Š Data Sources
-
๐Ÿข Enterprise Systems
-
-
-
-
-
-
- - - -![MCP Gateway](images/mcpgateway.gif) - -**โš ๏ธ Important**: MCP Gateway is not a standalone product - it is an open source component with **NO OFFICIAL SUPPORT** from IBM or its affiliates that can be integrated into your own solution architecture. If you choose to use it, you are responsible for evaluating its fit, securing the deployment, and managing its lifecycle. See [SECURITY.md](https://github.com/IBM/mcp-context-forge/blob/main/SECURITY.md) for more details, and the [roadmap](architecture/roadmap.md) for upcoming features. - ---- - -## What it Does - -- ๐Ÿšช Acts as a **gateway layer** in front of MCP servers or APIs -- ๐Ÿ”— Connects and federates multiple MCP backends (auto-discovery, fail-over, merging) -- ๐Ÿ”„ Virtualizes REST APIs and external MCP servers as compliant tools and servers -- ๐Ÿ› ๏ธ Centralizes registration and management of tools, prompts, and resources -- ๐Ÿ“ก Exposes all endpoints over HTTP/JSON-RPC, WebSocket, Server-Sent Events (SSE), and **stdio** -- ๐Ÿ“ฆ Provides a stdio wrapper (`mcpgateway-wrapper`) for terminal-based or headless MCP clients - ---- - -## Key Features - -- **Multi-Transport**: HTTP, WebSocket, SSE, Streamable HTTP and stdio with auto-negotiation -- **Federation & Health Checks**: Auto-discovery (mDNS or static), syncing, monitoring -- **Admin UI**: Real-time management (HTMX + Tailwind) -- **Tool Wrapping**: REST / CLI / local functions with JSON-Schema validation -- **Security**: JWT + Basic Auth, custom headers, rate limits, SSL control -- **Caching & Observability**: Redis/in-memory/database caching, metrics, structured logs -- **Virtual Servers**: Group tools/resources/prompts into MCP-compliant servers -- **Wrapper Mode**: `mcpgateway-wrapper` turns any remote gateway into a local stdio MCP server - -For upcoming capabilities, see the [Roadmap](architecture/roadmap.md). - -```mermaid -graph TD - subgraph UI_and_Auth - UI[๐Ÿ–ฅ๏ธ Admin UI] - Auth[๐Ÿ” Auth - JWT and Basic] - UI --> Core - Auth --> Core - end - - subgraph Gateway_Core - Core[๐Ÿšช MCP Gateway Core] - Protocol[๐Ÿ“ก Protocol - Init Ping Completion] - Federation[๐ŸŒ Federation Manager] - Transports[๐Ÿ”€ Transports - HTTP WS SSE Stdio] - Core --> Protocol - Core --> Federation - Core --> Transports - end - - subgraph Services - Tools[๐Ÿงฐ Tool Service] - Resources[๐Ÿ“ Resource Service] - Prompts[๐Ÿ“ Prompt Service] - Servers[๐Ÿงฉ Server Service] - Core --> Tools - Core --> Resources - Core --> Prompts - Core --> Servers - end - - subgraph Persistence - DB[๐Ÿ’พ Database - SQLAlchemy] - Tools --> DB - Resources --> DB - Prompts --> DB - Servers --> DB - end +In MCP Inspector, define `MCP_AUTH_TOKEN` and `MCP_SERVER_CATALOG_URLS` env variables, and select `python3` as the Command, and `-m mcpgateway.wrapper` as Arguments. + +```bash +echo $PWD/.venv/bin/python3 # Using the Python3 full path ensures you have a working venv +export MCP_SERVER_CATALOG_URLS='http://localhost:4444/servers/UUID_OF_SERVER_1' +export MCP_AUTH_TOKEN=${MCPGATEWAY_BEARER_TOKEN} +npx -y @modelcontextprotocol/inspector +``` - subgraph Caching - Cache[โšก Cache - Redis or Memory] - Core --> Cache - end +When using a MCP Client such as Claude with stdio: + +```json +{ + "mcpServers": { + "mcpgateway-wrapper": { + "command": "python", + "args": ["-m", "mcpgateway.wrapper"], + "env": { + "MCP_AUTH_TOKEN": "your-token-here", + "MCP_SERVER_CATALOG_URLS": "http://localhost:4444/servers/UUID_OF_SERVER_1", + "MCP_TOOL_CALL_TIMEOUT": "120" + } + } + } +} ``` +
+ +--- + +## Quick Start - Containers + +Use the official OCI image from GHCR with **Docker** *or* **Podman**. + --- -## Audience +### ๐Ÿณ Docker + +#### 1 - Minimum viable run + +```bash +docker run -d --name mcpgateway \ + -p 4444:4444 \ + -e MCPGATEWAY_UI_ENABLED=true \ + -e MCPGATEWAY_ADMIN_API_ENABLED=true \ + -e HOST=0.0.0.0 \ + -e JWT_SECRET_KEY=my-test-key \ + -e BASIC_AUTH_USER=admin \ + -e BASIC_AUTH_PASSWORD=changeme \ + -e AUTH_REQUIRED=true \ + -e DATABASE_URL=sqlite:///./mcp.db \ + ghcr.io/ibm/mcp-context-forge:0.5.0 + +# Tail logs (Ctrl+C to quit) +docker logs -f mcpgateway + +# Generating an API key +docker run --rm -it ghcr.io/ibm/mcp-context-forge:0.5.0 \ + python3 -m mcpgateway.utils.create_jwt_token --username admin --exp 0 --secret my-test-key +``` + +Browse to **[http://localhost:4444/admin](http://localhost:4444/admin)** (user `admin` / pass `changeme`). + +#### 2 - Persist the SQLite database -MCP Gateway serves: +```bash +mkdir -p $(pwd)/data -* **AI Platform Teams** building unified gateways for LLM tools & services -* **DevOps Engineers** deploying secure, observable, federated control planes -* **Open-source contributors** extending MCP tooling or adapters -* **Cloud Architects** running on Kubernetes, IBM Code Engine, AWS, Azure, or bare Docker +touch $(pwd)/data/mcp.db + +sudo chown -R :docker $(pwd)/data + +chmod 777 $(pwd)/data + +docker run -d --name mcpgateway \ + --restart unless-stopped \ + -p 4444:4444 \ + -v $(pwd)/data:/data \ + -e MCPGATEWAY_UI_ENABLED=true \ + -e MCPGATEWAY_ADMIN_API_ENABLED=true \ + -e DATABASE_URL=sqlite:////data/mcp.db \ + -e HOST=0.0.0.0 \ + -e JWT_SECRET_KEY=my-test-key \ + -e BASIC_AUTH_USER=admin \ + -e BASIC_AUTH_PASSWORD=changeme \ + ghcr.io/ibm/mcp-context-forge:0.5.0 +``` + +SQLite now lives on the host at `./data/mcp.db`. + +#### 3 - Local tool discovery (host network) + +```bash +mkdir -p $(pwd)/data + +touch $(pwd)/data/mcp.db + +sudo chown -R :docker $(pwd)/data + +chmod 777 $(pwd)/data + +docker run -d --name mcpgateway \ + --network=host \ + -e MCPGATEWAY_UI_ENABLED=true \ + -e MCPGATEWAY_ADMIN_API_ENABLED=true \ + -e HOST=0.0.0.0 \ + -e PORT=4444 \ + -e DATABASE_URL=sqlite:////data/mcp.db \ + -v $(pwd)/data:/data \ + ghcr.io/ibm/mcp-context-forge:0.5.0 +``` + +Using `--network=host` allows Docker to access the local network, allowing you to add MCP servers running on your host. See [Docker Host network driver documentation](https://docs.docker.com/engine/network/drivers/host/) for more details. --- -## Installation & Deployment +### ๐Ÿฆญ Podman (rootless-friendly) + +#### 1 - Basic run + +```bash +podman run -d --name mcpgateway \ + -p 4444:4444 \ + -e HOST=0.0.0.0 \ + -e DATABASE_URL=sqlite:///./mcp.db \ + ghcr.io/ibm/mcp-context-forge:0.5.0 +``` + +#### 2 - Persist SQLite + +```bash +mkdir -p $(pwd)/data + +touch $(pwd)/data/mcp.db + +sudo chown -R :docker $(pwd)/data + +chmod 777 $(pwd)/data + +podman run -d --name mcpgateway \ + --restart=on-failure \ + -p 4444:4444 \ + -v $(pwd)/data:/data \ + -e DATABASE_URL=sqlite:////data/mcp.db \ + ghcr.io/ibm/mcp-context-forge:0.5.0 +``` + +#### 3 - Host networking (rootless) -| Scenario | One-liner / CLI Snippet | Docs | -| ----------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------ | -| **Local (PyPI)** | `pip install mcp-contextforge-gateway && mcpgateway --host 0.0.0.0 --port 4444` | [Quick Start](overview/quick_start.md) | -| **Docker / Podman** | `docker run -p 4444:4444 ghcr.io/ibm/mcp-context-forge:` | [Containers](deployment/container.md) | -| **Docker-Compose (dev)** | `docker compose up` | [Compose](deployment/compose.md) | -| **Helm / Vanilla Kubernetes** | `helm repo add mcpgw https://IBM.github.io/mcp-context-forge && helm install mcpgw mcpgw/mcpgateway` | [Helm Chart](deployment/helm.md) | -| **Minikube (local k8s)** | `make minikube` | [Minikube Guide](deployment/minikube.md) | -| **OpenShift / OKD** | `oc apply -k openshift/` | [OpenShift](deployment/openshift.md) | -| **Argo CD / GitOps** | `kubectl apply -f argo.yaml` | [Argo CD](deployment/argocd.md) | -| **IBM Cloud - Code Engine** | `ibmcloud ce app create --name mcpgw --image ghcr.io/ibm/mcp-context-forge:` | [IBM Code Engine](deployment/ibm-code-engine.md) | -| **AWS - ECS (Fargate)** | `aws ecs create-service --cli-input-json file://ecs.json` | [AWS Guide](deployment/aws.md) | -| **AWS - EKS (Helm)** | `helm install mcpgw mcpgw/mcpgateway` | [AWS Guide](deployment/aws.md) | -| **Google Cloud Run** | `gcloud run deploy mcpgw --image ghcr.io/ibm/mcp-context-forge:` | [GCP Cloud Run](deployment/google-cloud-run.md) | -| **Google GKE (Helm)** | `helm install mcpgw mcpgw/mcpgateway` | [GCP Guide](deployment/google-cloud-run.md) | -| **Azure - Container Apps** | `az containerapp up --name mcpgw --image ghcr.io/ibm/mcp-context-forge:` | [Azure Guide](deployment/azure.md) | -| **Azure - AKS (Helm)** | `helm install mcpgw mcpgw/mcpgateway` | [Azure Guide](deployment/azure.md) | +```bash +mkdir -p $(pwd)/data +touch $(pwd)/data/mcp.db -> **PyPI Package**: [`mcp-contextforge-gateway`](https://pypi.org/project/mcp-contextforge-gateway/) +sudo chown -R :docker $(pwd)/data -> **OCI Image**: [`ghcr.io/ibm/mcp-context-forge:0.5.0`](https://github.com/IBM/mcp-context-forge/pkgs/container/mcp-context-forge) +chmod 777 $(pwd)/data + +podman run -d --name mcpgateway \ + --network=host \ + -v $(pwd)/data:/data \ + -e DATABASE_URL=sqlite:////data/mcp.db \ + ghcr.io/ibm/mcp-context-forge:0.5.0 +``` --- -## Get Started +
+โœ๏ธ Docker/Podman tips + +* **.env files** - Put all the `-e FOO=` lines into a file and replace them with `--env-file .env`. See the provided [.env.example](.env.example) for reference. +* **Pinned tags** - Use an explicit version (e.g. `v0.5.0`) instead of `latest` for reproducible builds. +* **JWT tokens** - Generate one in the running container: + + ```bash + docker exec mcpgateway python3 -m mcpgateway.utils.create_jwt_token -u admin -e 10080 --secret my-test-key + ``` +* **Upgrades** - Stop, remove, and rerun with the same `-v $(pwd)/data:/data` mount; your DB and config stay intact. -Jump straight to: +
-* [Quick Start Guide](overview/quick_start.md) -* [Features Overview](overview/features.md) -* [Admin UI Walk-through](overview/ui.md) -* [Using the `mcpgateway-wrapper`](using/mcpgateway-wrapper.md) -* [Deployment Options](deployment/index.md) +--- + +
+๐Ÿš‘ Smoke-test the running container -!!! note - Source โ†’ [https://github.com/IBM/mcp-context-forge](https://github.com/IBM/mcp-context-forge) +```bash +curl -s -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + http://localhost:4444/health | jq +curl -s -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + http://localhost:4444/tools | jq +curl -s -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + http://localhost:4444/version | jq +``` - Docs โ†’ [https://ibm.github.io/mcp-context-forge/](https://ibm.github.io/mcp-context-forge/) +
--- -## Authors and Contributors +
+๐Ÿ–ง Running the MCP Gateway stdio wrapper + +The `mcpgateway.wrapper` lets you connect to the gateway over **stdio** while keeping JWT authentication. You should run this from the MCP Client. The example below is just for testing. + +```bash +# Set environment variables +export MCPGATEWAY_BEARER_TOKEN=$(python3 -m mcpgateway.utils.create_jwt_token --username admin --exp 10080 --secret my-test-key) +export MCP_AUTH_TOKEN=${MCPGATEWAY_BEARER_TOKEN} +export MCP_SERVER_CATALOG_URLS='http://localhost:4444/servers/UUID_OF_SERVER_1' +export MCP_TOOL_CALL_TIMEOUT=120 +export MCP_WRAPPER_LOG_LEVEL=DEBUG # or OFF to disable logging + +docker run --rm -i \ + -e MCP_AUTH_TOKEN=$MCPGATEWAY_BEARER_TOKEN \ + -e MCP_SERVER_CATALOG_URLS=http://host.docker.internal:4444/servers/UUID_OF_SERVER_1 \ + -e MCP_TOOL_CALL_TIMEOUT=120 \ + -e MCP_WRAPPER_LOG_LEVEL=DEBUG \ + ghcr.io/ibm/mcp-context-forge:0.5.0 \ + python3 -m mcpgateway.wrapper +``` + +
+ +--- + +## Testing `mcpgateway.wrapper` by hand: + +Because the wrapper speaks JSON-RPC over stdin/stdout, you can interact with it using nothing more than a terminal or pipes. + +```bash +# Start the MCP Gateway Wrapper +export MCP_AUTH_TOKEN=${MCPGATEWAY_BEARER_TOKEN} +export MCP_SERVER_CATALOG_URLS=http://localhost:4444/servers/YOUR_SERVER_UUID +python3 -m mcpgateway.wrapper +``` + +
+Initialize the protocol + +```json +# Initialize the protocol +{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"demo","version":"0.0.1"}}} + +# Then after the reply: +{"jsonrpc":"2.0","method":"notifications/initialized","params":{}} + +# Get prompts +{"jsonrpc":"2.0","id":4,"method":"prompts/list"} +{"jsonrpc":"2.0","id":5,"method":"prompts/get","params":{"name":"greeting","arguments":{"user":"Bob"}}} + +# Get resources +{"jsonrpc":"2.0","id":6,"method":"resources/list"} +{"jsonrpc":"2.0","id":7,"method":"resources/read","params":{"uri":"https://example.com/some.txt"}} + +# Get / call tools +{"jsonrpc":"2.0","id":2,"method":"tools/list"} +{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_system_time","arguments":{"timezone":"Europe/Dublin"}}} +``` + +
+ +
+Expected responses from mcpgateway.wrapper + +```json +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"mcpgateway-wrapper","version":"0.5.0"}}} + +# When there's no tools +{"jsonrpc":"2.0","id":2,"result":{"tools":[]}} + +# After you add some tools and create a virtual server +{"jsonrpc":"2.0","id":2,"result":{"tools":[{"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true},"description":"Convert time between different timezones","inputSchema":{"properties":{"source_timezone":{"description":"Source IANA timezone name","type":"string"},"target_timezone":{"description":"Target IANA timezone name","type":"string"},"time":{"description":"Time to convert in RFC3339 format or common formats like '2006-01-02 15:04:05'","type":"string"}},"required":["time","source_timezone","target_timezone"],"type":"object"},"name":"convert_time"},{"annotations":{"readOnlyHint":false,"destructiveHint":true,"idempotentHint":false,"openWorldHint":true},"description":"Get current system time in specified timezone","inputSchema":{"properties":{"timezone":{"description":"IANA timezone name (e.g., 'America/New_York', 'Europe/London'). Defaults to UTC","type":"string"}},"type":"object"},"name":"get_system_time"}]}} + +# Running the time tool: +{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"2025-07-09T00:09:45+01:00"}]}} +``` + +
+ +### ๐Ÿงฉ Running from an MCP Client (`mcpgateway.wrapper`) + +The `mcpgateway.wrapper` exposes everything your Gateway knows about over **stdio**, so any MCP client that *can't* (or *shouldn't*) open an authenticated SSE stream still gets full tool-calling power. + +> **Remember** to substitute your real Gateway URL (and server ID) for `http://localhost:4444/servers/UUID_OF_SERVER_1`. +> When inside Docker/Podman, that often becomes `http://host.docker.internal:4444/servers/UUID_OF_SERVER_1` (macOS/Windows) or the gateway container's hostname (Linux). + +--- + +
+๐Ÿณ Docker / Podman + +```bash +docker run -i --rm \ + --network=host \ + -e MCP_SERVER_CATALOG_URLS=http://localhost:4444/servers/UUID_OF_SERVER_1 \ + -e MCP_AUTH_TOKEN=${MCPGATEWAY_BEARER_TOKEN} \ + -e MCP_TOOL_CALL_TIMEOUT=120 \ + ghcr.io/ibm/mcp-context-forge:0.5.0 \ + python3 -m mcpgateway.wrapper +``` + +
+ +--- + +
+๐Ÿ“ฆ pipx (one-liner install & run) + +```bash +# Install gateway package in its own isolated venv +pipx install --include-deps mcp-contextforge-gateway + +# Run the stdio wrapper +MCP_AUTH_TOKEN=${MCPGATEWAY_BEARER_TOKEN} \ +MCP_SERVER_CATALOG_URLS=http://localhost:4444/servers/UUID_OF_SERVER_1 \ +python3 -m mcpgateway.wrapper +# Alternatively with uv +uv run --directory . -m mcpgateway.wrapper +``` + +**Claude Desktop JSON** (uses the host Python that pipx injected): + +```json +{ + "mcpServers": { + "mcpgateway-wrapper": { + "command": "python3", + "args": ["-m", "mcpgateway.wrapper"], + "env": { + "MCP_AUTH_TOKEN": "", + "MCP_SERVER_CATALOG_URLS": "http://localhost:4444/servers/UUID_OF_SERVER_1", + "MCP_TOOL_CALL_TIMEOUT": "120" + } + } + } +} +``` + +
+ +--- + +
+โšก uv / uvx (light-speed venvs) + +#### 1 - Install uv (uvx is an alias it provides) + +```bash +# (a) official one-liner +curl -Ls https://astral.sh/uv/install.sh | sh + +# (b) or via pipx +pipx install uv +``` + +#### 2 - Create an on-the-spot venv & run the wrapper + +```bash +# Create venv in ~/.venv/mcpgateway (or current dir if you prefer) +uv venv ~/.venv/mcpgateway +source ~/.venv/mcpgateway/bin/activate + +# Install the gateway package using uv +uv pip install mcp-contextforge-gateway + +# Launch wrapper +MCP_AUTH_TOKEN=${MCPGATEWAY_BEARER_TOKEN} \ +MCP_SERVER_CATALOG_URLS=http://localhost:4444/servers/UUID_OF_SERVER_1 \ +uv run --directory . -m mcpgateway.wrapper # Use this just for testing, as the Client will run the uv command +``` + +#### Claude Desktop JSON (runs through **uvx**) + +```json +{ + "mcpServers": { + "mcpgateway-wrapper": { + "command": "uvx", + "args": [ + "run", + "--", + "python", + "-m", + "mcpgateway.wrapper" + ], + "env": { + "MCP_AUTH_TOKEN": "", + "MCP_SERVER_CATALOG_URLS": "http://localhost:4444/servers/UUID_OF_SERVER_1" + } + } +} +``` + +
+ +--- + +### ๐Ÿš€ Using with Claude Desktop (or any GUI MCP client) + +1. **Edit Config** โ†’ `File โ–ธ Settings โ–ธ Developer โ–ธ Edit Config` +2. Paste one of the JSON blocks above (Docker / pipx / uvx). +3. Restart the app so the new stdio server is spawned. +4. Open logs in the same menu to verify `mcpgateway-wrapper` started and listed your tools. + +Need help? See: + +* **MCP Debugging Guide** - [https://modelcontextprotocol.io/docs/tools/debugging](https://modelcontextprotocol.io/docs/tools/debugging) + +--- + +## ๐Ÿš€ Quick Start: VS Code Dev Container + +Spin up a fully-loaded dev environment (Python 3.11, Docker/Podman CLI, all project dependencies) in just two clicks. + +--- + +
+๐Ÿ“‹ Prerequisites + +* **VS Code** with the [Dev Containers extension](https://code.visualstudio.com/docs/devcontainers/containers) +* **Docker** or **Podman** installed and running locally + +
+ +
+๐Ÿงฐ Setup Instructions + +### 1 - Clone & Open + +```bash +git clone https://github.com/ibm/mcp-context-forge.git +cd mcp-context-forge +code . +``` + +VS Code will detect the `.devcontainer` and prompt: +**"Reopen in Container"** +*or* manually run: Ctrl/Cmd โ‡ง P โ†’ **Dev Containers: Reopen in Container** + +--- + +### 2 - First-Time Build (Automatic) + +The container build will: + +* Install system packages & Python 3.11 +* Run `make install-dev` to pull all dependencies +* Execute tests to verify the toolchain + +You'll land in `/workspace` ready to develop. + +
+ +
+๐Ÿ› ๏ธ Daily Developer Workflow + +Common tasks inside the container: + +```bash +# Start dev server (hot reload) +make dev # http://localhost:4444 + +# Run tests & linters +make test +make lint +``` + +Optional: + +* `make bash` - drop into an interactive shell +* `make clean` - clear build artefacts & caches +* Port forwarding is automatic (customize via `.devcontainer/devcontainer.json`) + +
+ +
+โ˜๏ธ GitHub Codespaces: 1-Click Cloud IDE + +No local Docker? Use Codespaces: + +1. Go to the repo โ†’ **Code โ–ธ Codespaces โ–ธ Create codespace on main** +2. Wait for the container image to build in the cloud +3. Develop using the same workflow above + +
+ +--- + +## Quick Start (manual install) + +### Prerequisites + +* **Python โ‰ฅ 3.10** +* **GNU Make** (optional, but all common workflows are available as Make targets) +* Optional: **Docker / Podman** for containerized runs + +### One-liner (dev) + +```bash +make venv install serve +``` + +What it does: + +1. Creates / activates a `.venv` in your home folder `~/.venv/mcpgateway` +2. Installs the gateway and necessary dependencies +3. Launches **Gunicorn** (Uvicorn workers) on [http://localhost:4444](http://localhost:4444) + +For development, you can use: + +```bash +make install-dev # Install development dependencies, ex: linters and test harness +make lint # optional: run style checks (ruff, mypy, etc.) +``` + +### Containerized (self-signed TLS) + +## Container Runtime Support + +This project supports both Docker and Podman. The Makefile automatically detects +which runtime is available and handles image naming differences. + +### Auto-detection +```bash +make container-build # Uses podman if available, otherwise docker + +> You can use docker or podman, ex: + +```bash +make podman # build production image +make podman-run-ssl # run at https://localhost:4444 +# or listen on port 4444 on your host directly, adds --network=host to podman +make podman-run-ssl-host +``` + +### Smoke-test the API + +```bash +curl -k -sX GET \ + -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + https://localhost:4444/tools | jq +``` + +You should receive `[]` until you register a tool. + +--- + +## Installation + +### Via Make + +```bash +make venv install # create .venv + install deps +make serve # gunicorn on :4444 +``` + +### UV (alternative) + +```bash +uv venv && source .venv/bin/activate +uv pip install -e '.[dev]' # IMPORTANT: in zsh, quote to disable glob expansion! +``` + +### pip (alternative) + +```bash +python3 -m venv .venv && source .venv/bin/activate +pip install -e ".[dev]" +``` + +### Optional (PostgreSQL adapter) + +You can configure the gateway with SQLite, PostgreSQL (or any other compatible database) in .env. + +When using PostgreSQL, you need to install `psycopg2` driver. + +```bash +uv pip install psycopg2-binary # dev convenience +# or +uv pip install psycopg2 # production build +``` + +#### Quick Postgres container + +```bash +docker run --name mcp-postgres \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=mysecretpassword \ + -e POSTGRES_DB=mcp \ + -p 5432:5432 -d postgres +``` + +A `make compose-up` target is provided along with a [docker-compose.yml](docker-compose.yml) file to make this process simpler. + +--- + +## Configuration (`.env` or env vars) + +> โš ๏ธ If any required `.env` variable is missing or invalid, the gateway will fail fast at startup with a validation error via Pydantic. + +You can get started by copying the provided [.env.example](.env.example) to `.env` and making the necessary edits to fit your environment. + +
+๐Ÿ”ง Environment Configuration Variables + +### Basic + +| Setting | Description | Default | Options | +| --------------- | ---------------------------------------- | ---------------------- | ---------------------- | +| `APP_NAME` | Gateway / OpenAPI title | `MCP Gateway` | string | +| `HOST` | Bind address for the app | `127.0.0.1` | IPv4/IPv6 | +| `PORT` | Port the server listens on | `4444` | 1-65535 | +| `DATABASE_URL` | SQLAlchemy connection URL | `sqlite:///./mcp.db` | any SQLAlchemy dialect | +| `APP_ROOT_PATH` | Subpath prefix for app (e.g. `/gateway`) | (empty) | string | +| `TEMPLATES_DIR` | Path to Jinja2 templates | `mcpgateway/templates` | path | +| `STATIC_DIR` | Path to static files | `mcpgateway/static` | path | + +> ๐Ÿ’ก Use `APP_ROOT_PATH=/foo` if reverse-proxying under a subpath like `https://host.com/foo/`. + +### Authentication + +| Setting | Description | Default | Options | +| --------------------- | ---------------------------------------------------------------- | ------------- | ---------- | +| `BASIC_AUTH_USER` | Username for Admin UI login and HTTP Basic authentication | `admin` | string | +| `BASIC_AUTH_PASSWORD` | Password for Admin UI login and HTTP Basic authentication | `changeme` | string | +| `AUTH_REQUIRED` | Require authentication for all API routes | `true` | bool | +| `JWT_SECRET_KEY` | Secret key used to **sign JWT tokens** for API access | `my-test-key` | string | +| `JWT_ALGORITHM` | Algorithm used to sign the JWTs (`HS256` is default, HMAC-based) | `HS256` | PyJWT algs | +| `TOKEN_EXPIRY` | Expiry of generated JWTs in minutes | `10080` | int > 0 | +| `AUTH_ENCRYPTION_SECRET` | Passphrase used to derive AES key for encrypting tool auth headers | `my-test-salt` | string | + +> ๐Ÿ” `BASIC_AUTH_USER`/`PASSWORD` are used for: +> +> * Logging into the web-based Admin UI +> * Accessing APIs via Basic Auth (`curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN"`) +> +> ๐Ÿ”‘ `JWT_SECRET_KEY` is used to: +> +> * Sign JSON Web Tokens (`Authorization: Bearer `) +> * Generate tokens via: +> +> ```bash +> export MCPGATEWAY_BEARER_TOKEN=$(python3 -m mcpgateway.utils.create_jwt_token --username admin --exp 0 --secret my-test-key) +> echo $MCPGATEWAY_BEARER_TOKEN +> ``` +> * Tokens allow non-interactive API clients to authenticate securely. +> +> ๐Ÿงช Set `AUTH_REQUIRED=false` during development if you want to disable all authentication (e.g. for local testing or open APIs) or clients that don't support SSE authentication. +> In production, you should use the SSE to stdio `mcpgateway-wrapper` for such tools that don't support authenticated SSE, while still ensuring the gateway uses authentication. +> +> ๐Ÿ” `AUTH_ENCRYPTION_SECRET` is used to encrypt and decrypt tool authentication credentials (`auth_value`). +> You must set the same value across environments to decode previously stored encrypted auth values. +> Recommended: use a long, random string. + +### UI Features + +| Setting | Description | Default | Options | +| ------------------------------ | -------------------------------------- | ------- | ------- | +| `MCPGATEWAY_UI_ENABLED` | Enable the interactive Admin dashboard | `false` | bool | +| `MCPGATEWAY_ADMIN_API_ENABLED` | Enable API endpoints for admin ops | `false` | bool | +| `MCPGATEWAY_BULK_IMPORT_ENABLED` | Enable bulk import endpoint for tools | `true` | bool | + +> ๐Ÿ–ฅ๏ธ Set both UI and Admin API to `false` to disable management UI and APIs in production. +> ๐Ÿ“ฅ The bulk import endpoint allows importing up to 200 tools in a single request via `/admin/tools/import`. + +### Security + +| Setting | Description | Default | Options | +| ------------------------- | ------------------------------ | ---------------------------------------------- | ---------- | +| `SKIP_SSL_VERIFY` | Skip upstream TLS verification | `false` | bool | +| `ENVIRONMENT` | Deployment environment (affects security defaults) | `development` | `development`/`production` | +| `APP_DOMAIN` | Domain for production CORS origins | `localhost` | string | +| `ALLOWED_ORIGINS` | CORS allow-list | Auto-configured by environment | JSON array | +| `CORS_ENABLED` | Enable CORS | `true` | bool | +| `CORS_ALLOW_CREDENTIALS` | Allow credentials in CORS | `true` | bool | +| `SECURE_COOKIES` | Force secure cookie flags | `true` | bool | +| `COOKIE_SAMESITE` | Cookie SameSite attribute | `lax` | `strict`/`lax`/`none` | +| `SECURITY_HEADERS_ENABLED` | Enable security headers middleware | `true` | bool | +| `X_FRAME_OPTIONS` | X-Frame-Options header value | `DENY` | `DENY`/`SAMEORIGIN` | +| `HSTS_ENABLED` | Enable HSTS header | `true` | bool | +| `HSTS_MAX_AGE` | HSTS max age in seconds | `31536000` | int | +| `REMOVE_SERVER_HEADERS` | Remove server identification | `true` | bool | +| `DOCS_ALLOW_BASIC_AUTH` | Allow Basic Auth for docs (in addition to JWT) | `false` | bool | + +> **CORS Configuration**: When `ENVIRONMENT=development`, CORS origins are automatically configured for common development ports (3000, 8080, gateway port). In production, origins are constructed from `APP_DOMAIN` (e.g., `https://yourdomain.com`, `https://app.yourdomain.com`). You can override this by explicitly setting `ALLOWED_ORIGINS`. +> +> **Security Headers**: The gateway automatically adds configurable security headers to all responses including CSP, X-Frame-Options, X-Content-Type-Options, X-Download-Options, and HSTS (on HTTPS). All headers can be individually enabled/disabled. Sensitive server headers are removed. +> +> **iframe Embedding**: By default, `X-Frame-Options: DENY` prevents iframe embedding for security. To allow embedding, set `X_FRAME_OPTIONS=SAMEORIGIN` (same domain) or disable with `X_FRAME_OPTIONS=""`. Also update CSP `frame-ancestors` directive if needed. +> +> **Cookie Security**: Authentication cookies are automatically configured with HttpOnly, Secure (in production), and SameSite attributes for CSRF protection. +> +> Note: do not quote the ALLOWED_ORIGINS values, this needs to be valid JSON, such as: +> ALLOWED_ORIGINS=["http://localhost", "http://localhost:4444"] +> +> Documentation endpoints (`/docs`, `/redoc`, `/openapi.json`) are always protected by authentication. +> By default, they require Bearer token authentication. Setting `DOCS_ALLOW_BASIC_AUTH=true` enables HTTP Basic Authentication as an additional method using the same credentials as `BASIC_AUTH_USER` and `BASIC_AUTH_PASSWORD`. + + +### Logging + +MCP Gateway provides flexible logging with **stdout/stderr output by default** and **optional file-based logging**. When file logging is enabled, it provides JSON formatting for structured logs and text formatting for console output. + +| Setting | Description | Default | Options | +| ----------------------- | ---------------------------------- | ----------------- | -------------------------- | +| `LOG_LEVEL` | Minimum log level | `INFO` | `DEBUG`...`CRITICAL` | +| `LOG_FORMAT` | Console log format | `json` | `json`, `text` | +| `LOG_TO_FILE` | **Enable file logging** | **`false`** | **`true`, `false`** | +| `LOG_FILE` | Log filename (when enabled) | `null` | `mcpgateway.log` | +| `LOG_FOLDER` | Directory for log files | `null` | `logs`, `/var/log/gateway` | +| `LOG_FILEMODE` | File write mode | `a+` | `a+` (append), `w` (overwrite)| +| `LOG_ROTATION_ENABLED` | **Enable log file rotation** | **`false`** | **`true`, `false`** | +| `LOG_MAX_SIZE_MB` | Max file size before rotation (MB) | `1` | Any positive integer | +| `LOG_BACKUP_COUNT` | Number of backup files to keep | `5` | Any non-negative integer | + +**Logging Behavior:** +- **Default**: Logs only to **stdout/stderr** with human-readable text format +- **File Logging**: When `LOG_TO_FILE=true`, logs to **both** file (JSON format) and console (text format) +- **Log Rotation**: When `LOG_ROTATION_ENABLED=true`, files rotate at `LOG_MAX_SIZE_MB` with `LOG_BACKUP_COUNT` backup files (e.g., `.log.1`, `.log.2`) +- **Directory Creation**: Log folder is automatically created if it doesn't exist +- **Centralized Service**: All modules use the unified `LoggingService` for consistent formatting + +**Example Configurations:** + +```bash +# Default: stdout/stderr only (recommended for containers) +LOG_LEVEL=INFO +# No additional config needed - logs to stdout/stderr + +# Optional: Enable file logging (no rotation) +LOG_TO_FILE=true +LOG_FOLDER=/var/log/mcpgateway +LOG_FILE=gateway.log +LOG_FILEMODE=a+ + +# Optional: Enable file logging with rotation +LOG_TO_FILE=true +LOG_ROTATION_ENABLED=true +LOG_MAX_SIZE_MB=10 +LOG_BACKUP_COUNT=3 +LOG_FOLDER=/var/log/mcpgateway +LOG_FILE=gateway.log +``` + +**Default Behavior:** +- Logs are written **only to stdout/stderr** in human-readable text format +- File logging is **disabled by default** (no files created) +- Set `LOG_TO_FILE=true` to enable optional file logging with JSON format + +### Observability (OpenTelemetry) + +MCP Gateway includes **vendor-agnostic OpenTelemetry support** for distributed tracing. Works with Phoenix, Jaeger, Zipkin, Tempo, DataDog, New Relic, and any OTLP-compatible backend. + +| Setting | Description | Default | Options | +| ------------------------------- | ---------------------------------------------- | --------------------- | ------------------------------------------ | +| `OTEL_ENABLE_OBSERVABILITY` | Master switch for observability | `true` | `true`, `false` | +| `OTEL_SERVICE_NAME` | Service identifier in traces | `mcp-gateway` | string | +| `OTEL_SERVICE_VERSION` | Service version in traces | `0.5.0` | string | +| `OTEL_DEPLOYMENT_ENVIRONMENT` | Environment tag (dev/staging/prod) | `development` | string | +| `OTEL_TRACES_EXPORTER` | Trace exporter backend | `otlp` | `otlp`, `jaeger`, `zipkin`, `console`, `none` | +| `OTEL_RESOURCE_ATTRIBUTES` | Custom resource attributes | (empty) | `key=value,key2=value2` | + +**OTLP Configuration** (for Phoenix, Tempo, DataDog, etc.): + +| Setting | Description | Default | Options | +| ------------------------------- | ---------------------------------------------- | --------------------- | ------------------------------------------ | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP collector endpoint | (none) | `http://localhost:4317` | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | OTLP protocol | `grpc` | `grpc`, `http/protobuf` | +| `OTEL_EXPORTER_OTLP_HEADERS` | Authentication headers | (empty) | `api-key=secret,x-auth=token` | +| `OTEL_EXPORTER_OTLP_INSECURE` | Skip TLS verification | `true` | `true`, `false` | + +**Alternative Backends** (optional): + +| Setting | Description | Default | Options | +| ------------------------------- | ---------------------------------------------- | --------------------- | ------------------------------------------ | +| `OTEL_EXPORTER_JAEGER_ENDPOINT` | Jaeger collector endpoint | `http://localhost:14268/api/traces` | URL | +| `OTEL_EXPORTER_ZIPKIN_ENDPOINT` | Zipkin collector endpoint | `http://localhost:9411/api/v2/spans` | URL | + +**Performance Tuning**: + +| Setting | Description | Default | Options | +| ------------------------------- | ---------------------------------------------- | --------------------- | ------------------------------------------ | +| `OTEL_TRACES_SAMPLER` | Sampling strategy | `parentbased_traceidratio` | `always_on`, `always_off`, `traceidratio` | +| `OTEL_TRACES_SAMPLER_ARG` | Sample rate (0.0-1.0) | `0.1` | float (0.1 = 10% sampling) | +| `OTEL_BSP_MAX_QUEUE_SIZE` | Max queued spans | `2048` | int > 0 | +| `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`| Max batch size for export | `512` | int > 0 | +| `OTEL_BSP_SCHEDULE_DELAY` | Export interval (ms) | `5000` | int > 0 | + +**Quick Start with Phoenix**: +```bash +# Start Phoenix for LLM observability +docker run -p 6006:6006 -p 4317:4317 arizephoenix/phoenix:latest + +# Configure gateway +export OTEL_ENABLE_OBSERVABILITY=true +export OTEL_TRACES_EXPORTER=otlp +export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 + +# Run gateway - traces automatically sent to Phoenix +mcpgateway +``` + +> ๐Ÿ” **What Gets Traced**: Tool invocations, prompt rendering, resource fetching, gateway federation, health checks, plugin execution (if enabled) +> +> ๐Ÿš€ **Zero Overhead**: When `OTEL_ENABLE_OBSERVABILITY=false`, all tracing is disabled with no performance impact +> +> ๐Ÿ“Š **View Traces**: Phoenix UI at `http://localhost:6006`, Jaeger at `http://localhost:16686`, or your configured backend + +### Transport + +| Setting | Description | Default | Options | +| ------------------------- | ---------------------------------- | ------- | ------------------------------- | +| `TRANSPORT_TYPE` | Enabled transports | `all` | `http`,`ws`,`sse`,`stdio`,`all` | +| `WEBSOCKET_PING_INTERVAL` | WebSocket ping (secs) | `30` | int > 0 | +| `SSE_RETRY_TIMEOUT` | SSE retry timeout (ms) | `5000` | int > 0 | +| `SSE_KEEPALIVE_ENABLED` | Enable SSE keepalive events | `true` | bool | +| `SSE_KEEPALIVE_INTERVAL` | SSE keepalive interval (secs) | `30` | int > 0 | +| `USE_STATEFUL_SESSIONS` | streamable http config | `false` | bool | +| `JSON_RESPONSE_ENABLED` | json/sse streams (streamable http) | `true` | bool | + +> **๐Ÿ’ก SSE Keepalive Events**: The gateway sends periodic keepalive events to prevent connection timeouts with proxies and load balancers. Disable with `SSE_KEEPALIVE_ENABLED=false` if your client doesn't handle unknown event types. Common intervals: 30s (default), 60s (AWS ALB), 240s (Azure). + +### Federation + +| Setting | Description | Default | Options | +| -------------------------- | ---------------------- | ------- | ---------- | +| `FEDERATION_ENABLED` | Enable federation | `true` | bool | +| `FEDERATION_DISCOVERY` | Auto-discover peers | `false` | bool | +| `FEDERATION_PEERS` | Comma-sep peer URLs | `[]` | JSON array | +| `FEDERATION_TIMEOUT` | Gateway timeout (secs) | `30` | int > 0 | +| `FEDERATION_SYNC_INTERVAL` | Sync interval (secs) | `300` | int > 0 | + +### Resources + +| Setting | Description | Default | Options | +| --------------------- | --------------------- | ---------- | ---------- | +| `RESOURCE_CACHE_SIZE` | LRU cache size | `1000` | int > 0 | +| `RESOURCE_CACHE_TTL` | Cache TTL (seconds) | `3600` | int > 0 | +| `MAX_RESOURCE_SIZE` | Max resource bytes | `10485760` | int > 0 | +| `ALLOWED_MIME_TYPES` | Acceptable MIME types | see code | JSON array | + +### Tools + +| Setting | Description | Default | Options | +| ----------------------- | ------------------------------ | ------- | ------- | +| `TOOL_TIMEOUT` | Tool invocation timeout (secs) | `60` | int > 0 | +| `MAX_TOOL_RETRIES` | Max retry attempts | `3` | int โ‰ฅ 0 | +| `TOOL_RATE_LIMIT` | Tool calls per minute | `100` | int > 0 | +| `TOOL_CONCURRENT_LIMIT` | Concurrent tool invocations | `10` | int > 0 | + +### Prompts + +| Setting | Description | Default | Options | +| ----------------------- | -------------------------------- | -------- | ------- | +| `PROMPT_CACHE_SIZE` | Cached prompt templates | `100` | int > 0 | +| `MAX_PROMPT_SIZE` | Max prompt template size (bytes) | `102400` | int > 0 | +| `PROMPT_RENDER_TIMEOUT` | Jinja render timeout (secs) | `10` | int > 0 | + +### Health Checks + +| Setting | Description | Default | Options | +| ----------------------- | ----------------------------------------- | ------- | ------- | +| `HEALTH_CHECK_INTERVAL` | Health poll interval (secs) | `60` | int > 0 | +| `HEALTH_CHECK_TIMEOUT` | Health request timeout (secs) | `10` | int > 0 | +| `UNHEALTHY_THRESHOLD` | Fail-count before peer deactivation, | `3` | int > 0 | +| | Set to -1 if deactivation is not needed. | | | + +### Database + +| Setting | Description | Default | Options | +| ----------------------- | ------------------------------- | ------- | ------- | +| `DB_POOL_SIZE` . | SQLAlchemy connection pool size | `200` | int > 0 | +| `DB_MAX_OVERFLOW`. | Extra connections beyond pool | `10` | int โ‰ฅ 0 | +| `DB_POOL_TIMEOUT`. | Wait for connection (secs) | `30` | int > 0 | +| `DB_POOL_RECYCLE`. | Recycle connections (secs) | `3600` | int > 0 | +| `DB_MAX_RETRIES` . | Max Retry Attempts | `3` | int > 0 | +| `DB_RETRY_INTERVAL_MS` | Retry Interval (ms) | `2000` | int > 0 | + +### Cache Backend + +| Setting | Description | Default | Options | +| ------------------------- | -------------------------- | -------- | ------------------------ | +| `CACHE_TYPE` | Backend (`memory`/`redis`) | `memory` | `none`, `memory`,`redis` | +| `REDIS_URL` | Redis connection URL | (none) | string or empty | +| `CACHE_PREFIX` | Key prefix | `mcpgw:` | string | +| `REDIS_MAX_RETRIES` | Max Retry Attempts | `3` | int > 0 | +| `REDIS_RETRY_INTERVAL_MS` | Retry Interval (ms) | `2000` | int > 0 | + +> ๐Ÿง  `none` disables caching entirely. Use `memory` for dev, `database` for persistence, or `redis` for distributed caching. + +### Database Management + +MCP Gateway uses Alembic for database migrations. Common commands: + +- `make db-current` - Show current database version +- `make db-upgrade` - Apply pending migrations +- `make db-migrate` - Create new migration +- `make db-history` - Show migration history +- `make db-status` - Detailed migration status + +#### Troubleshooting + +**Common Issues:** + +- **"No 'script_location' key found"**: Ensure you're running from the project root directory. + +- **"Unknown SSE event: keepalive" warnings**: Some MCP clients don't recognize keepalive events. These warnings are harmless and don't affect functionality. To disable: `SSE_KEEPALIVE_ENABLED=false` + +- **Connection timeouts with proxies/load balancers**: If experiencing timeouts, adjust keepalive interval to match your infrastructure: `SSE_KEEPALIVE_INTERVAL=60` (AWS ALB) or `240` (Azure). + +### Development + +| Setting | Description | Default | Options | +| ---------- | ---------------------- | ------- | ------- | +| `DEV_MODE` | Enable dev mode | `false` | bool | +| `RELOAD` | Auto-reload on changes | `false` | bool | +| `DEBUG` | Debug logging | `false` | bool | + +
+ +--- + +## Running + +### Makefile + +```bash + make serve # Run production Gunicorn server on + make serve-ssl # Run Gunicorn behind HTTPS on :4444 (uses ./certs) +``` + +### Script helper + +To run the development (uvicorn) server: + +```bash +make dev +# or +./run.sh --reload --log debug --workers 2 +``` + +> `run.sh` is a wrapper around `uvicorn` that loads `.env`, supports reload, and passes arguments to the server. + +Key flags: + +| Flag | Purpose | Example | +| ---------------- | ---------------- | ------------------ | +| `-e, --env FILE` | load env-file | `--env prod.env` | +| `-H, --host` | bind address | `--host 127.0.0.1` | +| `-p, --port` | listen port | `--port 8080` | +| `-w, --workers` | gunicorn workers | `--workers 4` | +| `-r, --reload` | auto-reload | `--reload` | + +### Manual (Uvicorn) + +```bash +uvicorn mcpgateway.main:app --host 0.0.0.0 --port 4444 --workers 4 +``` + +--- + +## Authentication examples + +```bash +# Generate a JWT token using JWT_SECRET_KEY and export it as MCPGATEWAY_BEARER_TOKEN +# Note that the module needs to be installed. If running locally use: +export MCPGATEWAY_BEARER_TOKEN=$(JWT_SECRET_KEY=my-test-key python3 -m mcpgateway.utils.create_jwt_token) + +# Use the JWT token in an API call +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/tools +``` + +--- + +## โ˜๏ธ AWS / Azure / OpenShift + +Deployment details can be found in the GitHub Pages. + +## โ˜๏ธ IBM Cloud Code Engine Deployment + +This project supports deployment to [IBM Cloud Code Engine](https://cloud.ibm.com/codeengine) using the **ibmcloud** CLI and the IBM Container Registry. + +
+โ˜๏ธ IBM Cloud Code Engine Deployment + +### ๐Ÿ”ง Prerequisites + +- Podman **or** Docker installed locally +- IBM Cloud CLI (use `make ibmcloud-cli-install` to install) +- An [IBM Cloud API key](https://cloud.ibm.com/iam/apikeys) with access to Code Engine & Container Registry +- Code Engine and Container Registry services **enabled** in your IBM Cloud account + +--- + +### ๐Ÿ“ฆ Environment Variables + +Create a **`.env`** file (or export the variables in your shell). +The first block is **required**; the second provides **tunable defaults** you can override: + +```bash +# โ”€โ”€ Required โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +IBMCLOUD_REGION=us-south +IBMCLOUD_RESOURCE_GROUP=default +IBMCLOUD_PROJECT=my-codeengine-project +IBMCLOUD_CODE_ENGINE_APP=mcpgateway +IBMCLOUD_IMAGE_NAME=us.icr.io/myspace/mcpgateway:latest +IBMCLOUD_IMG_PROD=mcpgateway/mcpgateway +IBMCLOUD_API_KEY=your_api_key_here # Optional - omit to use interactive `ibmcloud login --sso` + +# โ”€โ”€ Optional overrides (sensible defaults provided) โ”€โ”€โ”€โ”€โ”€โ”€ +IBMCLOUD_CPU=1 # vCPUs for the app +IBMCLOUD_MEMORY=4G # Memory allocation +IBMCLOUD_REGISTRY_SECRET=my-regcred # Name of the Container Registry secret +``` + +> โœ… **Quick check:** `make ibmcloud-check-env` + +--- + +### ๐Ÿš€ Make Targets + +| Target | Purpose | +| --------------------------- | ------------------------------------------------------------------------- | +| `make ibmcloud-cli-install` | Install IBM Cloud CLI and required plugins | +| `make ibmcloud-login` | Log in to IBM Cloud (API key or SSO) | +| `make ibmcloud-ce-login` | Select the Code Engine project & region | +| `make ibmcloud-tag` | Tag the local container image | +| `make ibmcloud-push` | Push the image to IBM Container Registry | +| `make ibmcloud-deploy` | **Create or update** the Code Engine application (uses CPU/memory/secret) | +| `make ibmcloud-ce-status` | Show current deployment status | +| `make ibmcloud-ce-logs` | Stream logs from the running app | +| `make ibmcloud-ce-rm` | Delete the Code Engine application | + +--- + +### ๐Ÿ“ Example Workflow + +```bash +make ibmcloud-check-env +make ibmcloud-cli-install +make ibmcloud-login +make ibmcloud-ce-login +make ibmcloud-tag +make ibmcloud-push +make ibmcloud-deploy +make ibmcloud-ce-status +make ibmcloud-ce-logs +``` + +
+ +--- + +## API Endpoints + +You can test the API endpoints through curl, or Swagger UI, and check detailed documentation on ReDoc: + +* **Swagger UI** โ†’ [http://localhost:4444/docs](http://localhost:4444/docs) +* **ReDoc** โ†’ [http://localhost:4444/redoc](http://localhost:4444/redoc) + +Generate an API Bearer token, and test the various API endpoints. + +
+๐Ÿ” Authentication & Health Checks + +```bash +# Generate a bearer token using the configured secret key (use the same as your .env) +export MCPGATEWAY_BEARER_TOKEN=$(python3 -m mcpgateway.utils.create_jwt_token -u admin --secret my-test-key) +echo ${MCPGATEWAY_BEARER_TOKEN} + +# Quickly confirm that authentication works and the gateway is healthy +curl -s -k -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" https://localhost:4444/health +# {"status":"healthy"} + +# Quickly confirm the gateway version & DB connectivity +curl -s -k -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" https://localhost:4444/version | jq +``` + +
+ +--- + +
+๐Ÿงฑ Protocol APIs (MCP) /protocol + +```bash +# Initialize MCP session +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "protocol_version":"2025-03-26", + "capabilities":{}, + "client_info":{"name":"MyClient","version":"1.0.0"} + }' \ + http://localhost:4444/protocol/initialize + +# Ping (JSON-RPC style) +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"ping"}' \ + http://localhost:4444/protocol/ping + +# Completion for prompt/resource arguments (not implemented) +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "ref":{"type":"ref/prompt","name":"example_prompt"}, + "argument":{"name":"topic","value":"py"} + }' \ + http://localhost:4444/protocol/completion/complete + +# Sampling (streaming) (not implemented) +curl -N -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "messages":[{"role":"user","content":{"type":"text","text":"Hello"}}], + "maxTokens":16 + }' \ + http://localhost:4444/protocol/sampling/createMessage +``` + +
+ +--- + +
+๐Ÿง  JSON-RPC Utility /rpc + +```bash +# Generic JSON-RPC calls (tools, gateways, roots, etc.) +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"list_tools"}' \ + http://localhost:4444/rpc +``` + +Handles any method name: `list_tools`, `list_gateways`, `prompts/get`, or invokes a tool if method matches a registered tool name . + +
+ +--- + +
+๐Ÿ”ง Tool Management /tools + + +```bash +# Register a new tool +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name":"clock_tool", + "url":"http://localhost:9000/rpc", + "description":"Returns current time", + "input_schema":{ + "type":"object", + "properties":{"timezone":{"type":"string"}}, + "required":[] + } + }' \ + http://localhost:4444/tools + +# List tools +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/tools + +# Get tool by ID +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/tools/1 + +# Update tool +curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ "description":"Updated desc" }' \ + http://localhost:4444/tools/1 + +# Toggle active status +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + http://localhost:4444/tools/1/toggle?activate=false +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + http://localhost:4444/tools/1/toggle?activate=true + +# Delete tool +curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/tools/1 +``` + +
+ +--- + +
+๐ŸŒ Gateway Management /gateways + +```bash +# Register an MCP server as a new gateway provider +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"peer_gateway","url":"http://peer:4444"}' \ + http://localhost:4444/gateways + +# List gateways +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/gateways + +# Get gateway by ID +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/gateways/1 + +# Update gateway +curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"description":"New description"}' \ + http://localhost:4444/gateways/1 + +# Toggle active status +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + http://localhost:4444/gateways/1/toggle?activate=false + +# Delete gateway +curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/gateways/1 +``` + +
+ +--- + +
+๐Ÿ“ Resource Management /resources + + +```bash +# Register resource +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "uri":"config://app/settings", + "name":"App Settings", + "content":"key=value" + }' \ + http://localhost:4444/resources + +# List resources +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/resources + +# Read a resource +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/resources/config://app/settings + +# Update resource +curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"content":"new=value"}' \ + http://localhost:4444/resources/config://app/settings + +# Delete resource +curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/resources/config://app/settings + +# Subscribe to updates (SSE) +curl -N -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/resources/subscribe/config://app/settings +``` + +
+ +--- + +
+๐Ÿ“ Prompt Management /prompts + +```bash +# Create prompt template +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name":"greet", + "template":"Hello, {{ user }}!", + "argument_schema":{ + "type":"object", + "properties":{"user":{"type":"string"}}, + "required":["user"] + } + }' \ + http://localhost:4444/prompts + +# List prompts +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/prompts + +# Get prompt (with args) +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"user":"Alice"}' \ + http://localhost:4444/prompts/greet + +# Get prompt (no args) +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/prompts/greet + +# Update prompt +curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"template":"Hi, {{ user }}!"}' \ + http://localhost:4444/prompts/greet + +# Toggle active +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + http://localhost:4444/prompts/5/toggle?activate=false + +# Delete prompt +curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/prompts/greet +``` + +
+ +--- + +
+๐ŸŒฒ Root Management /roots + +```bash +# List roots +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/roots + +# Add root +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"uri":"/data","name":"Data Root"}' \ + http://localhost:4444/roots + +# Remove root +curl -X DELETE -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/roots/%2Fdata + +# Subscribe to root changes (SSE) +curl -N -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/roots/changes +``` + +
+ +--- + +
+๐Ÿ–ฅ๏ธ Server Management /servers + +```bash +# List servers +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/servers + +# Get server +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/servers/UUID_OF_SERVER_1 + +# Create server +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name":"db","description":"Database","associatedTools": ["1","2","3"]}' \ + http://localhost:4444/servers + +# Update server +curl -X PUT -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"description":"Updated"}' \ + http://localhost:4444/servers/UUID_OF_SERVER_1 + +# Toggle active +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" \ + http://localhost:4444/servers/UUID_OF_SERVER_1/toggle?activate=false +``` + +
+ +--- + +
+๐Ÿ“Š Metrics /metrics + +```bash +# Get aggregated metrics +curl -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/metrics + +# Reset metrics (all or per-entity) +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/metrics/reset +curl -X POST -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/metrics/reset?entity=tool&id=1 +``` + +
+ +--- + +
+๐Ÿ“ก Events & Health + +```bash +# SSE: all events +curl -N -H "Authorization: Bearer $MCPGATEWAY_BEARER_TOKEN" http://localhost:4444/events + +# WebSocket +wscat -c ws://localhost:4444/ws \ + -H "Authorization: Basic $(echo -n admin:changeme|base64)" + +# Health check +curl http://localhost:4444/health +``` + +Full Swagger UI at `/docs`. + +
+ +--- + +
+๐Ÿ› ๏ธ Sample Tool + +```bash +uvicorn sample_tool.clock_tool:app --host 0.0.0.0 --port 9000 +``` + +```bash +curl -X POST -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","id":1,"method":"get_time","params":{"timezone":"UTC"}}' \ + http://localhost:9000/rpc +``` + +
+ +--- + +## Testing + +```bash +make test # Run unit tests +make lint # Run lint tools +``` + +## Doctest Coverage + +MCP Context Forge implements comprehensive doctest coverage to ensure all code examples in documentation are tested and verified: + +```bash +make doctest # Run all doctests +make doctest-verbose # Run with detailed output +make doctest-coverage # Generate coverage report +make doctest-check # Check coverage percentage +``` + +**Coverage Status:** +- โœ… **Transport Modules**: 100% (base, stdio, SSE, WebSocket, streamable HTTP) +- โœ… **Utility Functions**: 100% (slug generation, JWT tokens, validation) +- โœ… **Configuration**: 100% (settings, environment variables) +- ๐Ÿ”„ **Service Classes**: ~60% (in progress) +- ๐Ÿ”„ **Complex Classes**: ~40% (in progress) + +**Benefits:** +- All documented examples are automatically tested +- Documentation stays accurate and up-to-date +- Developers can run examples directly from docstrings +- Regression prevention through automated verification + +For detailed information, see the [Doctest Coverage Guide](https://ibm.github.io/mcp-context-forge/development/doctest-coverage/). + +--- + +## Project Structure + +
+๐Ÿ“ Directory and file structure for mcpgateway + +```bash +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ CI / Quality & Meta-files โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +โ”œโ”€โ”€ .bumpversion.cfg # Automated semantic-version bumps +โ”œโ”€โ”€ .coveragerc # Coverage.py settings +โ”œโ”€โ”€ .darglint # Doc-string linter rules +โ”œโ”€โ”€ .dockerignore # Context exclusions for image builds +โ”œโ”€โ”€ .editorconfig # Consistent IDE / editor behaviour +โ”œโ”€โ”€ .env # Local runtime variables (git-ignored) +โ”œโ”€โ”€ .env.ce # IBM Code Engine runtime env (ignored) +โ”œโ”€โ”€ .env.ce.example # Sample env for IBM Code Engine +โ”œโ”€โ”€ .env.example # Generic sample env file +โ”œโ”€โ”€ .env.gcr # Google Cloud Run runtime env (ignored) +โ”œโ”€โ”€ .eslintrc.json # ESLint rules for JS / TS assets +โ”œโ”€โ”€ .flake8 # Flake-8 configuration +โ”œโ”€โ”€ .gitattributes # Git attributes (e.g. EOL normalisation) +โ”œโ”€โ”€ .github # GitHub settings, CI/CD workflows & templates +โ”‚ โ”œโ”€โ”€ CODEOWNERS # Default reviewers +โ”‚ โ””โ”€โ”€ workflows/ # Bandit, Docker, CodeQL, Python Package, Container Deployment, etc. +โ”œโ”€โ”€ .gitignore # Git exclusion rules +โ”œโ”€โ”€ .hadolint.yaml # Hadolint rules for Dockerfiles +โ”œโ”€โ”€ .htmlhintrc # HTMLHint rules +โ”œโ”€โ”€ .markdownlint.json # Markdown-lint rules +โ”œโ”€โ”€ .pre-commit-config.yaml # Pre-commit hooks (ruff, black, mypy, ...) +โ”œโ”€โ”€ .pycodestyle # PEP-8 checker settings +โ”œโ”€โ”€ .pylintrc # Pylint configuration +โ”œโ”€โ”€ .pyspelling.yml # Spell-checker dictionary & filters +โ”œโ”€โ”€ .ruff.toml # Ruff linter / formatter settings +โ”œโ”€โ”€ .spellcheck-en.txt # Extra dictionary entries +โ”œโ”€โ”€ .stylelintrc.json # Stylelint rules for CSS +โ”œโ”€โ”€ .travis.yml # Legacy Travis CI config (reference) +โ”œโ”€โ”€ .whitesource # WhiteSource security-scanning config +โ”œโ”€โ”€ .yamllint # yamllint ruleset + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Documentation & Guidance โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +โ”œโ”€โ”€ CHANGELOG.md # Version-by-version change log +โ”œโ”€โ”€ CODE_OF_CONDUCT.md # Community behaviour guidelines +โ”œโ”€โ”€ CONTRIBUTING.md # How to file issues & send PRs +โ”œโ”€โ”€ DEVELOPING.md # Contributor workflows & style guide +โ”œโ”€โ”€ LICENSE # Apache License 2.0 +โ”œโ”€โ”€ README.md # Project overview & quick-start +โ”œโ”€โ”€ SECURITY.md # Security policy & CVE disclosure process +โ”œโ”€โ”€ TESTING.md # Testing strategy, fixtures & guidelines + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Containerisation & Runtime โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +โ”œโ”€โ”€ Containerfile # OCI image build (Docker / Podman) +โ”œโ”€โ”€ Containerfile.lite # FROM scratch UBI-Micro production build +โ”œโ”€โ”€ docker-compose.yml # Local multi-service stack +โ”œโ”€โ”€ podman-compose-sonarqube.yaml # One-liner SonarQube stack +โ”œโ”€โ”€ run-gunicorn.sh # Opinionated Gunicorn startup script +โ”œโ”€โ”€ run.sh # Uvicorn shortcut with arg parsing + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Build / Packaging / Tooling โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +โ”œโ”€โ”€ MANIFEST.in # sdist inclusion rules +โ”œโ”€โ”€ Makefile # Dev & deployment targets +โ”œโ”€โ”€ package-lock.json # Deterministic npm lock-file +โ”œโ”€โ”€ package.json # Front-end / docs tooling deps +โ”œโ”€โ”€ pyproject.toml # Poetry / PDM config & lint rules +โ”œโ”€โ”€ sonar-code.properties # SonarQube analysis settings +โ”œโ”€โ”€ uv.lock # UV resolver lock-file + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Kubernetes & Helm Assets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +โ”œโ”€โ”€ charts # Helm chart(s) for K8s / OpenShift +โ”‚ โ”œโ”€โ”€ mcp-stack # Umbrella chart +โ”‚ โ”‚ โ”œโ”€โ”€ Chart.yaml # Chart metadata +โ”‚ โ”‚ โ”œโ”€โ”€ templates/... # Manifest templates +โ”‚ โ”‚ โ””โ”€โ”€ values.yaml # Default values +โ”‚ โ””โ”€โ”€ README.md # Install / upgrade guide +โ”œโ”€โ”€ k8s # Raw (non-Helm) K8s manifests +โ”‚ โ””โ”€โ”€ *.yaml # Deployment, Service, PVC resources + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Documentation Source โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +โ”œโ”€โ”€ docs # MkDocs site source +โ”‚ โ”œโ”€โ”€ base.yml # MkDocs "base" configuration snippet (do not modify) +โ”‚ โ”œโ”€โ”€ mkdocs.yml # Site configuration (requires base.yml) +โ”‚ โ”œโ”€โ”€ requirements.txt # Python dependencies for the MkDocs site +โ”‚ โ”œโ”€โ”€ Makefile # Make targets for building/serving the docs +โ”‚ โ””โ”€โ”€ theme # Custom MkDocs theme assets +โ”‚ โ””โ”€โ”€ logo.png # Logo for the documentation theme +โ”‚ โ””โ”€โ”€ docs # Markdown documentation +โ”‚ โ”œโ”€โ”€ architecture/ # ADRs for the project +โ”‚ โ”œโ”€โ”€ articles/ # Long-form writeups +โ”‚ โ”œโ”€โ”€ blog/ # Blog posts +โ”‚ โ”œโ”€โ”€ deployment/ # Deployment guides (AWS, Azure, etc.) +โ”‚ โ”œโ”€โ”€ development/ # Development workflows & CI docs +โ”‚ โ”œโ”€โ”€ images/ # Diagrams & screenshots +โ”‚ โ”œโ”€โ”€ index.md # Top-level docs landing page +โ”‚ โ”œโ”€โ”€ manage/ # Management topics (backup, logging, tuning, upgrade) +โ”‚ โ”œโ”€โ”€ overview/ # Feature overviews & UI documentation +โ”‚ โ”œโ”€โ”€ security/ # Security guidance & policies +โ”‚ โ”œโ”€โ”€ testing/ # Testing strategy & fixtures +โ”‚ โ””โ”€โ”€ using/ # User-facing usage guides (agents, clients, etc.) +โ”‚ โ”œโ”€โ”€ media/ # Social media, press coverage, videos & testimonials +โ”‚ โ”‚ โ”œโ”€โ”€ press/ # Press articles and blog posts +โ”‚ โ”‚ โ”œโ”€โ”€ social/ # Tweets, LinkedIn posts, YouTube embeds +โ”‚ โ”‚ โ”œโ”€โ”€ testimonials/ # Customer quotes & community feedback +โ”‚ โ”‚ โ””โ”€โ”€ kit/ # Media kit & logos for bloggers & press +โ”œโ”€โ”€ dictionary.dic # Custom dictionary for spell-checker (make spellcheck) + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Application & Libraries โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +โ”œโ”€โ”€ agent_runtimes # Configurable agentic frameworks converted to MCP Servers +โ”œโ”€โ”€ mcpgateway # โ† main application package +โ”‚ โ”œโ”€โ”€ __init__.py # Package metadata & version constant +โ”‚ โ”œโ”€โ”€ admin.py # FastAPI routers for Admin UI +โ”‚ โ”œโ”€โ”€ cache +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ”œโ”€โ”€ resource_cache.py # LRU+TTL cache implementation +โ”‚ โ”‚ โ””โ”€โ”€ session_registry.py # Session โ†” cache mapping +โ”‚ โ”œโ”€โ”€ config.py # Pydantic settings loader +โ”‚ โ”œโ”€โ”€ db.py # SQLAlchemy models & engine setup +โ”‚ โ”œโ”€โ”€ federation +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ”œโ”€โ”€ discovery.py # Peer-gateway discovery +โ”‚ โ”‚ โ”œโ”€โ”€ forward.py # RPC forwarding +โ”‚ โ”œโ”€โ”€ handlers +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ””โ”€โ”€ sampling.py # Streaming sampling handler +โ”‚ โ”œโ”€โ”€ main.py # FastAPI app factory & startup events +โ”‚ โ”œโ”€โ”€ mcp.db # SQLite fixture for tests +โ”‚ โ”œโ”€โ”€ py.typed # PEP 561 marker (ships type hints) +โ”‚ โ”œโ”€โ”€ schemas.py # Shared Pydantic DTOs +โ”‚ โ”œโ”€โ”€ services +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ”œโ”€โ”€ completion_service.py # Prompt / argument completion +โ”‚ โ”‚ โ”œโ”€โ”€ gateway_service.py # Peer-gateway registry +โ”‚ โ”‚ โ”œโ”€โ”€ logging_service.py # Central logging helpers +โ”‚ โ”‚ โ”œโ”€โ”€ prompt_service.py # Prompt CRUD & rendering +โ”‚ โ”‚ โ”œโ”€โ”€ resource_service.py # Resource registration & retrieval +โ”‚ โ”‚ โ”œโ”€โ”€ root_service.py # File-system root registry +โ”‚ โ”‚ โ”œโ”€โ”€ server_service.py # Server registry & monitoring +โ”‚ โ”‚ โ””โ”€โ”€ tool_service.py # Tool registry & invocation +โ”‚ โ”œโ”€โ”€ static +โ”‚ โ”‚ โ”œโ”€โ”€ admin.css # Styles for Admin UI +โ”‚ โ”‚ โ””โ”€โ”€ admin.js # Behaviour for Admin UI +โ”‚ โ”œโ”€โ”€ templates +โ”‚ โ”‚ โ””โ”€โ”€ admin.html # HTMX/Alpine Admin UI template +โ”‚ โ”œโ”€โ”€ transports +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ”œโ”€โ”€ base.py # Abstract transport interface +โ”‚ โ”‚ โ”œโ”€โ”€ sse_transport.py # Server-Sent Events transport +โ”‚ โ”‚ โ”œโ”€โ”€ stdio_transport.py # stdio transport for embedding +โ”‚ โ”‚ โ””โ”€โ”€ websocket_transport.py # WS transport with ping/pong +โ”‚ โ”œโ”€โ”€ models.py # Core enums / type aliases +โ”‚ โ”œโ”€โ”€ utils +โ”‚ โ”‚ โ”œโ”€โ”€ create_jwt_token.py # CLI & library for JWT generation +โ”‚ โ”‚ โ”œโ”€โ”€ services_auth.py # Service-to-service auth dependency +โ”‚ โ”‚ โ””โ”€โ”€ verify_credentials.py # Basic / JWT auth helpers +โ”‚ โ”œโ”€โ”€ validation +โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”‚ โ””โ”€โ”€ jsonrpc.py # JSON-RPC 2.0 validation +โ”‚ โ””โ”€โ”€ version.py # Library version helper +โ”œโ”€โ”€ mcpgateway-wrapper # Stdio client wrapper (PyPI) +โ”‚ โ”œโ”€โ”€ pyproject.toml +โ”‚ โ”œโ”€โ”€ README.md +โ”‚ โ””โ”€โ”€ src/mcpgateway_wrapper/ +โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ””โ”€โ”€ server.py # Wrapper entry-point +โ”œโ”€โ”€ mcp-servers # Sample downstream MCP servers +โ”œโ”€โ”€ mcp.db # Default SQLite DB (auto-created) +โ”œโ”€โ”€ mcpgrid # Experimental grid client / PoC +โ”œโ”€โ”€ os_deps.sh # Installs system-level deps for CI + +# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Tests & QA Assets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +โ”œโ”€โ”€ test_readme.py # Guard: README stays in sync +โ”œโ”€โ”€ tests +โ”‚ โ”œโ”€โ”€ conftest.py # Shared fixtures +โ”‚ โ”œโ”€โ”€ e2e/... # End-to-end scenarios +โ”‚ โ”œโ”€โ”€ hey/... # Load-test logs & helper script +โ”‚ โ”œโ”€โ”€ integration/... # API-level integration tests +โ”‚ โ””โ”€โ”€ unit/... # Pure unit tests for business logic +``` + +
+ +--- + +## API Documentation + +* **Swagger UI** โ†’ [http://localhost:4444/docs](http://localhost:4444/docs) +* **ReDoc** โ†’ [http://localhost:4444/redoc](http://localhost:4444/redoc) +* **Admin Panel** โ†’ [http://localhost:4444/admin](http://localhost:4444/admin) + +--- + +## Makefile targets + +This project offer the following Makefile targets. Type `make` in the project root to show all targets. + +
+๐Ÿ”ง Available Makefile targets + +```bash +๐Ÿ MCP CONTEXT FORGE (An enterprise-ready Model Context Protocol Gateway) +๐Ÿ”ง SYSTEM-LEVEL DEPENDENCIES (DEV BUILD ONLY) +os-deps - Install Graphviz, Pandoc, Trivy, SCC used for dev docs generation and security scan +๐ŸŒฑ VIRTUAL ENVIRONMENT & INSTALLATION +venv - Create a fresh virtual environment with uv & friends +activate - Activate the virtual environment in the current shell +install - Install project into the venv +install-dev - Install project (incl. dev deps) into the venv +install-db - Install project (incl. postgres and redis) into venv +update - Update all installed deps inside the venv +check-env - Verify all required env vars in .env are present +โ–ถ๏ธ SERVE & TESTING +serve - Run production Gunicorn server on :4444 +certs - Generate self-signed TLS cert & key in ./certs (won't overwrite) +serve-ssl - Run Gunicorn behind HTTPS on :4444 (uses ./certs) +dev - Run fast-reload dev server (uvicorn) +run - Execute helper script ./run.sh +test - Run unit tests with pytest +test-curl - Smoke-test API endpoints with curl script +pytest-examples - Run README / examples through pytest-examples +clean - Remove caches, build artefacts, virtualenv, docs, certs, coverage, SBOM, etc. +๐Ÿ“Š COVERAGE & METRICS +coverage - Run tests with coverage, emit md/HTML/XML + badge +pip-licenses - Produce dependency license inventory (markdown) +scc - Quick LoC/complexity snapshot with scc +scc-report - Generate HTML LoC & per-file metrics with scc +๐Ÿ“š DOCUMENTATION & SBOM +docs - Build docs (graphviz + handsdown + images + SBOM) +images - Generate architecture & dependency diagrams +๐Ÿ” LINTING & STATIC ANALYSIS +lint - Run the full linting suite (see targets below) +black - Reformat code with black +autoflake - Remove unused imports / variables with autoflake +isort - Organise & sort imports with isort +flake8 - PEP-8 style & logical errors +pylint - Pylint static analysis +markdownlint - Lint Markdown files with markdownlint (requires markdownlint-cli) +mypy - Static type-checking with mypy +bandit - Security scan with bandit +pydocstyle - Docstring style checker +pycodestyle - Simple PEP-8 checker +pre-commit - Run all configured pre-commit hooks +ruff - Ruff linter + formatter +ty - Ty type checker from astral +pyright - Static type-checking with Pyright +radon - Code complexity & maintainability metrics +pyroma - Validate packaging metadata +importchecker - Detect orphaned imports +spellcheck - Spell-check the codebase +fawltydeps - Detect undeclared / unused deps +wily - Maintainability report +pyre - Static analysis with Facebook Pyre +depend - List dependencies in โ‰ˆrequirements format +snakeviz - Profile & visualise with snakeviz +pstats - Generate PNG call-graph from cProfile stats +spellcheck-sort - Sort local spellcheck dictionary +tox - Run tox across multi-Python versions +sbom - Produce a CycloneDX SBOM and vulnerability scan +pytype - Flow-sensitive type checker +check-manifest - Verify sdist/wheel completeness +yamllint - Lint YAML files (uses .yamllint) +jsonlint - Validate every *.json file with jq (--exit-status) +tomllint - Validate *.toml files with tomlcheck +๐Ÿ•ธ๏ธ WEBPAGE LINTERS & STATIC ANALYSIS (HTML/CSS/JS lint + security scans + formatting) +install-web-linters - Install HTMLHint, Stylelint, ESLint, Retire.js & Prettier via npm +lint-web - Run HTMLHint, Stylelint, ESLint, Retire.js and npm audit +format-web - Format HTML, CSS & JS files with Prettier +osv-install - Install/upgrade osv-scanner (Go) +osv-scan-source - Scan source & lockfiles for CVEs +osv-scan-image - Scan the built container image for CVEs +osv-scan - Run all osv-scanner checks (source, image, licence) +๐Ÿ“ก SONARQUBE ANALYSIS +sonar-deps-podman - Install podman-compose + supporting tools +sonar-deps-docker - Install docker-compose + supporting tools +sonar-up-podman - Launch SonarQube with podman-compose +sonar-up-docker - Launch SonarQube with docker-compose +sonar-submit-docker - Run containerized Sonar Scanner CLI with Docker +sonar-submit-podman - Run containerized Sonar Scanner CLI with Podman +pysonar-scanner - Run scan with Python wrapper (pysonar-scanner) +sonar-info - How to create a token & which env vars to export +๐Ÿ›ก๏ธ SECURITY & PACKAGE SCANNING +trivy - Scan container image for CVEs (HIGH/CRIT). Needs podman socket enabled +grype-scan - Scan container for security audit and vulnerability scanning +dockle - Lint the built container image via tarball (no daemon/socket needed) +hadolint - Lint Containerfile/Dockerfile(s) with hadolint +pip-audit - Audit Python dependencies for published CVEs +๐Ÿ“ฆ DEPENDENCY MANAGEMENT +deps-update - Run update-deps.py to update all dependencies in pyproject.toml and docs/requirements.txt +containerfile-update - Update base image in Containerfile to latest tag +๐Ÿ“ฆ PACKAGING & PUBLISHING +dist - Clean-build wheel *and* sdist into ./dist +wheel - Build wheel only +sdist - Build source distribution only +verify - Build + twine + check-manifest + pyroma (no upload) +publish - Verify, then upload to PyPI (needs TWINE_* creds) +๐Ÿฆญ PODMAN CONTAINER BUILD & RUN +podman-dev - Build development container image +podman - Build container image +podman-prod - Build production container image (using ubi-micro โ†’ scratch). Not supported on macOS. +podman-run - Run the container on HTTP (port 4444) +podman-run-shell - Run the container on HTTP (port 4444) and start a shell +podman-run-ssl - Run the container on HTTPS (port 4444, self-signed) +podman-run-ssl-host - Run the container on HTTPS with --network=host (port 4444, self-signed) +podman-stop - Stop & remove the container +podman-test - Quick curl smoke-test against the container +podman-logs - Follow container logs (โŒƒC to quit) +podman-stats - Show container resource stats (if supported) +podman-top - Show live top-level process info in container +podman-shell - Open an interactive shell inside the Podman container +๐Ÿ‹ DOCKER BUILD & RUN +docker-dev - Build development Docker image +docker - Build production Docker image +docker-prod - Build production container image (using ubi-micro โ†’ scratch). Not supported on macOS. +docker-run - Run the container on HTTP (port 4444) +docker-run-ssl - Run the container on HTTPS (port 4444, self-signed) +docker-stop - Stop & remove the container +docker-test - Quick curl smoke-test against the container +docker-logs - Follow container logs (โŒƒC to quit) +docker-stats - Show container resource usage stats (non-streaming) +docker-top - Show top-level process info in Docker container +docker-shell - Open an interactive shell inside the Docker container +๐Ÿ› ๏ธ COMPOSE STACK - Build / start / stop the multi-service stack +compose-up - Bring the whole stack up (detached) +compose-restart - Recreate changed containers, pulling / building as needed +compose-build - Build (or rebuild) images defined in the compose file +compose-pull - Pull the latest images only +compose-logs - Tail logs from all services (Ctrl-C to exit) +compose-ps - Show container status table +compose-shell - Open an interactive shell in the "gateway" container +compose-stop - Gracefully stop the stack (keep containers) +compose-down - Stop & remove containers (keep named volumes) +compose-rm - Remove *stopped* containers +compose-clean - โœจ Down **and** delete named volumes (data-loss โš ) +โ˜๏ธ IBM CLOUD CODE ENGINE +ibmcloud-check-env - Verify all required IBM Cloud env vars are set +ibmcloud-cli-install - Auto-install IBM Cloud CLI + required plugins (OS auto-detected) +ibmcloud-login - Login to IBM Cloud CLI using IBMCLOUD_API_KEY (--sso) +ibmcloud-ce-login - Set Code Engine target project and region +ibmcloud-list-containers - List deployed Code Engine apps +ibmcloud-tag - Tag container image for IBM Container Registry +ibmcloud-push - Push image to IBM Container Registry +ibmcloud-deploy - Deploy (or update) container image in Code Engine +ibmcloud-ce-logs - Stream logs for the deployed application +ibmcloud-ce-status - Get deployment status +ibmcloud-ce-rm - Delete the Code Engine application +๐Ÿงช MINIKUBE LOCAL CLUSTER +minikube-install - Install Minikube (macOS, Linux, or Windows via choco) +helm-install - Install Helm CLI (macOS, Linux, or Windows) +minikube-start - Start local Minikube cluster with Ingress + DNS + metrics-server +minikube-stop - Stop the Minikube cluster +minikube-delete - Delete the Minikube cluster +minikube-image-load - Build and load ghcr.io/ibm/mcp-context-forge:latest into Minikube +minikube-k8s-apply - Apply Kubernetes manifests from deployment/k8s/ +minikube-status - Show status of Minikube and ingress pods +๐Ÿ› ๏ธ HELM CHART TASKS +helm-lint - Lint the Helm chart (static analysis) +helm-package - Package the chart into dist/ as mcp-stack-.tgz +helm-deploy - Upgrade/Install chart into Minikube (profile mcpgw) +helm-delete - Uninstall the chart release from Minikube +๐Ÿ  LOCAL PYPI SERVER +local-pypi-install - Install pypiserver for local testing +local-pypi-start - Start local PyPI server on :8084 (no auth) +local-pypi-start-auth - Start local PyPI server with basic auth (admin/admin) +local-pypi-stop - Stop local PyPI server +local-pypi-upload - Upload existing package to local PyPI (no auth) +local-pypi-upload-auth - Upload existing package to local PyPI (with auth) +local-pypi-test - Install package from local PyPI +local-pypi-clean - Full cycle: build โ†’ upload โ†’ install locally +๐Ÿ  LOCAL DEVPI SERVER +devpi-install - Install devpi server and client +devpi-init - Initialize devpi server (first time only) +devpi-start - Start devpi server +devpi-stop - Stop devpi server +devpi-setup-user - Create user and dev index +devpi-upload - Upload existing package to devpi +devpi-test - Install package from devpi +devpi-clean - Full cycle: build โ†’ upload โ†’ install locally +devpi-status - Show devpi server status +devpi-web - Open devpi web interface +``` +
+ +## ๐Ÿ” Troubleshooting + +
+Port publishing on WSL2 (rootless Podman & Docker Desktop) + +### Diagnose the listener + +```bash +# Inside your WSL distro +ss -tlnp | grep 4444 # Use ss +netstat -anp | grep 4444 # or netstat +``` + +*Seeing `:::4444 LISTEN rootlessport` is normal* - the IPv6 wildcard +socket (`::`) also accepts IPv4 traffic **when** +`net.ipv6.bindv6only = 0` (default on Linux). + +### Why localhost fails on Windows + +WSL 2's NAT layer rewrites only the *IPv6* side of the dual-stack listener. From Windows, `http://127.0.0.1:4444` (or Docker Desktop's "localhost") therefore times-out. + +#### Fix (Podman rootless) + +```bash +# Inside the WSL distro +echo "wsl" | sudo tee /etc/containers/podman-machine +systemctl --user restart podman.socket +``` + +`ss` should now show `0.0.0.0:4444` instead of `:::4444`, and the +service becomes reachable from Windows *and* the LAN. + +#### Fix (Docker Desktop > 4.19) + +Docker Desktop adds a "WSL integration" switch per-distro. +Turn it **on** for your distro, restart Docker Desktop, then restart the +container: + +```bash +docker restart mcpgateway +``` + +
+ +
+Gateway starts but immediately exits ("Failed to read DATABASE_URL") + +Copy `.env.example` to `.env` first: + +```bash +cp .env.example .env +``` + +Then edit `DATABASE_URL`, `JWT_SECRET_KEY`, `BASIC_AUTH_PASSWORD`, etc. +Missing or empty required vars cause a fast-fail at startup. + +
+ +## Contributing + +1. Fork the repo, create a feature branch. +2. Run `make lint` and fix any issues. +3. Keep `make test` green and 100% coverage. +4. Open a PR - describe your changes clearly. + +See [CONTRIBUTING.md](CONTRIBUTING.md) for more details. +--- + +## Changelog + +A complete changelog can be found here: [CHANGELOG.md](./CHANGELOG.md) + +## License + +Licensed under the **Apache License 2.0** - see [LICENSE](./LICENSE) + + +## Core Authors and Maintainers + +- [Mihai Criveti](https://www.linkedin.com/in/crivetimihai) - Distinguished Engineer, Agentic AI + +Special thanks to our contributors for helping us improve ContextForge MCP Gateway: + + + + + +## Star History and Project Activity -* **Mihai Criveti** - IBM Distinguished Engineer, Agentic AI +[![Star History Chart](https://api.star-history.com/svg?repos=ibm/mcp-context-forge&type=Date)](https://www.star-history.com/#ibm/mcp-context-forge&Date) - + +[![PyPi Downloads](https://static.pepy.tech/badge/mcp-contextforge-gateway/month)](https://pepy.tech/project/mcp-contextforge-gateway)  +[![Stars](https://img.shields.io/github/stars/ibm/mcp-context-forge?style=social)](https://github.com/ibm/mcp-context-forge/stargazers)  +[![Forks](https://img.shields.io/github/forks/ibm/mcp-context-forge?style=social)](https://github.com/ibm/mcp-context-forge/network/members)  +[![Contributors](https://img.shields.io/github/contributors/ibm/mcp-context-forge)](https://github.com/ibm/mcp-context-forge/graphs/contributors)  +[![Last Commit](https://img.shields.io/github/last-commit/ibm/mcp-context-forge)](https://github.com/ibm/mcp-context-forge/commits)  +[![Open Issues](https://img.shields.io/github/issues/ibm/mcp-context-forge)](https://github.com/ibm/mcp-context-forge/issues)  diff --git a/docs/docs/using/plugins/.pages b/docs/docs/using/plugins/.pages index 35fd5a113..a54e542d8 100644 --- a/docs/docs/using/plugins/.pages +++ b/docs/docs/using/plugins/.pages @@ -1,2 +1,3 @@ nav: - index.md + - lifecycle.md From 4e24a9bd9b7d803a38110f5d86efb9902777b696 Mon Sep 17 00:00:00 2001 From: Frederico Araujo Date: Mon, 18 Aug 2025 15:21:58 -0400 Subject: [PATCH 64/65] docs: add missing plugin lifecycle doc Signed-off-by: Frederico Araujo --- docs/docs/using/plugins/lifecycle.md | 163 +++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 docs/docs/using/plugins/lifecycle.md diff --git a/docs/docs/using/plugins/lifecycle.md b/docs/docs/using/plugins/lifecycle.md new file mode 100644 index 000000000..0ac962583 --- /dev/null +++ b/docs/docs/using/plugins/lifecycle.md @@ -0,0 +1,163 @@ +# Plugin Lifecycle + +The plugin framework includes CLI tools to help you create, test, and deploy your plugins. + +## Development Flow + +```mermaid +flowchart LR + + A["template"] + B(["$> bootstrap"]) + C(["$> build"]) + D(["$> serve"]) + + subgraph dev + A -.-> B + end + + subgraph deploy + C --> D + end + + B --> C + + style A stroke-dasharray: 3 3; +``` + +### Bootstrap + +Creating a new plugin for Context Forge only takes a few minutes! + +Using the `mcpplugins` tool (installed with Context Forge), + +```bash +mcpplugins bootstrap --destination your/plugin/dir +``` + +The interactive prompt you guide you to enter plugin metadata, and will boostrap a complete plugin project for you including everything you need to kick the tires writing your new plugin. + +For a full list of options, check: + +```bash +mcpplugins bootstrap --help +``` + +!!! tip + When prompted for the choosing the plugin type, select `external` to create standalone plugins (with their own lock files and dependency trees). + Select `native` if you want to create a plugin that embeds and gets loaded directly into the gateway environment. + + +The examples under the `plugins` directory in the gateway repository serve as a guide of how to develop and test `native` plugins. + +The following instructions apply to `external` plugins. First, change directory to work with your newly bootstrapped plugin: + +```bash +cd your/plugin/dir +cp .env.template .env +``` + +### Configuration + +There are two main configuration files for your project. + +```bash +./resources + /plugins/config.yaml # configuration for your plugin and the plugin loader + /runtime/config.yaml # configuration for the plugin server runtime +``` + +Inspect those two files and get familiar with them. There are many options you can customize, depending on your use case. + +### Dependencies + +Plugins are Python packages with dependencies managed by `uv`. Just like the gateway, you can add, lock, lint, and ensure that best practices are followed when creating your plugins. To ensure compatibility with the gateway linters, we do not directly install them in the template. Instead, please install the gateway package with the dev extras, which will include all required linters. + +### Test + +To run all unit tests for your plugins: + +```bash +make test +``` + +### Build + +To build a container image (runtime) containing a standardized plugin server, run: + +```bash +make build +``` + +### Serve + +To start the plugin server: + +```bash +make start +``` + +By default, this will start a Streamable HTTP MCP server on `http://localhost:8000/mcp`. + +You can run `mcp inspector` to check your new server (note, it requires `npm`): + +```bash +npx @modelcontextprotocol/inspector +``` + +## Gateway Integration + +Let's assume you have boostrapped the following plugin (`resources/plugins/config.yaml`) with default runtime (`resources/runtime/config.yaml`) options: + +```yaml +plugins: + - name: "MyFilter" + kind: "myfilter.plugin.MyFilter" + description: "A filter plugin" + version: "0.1.0" + author: "Frederico Araujo" + hooks: ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke"] + tags: ["plugin"] + mode: "enforce" # enforce | permissive | disabled + priority: 150 + conditions: + # Apply to specific tools/servers + - server_ids: [] # Apply to all servers + tenant_ids: [] # Apply to all tenants + config: + # Plugin config dict passed to the plugin constructor + +# Plugin directories to scan +plugin_dirs: + - "myfilter" + +# Global plugin settings +plugin_settings: + parallel_execution_within_band: true + plugin_timeout: 30 + fail_on_plugin_error: false + enable_plugin_api: true + plugin_health_check_interval: 60 +``` + +To integrate this plugin with the gateway, all you need to do is copying the following configuration under the `plugins` list in the gateway's `plugins/config.yaml` file: + +```yaml +plugins: + # External Filter Plugin + - name: "MyFilter" + kind: "external" + priority: 10 # adjust the priority + mcp: + proto: STREAMABLEHTTP + url: http://localhost:8000/mcp +``` + +Then, start the gateway: + +```bash +make serve +``` + +!!! note + `PLUGINS_ENABLED=true` should be set in your gateway `.env` file. \ No newline at end of file From cf7df0024d7c308206bb27c32741991df6559e34 Mon Sep 17 00:00:00 2001 From: Mihai Criveti Date: Mon, 18 Aug 2025 23:46:21 +0100 Subject: [PATCH 65/65] Review, rebase and lint Signed-off-by: Mihai Criveti --- .env.example | 2 +- docs/docs/using/plugins/lifecycle.md | 2 +- .../plugins/framework/external/__init__.py | 1 + .../plugins/framework/external/mcp/client.py | 2 +- .../framework/external/mcp/server/runtime.py | 32 +- .../framework/external/mcp/server/server.py | 2 +- mcpgateway/plugins/framework/loader/plugin.py | 2 +- mcpgateway/plugins/framework/manager.py | 2 +- mcpgateway/plugins/framework/models.py | 10 +- mcpgateway/plugins/framework/utils.py | 12 +- mcpgateway/plugins/tools/cli.py | 7 +- plugin_templates/external/.env.template | 2 +- plugin_templates/external/Makefile.jinja | 510 +++++++++--------- plugin_templates/external/README.md.jinja | 2 +- .../resources/runtime/config.yaml.jinja | 2 +- plugin_templates/external/run-server.sh | 2 +- plugin_templates/external/tests/pytest.ini | 2 +- plugin_templates/external/tests/test_all.py | 1 + tests/unit/mcpgateway/plugins/__init__.py | 2 +- .../plugins/fixtures/plugins/__init__.py | 2 +- .../plugins/fixtures/plugins/passthrough.py | 5 +- .../mcpgateway/plugins/framework/__init__.py | 2 +- .../plugins/framework/external/__init__.py | 2 +- .../framework/external/mcp/__init__.py | 2 +- .../framework/external/mcp/server/__init__.py | 2 +- .../external/mcp/server/test_runtime.py | 17 +- .../external/mcp/test_client_stdio.py | 1 + .../mcp/test_client_streamable_http.py | 1 + .../plugins/framework/loader/__init__.py | 2 +- .../plugins/framework/test_errors.py | 2 +- .../mcpgateway/plugins/plugins/__init__.py | 2 +- .../plugins/plugins/pii_filter/__init__.py | 2 +- .../plugins/resource_filter/__init__.py | 2 +- .../unit/mcpgateway/plugins/tools/test_cli.py | 2 +- tests/unit/mcpgateway/utils/__init__.py | 2 +- 35 files changed, 325 insertions(+), 320 deletions(-) diff --git a/.env.example b/.env.example index 01d5542f4..3f94d0b94 100644 --- a/.env.example +++ b/.env.example @@ -173,4 +173,4 @@ PLUGINS_CLI_COMPLETION=false # markdown: allow markdown in help strings # disabled: disable markup # If unset (commented out), uses "rich" if rich is detected, otherwise disables it. -PLUGINS_CLI_MARKUP_MODE=rich \ No newline at end of file +PLUGINS_CLI_MARKUP_MODE=rich diff --git a/docs/docs/using/plugins/lifecycle.md b/docs/docs/using/plugins/lifecycle.md index 0ac962583..4b6829ab8 100644 --- a/docs/docs/using/plugins/lifecycle.md +++ b/docs/docs/using/plugins/lifecycle.md @@ -160,4 +160,4 @@ make serve ``` !!! note - `PLUGINS_ENABLED=true` should be set in your gateway `.env` file. \ No newline at end of file + `PLUGINS_ENABLED=true` should be set in your gateway `.env` file. diff --git a/mcpgateway/plugins/framework/external/__init__.py b/mcpgateway/plugins/framework/external/__init__.py index e1ac9d70d..918fa6921 100644 --- a/mcpgateway/plugins/framework/external/__init__.py +++ b/mcpgateway/plugins/framework/external/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """External plugin which connects to a remote server. Copyright 2025 diff --git a/mcpgateway/plugins/framework/external/mcp/client.py b/mcpgateway/plugins/framework/external/mcp/client.py index ec8509725..4363c3ca8 100644 --- a/mcpgateway/plugins/framework/external/mcp/client.py +++ b/mcpgateway/plugins/framework/external/mcp/client.py @@ -107,7 +107,7 @@ async def __connect_to_stdio_server(self, server_script_path: str) -> None: ValueError: if stdio script is not a python script. """ is_python = server_script_path.endswith(PYTHON_SUFFIX) if server_script_path else False - if not (is_python): + if not is_python: raise ValueError("Server script must be a .py file") current_env = os.environ.copy() diff --git a/mcpgateway/plugins/framework/external/mcp/server/runtime.py b/mcpgateway/plugins/framework/external/mcp/server/runtime.py index 87f8d6f22..75224cafd 100644 --- a/mcpgateway/plugins/framework/external/mcp/server/runtime.py +++ b/mcpgateway/plugins/framework/external/mcp/server/runtime.py @@ -36,17 +36,17 @@ logger = logging.getLogger(__name__) -server = None +SERVER = None @mcp_tool(name="get_plugin_configs", description="Get the plugin configurations installed on the server") async def get_plugin_configs() -> list[dict]: - """Return a list of plugin configurations for plugins currently installed on the MCP server. + """Return a list of plugin configurations for plugins currently installed on the MCP SERVER. Returns: A list of plugin configurations. """ - return await server.get_plugin_configs() + return await SERVER.get_plugin_configs() @mcp_tool(name="get_plugin_config", description="Get the plugin configuration installed on the server given a plugin name") @@ -59,7 +59,7 @@ async def get_plugin_config(name: str) -> dict: Returns: A list of plugin configurations. """ - return await server.get_plugin_config(name) + return await SERVER.get_plugin_config(name) @mcp_tool(name="prompt_pre_fetch", description="Execute prompt prefetch hook for a plugin") @@ -91,7 +91,7 @@ def prompt_pre_fetch_func(plugin: Plugin, payload: PromptPrehookPayload, context """ return plugin.prompt_pre_fetch(payload, context) - return await server.invoke_hook(PromptPrehookPayload, prompt_pre_fetch_func, plugin_name, payload, context) + return await SERVER.invoke_hook(PromptPrehookPayload, prompt_pre_fetch_func, plugin_name, payload, context) @mcp_tool(name="prompt_post_fetch", description="Execute prompt postfetch hook for a plugin") @@ -123,7 +123,7 @@ def prompt_post_fetch_func(plugin: Plugin, payload: PromptPosthookPayload, conte """ return plugin.prompt_post_fetch(payload, context) - return await server.invoke_hook(PromptPosthookPayload, prompt_post_fetch_func, plugin_name, payload, context) + return await SERVER.invoke_hook(PromptPosthookPayload, prompt_post_fetch_func, plugin_name, payload, context) @mcp_tool(name="tool_pre_invoke", description="Execute tool pre-invoke hook for a plugin") @@ -155,7 +155,7 @@ def tool_pre_invoke_func(plugin: Plugin, payload: ToolPreInvokePayload, context: """ return plugin.tool_pre_invoke(payload, context) - return await server.invoke_hook(ToolPreInvokePayload, tool_pre_invoke_func, plugin_name, payload, context) + return await SERVER.invoke_hook(ToolPreInvokePayload, tool_pre_invoke_func, plugin_name, payload, context) @mcp_tool(name="tool_post_invoke", description="Execute tool post-invoke hook for a plugin") @@ -187,7 +187,7 @@ def tool_post_invoke_func(plugin: Plugin, payload: ToolPostInvokePayload, contex """ return plugin.tool_post_invoke(payload, context) - return await server.invoke_hook(ToolPostInvokePayload, tool_post_invoke_func, plugin_name, payload, context) + return await SERVER.invoke_hook(ToolPostInvokePayload, tool_post_invoke_func, plugin_name, payload, context) @mcp_tool(name="resource_pre_fetch", description="Execute resource prefetch hook for a plugin") @@ -219,7 +219,7 @@ def resource_pre_fetch_func(plugin: Plugin, payload: ResourcePreFetchPayload, co """ return plugin.resource_pre_fetch(payload, context) - return await server.invoke_hook(ResourcePreFetchPayload, resource_pre_fetch_func, plugin_name, payload, context) + return await SERVER.invoke_hook(ResourcePreFetchPayload, resource_pre_fetch_func, plugin_name, payload, context) @mcp_tool(name="resource_post_fetch", description="Execute resource postfetch hook for a plugin") @@ -251,25 +251,25 @@ def resource_post_fetch_func(plugin: Plugin, payload: ResourcePostFetchPayload, """ return plugin.resource_post_fetch(payload, context) - return await server.invoke_hook(ResourcePostFetchPayload, resource_post_fetch_func, plugin_name, payload, context) + return await SERVER.invoke_hook(ResourcePostFetchPayload, resource_post_fetch_func, plugin_name, payload, context) async def run(): # pragma: no cover - """Run the external plugin server. + """Run the external plugin SERVER. Raises: - Exception: if unnable to run the plugin server. + Exception: if unnable to run the plugin SERVER. """ - global server - server = ExternalPluginServer() - if await server.initialize(): + global SERVER # pylint: disable=global-statement + SERVER = ExternalPluginServer() + if await SERVER.initialize(): try: await main_async() except Exception: logger.exception("Caught error while executing plugin server") raise finally: - await server.shutdown() + await SERVER.shutdown() if __name__ == "__main__": # pragma: no cover diff --git a/mcpgateway/plugins/framework/external/mcp/server/server.py b/mcpgateway/plugins/framework/external/mcp/server/server.py index 2dcfe4302..808190f4d 100644 --- a/mcpgateway/plugins/framework/external/mcp/server/server.py +++ b/mcpgateway/plugins/framework/external/mcp/server/server.py @@ -19,7 +19,7 @@ from pydantic import BaseModel # First-Party -from mcpgateway.plugins.framework import Plugin +from mcpgateway.plugins.framework.base import Plugin from mcpgateway.plugins.framework.errors import convert_exception_to_error from mcpgateway.plugins.framework.loader.config import ConfigLoader from mcpgateway.plugins.framework.manager import DEFAULT_PLUGIN_TIMEOUT, PluginManager diff --git a/mcpgateway/plugins/framework/loader/plugin.py b/mcpgateway/plugins/framework/loader/plugin.py index 3803d1b9f..e16739964 100644 --- a/mcpgateway/plugins/framework/loader/plugin.py +++ b/mcpgateway/plugins/framework/loader/plugin.py @@ -23,7 +23,7 @@ logger = logging.getLogger(__name__) -class PluginLoader(object): +class PluginLoader: """A plugin loader object for loading and instantiating plugins. Examples: diff --git a/mcpgateway/plugins/framework/manager.py b/mcpgateway/plugins/framework/manager.py index 08e33c99a..5f9a880d5 100644 --- a/mcpgateway/plugins/framework/manager.py +++ b/mcpgateway/plugins/framework/manager.py @@ -206,7 +206,7 @@ async def execute( if pluginref.plugin.mode == PluginMode.ENFORCE: logger.warning(f"Plugin {pluginref.plugin.name} blocked request in enforce mode") return (PluginResult[T](continue_processing=False, modified_payload=current_payload, violation=result.violation, metadata=combined_metadata), res_local_contexts) - elif pluginref.plugin.mode == PluginMode.PERMISSIVE: + if pluginref.plugin.mode == PluginMode.PERMISSIVE: logger.warning(f"Plugin {pluginref.plugin.name} would block (permissive mode): {result.violation.description if result.violation else 'No description'}") except asyncio.TimeoutError: diff --git a/mcpgateway/plugins/framework/models.py b/mcpgateway/plugins/framework/models.py index 03d09241b..222521494 100644 --- a/mcpgateway/plugins/framework/models.py +++ b/mcpgateway/plugins/framework/models.py @@ -268,7 +268,7 @@ def validate_script(cls, script: str | None) -> str | None: file_path = Path(script) if not file_path.is_file(): raise ValueError(f"MCP server script {script} does not exist.") - elif file_path.suffix != PYTHON_SUFFIX: + if file_path.suffix != PYTHON_SUFFIX: raise ValueError(f"MCP server script {script} does not have a .py suffix.") return script @@ -309,7 +309,7 @@ class PluginConfig(BaseModel): mcp: Optional[MCPConfig] = None @model_validator(mode=AFTER) - def check_url_or_script_filled(self) -> Self: + def check_url_or_script_filled(self) -> Self: # pylint: disable=bad-classmethod-argument """Checks to see that at least one of url or script are set depending on MCP server configuration. Raises: @@ -322,14 +322,14 @@ def check_url_or_script_filled(self) -> Self: return self if self.mcp.proto == TransportType.STDIO and not self.mcp.script: raise ValueError(f"Plugin {self.name} has transport type set to SSE but no script value") - elif (self.mcp.proto == TransportType.STREAMABLEHTTP or self.mcp.proto == TransportType.SSE) and not self.mcp.url: + if self.mcp.proto in (TransportType.STREAMABLEHTTP, TransportType.SSE) and not self.mcp.url: raise ValueError(f"Plugin {self.name} has transport type set to StreamableHTTP but no url value") - elif self.mcp.proto != TransportType.SSE and self.mcp.proto != TransportType.STREAMABLEHTTP and self.mcp.proto != TransportType.STDIO: + if self.mcp.proto not in (TransportType.SSE, TransportType.STREAMABLEHTTP, TransportType.STDIO): raise ValueError(f"Plugin {self.name} must set transport type to either SSE or STREAMABLEHTTP or STDIO") return self @model_validator(mode=AFTER) - def check_config_and_external(self, info: ValidationInfo) -> Self: + def check_config_and_external(self, info: ValidationInfo) -> Self: # pylint: disable=bad-classmethod-argument """Checks to see that a plugin's 'config' section is not defined if the kind is 'external'. This is because developers cannot override items in the plugin config section for external plugins. Args: diff --git a/mcpgateway/plugins/framework/utils.py b/mcpgateway/plugins/framework/utils.py index 6921455a0..cb50b50ea 100644 --- a/mcpgateway/plugins/framework/utils.py +++ b/mcpgateway/plugins/framework/utils.py @@ -142,7 +142,7 @@ def pre_prompt_matches(payload: PromptPrehookPayload, conditions: list[PluginCon current_result = False if current_result: return True - elif index < len(conditions) - 1: + if index < len(conditions) - 1: current_result = True return current_result @@ -167,7 +167,7 @@ def post_prompt_matches(payload: PromptPosthookPayload, conditions: list[PluginC current_result = False if current_result: return True - elif index < len(conditions) - 1: + if index < len(conditions) - 1: current_result = True return current_result @@ -203,7 +203,7 @@ def pre_tool_matches(payload: ToolPreInvokePayload, conditions: list[PluginCondi current_result = False if current_result: return True - elif index < len(conditions) - 1: + if index < len(conditions) - 1: current_result = True return current_result @@ -239,7 +239,7 @@ def post_tool_matches(payload: ToolPostInvokePayload, conditions: list[PluginCon current_result = False if current_result: return True - elif index < len(conditions) - 1: + if index < len(conditions) - 1: current_result = True return current_result @@ -275,7 +275,7 @@ def pre_resource_matches(payload: ResourcePreFetchPayload, conditions: list[Plug current_result = False if current_result: return True - elif index < len(conditions) - 1: + if index < len(conditions) - 1: current_result = True return current_result @@ -313,6 +313,6 @@ def post_resource_matches(payload: ResourcePostFetchPayload, conditions: list[Pl current_result = False if current_result: return True - elif index < len(conditions) - 1: + if index < len(conditions) - 1: current_result = True return current_result diff --git a/mcpgateway/plugins/tools/cli.py b/mcpgateway/plugins/tools/cli.py index 9b00774f8..75e921cdd 100644 --- a/mcpgateway/plugins/tools/cli.py +++ b/mcpgateway/plugins/tools/cli.py @@ -29,7 +29,7 @@ import logging from pathlib import Path import shutil -import subprocess +import subprocess # nosec B404 # Safe: Used only for git commands with hardcoded args from typing import Optional # Third-Party @@ -99,7 +99,7 @@ def git_user_name() -> str: True """ try: - res = subprocess.run(["git", "config", "user.name"], stdout=subprocess.PIPE) + res = subprocess.run(["git", "config", "user.name"], stdout=subprocess.PIPE, check=False) # nosec B607 B603 # Safe: hardcoded git command return res.stdout.strip().decode() if not res.returncode else DEFAULT_AUTHOR_NAME except Exception: return DEFAULT_AUTHOR_NAME @@ -117,7 +117,7 @@ def git_user_email() -> str: True """ try: - res = subprocess.run(["git", "config", "user.email"], stdout=subprocess.PIPE) + res = subprocess.run(["git", "config", "user.email"], stdout=subprocess.PIPE, check=False) # nosec B607 B603 # Safe: hardcoded git command return res.stdout.strip().decode() if not res.returncode else DEFAULT_AUTHOR_EMAIL except Exception: return DEFAULT_AUTHOR_EMAIL @@ -165,7 +165,6 @@ def bootstrap( @app.callback() def callback(): # pragma: no cover """This function exists to force 'bootstrap' to be a subcommand.""" - pass # @app.command(help="Installs plugins into a Python environment.") diff --git a/plugin_templates/external/.env.template b/plugin_templates/external/.env.template index 89b6fe43b..6d9faf358 100644 --- a/plugin_templates/external/.env.template +++ b/plugin_templates/external/.env.template @@ -20,4 +20,4 @@ PLUGINS_CLI_MARKUP_MODE=rich PLUGINS_CONFIG=./resources/plugins/config.yaml # Configuration path for chuck mcp runtime -CHUK_MCP_CONFIG_PATH=./resources/runtime/config.yaml \ No newline at end of file +CHUK_MCP_CONFIG_PATH=./resources/runtime/config.yaml diff --git a/plugin_templates/external/Makefile.jinja b/plugin_templates/external/Makefile.jinja index 9bc7613ae..b864d2fee 100644 --- a/plugin_templates/external/Makefile.jinja +++ b/plugin_templates/external/Makefile.jinja @@ -18,22 +18,22 @@ VENV_DIR ?= $(VENVS_DIR)/$(PROJECT_NAME) # ============================================================================= black: - @echo "๐ŸŽจ black $(TARGET)..." && $(VENV_DIR)/bin/black -l 200 $(TARGET) + @echo "๐ŸŽจ black $(TARGET)..." && $(VENV_DIR)/bin/black -l 200 $(TARGET) black-check: - @echo "๐ŸŽจ black --check $(TARGET)..." && $(VENV_DIR)/bin/black -l 200 --check --diff $(TARGET) + @echo "๐ŸŽจ black --check $(TARGET)..." && $(VENV_DIR)/bin/black -l 200 --check --diff $(TARGET) ruff: - @echo "โšก ruff $(TARGET)..." && $(VENV_DIR)/bin/ruff check $(TARGET) && $(VENV_DIR)/bin/ruff format $(TARGET) + @echo "โšก ruff $(TARGET)..." && $(VENV_DIR)/bin/ruff check $(TARGET) && $(VENV_DIR)/bin/ruff format $(TARGET) ruff-check: - @echo "โšก ruff check $(TARGET)..." && $(VENV_DIR)/bin/ruff check $(TARGET) + @echo "โšก ruff check $(TARGET)..." && $(VENV_DIR)/bin/ruff check $(TARGET) ruff-fix: - @echo "โšก ruff check --fix $(TARGET)..." && $(VENV_DIR)/bin/ruff check --fix $(TARGET) + @echo "โšก ruff check --fix $(TARGET)..." && $(VENV_DIR)/bin/ruff check --fix $(TARGET) ruff-format: - @echo "โšก ruff format $(TARGET)..." && $(VENV_DIR)/bin/ruff format $(TARGET) + @echo "โšก ruff format $(TARGET)..." && $(VENV_DIR)/bin/ruff format $(TARGET) # ============================================================================= # Container runtime configuration and operations @@ -54,7 +54,7 @@ CONTAINER_PORT ?= 8000 CONTAINER_INTERNAL_PORT ?= 8000 print-runtime: - @echo Using container runtime: $(CONTAINER_RUNTIME) + @echo Using container runtime: $(CONTAINER_RUNTIME) # Base image name (without any prefix) IMAGE_BASE ?= mcpgateway/$(PROJECT_NAME) @@ -74,10 +74,10 @@ else endif print-image: - @echo "๐Ÿณ Container Runtime: $(CONTAINER_RUNTIME)" - @echo "Using image: $(IMAGE_LOCAL)" - @echo "Development image: $(IMAGE_LOCAL_DEV)" - @echo "Push image: $(IMAGE_PUSH)" + @echo "๐Ÿณ Container Runtime: $(CONTAINER_RUNTIME)" + @echo "Using image: $(IMAGE_LOCAL)" + @echo "Development image: $(IMAGE_LOCAL_DEV)" + @echo "Push image: $(IMAGE_PUSH)" {% raw %} @@ -99,195 +99,195 @@ CONTAINER_FILE ?= $(shell [ -f "Containerfile" ] && echo "Containerfile" || echo COMMA := , container-info: - @echo "๐Ÿณ Container Runtime Configuration" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - @echo "Runtime: $(CONTAINER_RUNTIME)" - @echo "Base Image: $(IMAGE_BASE)" - @echo "Tag: $(IMAGE_TAG)" - @echo "Local Image: $(IMAGE_LOCAL)" - @echo "Push Image: $(IMAGE_PUSH)" - @echo "Actual Image: $(call get_image_name)" - @echo "Container File: $(CONTAINER_FILE)" - @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "๐Ÿณ Container Runtime Configuration" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" + @echo "Runtime: $(CONTAINER_RUNTIME)" + @echo "Base Image: $(IMAGE_BASE)" + @echo "Tag: $(IMAGE_TAG)" + @echo "Local Image: $(IMAGE_LOCAL)" + @echo "Push Image: $(IMAGE_PUSH)" + @echo "Actual Image: $(call get_image_name)" + @echo "Container File: $(CONTAINER_FILE)" + @echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" # Auto-detect platform based on uname PLATFORM ?= linux/$(shell uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/') container-build: - @echo "๐Ÿ”จ Building with $(CONTAINER_RUNTIME) for platform $(PLATFORM)..." - $(CONTAINER_RUNTIME) build \ - --platform=$(PLATFORM) \ - -f $(CONTAINER_FILE) \ - --tag $(IMAGE_BASE):$(IMAGE_TAG) \ - . - @echo "โœ… Built image: $(call get_image_name)" - $(CONTAINER_RUNTIME) images $(IMAGE_BASE):$(IMAGE_TAG) + @echo "๐Ÿ”จ Building with $(CONTAINER_RUNTIME) for platform $(PLATFORM)..." + $(CONTAINER_RUNTIME) build \ + --platform=$(PLATFORM) \ + -f $(CONTAINER_FILE) \ + --tag $(IMAGE_BASE):$(IMAGE_TAG) \ + . + @echo "โœ… Built image: $(call get_image_name)" + $(CONTAINER_RUNTIME) images $(IMAGE_BASE):$(IMAGE_TAG) container-run: container-check-image - @echo "๐Ÿš€ Running with $(CONTAINER_RUNTIME)..." - -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true - -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true - $(CONTAINER_RUNTIME) run --name $(PROJECT_NAME) \ - --env-file=.env \ - -p $(CONTAINER_PORT):$(CONTAINER_INTERNAL_PORT) \ - --restart=always \ - --memory=$(CONTAINER_MEMORY) --cpus=$(CONTAINER_CPUS) \ - --health-cmd="curl --fail http://localhost:$(CONTAINER_INTERNAL_PORT)/health || exit 1" \ - --health-interval=1m --health-retries=3 \ - --health-start-period=30s --health-timeout=10s \ - -d $(call get_image_name) - @sleep 2 - @echo "โœ… Container started" - @echo "๐Ÿ” Health check status:" - @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo "No health check configured" + @echo "๐Ÿš€ Running with $(CONTAINER_RUNTIME)..." + -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true + -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true + $(CONTAINER_RUNTIME) run --name $(PROJECT_NAME) \ + --env-file=.env \ + -p $(CONTAINER_PORT):$(CONTAINER_INTERNAL_PORT) \ + --restart=always \ + --memory=$(CONTAINER_MEMORY) --cpus=$(CONTAINER_CPUS) \ + --health-cmd="curl --fail http://localhost:$(CONTAINER_INTERNAL_PORT)/health || exit 1" \ + --health-interval=1m --health-retries=3 \ + --health-start-period=30s --health-timeout=10s \ + -d $(call get_image_name) + @sleep 2 + @echo "โœ… Container started" + @echo "๐Ÿ” Health check status:" + @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo "No health check configured" container-run-host: container-check-image - @echo "๐Ÿš€ Running with $(CONTAINER_RUNTIME)..." - -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true - -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true - $(CONTAINER_RUNTIME) run --name $(PROJECT_NAME) \ - --env-file=.env \ - --network=host \ - -p $(CONTAINER_PORT):$(CONTAINER_INTERNAL_PORT) \ - --restart=always \ - --memory=$(CONTAINER_MEMORY) --cpus=$(CONTAINER_CPUS) \ - --health-cmd="curl --fail http://localhost:$(CONTAINER_INTERNAL_PORT)/health || exit 1" \ - --health-interval=1m --health-retries=3 \ - --health-start-period=30s --health-timeout=10s \ - -d $(call get_image_name) - @sleep 2 - @echo "โœ… Container started" - @echo "๐Ÿ” Health check status:" - @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo "No health check configured" + @echo "๐Ÿš€ Running with $(CONTAINER_RUNTIME)..." + -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true + -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true + $(CONTAINER_RUNTIME) run --name $(PROJECT_NAME) \ + --env-file=.env \ + --network=host \ + -p $(CONTAINER_PORT):$(CONTAINER_INTERNAL_PORT) \ + --restart=always \ + --memory=$(CONTAINER_MEMORY) --cpus=$(CONTAINER_CPUS) \ + --health-cmd="curl --fail http://localhost:$(CONTAINER_INTERNAL_PORT)/health || exit 1" \ + --health-interval=1m --health-retries=3 \ + --health-start-period=30s --health-timeout=10s \ + -d $(call get_image_name) + @sleep 2 + @echo "โœ… Container started" + @echo "๐Ÿ” Health check status:" + @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo "No health check configured" container-push: container-check-image - @echo "๐Ÿ“ค Preparing to push image..." - @# For Podman, we need to remove localhost/ prefix for push - @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ - actual_image=$$($(CONTAINER_RUNTIME) images --format "{{.Repository}}:{{.Tag}}" | grep -E "$(IMAGE_BASE):$(IMAGE_TAG)" | head -1); \ - if echo "$$actual_image" | grep -q "^localhost/"; then \ - echo "๐Ÿท๏ธ Tagging for push (removing localhost/ prefix)..."; \ - $(CONTAINER_RUNTIME) tag "$$actual_image" $(IMAGE_PUSH); \ - fi; \ - fi - $(CONTAINER_RUNTIME) push $(IMAGE_PUSH) - @echo "โœ… Pushed: $(IMAGE_PUSH)" + @echo "๐Ÿ“ค Preparing to push image..." + @# For Podman, we need to remove localhost/ prefix for push + @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ + actual_image=$$($(CONTAINER_RUNTIME) images --format "{{.Repository}}:{{.Tag}}" | grep -E "$(IMAGE_BASE):$(IMAGE_TAG)" | head -1); \ + if echo "$$actual_image" | grep -q "^localhost/"; then \ + echo "๐Ÿท๏ธ Tagging for push (removing localhost/ prefix)..."; \ + $(CONTAINER_RUNTIME) tag "$$actual_image" $(IMAGE_PUSH); \ + fi; \ + fi + $(CONTAINER_RUNTIME) push $(IMAGE_PUSH) + @echo "โœ… Pushed: $(IMAGE_PUSH)" container-check-image: - @echo "๐Ÿ” Checking for image..." - @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ - if ! $(CONTAINER_RUNTIME) image exists $(IMAGE_LOCAL) 2>/dev/null && \ - ! $(CONTAINER_RUNTIME) image exists $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null; then \ - echo "โŒ Image not found: $(IMAGE_LOCAL)"; \ - echo "๐Ÿ’ก Run 'make container-build' first"; \ - exit 1; \ - fi; \ - else \ - if ! $(CONTAINER_RUNTIME) images -q $(IMAGE_LOCAL) 2>/dev/null | grep -q . && \ - ! $(CONTAINER_RUNTIME) images -q $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null | grep -q .; then \ - echo "โŒ Image not found: $(IMAGE_LOCAL)"; \ - echo "๐Ÿ’ก Run 'make container-build' first"; \ - exit 1; \ - fi; \ - fi - @echo "โœ… Image found" + @echo "๐Ÿ” Checking for image..." + @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ + if ! $(CONTAINER_RUNTIME) image exists $(IMAGE_LOCAL) 2>/dev/null && \ + ! $(CONTAINER_RUNTIME) image exists $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null; then \ + echo "โŒ Image not found: $(IMAGE_LOCAL)"; \ + echo "๐Ÿ’ก Run 'make container-build' first"; \ + exit 1; \ + fi; \ + else \ + if ! $(CONTAINER_RUNTIME) images -q $(IMAGE_LOCAL) 2>/dev/null | grep -q . && \ + ! $(CONTAINER_RUNTIME) images -q $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null | grep -q .; then \ + echo "โŒ Image not found: $(IMAGE_LOCAL)"; \ + echo "๐Ÿ’ก Run 'make container-build' first"; \ + exit 1; \ + fi; \ + fi + @echo "โœ… Image found" container-stop: - @echo "๐Ÿ›‘ Stopping container..." - -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true - -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true - @echo "โœ… Container stopped and removed" + @echo "๐Ÿ›‘ Stopping container..." + -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true + -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true + @echo "โœ… Container stopped and removed" container-logs: - @echo "๐Ÿ“œ Streaming logs (Ctrl+C to exit)..." - $(CONTAINER_RUNTIME) logs -f $(PROJECT_NAME) + @echo "๐Ÿ“œ Streaming logs (Ctrl+C to exit)..." + $(CONTAINER_RUNTIME) logs -f $(PROJECT_NAME) container-shell: - @echo "๐Ÿ”ง Opening shell in container..." - @if ! $(CONTAINER_RUNTIME) ps -q -f name=$(PROJECT_NAME) | grep -q .; then \ - echo "โŒ Container $(PROJECT_NAME) is not running"; \ - echo "๐Ÿ’ก Run 'make container-run' first"; \ - exit 1; \ - fi - @$(CONTAINER_RUNTIME) exec -it $(PROJECT_NAME) /bin/bash 2>/dev/null || \ - $(CONTAINER_RUNTIME) exec -it $(PROJECT_NAME) /bin/sh + @echo "๐Ÿ”ง Opening shell in container..." + @if ! $(CONTAINER_RUNTIME) ps -q -f name=$(PROJECT_NAME) | grep -q .; then \ + echo "โŒ Container $(PROJECT_NAME) is not running"; \ + echo "๐Ÿ’ก Run 'make container-run' first"; \ + exit 1; \ + fi + @$(CONTAINER_RUNTIME) exec -it $(PROJECT_NAME) /bin/bash 2>/dev/null || \ + $(CONTAINER_RUNTIME) exec -it $(PROJECT_NAME) /bin/sh container-health: - @echo "๐Ÿฅ Checking container health..." - @if ! $(CONTAINER_RUNTIME) ps -q -f name=$(PROJECT_NAME) | grep -q .; then \ - echo "โŒ Container $(PROJECT_NAME) is not running"; \ - exit 1; \ - fi - @echo "Status: $$($(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo 'No health check')" - @echo "Logs:" - @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{range .State.Health.Log}}{{.Output}}{{end}}' 2>/dev/null || true + @echo "๐Ÿฅ Checking container health..." + @if ! $(CONTAINER_RUNTIME) ps -q -f name=$(PROJECT_NAME) | grep -q .; then \ + echo "โŒ Container $(PROJECT_NAME) is not running"; \ + exit 1; \ + fi + @echo "Status: $$($(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo 'No health check')" + @echo "Logs:" + @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{range .State.Health.Log}}{{.Output}}{{end}}' 2>/dev/null || true container-build-multi: - @echo "๐Ÿ”จ Building multi-architecture image..." - @if [ "$(CONTAINER_RUNTIME)" = "docker" ]; then \ - if ! docker buildx inspect $(PROJECT_NAME)-builder >/dev/null 2>&1; then \ - echo "๐Ÿ“ฆ Creating buildx builder..."; \ - docker buildx create --name $(PROJECT_NAME)-builder; \ - fi; \ - docker buildx use $(PROJECT_NAME)-builder; \ - docker buildx build \ - --platform=linux/amd64,linux/arm64 \ - -f $(CONTAINER_FILE) \ - --tag $(IMAGE_BASE):$(IMAGE_TAG) \ - --push \ - .; \ - elif [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ - echo "๐Ÿ“ฆ Building manifest with Podman..."; \ - $(CONTAINER_RUNTIME) build --platform=linux/amd64,linux/arm64 \ - -f $(CONTAINER_FILE) \ - --manifest $(IMAGE_BASE):$(IMAGE_TAG) \ - .; \ - echo "๐Ÿ’ก To push: podman manifest push $(IMAGE_BASE):$(IMAGE_TAG)"; \ - else \ - echo "โŒ Multi-arch builds require Docker buildx or Podman"; \ - exit 1; \ - fi + @echo "๐Ÿ”จ Building multi-architecture image..." + @if [ "$(CONTAINER_RUNTIME)" = "docker" ]; then \ + if ! docker buildx inspect $(PROJECT_NAME)-builder >/dev/null 2>&1; then \ + echo "๐Ÿ“ฆ Creating buildx builder..."; \ + docker buildx create --name $(PROJECT_NAME)-builder; \ + fi; \ + docker buildx use $(PROJECT_NAME)-builder; \ + docker buildx build \ + --platform=linux/amd64,linux/arm64 \ + -f $(CONTAINER_FILE) \ + --tag $(IMAGE_BASE):$(IMAGE_TAG) \ + --push \ + .; \ + elif [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ + echo "๐Ÿ“ฆ Building manifest with Podman..."; \ + $(CONTAINER_RUNTIME) build --platform=linux/amd64,linux/arm64 \ + -f $(CONTAINER_FILE) \ + --manifest $(IMAGE_BASE):$(IMAGE_TAG) \ + .; \ + echo "๐Ÿ’ก To push: podman manifest push $(IMAGE_BASE):$(IMAGE_TAG)"; \ + else \ + echo "โŒ Multi-arch builds require Docker buildx or Podman"; \ + exit 1; \ + fi # Helper targets for debugging image issues image-list: - @echo "๐Ÿ“‹ Images matching $(IMAGE_BASE):" - @$(CONTAINER_RUNTIME) images --format "table {{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Created}}\t{{.Size}}" | \ - grep -E "(IMAGE|$(IMAGE_BASE))" || echo "No matching images found" + @echo "๐Ÿ“‹ Images matching $(IMAGE_BASE):" + @$(CONTAINER_RUNTIME) images --format "table {{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Created}}\t{{.Size}}" | \ + grep -E "(IMAGE|$(IMAGE_BASE))" || echo "No matching images found" image-clean: - @echo "๐Ÿงน Removing all $(IMAGE_BASE) images..." - @$(CONTAINER_RUNTIME) images --format "{{.Repository}}:{{.Tag}}" | \ - grep -E "(localhost/)?$(IMAGE_BASE)" | \ - xargs $(XARGS_FLAGS) $(CONTAINER_RUNTIME) rmi -f 2>/dev/null - @echo "โœ… Images cleaned" + @echo "๐Ÿงน Removing all $(IMAGE_BASE) images..." + @$(CONTAINER_RUNTIME) images --format "{{.Repository}}:{{.Tag}}" | \ + grep -E "(localhost/)?$(IMAGE_BASE)" | \ + xargs $(XARGS_FLAGS) $(CONTAINER_RUNTIME) rmi -f 2>/dev/null + @echo "โœ… Images cleaned" # Fix image naming issues image-retag: - @echo "๐Ÿท๏ธ Retagging images for consistency..." - @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ - if $(CONTAINER_RUNTIME) image exists $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null; then \ - $(CONTAINER_RUNTIME) tag $(IMAGE_BASE):$(IMAGE_TAG) $(IMAGE_LOCAL) 2>/dev/null || true; \ - fi; \ - else \ - if $(CONTAINER_RUNTIME) images -q $(IMAGE_LOCAL) 2>/dev/null | grep -q .; then \ - $(CONTAINER_RUNTIME) tag $(IMAGE_LOCAL) $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null || true; \ - fi; \ - fi - @echo "โœ… Images retagged" # This always shows success + @echo "๐Ÿท๏ธ Retagging images for consistency..." + @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \ + if $(CONTAINER_RUNTIME) image exists $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null; then \ + $(CONTAINER_RUNTIME) tag $(IMAGE_BASE):$(IMAGE_TAG) $(IMAGE_LOCAL) 2>/dev/null || true; \ + fi; \ + else \ + if $(CONTAINER_RUNTIME) images -q $(IMAGE_LOCAL) 2>/dev/null | grep -q .; then \ + $(CONTAINER_RUNTIME) tag $(IMAGE_LOCAL) $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null || true; \ + fi; \ + fi + @echo "โœ… Images retagged" # This always shows success # Runtime switching helpers use-docker: - @echo "export CONTAINER_RUNTIME=docker" - @echo "๐Ÿ’ก Run: export CONTAINER_RUNTIME=docker" + @echo "export CONTAINER_RUNTIME=docker" + @echo "๐Ÿ’ก Run: export CONTAINER_RUNTIME=docker" use-podman: - @echo "export CONTAINER_RUNTIME=podman" - @echo "๐Ÿ’ก Run: export CONTAINER_RUNTIME=podman" + @echo "export CONTAINER_RUNTIME=podman" + @echo "๐Ÿ’ก Run: export CONTAINER_RUNTIME=podman" show-runtime: - @echo "Current runtime: $(CONTAINER_RUNTIME)" - @echo "Detected from: $$(command -v $(CONTAINER_RUNTIME) || echo 'not found')" # Added - @echo "To switch: make use-docker or make use-podman" + @echo "Current runtime: $(CONTAINER_RUNTIME)" + @echo "Detected from: $$(command -v $(CONTAINER_RUNTIME) || echo 'not found')" # Added + @echo "To switch: make use-docker or make use-podman" {% endraw %} @@ -297,147 +297,147 @@ show-runtime: .PHONY: venv venv: - @rm -Rf "$(VENV_DIR)" - @test -d "$(VENVS_DIR)" || mkdir -p "$(VENVS_DIR)" - @python3 -m venv "$(VENV_DIR)" - @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m pip install --upgrade pip setuptools pdm uv" - @echo -e "โœ… Virtual env created.\n๐Ÿ’ก Enter it with:\n . $(VENV_DIR)/bin/activate\n" + @rm -Rf "$(VENV_DIR)" + @test -d "$(VENVS_DIR)" || mkdir -p "$(VENVS_DIR)" + @python3 -m venv "$(VENV_DIR)" + @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m pip install --upgrade pip setuptools pdm uv" + @echo -e "โœ… Virtual env created.\n๐Ÿ’ก Enter it with:\n . $(VENV_DIR)/bin/activate\n" .PHONY: install install: venv - $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`))) - @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m uv pip install ." + $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`))) + @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m uv pip install ." .PHONY: install-editable install-editable: venv - $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`))) - @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m uv pip install -e ." + $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`))) + @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m uv pip install -e ." .PHONY: uninstall uninstall: - pip uninstall $(PACKAGE_NAME) + pip uninstall $(PACKAGE_NAME) .PHONY: dist dist: clean ## Build wheel + sdist into ./dist - @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv - @/bin/bash -eu -c "\ - source $(VENV_DIR)/bin/activate && \ - python3 -m pip install --quiet --upgrade pip build && \ - python3 -m build" - @echo '๐Ÿ›  Wheel & sdist written to ./dist' + @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv + @/bin/bash -eu -c "\ + source $(VENV_DIR)/bin/activate && \ + python3 -m pip install --quiet --upgrade pip build && \ + python3 -m build" + @echo '๐Ÿ›  Wheel & sdist written to ./dist' .PHONY: wheel wheel: ## Build wheel only - @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv - @/bin/bash -eu -c "\ - source $(VENV_DIR)/bin/activate && \ - python3 -m pip install --quiet --upgrade pip build && \ - python3 -m build -w" - @echo '๐Ÿ›  Wheel written to ./dist' + @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv + @/bin/bash -eu -c "\ + source $(VENV_DIR)/bin/activate && \ + python3 -m pip install --quiet --upgrade pip build && \ + python3 -m build -w" + @echo '๐Ÿ›  Wheel written to ./dist' .PHONY: sdist sdist: ## Build source distribution only - @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv - @/bin/bash -eu -c "\ - source $(VENV_DIR)/bin/activate && \ - python3 -m pip install --quiet --upgrade pip build && \ - python3 -m build -s" - @echo '๐Ÿ›  Source distribution written to ./dist' + @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv + @/bin/bash -eu -c "\ + source $(VENV_DIR)/bin/activate && \ + python3 -m pip install --quiet --upgrade pip build && \ + python3 -m build -s" + @echo '๐Ÿ›  Source distribution written to ./dist' .PHONY: verify verify: dist ## Build, run metadata & manifest checks - @/bin/bash -c "source $(VENV_DIR)/bin/activate && \ - twine check dist/* && \ - check-manifest && \ - pyroma -d ." - @echo "โœ… Package verified - ready to publish." + @/bin/bash -c "source $(VENV_DIR)/bin/activate && \ + twine check dist/* && \ + check-manifest && \ + pyroma -d ." + @echo "โœ… Package verified - ready to publish." .PHONY: lint-fix lint-fix: - @# Handle file arguments - @target_file="$(word 2,$(MAKECMDGOALS))"; \ - if [ -n "$$target_file" ] && [ "$$target_file" != "" ]; then \ - actual_target="$$target_file"; \ - else \ - actual_target="$(TARGET)"; \ - fi; \ - for target in $$(echo $$actual_target); do \ - if [ ! -e "$$target" ]; then \ - echo "โŒ File/directory not found: $$target"; \ - exit 1; \ - fi; \ - done; \ - echo "๐Ÿ”ง Fixing lint issues in $$actual_target..."; \ - $(MAKE) --no-print-directory black TARGET="$$actual_target"; \ - $(MAKE) --no-print-directory ruff-fix TARGET="$$actual_target" + @# Handle file arguments + @target_file="$(word 2,$(MAKECMDGOALS))"; \ + if [ -n "$$target_file" ] && [ "$$target_file" != "" ]; then \ + actual_target="$$target_file"; \ + else \ + actual_target="$(TARGET)"; \ + fi; \ + for target in $$(echo $$actual_target); do \ + if [ ! -e "$$target" ]; then \ + echo "โŒ File/directory not found: $$target"; \ + exit 1; \ + fi; \ + done; \ + echo "๐Ÿ”ง Fixing lint issues in $$actual_target..."; \ + $(MAKE) --no-print-directory black TARGET="$$actual_target"; \ + $(MAKE) --no-print-directory ruff-fix TARGET="$$actual_target" .PHONY: lint-check lint-check: - @# Handle file arguments - @target_file="$(word 2,$(MAKECMDGOALS))"; \ - if [ -n "$$target_file" ] && [ "$$target_file" != "" ]; then \ - actual_target="$$target_file"; \ - else \ - actual_target="$(TARGET)"; \ - fi; \ - for target in $$(echo $$actual_target); do \ - if [ ! -e "$$target" ]; then \ - echo "โŒ File/directory not found: $$target"; \ - exit 1; \ - fi; \ - done; \ - echo "๐Ÿ”ง Fixing lint issues in $$actual_target..."; \ - $(MAKE) --no-print-directory black-check TARGET="$$actual_target"; \ - $(MAKE) --no-print-directory ruff-check TARGET="$$actual_target" + @# Handle file arguments + @target_file="$(word 2,$(MAKECMDGOALS))"; \ + if [ -n "$$target_file" ] && [ "$$target_file" != "" ]; then \ + actual_target="$$target_file"; \ + else \ + actual_target="$(TARGET)"; \ + fi; \ + for target in $$(echo $$actual_target); do \ + if [ ! -e "$$target" ]; then \ + echo "โŒ File/directory not found: $$target"; \ + exit 1; \ + fi; \ + done; \ + echo "๐Ÿ”ง Fixing lint issues in $$actual_target..."; \ + $(MAKE) --no-print-directory black-check TARGET="$$actual_target"; \ + $(MAKE) --no-print-directory ruff-check TARGET="$$actual_target" .PHONY: lock lock: - $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`. Please run `make init`))) - uv lock + $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`. Please run `make init`))) + uv lock .PHONY: test test: - pytest tests + pytest tests .PHONY: serve serve: - @echo "Implement me." + @echo "Implement me." .PHONY: build build: - @$(MAKE) container-build + @$(MAKE) container-build .PHONY: start start: - @$(MAKE) container-run + @$(MAKE) container-run .PHONY: stop stop: - @$(MAKE) container-stop + @$(MAKE) container-stop .PHONY: clean clean: - find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete - rm -rf *.egg-info .pytest_cache tests/.pytest_cache build dist .ruff_cache .coverage + find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete + rm -rf *.egg-info .pytest_cache tests/.pytest_cache build dist .ruff_cache .coverage .PHONY: help help: - @echo "This Makefile is offered for convenience." - @echo "" - @echo "The following are the valid targets for this Makefile:" - @echo "...install Install package from sources" - @echo "...install-editable Install package from sources in editabled mode" - @echo "...uninstall Uninstall package" - @echo "...dist Clean-build wheel *and* sdist into ./dist" - @echo "...wheel Build wheel only" - @echo "...sdist Build source distribution only" - @echo "...verify Build + twine + check-manifest + pyroma (no upload)" - @echo "...serve Start API server locally" - @echo "...build Build API server container image" - @echo "...start Start the API server container" - @echo "...start Stop the API server container" - @echo "...lock Lock dependencies" - @echo "...lint-fix Check and fix lint errors" - @echo "...lint-check Check for lint errors" - @echo "...test Run all tests" - @echo "...clean Remove all artifacts and builds" + @echo "This Makefile is offered for convenience." + @echo "" + @echo "The following are the valid targets for this Makefile:" + @echo "...install Install package from sources" + @echo "...install-editable Install package from sources in editabled mode" + @echo "...uninstall Uninstall package" + @echo "...dist Clean-build wheel *and* sdist into ./dist" + @echo "...wheel Build wheel only" + @echo "...sdist Build source distribution only" + @echo "...verify Build + twine + check-manifest + pyroma (no upload)" + @echo "...serve Start API server locally" + @echo "...build Build API server container image" + @echo "...start Start the API server container" + @echo "...start Stop the API server container" + @echo "...lock Lock dependencies" + @echo "...lint-fix Check and fix lint errors" + @echo "...lint-check Check for lint errors" + @echo "...test Run all tests" + @echo "...clean Remove all artifacts and builds" diff --git a/plugin_templates/external/README.md.jinja b/plugin_templates/external/README.md.jinja index a7002efae..f841457e8 100644 --- a/plugin_templates/external/README.md.jinja +++ b/plugin_templates/external/README.md.jinja @@ -54,4 +54,4 @@ To stop the container: ```bash make stop -``` \ No newline at end of file +``` diff --git a/plugin_templates/external/resources/runtime/config.yaml.jinja b/plugin_templates/external/resources/runtime/config.yaml.jinja index 629f5e7d9..a37669f70 100644 --- a/plugin_templates/external/resources/runtime/config.yaml.jinja +++ b/plugin_templates/external/resources/runtime/config.yaml.jinja @@ -68,4 +68,4 @@ session_tools: artifacts: enabled: false # Must explicitly enable storage_provider: "filesystem" - session_provider: "memory" \ No newline at end of file + session_provider: "memory" diff --git a/plugin_templates/external/run-server.sh b/plugin_templates/external/run-server.sh index 88c3ed4a1..d73f57de5 100755 --- a/plugin_templates/external/run-server.sh +++ b/plugin_templates/external/run-server.sh @@ -40,4 +40,4 @@ CHUK_MCP_CONFIG_PATH=${CHUK_MCP_CONFIG_PATH:-./resources/runtime/config.yaml} echo "โœ“ Using plugin config from: ${PLUGINS_CONFIG_PATH}" echo "โœ“ Running API server with config from: ${CHUK_MCP_CONFIG_PATH}" -python ${API_SERVER_SCRIPT} \ No newline at end of file +python ${API_SERVER_SCRIPT} diff --git a/plugin_templates/external/tests/pytest.ini b/plugin_templates/external/tests/pytest.ini index d8d63180b..ff60648e6 100644 --- a/plugin_templates/external/tests/pytest.ini +++ b/plugin_templates/external/tests/pytest.ini @@ -10,4 +10,4 @@ addopts = --cov --cov-report term-missing env_files = .env pythonpath = . src filterwarnings = - ignore::DeprecationWarning:pydantic.* \ No newline at end of file + ignore::DeprecationWarning:pydantic.* diff --git a/plugin_templates/external/tests/test_all.py b/plugin_templates/external/tests/test_all.py index 0c190be75..8accde750 100644 --- a/plugin_templates/external/tests/test_all.py +++ b/plugin_templates/external/tests/test_all.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Tests for registered plugins.""" # Third-Party diff --git a/tests/unit/mcpgateway/plugins/__init__.py b/tests/unit/mcpgateway/plugins/__init__.py index 63f957fa7..da59bf6fa 100644 --- a/tests/unit/mcpgateway/plugins/__init__.py +++ b/tests/unit/mcpgateway/plugins/__init__.py @@ -5,4 +5,4 @@ SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor -""" \ No newline at end of file +""" diff --git a/tests/unit/mcpgateway/plugins/fixtures/plugins/__init__.py b/tests/unit/mcpgateway/plugins/fixtures/plugins/__init__.py index 63f957fa7..da59bf6fa 100644 --- a/tests/unit/mcpgateway/plugins/fixtures/plugins/__init__.py +++ b/tests/unit/mcpgateway/plugins/fixtures/plugins/__init__.py @@ -5,4 +5,4 @@ SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor -""" \ No newline at end of file +""" diff --git a/tests/unit/mcpgateway/plugins/fixtures/plugins/passthrough.py b/tests/unit/mcpgateway/plugins/fixtures/plugins/passthrough.py index 2f437a1e3..555bc1f9f 100644 --- a/tests/unit/mcpgateway/plugins/fixtures/plugins/passthrough.py +++ b/tests/unit/mcpgateway/plugins/fixtures/plugins/passthrough.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Passthrough plugin. @@ -73,7 +74,7 @@ async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: Plugin The result of the plugin's analysis, including whether the tool result should proceed. """ return ToolPostInvokeResult(continue_processing=True) - + async def resource_post_fetch(self, payload: ResourcePostFetchPayload, context: PluginContext) -> ResourcePostFetchResult: """Plugin hook run after a resource was fetched. @@ -85,7 +86,7 @@ async def resource_post_fetch(self, payload: ResourcePostFetchPayload, context: The result of the plugin's analysis, including whether the resource result should proceed. """ return ResourcePostFetchResult(continue_processing=True) - + async def resource_pre_fetch(self, payload: ResourcePreFetchPayload, context: PluginContext) -> ResourcePreFetchResult: """Plugin hook run before a resource was fetched. diff --git a/tests/unit/mcpgateway/plugins/framework/__init__.py b/tests/unit/mcpgateway/plugins/framework/__init__.py index 63f957fa7..da59bf6fa 100644 --- a/tests/unit/mcpgateway/plugins/framework/__init__.py +++ b/tests/unit/mcpgateway/plugins/framework/__init__.py @@ -5,4 +5,4 @@ SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor -""" \ No newline at end of file +""" diff --git a/tests/unit/mcpgateway/plugins/framework/external/__init__.py b/tests/unit/mcpgateway/plugins/framework/external/__init__.py index 63f957fa7..da59bf6fa 100644 --- a/tests/unit/mcpgateway/plugins/framework/external/__init__.py +++ b/tests/unit/mcpgateway/plugins/framework/external/__init__.py @@ -5,4 +5,4 @@ SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor -""" \ No newline at end of file +""" diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/__init__.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/__init__.py index 63f957fa7..da59bf6fa 100644 --- a/tests/unit/mcpgateway/plugins/framework/external/mcp/__init__.py +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/__init__.py @@ -5,4 +5,4 @@ SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor -""" \ No newline at end of file +""" diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/server/__init__.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/server/__init__.py index 63f957fa7..da59bf6fa 100644 --- a/tests/unit/mcpgateway/plugins/framework/external/mcp/server/__init__.py +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/server/__init__.py @@ -5,4 +5,4 @@ SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor -""" \ No newline at end of file +""" diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/server/test_runtime.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/server/test_runtime.py index 473166a91..15cdc1e76 100644 --- a/tests/unit/mcpgateway/plugins/framework/external/mcp/server/test_runtime.py +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/server/test_runtime.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Tests for external client on stdio. @@ -38,21 +39,21 @@ def server(): @pytest.mark.asyncio async def test_get_plugin_configs(monkeypatch, server): - monkeypatch.setattr(runtime, "server", server) + monkeypatch.setattr(runtime, "SERVER", server) configs = await runtime.get_plugin_configs() assert len(configs) > 0 @pytest.mark.asyncio async def test_get_plugin_config(monkeypatch, server): - monkeypatch.setattr(runtime, "server", server) + monkeypatch.setattr(runtime, "SERVER", server) config = await runtime.get_plugin_config(name="DenyListPlugin") assert config["name"] == "DenyListPlugin" @pytest.mark.asyncio async def test_prompt_pre_fetch(monkeypatch, server): - monkeypatch.setattr(runtime, "server", server) + monkeypatch.setattr(runtime, "SERVER", server) payload = PromptPrehookPayload(name="test_prompt", args={"user": "This is so innovative"}) context = PluginContext(request_id="1", server_id="2") result = await runtime.prompt_pre_fetch("DenyListPlugin", payload=payload, context=context) @@ -62,7 +63,7 @@ async def test_prompt_pre_fetch(monkeypatch, server): @pytest.mark.asyncio async def test_prompt_post_fetch(monkeypatch, server): - monkeypatch.setattr(runtime, "server", server) + monkeypatch.setattr(runtime, "SERVER", server) message = Message(content=TextContent(type="text", text="crap prompt"), role=Role.USER) prompt_result = PromptResult(messages=[message]) payload = PromptPosthookPayload(name="test_prompt", result=prompt_result) @@ -75,7 +76,7 @@ async def test_prompt_post_fetch(monkeypatch, server): @pytest.mark.asyncio async def test_tool_pre_invoke(monkeypatch, server): - monkeypatch.setattr(runtime, "server", server) + monkeypatch.setattr(runtime, "SERVER", server) payload = ToolPreInvokePayload(name="test_tool", args={"arg0": "Good argument"}) context = PluginContext(request_id="1", server_id="2") result = await runtime.tool_pre_invoke("ReplaceBadWordsPlugin", payload=payload, context=context) @@ -85,7 +86,7 @@ async def test_tool_pre_invoke(monkeypatch, server): @pytest.mark.asyncio async def test_tool_post_invoke(monkeypatch, server): - monkeypatch.setattr(runtime, "server", server) + monkeypatch.setattr(runtime, "SERVER", server) message = Message(content=TextContent(type="text", text="crap result"), role=Role.USER) prompt_result = ToolPostInvokeResult(messages=[message]) payload = ToolPostInvokePayload(name="test_prompt", result=prompt_result) @@ -98,7 +99,7 @@ async def test_tool_post_invoke(monkeypatch, server): @pytest.mark.asyncio async def test_resource_pre_fetch(monkeypatch, server): - monkeypatch.setattr(runtime, "server", server) + monkeypatch.setattr(runtime, "SERVER", server) payload = ResourcePreFetchPayload(uri="resource", metadata={"arg0": "Good argument"}) context = PluginContext(request_id="1", server_id="2") result = await runtime.resource_pre_fetch("ResourceFilterExample", payload=payload, context=context) @@ -108,7 +109,7 @@ async def test_resource_pre_fetch(monkeypatch, server): @pytest.mark.asyncio async def test_tool_post_invoke(monkeypatch, server): - monkeypatch.setattr(runtime, "server", server) + monkeypatch.setattr(runtime, "SERVER", server) payload = ResourcePostFetchPayload(uri="resource", content="content") context = PluginContext(request_id="1", server_id="2") result = await runtime.resource_post_fetch("ResourceFilterExample", payload=payload, context=context) diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py index 54a32f93d..5d06498a2 100644 --- a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_stdio.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Tests for external client on stdio. diff --git a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py index 503389b52..219f8307b 100644 --- a/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py +++ b/tests/unit/mcpgateway/plugins/framework/external/mcp/test_client_streamable_http.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Tests for external client on streamable http. diff --git a/tests/unit/mcpgateway/plugins/framework/loader/__init__.py b/tests/unit/mcpgateway/plugins/framework/loader/__init__.py index 63f957fa7..da59bf6fa 100644 --- a/tests/unit/mcpgateway/plugins/framework/loader/__init__.py +++ b/tests/unit/mcpgateway/plugins/framework/loader/__init__.py @@ -5,4 +5,4 @@ SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor -""" \ No newline at end of file +""" diff --git a/tests/unit/mcpgateway/plugins/framework/test_errors.py b/tests/unit/mcpgateway/plugins/framework/test_errors.py index 58326451e..e55590e96 100644 --- a/tests/unit/mcpgateway/plugins/framework/test_errors.py +++ b/tests/unit/mcpgateway/plugins/framework/test_errors.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Tests for errors module. @@ -19,4 +20,3 @@ async def test_convert_exception_to_error(): assert plugin_error.error.message == "ValueError('This is some error.')" assert plugin_error.error.plugin_name == "SomePluginName" - diff --git a/tests/unit/mcpgateway/plugins/plugins/__init__.py b/tests/unit/mcpgateway/plugins/plugins/__init__.py index 63f957fa7..da59bf6fa 100644 --- a/tests/unit/mcpgateway/plugins/plugins/__init__.py +++ b/tests/unit/mcpgateway/plugins/plugins/__init__.py @@ -5,4 +5,4 @@ SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor -""" \ No newline at end of file +""" diff --git a/tests/unit/mcpgateway/plugins/plugins/pii_filter/__init__.py b/tests/unit/mcpgateway/plugins/plugins/pii_filter/__init__.py index 63f957fa7..da59bf6fa 100644 --- a/tests/unit/mcpgateway/plugins/plugins/pii_filter/__init__.py +++ b/tests/unit/mcpgateway/plugins/plugins/pii_filter/__init__.py @@ -5,4 +5,4 @@ SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor -""" \ No newline at end of file +""" diff --git a/tests/unit/mcpgateway/plugins/plugins/resource_filter/__init__.py b/tests/unit/mcpgateway/plugins/plugins/resource_filter/__init__.py index 63f957fa7..da59bf6fa 100644 --- a/tests/unit/mcpgateway/plugins/plugins/resource_filter/__init__.py +++ b/tests/unit/mcpgateway/plugins/plugins/resource_filter/__init__.py @@ -5,4 +5,4 @@ SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor -""" \ No newline at end of file +""" diff --git a/tests/unit/mcpgateway/plugins/tools/test_cli.py b/tests/unit/mcpgateway/plugins/tools/test_cli.py index 9663bc9df..0cad66394 100644 --- a/tests/unit/mcpgateway/plugins/tools/test_cli.py +++ b/tests/unit/mcpgateway/plugins/tools/test_cli.py @@ -46,4 +46,4 @@ def test_install_manifest(): data = yaml.safe_load(f) manifest = InstallManifest.model_validate(data) assert manifest - assert len(manifest.packages) > 0 \ No newline at end of file + assert len(manifest.packages) > 0 diff --git a/tests/unit/mcpgateway/utils/__init__.py b/tests/unit/mcpgateway/utils/__init__.py index 63f957fa7..da59bf6fa 100644 --- a/tests/unit/mcpgateway/utils/__init__.py +++ b/tests/unit/mcpgateway/utils/__init__.py @@ -5,4 +5,4 @@ SPDX-License-Identifier: Apache-2.0 Authors: Teryl Taylor -""" \ No newline at end of file +"""