diff --git a/docs/middleware/groups.md b/docs/middleware/groups.md new file mode 100644 index 0000000..6132b6c --- /dev/null +++ b/docs/middleware/groups.md @@ -0,0 +1,139 @@ +# Groups + +When MCP servers expose many tools, agents can become overwhelmed with options, leading to poor tool selection and increased token usage. The `GroupsMiddleware` in wags enables progressive tool disclosure by organizing tools into hierarchical groups that agents can enable or disable as needed. + +## How It Works + +Tools are assigned to groups using the `@in_group` decorator. The middleware: + +1. Hides all grouped tools initially (or starts with configured `initial_groups`) +2. Exposes `enable_tools` and `disable_tools` meta-tools +3. Progressively reveals child groups as parents are enabled +4. Enforces optional `max_tools` limits + +## Example + +```python linenums="1" title="handlers.py" +from wags.middleware import GroupsMiddleware, GroupDefinition, in_group + +class GithubHandlers: + @in_group("issues") + async def create_issue(self, owner: str, repo: str, title: str): + pass + + @in_group("issues") + async def list_issues(self, owner: str, repo: str): + pass + + @in_group("repos") + async def create_repository(self, name: str): + pass +``` + +Configure the middleware with group definitions: + +```python title="main.py" +from wags.middleware import GroupsMiddleware, GroupDefinition + +handlers = GithubHandlers() +mcp.add_middleware( + GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issue tracking"), + "repos": GroupDefinition(description="Repository management"), + }, + handlers=handlers, + initial_groups=["issues"], # Start with issues enabled + max_tools=10, # Optional limit + ) +) +``` + +## Hierarchical Groups + +Groups can be nested using the `parent` parameter for progressive disclosure: + +```python +groups = { + "code": GroupDefinition(description="Code management"), + "repos": GroupDefinition(description="Repositories", parent="code"), + "branches": GroupDefinition(description="Branches", parent="repos"), +} +``` + +With this hierarchy: + +- Only `code` is visible initially (root group) +- Enabling `code` reveals `repos` as an option +- Enabling `repos` reveals `branches` as an option +- Disabling `code` cascades to disable `repos` and `branches` + +## Agent Interaction + +When an agent calls `enable_tools(groups=["issues"])`, it receives a structured JSON response: + +```json +{ + "enabled": ["issues"], + "enabled_groups": ["issues"], + "available_tools": ["create_issue", "list_issues"], + "available_groups": [], + "errors": [] +} +``` + +The response includes: + +- `enabled`: Groups that were newly enabled by this call +- `enabled_groups`: All currently enabled groups +- `available_tools`: Tools now available to the agent +- `available_groups`: Child groups that can now be enabled +- `errors`: Any validation errors (unknown groups, already enabled, etc.) + +Similarly, `disable_tools` returns: + +```json +{ + "disabled": ["issues"], + "enabled_groups": [], + "available_tools": [], + "errors": [] +} +``` + +A `tools/list_changed` notification is sent whenever groups are enabled or disabled, prompting the client to refresh its tool list. + +When a tool from a disabled group is called, the agent receives an error message with a hint about which group to enable. + +## API Documentation + +::: wags.middleware.groups.in_group + options: + show_source: false + members: [] + show_signature: false + +::: wags.middleware.groups.GroupDefinition + options: + show_source: false + members: [] + show_signature: false + +::: wags.middleware.groups.GroupsMiddleware + options: + show_source: false + show_bases: false + members: [] + show_signature: false + +::: wags.middleware.groups.EnableToolsResult + options: + show_source: false + members: [] + show_signature: false + +::: wags.middleware.groups.DisableToolsResult + options: + show_source: false + members: [] + show_signature: false diff --git a/docs/middleware/overview.md b/docs/middleware/overview.md index ecfc364..d22225b 100644 --- a/docs/middleware/overview.md +++ b/docs/middleware/overview.md @@ -17,6 +17,7 @@ The wags middleware toolkit further provides *fi Understand what features different middlewares provide and how to configure them: +- [Groups](groups.md) for progressive tool disclosure via hierarchical groups. - [TodoList](todo.md) to ensure agents perform complex tasks correctly. - [Roots](roots.md) to enable client-configured fine-grained access control for MCP servers. - [Elicitation](elicitation.md) add human-in-the-loop features to improve UX. diff --git a/mkdocs.yml b/mkdocs.yml index 5690849..2312425 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,6 +41,7 @@ nav: - Onboarding Servers: onboarding.md - Middleware: - Overview: middleware/overview.md + - Groups: middleware/groups.md - TodoList: middleware/todo.md - Roots: middleware/roots.md - Elicitation: middleware/elicitation.md diff --git a/pyproject.toml b/pyproject.toml index c868553..fb3ebbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,9 @@ override-dependencies = [ "google-genai>=1.33.0", # Override mcpuniverse's google-genai==1.16.1 constraint "mistralai>=1.7.0", # Override mcpuniverse's mistralai==1.6.0 constraint "python-dotenv>=1.1.0", # Override mcpuniverse's python-dotenv==1.0.1 constraint + "uvicorn>=0.35.0", # Override mcpuniverse's uvicorn==0.34.0 constraint + "fastapi>=0.121.0", # Override mcpuniverse's fastapi==0.115.12 constraint + "tiktoken>=0.12.0", # Override mcpuniverse's tiktoken==0.11.0 constraint "mcp @ git+https://github.com/chughtapan/python-sdk.git@wags-dev", # Align with core dependency ] diff --git a/src/wags/middleware/__init__.py b/src/wags/middleware/__init__.py index 0cc368a..abdf21e 100644 --- a/src/wags/middleware/__init__.py +++ b/src/wags/middleware/__init__.py @@ -1,11 +1,23 @@ """WAGS middleware components.""" from .elicitation import ElicitationMiddleware, RequiresElicitation +from .groups import ( + DisableToolsResult, + EnableToolsResult, + GroupDefinition, + GroupsMiddleware, + in_group, +) from .roots import RootsMiddleware, requires_root __all__ = [ + "DisableToolsResult", "ElicitationMiddleware", + "EnableToolsResult", + "GroupDefinition", + "GroupsMiddleware", "RequiresElicitation", "RootsMiddleware", + "in_group", "requires_root", ] diff --git a/src/wags/middleware/groups.py b/src/wags/middleware/groups.py new file mode 100644 index 0000000..1dc6ef5 --- /dev/null +++ b/src/wags/middleware/groups.py @@ -0,0 +1,361 @@ +"""Middleware for progressive tool disclosure via groups. + +Tools can be assigned to groups via handler decorators (@in_group) or +tool metadata (GROUPS_META_KEY). Agents use enable_tools/disable_tools +meta-tools to control which groups are active. +""" + +import inspect +from collections.abc import Callable, Sequence +from dataclasses import dataclass +from typing import Any, TypeVar + +import mcp.types as mt +from fastmcp.exceptions import ToolError +from fastmcp.server.middleware.middleware import CallNext, MiddlewareContext +from fastmcp.tools.tool import Tool, ToolResult +from pydantic import BaseModel + +from wags.middleware.base import WagsMiddlewareBase + + +class EnableToolsResult(BaseModel): + """Structured result from enable_tools meta-tool.""" + + enabled: list[str] + enabled_groups: list[str] + available_tools: list[str] + available_groups: list[str] + errors: list[str] + + +class DisableToolsResult(BaseModel): + """Structured result from disable_tools meta-tool.""" + + disabled: list[str] + enabled_groups: list[str] + available_tools: list[str] + errors: list[str] + + +F = TypeVar("F", bound=Callable[..., Any]) + +# Metadata key for groups in tool meta +GROUPS_META_KEY = "io.modelcontextprotocol/groups" + + +def in_group(*group_names: str) -> Callable[[F], F]: + """Mark a handler method as belonging to one or more groups. + + Can be stacked to add tool to multiple groups: + + @in_group("issues") + @in_group("communications") + async def create_issue(self, ...): + pass + + Args: + group_names: Names of groups this tool belongs to + """ + + def decorator(func: F) -> F: + existing: set[str] = getattr(func, "__groups__", set()) + updated = existing | set(group_names) + setattr(func, "__groups__", updated) + return func + + return decorator + + +@dataclass +class GroupDefinition: + """Definition of a tool group.""" + + description: str + parent: str | None = None + + +class GroupsMiddleware(WagsMiddlewareBase): + """Middleware for progressive tool disclosure via groups.""" + + def __init__( + self, + groups: dict[str, GroupDefinition], + handlers: Any | None = None, + initial_groups: list[str] | None = None, + max_tools: int | None = None, + ): + super().__init__(handlers=handlers) + self.group_definitions = groups + self.max_tools = max_tools + self._enabled_groups: set[str] = set() + self._tool_to_groups: dict[str, set[str]] = {} + self._all_tools: Sequence[Tool] | None = None + self._children_map: dict[str, set[str]] = {} + + self._build_hierarchy() + if handlers: + self._scan_handlers(handlers) + + for g in initial_groups or []: + if g not in self.group_definitions: + raise ValueError(f"Unknown group: {g}") + if not self._is_group_visible(g): + parent = self.group_definitions[g].parent + raise ValueError(f"Cannot initially enable '{g}': parent '{parent}' not enabled") + self._enable_group(g) + + def _build_hierarchy(self) -> None: + """Build parent-child relationships from group definitions.""" + for name, defn in self.group_definitions.items(): + if defn.parent: + if defn.parent not in self.group_definitions: + raise ValueError(f"Group '{name}' has unknown parent '{defn.parent}'") + self._children_map.setdefault(defn.parent, set()).add(name) + + def _get_all_descendants(self, group_name: str) -> set[str]: + """Get all descendant groups (children, grandchildren, etc.).""" + descendants: set[str] = set() + to_process = [group_name] + while to_process: + current = to_process.pop() + for child in self._children_map.get(current, []): + if child not in descendants: + descendants.add(child) + to_process.append(child) + return descendants + + def _enable_group(self, group_name: str) -> bool: + """Enable a group. Returns True if newly enabled.""" + if group_name in self._enabled_groups: + return False + self._enabled_groups.add(group_name) + return True + + def _disable_group_with_descendants(self, group_name: str) -> set[str]: + """Disable a group and all its enabled descendants.""" + newly_disabled: set[str] = set() + if group_name in self._enabled_groups: + self._enabled_groups.discard(group_name) + newly_disabled.add(group_name) + for descendant in self._get_all_descendants(group_name): + if descendant in self._enabled_groups: + self._enabled_groups.discard(descendant) + newly_disabled.add(descendant) + return newly_disabled + + def _is_group_visible(self, group_name: str) -> bool: + """A group is visible if it's a root or its parent is enabled.""" + defn = self.group_definitions[group_name] + return defn.parent is None or defn.parent in self._enabled_groups + + def _scan_handlers(self, handlers: Any) -> None: + """Scan handler methods for @in_group decorators.""" + for name in dir(handlers): + if name.startswith("_"): + continue + method = getattr(handlers, name) + if callable(method) and inspect.iscoroutinefunction(method): + groups: set[str] = getattr(method, "__groups__", set()) + if groups: + for g in groups: + if g not in self.group_definitions: + raise ValueError( + f"Handler '{name}' references unknown group '{g}'. Define it in group_definitions." + ) + self._tool_to_groups[name] = groups + + def _discover_groups_from_metadata(self, tools: Sequence[Tool]) -> None: + """Discover group memberships from tool metadata.""" + for tool in tools: + if tool.name in self._tool_to_groups: + continue + if hasattr(tool, "meta") and tool.meta: + groups = tool.meta.get(GROUPS_META_KEY, []) + if groups: + valid_groups = {g for g in groups if g in self.group_definitions} + if valid_groups: + self._tool_to_groups[tool.name] = valid_groups + + def _get_enabled_tools(self) -> set[str]: + """Get tool names from enabled groups.""" + return {tool_name for tool_name, groups in self._tool_to_groups.items() if groups & self._enabled_groups} + + def _count_tools_if_enabled(self, group_name: str) -> int: + """Count total tools if group were enabled.""" + simulated_enabled = self._enabled_groups | {group_name} + return sum(1 for groups in self._tool_to_groups.values() if groups & simulated_enabled) + + def _build_enable_tools_description(self) -> str: + """Build description showing available groups with progressive disclosure.""" + lines = ["Enable tool groups for use.", "", "Available groups:"] + + def format_group(name: str, indent: int = 0) -> list[str]: + prefix = " " * indent + "- " if indent else "- " + defn = self.group_definitions[name] + status = " (enabled)" if name in self._enabled_groups else "" + result = [f"{prefix}{name}: {defn.description}{status}"] + if name in self._enabled_groups: + for child in sorted(self._children_map.get(name, [])): + result.extend(format_group(child, indent + 1)) + return result + + root_groups = [n for n, d in self.group_definitions.items() if d.parent is None] + for name in sorted(root_groups): + lines.extend(format_group(name)) + + if self.max_tools: + current_count = len(self._get_enabled_tools()) + lines.append(f"\nMax tools limit: {self.max_tools} (current: {current_count})") + return "\n".join(lines) + + def _build_disable_tools_description(self) -> str: + """Build description showing currently enabled groups.""" + lines = ["Disable tool groups to reduce context.", ""] + if self._enabled_groups: + lines.append("Currently enabled:") + lines.extend( + f"- {name}: {self.group_definitions[name].description}" for name in sorted(self._enabled_groups) + ) + else: + lines.append("No groups currently enabled.") + return "\n".join(lines) + + def _create_meta_tools(self) -> list[Tool]: + """Create enable_tools and disable_tools meta-tools.""" + + async def enable_tools_fn(groups: list[str]) -> EnableToolsResult: + raise NotImplementedError + + async def disable_tools_fn(groups: list[str]) -> DisableToolsResult: + raise NotImplementedError + + return [ + Tool.from_function( + fn=enable_tools_fn, + name="enable_tools", + description=self._build_enable_tools_description(), + output_schema=None, + ), + Tool.from_function( + fn=disable_tools_fn, + name="disable_tools", + description=self._build_disable_tools_description(), + output_schema=None, + ), + ] + + def _validate_enable_group(self, group_name: str) -> str | None: + """Return error message if group can't be enabled, None if valid.""" + if group_name not in self.group_definitions: + return f"Unknown group: {group_name}" + if not self._is_group_visible(group_name): + parent = self.group_definitions[group_name].parent + return f"Group '{group_name}' not visible. Enable parent '{parent}' first." + if group_name in self._enabled_groups: + return f"Group already enabled: {group_name}" + if self.max_tools: + projected = self._count_tools_if_enabled(group_name) + if projected > self.max_tools: + return ( + f"Cannot enable '{group_name}': would result in {projected} tools, " + f"exceeding max_tools={self.max_tools}. Disable some groups first." + ) + return None + + def _get_available_children(self) -> set[str]: + """Get child groups that are visible but not yet enabled.""" + available: set[str] = set() + for g in self._enabled_groups: + for child in self._children_map.get(g, set()): + if child not in self._enabled_groups: + available.add(child) + return available + + async def _enable_tools(self, groups: list[str]) -> EnableToolsResult: + """Process enable_tools request.""" + enabled: list[str] = [] + errors: list[str] = [] + for group_name in groups: + if error := self._validate_enable_group(group_name): + errors.append(error) + elif self._enable_group(group_name): + enabled.append(group_name) + return EnableToolsResult( + enabled=enabled, + enabled_groups=sorted(self._enabled_groups), + available_tools=sorted(self._get_enabled_tools()), + available_groups=sorted(self._get_available_children()), + errors=errors, + ) + + async def _disable_tools(self, groups: list[str]) -> DisableToolsResult: + """Process disable_tools request.""" + all_disabled: list[str] = [] + errors: list[str] = [] + for group_name in groups: + if group_name not in self.group_definitions: + errors.append(f"Unknown group: {group_name}") + elif group_name not in self._enabled_groups: + errors.append(f"Group not enabled: {group_name}") + else: + newly_disabled = self._disable_group_with_descendants(group_name) + all_disabled.extend(sorted(newly_disabled)) + return DisableToolsResult( + disabled=all_disabled, + enabled_groups=sorted(self._enabled_groups), + available_tools=sorted(self._get_enabled_tools()), + errors=errors, + ) + + async def on_list_tools( + self, + context: MiddlewareContext[mt.ListToolsRequest], + call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]], + ) -> Sequence[Tool]: + """Filter tools to only enabled groups + meta-tools.""" + all_tools = await call_next(context) + self._all_tools = all_tools + self._discover_groups_from_metadata(all_tools) + + enabled_tool_names = self._get_enabled_tools() + filtered = [t for t in all_tools if t.name in enabled_tool_names] + return self._create_meta_tools() + filtered + + async def _send_tools_list_changed(self, context: MiddlewareContext) -> None: + """Send tools/list_changed notification to client.""" + if context.fastmcp_context and hasattr(context.fastmcp_context, "session"): + session = context.fastmcp_context.session + if hasattr(session, "send_tool_list_changed"): + await session.send_tool_list_changed() + + async def on_call_tool( + self, + context: MiddlewareContext[mt.CallToolRequestParams], + call_next: CallNext[mt.CallToolRequestParams, Any], + ) -> Any: + """Handle meta-tool calls and block disabled tool calls.""" + tool_name = context.message.name + args = context.message.arguments or {} + + if tool_name == "enable_tools": + enable_result = await self._enable_tools(args.get("groups", [])) + if enable_result.enabled: + await self._send_tools_list_changed(context) + return ToolResult(structured_content=enable_result) + + if tool_name == "disable_tools": + disable_result = await self._disable_tools(args.get("groups", [])) + if disable_result.disabled: + await self._send_tools_list_changed(context) + return ToolResult(structured_content=disable_result) + + if tool_name not in self._get_enabled_tools(): + containing_groups = self._tool_to_groups.get(tool_name, set()) + hint = f" Try: enable_tools(groups={sorted(containing_groups)})" if containing_groups else "" + raise ToolError( + f"Tool '{tool_name}' is not available. Enable its group first.{hint} " + f"Currently enabled: {sorted(self._enabled_groups) or 'none'}" + ) + + return await call_next(context) diff --git a/tests/benchmarks/bfcl/elicitation.py b/tests/benchmarks/bfcl/elicitation.py index c87097f..5307718 100644 --- a/tests/benchmarks/bfcl/elicitation.py +++ b/tests/benchmarks/bfcl/elicitation.py @@ -176,7 +176,8 @@ async def handle( func_name = self.extract_function_name(message) self.structured_logger.log_elicitation(func_name, "accepted", ground_truth_params) return ElicitResult( - action="accept", content=cast(dict[str, str | int | float | bool | None], ground_truth_params) + action="accept", + content=cast(dict[str, str | int | float | bool | list[str] | None], ground_truth_params), ) # No matching function found or no text params diff --git a/tests/e2e/groups/__init__.py b/tests/e2e/groups/__init__.py new file mode 100644 index 0000000..37d542e --- /dev/null +++ b/tests/e2e/groups/__init__.py @@ -0,0 +1 @@ +"""E2E tests for GroupsMiddleware.""" diff --git a/tests/e2e/groups/conftest.py b/tests/e2e/groups/conftest.py new file mode 100644 index 0000000..0b0219d --- /dev/null +++ b/tests/e2e/groups/conftest.py @@ -0,0 +1,24 @@ +"""Pytest configuration for groups E2E tests.""" + +import os +from collections.abc import Generator + +import pytest +from fast_agent import FastAgent + + +@pytest.fixture +def fast_agent(request: pytest.FixtureRequest) -> Generator[FastAgent]: + """Create FastAgent configured for groups tests.""" + test_dir = os.path.dirname(__file__) + original_cwd = os.getcwd() + os.chdir(test_dir) + + agent = FastAgent( + "Groups E2E Tests", + config_path=os.path.join(test_dir, "fastagent.config.yaml"), + ignore_unknown_args=True, + ) + + yield agent + os.chdir(original_cwd) diff --git a/tests/e2e/groups/demo.py b/tests/e2e/groups/demo.py new file mode 100755 index 0000000..a7f72cf --- /dev/null +++ b/tests/e2e/groups/demo.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +"""Interactive demo of GroupsMiddleware with FastAgent. + +Usage: + python tests/e2e/groups/demo.py + python tests/e2e/groups/demo.py --model gpt-5 +""" + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))) + +from fast_agent import FastAgent + +script_dir = os.path.dirname(__file__) +os.chdir(script_dir) + +fast = FastAgent( + "Groups Demo", + config_path=os.path.join(script_dir, "fastagent.config.yaml"), +) + + +@fast.agent( + name="demo", + servers=["mock-github-groups"], + instruction="You are an autonomous assistant. Complete tasks without asking for confirmation.", +) +async def demo() -> None: + async with fast.run() as agent: + await agent() + + +if __name__ == "__main__": + import asyncio + + asyncio.run(demo()) diff --git a/tests/e2e/groups/fastagent.config.yaml b/tests/e2e/groups/fastagent.config.yaml new file mode 100644 index 0000000..2243061 --- /dev/null +++ b/tests/e2e/groups/fastagent.config.yaml @@ -0,0 +1,8 @@ +mcp: + servers: + mock-github-groups: + transport: stdio + command: fastmcp + args: + - run + - server.py diff --git a/tests/e2e/groups/server.py b/tests/e2e/groups/server.py new file mode 100644 index 0000000..a2ad44f --- /dev/null +++ b/tests/e2e/groups/server.py @@ -0,0 +1,103 @@ +"""Mock GitHub server with GroupsMiddleware for E2E testing.""" + +from typing import Any + +from fastmcp import FastMCP + +from wags.middleware.groups import GroupDefinition, GroupsMiddleware, in_group +from wags.proxy import create_proxy + +server = FastMCP("mock-github") + + +class GithubHandlers: + @in_group("repo_management") + async def create_repository(self, name: str, private: bool = False) -> None: + pass + + @in_group("branches") + async def create_branch(self, repo: str, branch_name: str) -> None: + pass + + @in_group("branches") + async def list_branches(self, repo: str) -> None: + pass + + @in_group("issues") + async def create_issue(self, title: str, body: str) -> None: + pass + + @in_group("issues") + async def list_issues(self) -> None: + pass + + @in_group("pull_requests") + async def create_pull_request(self, title: str, head: str, base: str) -> None: + pass + + +@server.tool() +async def create_repository(name: str, private: bool = False) -> dict[str, Any]: + """Create a new repository.""" + return {"id": 123, "name": name, "private": private, "created": True} + + +@server.tool() +async def create_branch(repo: str, branch_name: str) -> dict[str, Any]: + """Create a new branch.""" + return {"repo": repo, "branch_name": branch_name, "created": True} + + +@server.tool() +async def list_branches(repo: str) -> list[dict[str, Any]]: + """List all branches.""" + return [ + {"name": "main", "protected": True}, + {"name": "develop", "protected": False}, + ] + + +@server.tool() +async def create_issue(title: str, body: str) -> dict[str, Any]: + """Create a new issue.""" + return {"id": 1, "title": title, "body": body, "state": "open"} + + +@server.tool() +async def list_issues() -> list[dict[str, Any]]: + """List all issues.""" + return [ + {"id": 1, "title": "Issue 1", "state": "open"}, + {"id": 2, "title": "Issue 2", "state": "closed"}, + ] + + +@server.tool() +async def create_pull_request(title: str, head: str, base: str) -> dict[str, Any]: + """Create a pull request.""" + return {"id": 10, "title": title, "head": head, "base": base, "state": "open"} + + +# Group hierarchy: +# code_management (root) +# - repo_management +# - branches (3 levels deep) +# - pull_requests +# issues (root, independent) +proxy = create_proxy(server, server_name="mock-github-groups") +proxy.add_middleware( + GroupsMiddleware( + groups={ + "code_management": GroupDefinition(description="Code and repository management"), + "repo_management": GroupDefinition(description="Repository creation", parent="code_management"), + "branches": GroupDefinition(description="Branch management", parent="repo_management"), + "pull_requests": GroupDefinition(description="Pull request management", parent="code_management"), + "issues": GroupDefinition(description="Issue tracking"), + }, + handlers=GithubHandlers(), + initial_groups=[], + max_tools=6, + ) +) + +mcp = proxy diff --git a/tests/e2e/groups/test_groups_e2e.py b/tests/e2e/groups/test_groups_e2e.py new file mode 100644 index 0000000..5b29af4 --- /dev/null +++ b/tests/e2e/groups/test_groups_e2e.py @@ -0,0 +1,218 @@ +"""E2E tests for GroupsMiddleware with LLM agents.""" + +import json +from typing import Any + +import pytest +from fast_agent import FastAgent + +from tests.utils.fastagent_helpers import MessageSerializer + + +def extract_tool_calls(agent: Any) -> list[dict[str, Any]]: + """Extract all tool calls from agent message history.""" + messages = agent._agent(None).message_history + complete_json = MessageSerializer.serialize_complete(messages) + complete_data = json.loads(complete_json) + tool_calls = MessageSerializer.extract_tool_calls_by_turn(complete_data) + return [call for turn in tool_calls for call in turn] + + +def find_call(calls: list[dict[str, Any]], tool_suffix: str) -> dict[str, Any] | None: + """Find first tool call ending with given suffix.""" + return next((c for c in calls if c["function"].endswith(tool_suffix)), None) + + +def find_all_calls(calls: list[dict[str, Any]], tool_suffix: str) -> list[dict[str, Any]]: + """Find all tool calls ending with given suffix.""" + return [c for c in calls if c["function"].endswith(tool_suffix)] + + +def get_all_enabled_groups(calls: list[dict[str, Any]]) -> list[str]: + """Collect all groups enabled across all enable_tools calls.""" + groups = [] + for call in find_all_calls(calls, "enable_tools"): + groups.extend(call["arguments"]["groups"]) + return groups + + +class TestGroupsMiddleware: + @pytest.mark.asyncio + @pytest.mark.verified_models(["gpt-5", "claude-sonnet-4-5"]) + async def test_progressive_disclosure(self, fast_agent: FastAgent, model: str) -> None: + """Agent enables groups before using tools.""" + fast = fast_agent + + @fast.agent( + name="groups_test", + model=model, + servers=["mock-github-groups"], + instruction=( + "You are a helpful assistant. When you need to use a tool that is not available, " + "first call enable_tools to make it available." + ), + ) + async def test_workflow() -> None: + async with fast.run() as agent: + await agent.send("Create an issue titled 'Bug report' with body 'Found a bug in the API'") + calls = extract_tool_calls(agent) + + enable_call = find_call(calls, "enable_tools") + assert enable_call, "Agent should call enable_tools" + assert "issues" in enable_call["arguments"]["groups"] + + create_call = find_call(calls, "create_issue") + assert create_call, "Agent should call create_issue" + assert create_call["arguments"]["title"] == "Bug report" + + await test_workflow() + + @pytest.mark.asyncio + @pytest.mark.verified_models(["gpt-5", "claude-sonnet-4-5"]) + async def test_group_hierarchy(self, fast_agent: FastAgent, model: str) -> None: + """Agent navigates parent-child group hierarchy.""" + fast = fast_agent + + @fast.agent( + name="hierarchy_test", + model=model, + servers=["mock-github-groups"], + instruction=( + "You are a helpful assistant. Use enable_tools to discover and enable tool groups. " + "Some groups have parent-child relationships - enable parents first to reveal children." + ), + ) + async def test_hierarchy() -> None: + async with fast.run() as agent: + await agent.send("Create a repository named 'test-repo'.") + calls = extract_tool_calls(agent) + + assert find_all_calls(calls, "enable_tools"), "Agent should call enable_tools" + + enabled = get_all_enabled_groups(calls) + assert "code_management" in enabled, "Agent should enable code_management parent" + assert "repo_management" in enabled, "Agent should enable repo_management" + assert find_call(calls, "create_repository"), "Agent should call create_repository" + + await test_hierarchy() + + @pytest.mark.asyncio + @pytest.mark.verified_models(["gpt-5", "claude-sonnet-4-5"]) + async def test_disable_groups(self, fast_agent: FastAgent, model: str) -> None: + """Agent disables groups after use to reduce context.""" + fast = fast_agent + + @fast.agent( + name="disable_test", + model=model, + servers=["mock-github-groups"], + instruction=("You are a helpful assistant. After completing a task, disable groups you no longer need."), + ) + async def test_disable() -> None: + async with fast.run() as agent: + await agent.send( + "First enable the issues group, then create an issue titled 'Test'. " + "After creating the issue, disable the issues group since we're done with it." + ) + calls = extract_tool_calls(agent) + + assert find_call(calls, "enable_tools"), "Agent should call enable_tools" + assert find_call(calls, "create_issue"), "Agent should call create_issue" + assert find_call(calls, "disable_tools"), "Agent should call disable_tools" + + await test_disable() + + @pytest.mark.asyncio + @pytest.mark.verified_models(["gpt-5", "claude-sonnet-4-5"]) + async def test_deep_hierarchy(self, fast_agent: FastAgent, model: str) -> None: + """Agent navigates 3+ level group hierarchy.""" + fast = fast_agent + + @fast.agent( + name="deep_hierarchy_test", + model=model, + servers=["mock-github-groups"], + instruction=( + "You are a helpful assistant. Use enable_tools to discover and enable tool groups. " + "Groups may have parent-child relationships - enable parents first to reveal children." + ), + ) + async def test_deep_hierarchy() -> None: + async with fast.run() as agent: + await agent.send("Create a branch named 'feature-x' in repo 'my-repo'.") + calls = extract_tool_calls(agent) + + assert find_all_calls(calls, "enable_tools"), "Agent should call enable_tools" + + enabled = get_all_enabled_groups(calls) + assert "code_management" in enabled, "Agent should enable code_management (level 1)" + assert "repo_management" in enabled, "Agent should enable repo_management (level 2)" + assert "branches" in enabled, "Agent should enable branches (level 3)" + + branch_calls = find_all_calls(calls, "create_branch") + assert branch_calls, "Agent should call create_branch" + correct = next( + (c for c in branch_calls if c["arguments"].get("branch_name") == "feature-x"), + None, + ) + assert correct, f"Expected branch_name='feature-x', got: {[c['arguments'] for c in branch_calls]}" + + await test_deep_hierarchy() + + @pytest.mark.asyncio + @pytest.mark.verified_models(["gpt-5", "claude-sonnet-4-5"]) + async def test_error_recovery(self, fast_agent: FastAgent, model: str) -> None: + """Agent recovers from disabled tool error by enabling the right group.""" + fast = fast_agent + + @fast.agent( + name="recovery_test", + model=model, + servers=["mock-github-groups"], + instruction=( + "You are a helpful assistant. If a tool is not available, the error will tell you " + "which group to enable. Use enable_tools to make the tool available, then retry." + ), + ) + async def test_recovery() -> None: + async with fast.run() as agent: + await agent.send("Please create an issue titled 'Login broken' with body 'Cannot log in to the app'.") + calls = extract_tool_calls(agent) + + assert find_call(calls, "enable_tools"), "Agent should call enable_tools" + + create_call = find_call(calls, "create_issue") + assert create_call, "Agent should call create_issue" + assert create_call["arguments"]["title"] == "Login broken" + + await test_recovery() + + @pytest.mark.asyncio + @pytest.mark.verified_models(["gpt-5", "claude-sonnet-4-5"]) + async def test_max_tools_limit(self, fast_agent: FastAgent, model: str) -> None: + """Agent handles max_tools limit by disabling groups to make room.""" + fast = fast_agent + + @fast.agent( + name="max_tools_test", + model=model, + servers=["mock-github-groups"], + instruction=( + "You are a helpful assistant. The server has a max_tools limit. " + "If enabling a group would exceed the limit, disable unneeded groups first." + ), + ) + async def test_max_tools() -> None: + async with fast.run() as agent: + await agent.send( + "First, enable the issues group. Then enable the pull_requests group. " + "If you hit a max_tools limit, disable issues first before enabling pull_requests." + ) + calls = extract_tool_calls(agent) + + assert find_all_calls(calls, "enable_tools"), "Agent should call enable_tools" + + enabled = get_all_enabled_groups(calls) + assert "pull_requests" in enabled, "Agent should enable pull_requests group" + + await test_max_tools() diff --git a/tests/integration/test_groups_middleware.py b/tests/integration/test_groups_middleware.py new file mode 100644 index 0000000..376bc23 --- /dev/null +++ b/tests/integration/test_groups_middleware.py @@ -0,0 +1,540 @@ +"""Integration tests for GroupsMiddleware with FastMCP.""" + +from typing import Any + +import pytest +from fastmcp import FastMCP +from fastmcp.client import Client +from fastmcp.exceptions import ToolError + +from wags import create_proxy +from wags.middleware.groups import ( + GROUPS_META_KEY, + GroupDefinition, + GroupsMiddleware, + in_group, +) + + +class IssuesHandlers: + """Handler stubs for issues-only tests.""" + + @in_group("issues") + async def create_issue(self, owner: str, repo: str, title: str) -> None: + pass + + +class GitHubHandlers: + """Handler stubs that define group membership via decorators.""" + + @in_group("issues") + async def create_issue(self, owner: str, repo: str, title: str) -> None: + pass # Stub - actual execution by proxied server + + @in_group("issues") + async def list_issues(self, owner: str, repo: str) -> None: + pass + + @in_group("repo") + async def create_repository(self, name: str) -> None: + pass + + +@pytest.mark.asyncio +class TestGroupsMiddlewareIntegration: + """Integration tests for GroupsMiddleware with proxy architecture.""" + + async def test_middleware_filters_tools_by_group(self) -> None: + """Test GroupsMiddleware filters tools by enabled groups.""" + # Create backend server with actual tools + backend = FastMCP("github-backend") + + @backend.tool + async def create_issue(owner: str, repo: str, title: str) -> dict[str, Any]: + return {"owner": owner, "repo": repo, "title": title} + + @backend.tool + async def list_issues(owner: str, repo: str) -> list[dict[str, Any]]: + return [{"id": 1, "title": "Issue 1"}] + + @backend.tool + async def create_repository(name: str) -> dict[str, Any]: + return {"name": name} + + # Create proxy with middleware + handlers = GitHubHandlers() + proxy = create_proxy(backend, server_name="github-proxy") + proxy.add_middleware( + GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issue tracking"), + "repo": GroupDefinition(description="Repository management"), + }, + handlers=handlers, + initial_groups=["issues"], + ) + ) + + async with Client(proxy) as client: + tools = await client.list_tools() + tool_names = {t.name for t in tools} + + # Meta-tools always present + assert "enable_tools" in tool_names + assert "disable_tools" in tool_names + + # Issues tools present (group enabled) + assert "create_issue" in tool_names + assert "list_issues" in tool_names + + # Repo tools not present (group not enabled) + assert "create_repository" not in tool_names + + async def test_enabled_tool_call_passes_through(self) -> None: + """Test calling an enabled tool passes through to backend.""" + backend = FastMCP("backend") + + @backend.tool + async def create_issue(owner: str, repo: str, title: str) -> dict[str, Any]: + return {"created": f"{owner}/{repo}: {title}"} + + handlers = IssuesHandlers() + proxy = create_proxy(backend, server_name="proxy") + proxy.add_middleware( + GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issue tracking")}, + handlers=handlers, + initial_groups=["issues"], + ) + ) + + async with Client(proxy) as client: + result = await client.call_tool( + "create_issue", + {"owner": "myorg", "repo": "myrepo", "title": "Test Issue"}, + ) + assert result.data["created"] == "myorg/myrepo: Test Issue" + + async def test_disabled_tool_call_returns_error(self) -> None: + """Test calling a disabled tool returns error message.""" + backend = FastMCP("backend") + + @backend.tool + async def create_issue(owner: str, repo: str, title: str) -> dict[str, Any]: + return {"created": f"{owner}/{repo}: {title}"} + + handlers = IssuesHandlers() + proxy = create_proxy(backend, server_name="proxy") + proxy.add_middleware( + GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issue tracking")}, + handlers=handlers, + # issues NOT enabled + ) + ) + + async with Client(proxy) as client: + # Disabled tools raise ToolError with helpful message + with pytest.raises(ToolError) as exc_info: + await client.call_tool( + "create_issue", + {"owner": "myorg", "repo": "myrepo", "title": "Test"}, + ) + error_msg = str(exc_info.value) + assert "not available" in error_msg + assert "enable_tools" in error_msg + + async def test_enable_tools_makes_tools_visible(self) -> None: + """Test enable_tools makes group's tools visible.""" + backend = FastMCP("backend") + + @backend.tool + async def create_issue(owner: str, repo: str, title: str) -> dict[str, Any]: + return {"created": f"{owner}/{repo}: {title}"} + + handlers = IssuesHandlers() + proxy = create_proxy(backend, server_name="proxy") + proxy.add_middleware( + GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issue tracking")}, + handlers=handlers, + ) + ) + + async with Client(proxy) as client: + # Initially not visible + tools = await client.list_tools() + assert "create_issue" not in {t.name for t in tools} + + # Enable group + result = await client.call_tool("enable_tools", {"groups": ["issues"]}) + assert result.structured_content["enabled"] == ["issues"] + + # Now visible + tools = await client.list_tools() + assert "create_issue" in {t.name for t in tools} + + async def test_disable_tools_hides_tools(self) -> None: + """Test disable_tools hides group's tools.""" + backend = FastMCP("backend") + + @backend.tool + async def create_issue(owner: str, repo: str, title: str) -> dict[str, Any]: + return {"created": f"{owner}/{repo}: {title}"} + + handlers = IssuesHandlers() + proxy = create_proxy(backend, server_name="proxy") + proxy.add_middleware( + GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issue tracking")}, + handlers=handlers, + initial_groups=["issues"], + ) + ) + + async with Client(proxy) as client: + # Initially visible + tools = await client.list_tools() + assert "create_issue" in {t.name for t in tools} + + # Disable group + result = await client.call_tool("disable_tools", {"groups": ["issues"]}) + assert result.structured_content["disabled"] == ["issues"] + + # Now hidden + tools = await client.list_tools() + assert "create_issue" not in {t.name for t in tools} + + async def test_handler_decorators_discovered(self) -> None: + """Test @in_group decorated handlers are discovered.""" + + class MultiGroupHandlers: + @in_group("issues") + async def create_issue(self, title: str) -> None: + pass + + @in_group("issues") + @in_group("communications") + async def add_comment(self, target_id: int, body: str) -> None: + pass + + @in_group("repo") + async def create_repo(self, name: str) -> None: + pass + + middleware = GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issue tracking"), + "repo": GroupDefinition(description="Repository management"), + "communications": GroupDefinition(description="Communications"), + }, + handlers=MultiGroupHandlers(), + ) + + # Verify tool-to-group mapping was discovered + assert "create_issue" in middleware._tool_to_groups + assert middleware._tool_to_groups["create_issue"] == {"issues"} + assert "create_repo" in middleware._tool_to_groups + assert middleware._tool_to_groups["create_repo"] == {"repo"} + # add_comment belongs to multiple groups + assert "add_comment" in middleware._tool_to_groups + assert middleware._tool_to_groups["add_comment"] == {"issues", "communications"} + + +@pytest.mark.asyncio +class TestProgressiveDisclosure: + """Tests for progressive disclosure of groups.""" + + async def test_child_groups_hidden_until_parent_enabled(self) -> None: + """Test child groups not visible until parent enabled.""" + backend = FastMCP("backend") + proxy = create_proxy(backend, server_name="proxy") + proxy.add_middleware( + GroupsMiddleware( + groups={ + "comms": GroupDefinition(description="Communications"), + "email": GroupDefinition(description="Email", parent="comms"), + "calendar": GroupDefinition(description="Calendar", parent="comms"), + }, + ) + ) + + async with Client(proxy) as client: + # Check initial description - only root groups visible + tools = await client.list_tools() + enable_tool = next(t for t in tools if t.name == "enable_tools") + assert "comms: Communications" in enable_tool.description + assert "email" not in enable_tool.description + + # Enable parent group + result = await client.call_tool("enable_tools", {"groups": ["comms"]}) + assert result.structured_content["enabled"] == ["comms"] + assert "email" in result.structured_content["available_groups"] + + # Now children visible in description + tools = await client.list_tools() + enable_tool = next(t for t in tools if t.name == "enable_tools") + assert "email" in enable_tool.description + assert "calendar" in enable_tool.description + + async def test_cannot_enable_child_before_parent(self) -> None: + """Test enabling child before parent returns error.""" + backend = FastMCP("backend") + proxy = create_proxy(backend, server_name="proxy") + proxy.add_middleware( + GroupsMiddleware( + groups={ + "parent": GroupDefinition(description="Parent"), + "child": GroupDefinition(description="Child", parent="parent"), + }, + ) + ) + + async with Client(proxy) as client: + result = await client.call_tool("enable_tools", {"groups": ["child"]}) + text = result.content[0].text + assert "not visible" in text + assert "Enable parent 'parent' first" in text + + async def test_disabling_parent_cascades_to_children(self) -> None: + """Test disabling parent also disables all children.""" + + class NestedHandlers: + @in_group("email") + async def send_email(self, to: str, body: str) -> None: + pass + + backend = FastMCP("backend") + + @backend.tool + async def send_email(to: str, body: str) -> dict[str, Any]: + return {"sent_to": to} + + handlers = NestedHandlers() + proxy = create_proxy(backend, server_name="proxy") + proxy.add_middleware( + GroupsMiddleware( + groups={ + "comms": GroupDefinition(description="Communications"), + "email": GroupDefinition(description="Email", parent="comms"), + }, + handlers=handlers, + initial_groups=["comms", "email"], + ) + ) + + async with Client(proxy) as client: + # Verify email tool is available + tools = await client.list_tools() + assert "send_email" in {t.name for t in tools} + + # Disable parent + result = await client.call_tool("disable_tools", {"groups": ["comms"]}) + text = result.content[0].text + assert "comms" in text + assert "email" in text # Child also disabled + + # Email tool no longer available + tools = await client.list_tools() + assert "send_email" not in {t.name for t in tools} + + async def test_deeply_nested_groups(self) -> None: + """Test deeply nested groups (3+ levels) progressively revealed.""" + backend = FastMCP("backend") + proxy = create_proxy(backend, server_name="proxy") + proxy.add_middleware( + GroupsMiddleware( + groups={ + "root": GroupDefinition(description="Root"), + "level1": GroupDefinition(description="Level 1", parent="root"), + "level2": GroupDefinition(description="Level 2", parent="level1"), + "level3": GroupDefinition(description="Level 3", parent="level2"), + }, + ) + ) + + async with Client(proxy) as client: + # Check initial description shows only root + tools = await client.list_tools() + enable_tool = next(t for t in tools if t.name == "enable_tools") + assert "root: Root" in enable_tool.description + assert "level1" not in enable_tool.description + + # Enable root -> level1 becomes visible + await client.call_tool("enable_tools", {"groups": ["root"]}) + tools = await client.list_tools() + enable_tool = next(t for t in tools if t.name == "enable_tools") + assert "level1" in enable_tool.description + assert "level2" not in enable_tool.description + + # Enable level1 -> level2 becomes visible + await client.call_tool("enable_tools", {"groups": ["level1"]}) + tools = await client.list_tools() + enable_tool = next(t for t in tools if t.name == "enable_tools") + assert "level2" in enable_tool.description + assert "level3" not in enable_tool.description + + +@pytest.mark.asyncio +class TestMetadataDiscovery: + """Tests for tool metadata discovery.""" + + async def test_tools_with_groups_meta_discovered(self) -> None: + """Test tools with GROUPS_META_KEY are auto-discovered.""" + backend = FastMCP("backend") + + # Tool with groups metadata (no handler needed) + @backend.tool(meta={GROUPS_META_KEY: ["issues"]}) + async def create_issue(title: str) -> dict[str, Any]: + return {"title": title} + + proxy = create_proxy(backend, server_name="proxy") + middleware = GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issue tracking")}, + initial_groups=["issues"], + ) + proxy.add_middleware(middleware) + + async with Client(proxy) as client: + tools = await client.list_tools() + tool_names = {t.name for t in tools} + assert "create_issue" in tool_names + + async def test_handler_decorators_take_precedence(self) -> None: + """Test handler decorators take precedence over metadata.""" + + class Handlers: + @in_group("issues") # Handler says "issues" + async def my_tool(self) -> None: + pass + + backend = FastMCP("backend") + + # Tool metadata says "repo", but handler decorator says "issues" + @backend.tool(meta={GROUPS_META_KEY: ["repo"]}) + async def my_tool() -> dict[str, Any]: + return {} + + handlers = Handlers() + middleware = GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issue tracking"), + "repo": GroupDefinition(description="Repository"), + }, + handlers=handlers, + ) + + # Handler decoration takes precedence + assert middleware._tool_to_groups.get("my_tool") == {"issues"} + + +@pytest.mark.asyncio +class TestMaxToolsLimit: + """Tests for max_tools enforcement.""" + + async def test_max_tools_blocks_over_limit(self) -> None: + """Test max_tools enforcement blocks over-limit.""" + + class Handlers: + @in_group("group1") + async def tool1(self) -> None: + pass + + @in_group("group1") + async def tool2(self) -> None: + pass + + @in_group("group2") + async def tool3(self) -> None: + pass + + backend = FastMCP("backend") + + @backend.tool + async def tool1() -> dict[str, Any]: + return {} + + @backend.tool + async def tool2() -> dict[str, Any]: + return {} + + @backend.tool + async def tool3() -> dict[str, Any]: + return {} + + handlers = Handlers() + proxy = create_proxy(backend, server_name="proxy") + proxy.add_middleware( + GroupsMiddleware( + groups={ + "group1": GroupDefinition(description="Group 1"), + "group2": GroupDefinition(description="Group 2"), + }, + handlers=handlers, + initial_groups=["group1"], + max_tools=2, # Already at limit with group1 + ) + ) + + async with Client(proxy) as client: + result = await client.call_tool("enable_tools", {"groups": ["group2"]}) + text = result.content[0].text + assert "exceeding max_tools=2" in text + assert "Disable some groups first" in text + + +@pytest.mark.asyncio +class TestProxyIntegration: + """Tests for full proxy integration.""" + + async def test_groups_middleware_with_proxy(self) -> None: + """Test GroupsMiddleware integrates with create_proxy.""" + # Create backend server + backend = FastMCP("backend") + + @backend.tool + async def create_issue(title: str) -> dict[str, Any]: + return {"title": title, "source": "backend"} + + @backend.tool + async def create_repo(name: str) -> dict[str, Any]: + return {"name": name, "source": "backend"} + + # Create handlers with group metadata + class Handlers: + @in_group("issues") + async def create_issue(self, title: str) -> None: + pass + + @in_group("repo") + async def create_repo(self, name: str) -> None: + pass + + handlers = Handlers() + proxy = create_proxy(backend, server_name="test-proxy") + proxy.add_middleware( + GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issue tracking"), + "repo": GroupDefinition(description="Repository"), + }, + handlers=handlers, + initial_groups=["issues"], + ) + ) + + async with Client(proxy) as client: + # Issues enabled, repo not + tools = await client.list_tools() + tool_names = {t.name for t in tools} + assert "create_issue" in tool_names + assert "create_repo" not in tool_names + + # Call enabled tool + result = await client.call_tool("create_issue", {"title": "Test"}) + assert result.data["source"] == "backend" + + # Call disabled tool - raises ToolError + with pytest.raises(ToolError) as exc_info: + await client.call_tool("create_repo", {"name": "test"}) + assert "not available" in str(exc_info.value) diff --git a/tests/unit/middleware/test_groups.py b/tests/unit/middleware/test_groups.py new file mode 100644 index 0000000..df10f91 --- /dev/null +++ b/tests/unit/middleware/test_groups.py @@ -0,0 +1,616 @@ +"""Unit tests for GroupsMiddleware.""" + +import json +from typing import Any, cast +from unittest.mock import AsyncMock, MagicMock + +import pytest +from fastmcp.exceptions import ToolError +from fastmcp.server.middleware.middleware import MiddlewareContext +from fastmcp.tools.tool import Tool +from mcp.types import CallToolRequestParams, ListToolsRequest + +from wags.middleware.groups import ( + GROUPS_META_KEY, + GroupDefinition, + GroupsMiddleware, + in_group, +) + + +async def call_meta_tool(middleware: GroupsMiddleware, tool_name: str, groups: list[str]) -> dict[str, Any]: + """Helper to call enable_tools/disable_tools and return structured result.""" + message = CallToolRequestParams(name=tool_name, arguments={"groups": groups}) + context = MiddlewareContext(message=message) + result = await middleware.on_call_tool(context, AsyncMock()) + mcp_result = result.to_mcp_result() + if isinstance(mcp_result, tuple): + return cast(dict[str, Any], mcp_result[1]) + return cast(dict[str, Any], json.loads(mcp_result[0].text)) + + +class TestInGroupDecorator: + def test_single_group(self) -> None: + @in_group("issues") + async def handler() -> None: + pass + + assert getattr(handler, "__groups__") == {"issues"} + + def test_stacking_decorators(self) -> None: + @in_group("issues") + @in_group("communications") + async def handler() -> None: + pass + + assert getattr(handler, "__groups__") == {"issues", "communications"} + + def test_multiple_groups_single_call(self) -> None: + @in_group("issues", "communications") + async def handler() -> None: + pass + + assert getattr(handler, "__groups__") == {"issues", "communications"} + + +class TestGroupDefinition: + def test_basic(self) -> None: + defn = GroupDefinition(description="Test group") + assert defn.description == "Test group" + assert defn.parent is None + + def test_with_parent(self) -> None: + defn = GroupDefinition(description="Child group", parent="parent") + assert defn.parent == "parent" + + +class TestGroupsMiddlewareInit: + def test_unknown_parent_raises_error(self) -> None: + with pytest.raises(ValueError, match="unknown parent 'unknown'"): + GroupsMiddleware(groups={"child": GroupDefinition(description="Child", parent="unknown")}) + + def test_handler_unknown_group_raises_error(self) -> None: + class Handlers: + @in_group("unknown_group") + async def handler(self) -> None: + pass + + with pytest.raises(ValueError, match="references unknown group"): + GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issues")}, + handlers=Handlers(), + ) + + def test_initial_groups_unknown(self) -> None: + with pytest.raises(ValueError, match="Unknown group: unknown"): + GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issues")}, + initial_groups=["unknown"], + ) + + def test_initial_groups_child_before_parent(self) -> None: + with pytest.raises(ValueError, match="parent 'comms' not enabled"): + GroupsMiddleware( + groups={ + "comms": GroupDefinition(description="Communications"), + "email": GroupDefinition(description="Email", parent="comms"), + }, + initial_groups=["email"], + ) + + +class TestGroupHierarchy: + def test_build_hierarchy(self) -> None: + middleware = GroupsMiddleware( + groups={ + "comms": GroupDefinition(description="Communications"), + "email": GroupDefinition(description="Email", parent="comms"), + "calendar": GroupDefinition(description="Calendar", parent="comms"), + } + ) + assert middleware._children_map == {"comms": {"email", "calendar"}} + + def test_get_all_descendants(self) -> None: + middleware = GroupsMiddleware( + groups={ + "root": GroupDefinition(description="Root"), + "level1": GroupDefinition(description="Level 1", parent="root"), + "level2": GroupDefinition(description="Level 2", parent="level1"), + "level2b": GroupDefinition(description="Level 2b", parent="level1"), + } + ) + assert middleware._get_all_descendants("root") == { + "level1", + "level2", + "level2b", + } + + def test_visibility(self) -> None: + middleware = GroupsMiddleware( + groups={ + "root": GroupDefinition(description="Root"), + "child": GroupDefinition(description="Child", parent="root"), + } + ) + assert middleware._is_group_visible("root") is True + assert middleware._is_group_visible("child") is False + + middleware._enable_group("root") + assert middleware._is_group_visible("child") is True + + +class TestHandlerScanning: + def test_finds_decorated_methods(self) -> None: + class Handlers: + @in_group("issues") + async def create_issue(self) -> None: + pass + + @in_group("repo") + async def create_repo(self) -> None: + pass + + middleware = GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issues"), + "repo": GroupDefinition(description="Repo"), + }, + handlers=Handlers(), + ) + assert middleware._tool_to_groups == { + "create_issue": {"issues"}, + "create_repo": {"repo"}, + } + + def test_undecorated_methods_ignored(self) -> None: + class Handlers: + @in_group("issues") + async def decorated(self) -> None: + pass + + async def undecorated(self) -> None: + pass + + middleware = GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issues")}, + handlers=Handlers(), + ) + assert "undecorated" not in middleware._tool_to_groups + + +class TestToolFiltering: + def test_get_enabled_tools(self) -> None: + class Handlers: + @in_group("issues") + async def create_issue(self) -> None: + pass + + @in_group("repo") + async def create_repo(self) -> None: + pass + + middleware = GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issues"), + "repo": GroupDefinition(description="Repo"), + }, + handlers=Handlers(), + initial_groups=["issues"], + ) + assert middleware._get_enabled_tools() == {"create_issue"} + + def test_tool_in_multiple_groups(self) -> None: + class Handlers: + @in_group("issues") + @in_group("communications") + async def add_comment(self) -> None: + pass + + middleware = GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issues"), + "communications": GroupDefinition(description="Communications"), + }, + handlers=Handlers(), + ) + + middleware._enable_group("issues") + assert middleware._get_enabled_tools() == {"add_comment"} + + middleware._enable_group("communications") + assert middleware._get_enabled_tools() == {"add_comment"} + + def test_count_tools_if_enabled(self) -> None: + class Handlers: + @in_group("issues") + async def create_issue(self) -> None: + pass + + @in_group("issues") + async def list_issues(self) -> None: + pass + + @in_group("repo") + async def create_repo(self) -> None: + pass + + middleware = GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issues"), + "repo": GroupDefinition(description="Repo"), + }, + handlers=Handlers(), + ) + assert middleware._count_tools_if_enabled("issues") == 2 + assert middleware._count_tools_if_enabled("repo") == 1 + + +class TestEnableTools: + @pytest.mark.asyncio + async def test_enable_root_group(self) -> None: + middleware = GroupsMiddleware(groups={"issues": GroupDefinition(description="Issues")}) + result = await call_meta_tool(middleware, "enable_tools", ["issues"]) + assert result["enabled"] == ["issues"] + assert "issues" in middleware._enabled_groups + + @pytest.mark.asyncio + async def test_child_before_parent_fails(self) -> None: + middleware = GroupsMiddleware( + groups={ + "comms": GroupDefinition(description="Communications"), + "email": GroupDefinition(description="Email", parent="comms"), + } + ) + result = await call_meta_tool(middleware, "enable_tools", ["email"]) + assert any("not visible" in e for e in result["errors"]) + assert "email" not in middleware._enabled_groups + + @pytest.mark.asyncio + async def test_child_after_parent(self) -> None: + middleware = GroupsMiddleware( + groups={ + "comms": GroupDefinition(description="Communications"), + "email": GroupDefinition(description="Email", parent="comms"), + } + ) + await call_meta_tool(middleware, "enable_tools", ["comms"]) + result = await call_meta_tool(middleware, "enable_tools", ["email"]) + assert result["enabled"] == ["email"] + + @pytest.mark.asyncio + async def test_unknown_group(self) -> None: + middleware = GroupsMiddleware(groups={"issues": GroupDefinition(description="Issues")}) + result = await call_meta_tool(middleware, "enable_tools", ["unknown"]) + assert "Unknown group: unknown" in result["errors"] + + @pytest.mark.asyncio + async def test_already_enabled(self) -> None: + middleware = GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issues")}, + initial_groups=["issues"], + ) + result = await call_meta_tool(middleware, "enable_tools", ["issues"]) + assert "Group already enabled: issues" in result["errors"] + + @pytest.mark.asyncio + async def test_max_tools_enforcement(self) -> None: + class Handlers: + @in_group("issues") + async def issue1(self) -> None: + pass + + @in_group("issues") + async def issue2(self) -> None: + pass + + @in_group("repo") + async def repo1(self) -> None: + pass + + middleware = GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issues"), + "repo": GroupDefinition(description="Repo"), + }, + handlers=Handlers(), + initial_groups=["issues"], + max_tools=2, + ) + result = await call_meta_tool(middleware, "enable_tools", ["repo"]) + assert any("exceeding max_tools=2" in e for e in result["errors"]) + + @pytest.mark.asyncio + async def test_available_groups_in_response(self) -> None: + middleware = GroupsMiddleware( + groups={ + "comms": GroupDefinition(description="Communications"), + "email": GroupDefinition(description="Email", parent="comms"), + "calendar": GroupDefinition(description="Calendar", parent="comms"), + } + ) + result = await call_meta_tool(middleware, "enable_tools", ["comms"]) + assert set(result["available_groups"]) == {"email", "calendar"} + + +class TestDisableTools: + @pytest.mark.asyncio + async def test_disable_leaf_group(self) -> None: + middleware = GroupsMiddleware( + groups={ + "comms": GroupDefinition(description="Communications"), + "email": GroupDefinition(description="Email", parent="comms"), + }, + initial_groups=["comms", "email"], + ) + result = await call_meta_tool(middleware, "disable_tools", ["email"]) + assert "email" in result["disabled"] + assert "email" not in middleware._enabled_groups + assert "comms" in middleware._enabled_groups + + @pytest.mark.asyncio + async def test_parent_cascades_to_children(self) -> None: + middleware = GroupsMiddleware( + groups={ + "comms": GroupDefinition(description="Communications"), + "email": GroupDefinition(description="Email", parent="comms"), + "calendar": GroupDefinition(description="Calendar", parent="comms"), + }, + initial_groups=["comms", "email", "calendar"], + ) + result = await call_meta_tool(middleware, "disable_tools", ["comms"]) + assert set(result["disabled"]) == {"comms", "email", "calendar"} + assert len(middleware._enabled_groups) == 0 + + @pytest.mark.asyncio + async def test_unknown_group(self) -> None: + middleware = GroupsMiddleware(groups={"issues": GroupDefinition(description="Issues")}) + result = await call_meta_tool(middleware, "disable_tools", ["unknown"]) + assert "Unknown group: unknown" in result["errors"] + + @pytest.mark.asyncio + async def test_not_enabled(self) -> None: + middleware = GroupsMiddleware(groups={"issues": GroupDefinition(description="Issues")}) + result = await call_meta_tool(middleware, "disable_tools", ["issues"]) + assert "Group not enabled: issues" in result["errors"] + + +class TestOnListTools: + @pytest.mark.asyncio + async def test_filters_to_enabled_groups(self) -> None: + class Handlers: + @in_group("issues") + async def create_issue(self) -> None: + pass + + @in_group("repo") + async def create_repo(self) -> None: + pass + + middleware = GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issues"), + "repo": GroupDefinition(description="Repo"), + }, + handlers=Handlers(), + initial_groups=["issues"], + ) + + async def mock_call_next(context: MiddlewareContext[ListToolsRequest]) -> list[Tool]: + return [ + Tool.from_function(lambda: None, name="create_issue", description=""), + Tool.from_function(lambda: None, name="create_repo", description=""), + ] + + context = MiddlewareContext(message=ListToolsRequest()) + result = await middleware.on_list_tools(context, mock_call_next) + tool_names = {t.name for t in result} + + assert "enable_tools" in tool_names + assert "disable_tools" in tool_names + assert "create_issue" in tool_names + assert "create_repo" not in tool_names + + @pytest.mark.asyncio + async def test_discovers_groups_from_metadata(self) -> None: + middleware = GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issues")}, + initial_groups=["issues"], + ) + + mock_tool = Tool.from_function( + lambda: None, + name="create_issue", + description="", + meta={GROUPS_META_KEY: ["issues"]}, + ) + + async def mock_call_next(context: MiddlewareContext[ListToolsRequest]) -> list[Tool]: + return [mock_tool] + + context = MiddlewareContext(message=ListToolsRequest()) + result = await middleware.on_list_tools(context, mock_call_next) + assert "create_issue" in {t.name for t in result} + + +class TestOnCallTool: + @pytest.mark.asyncio + async def test_meta_tool_returns_structured_content(self) -> None: + middleware = GroupsMiddleware(groups={"issues": GroupDefinition(description="Issues")}) + message = CallToolRequestParams(name="enable_tools", arguments={"groups": ["issues"]}) + context = MiddlewareContext(message=message) + + async def should_not_call(context: MiddlewareContext[CallToolRequestParams]) -> Any: + raise AssertionError("Should not call next for meta-tool") + + result = await middleware.on_call_tool(context, should_not_call) + mcp_result = result.to_mcp_result() + assert isinstance(mcp_result, tuple) + assert mcp_result[1]["enabled"] == ["issues"] + + @pytest.mark.asyncio + async def test_enabled_tool_passes_through(self) -> None: + class Handlers: + @in_group("issues") + async def create_issue(self) -> None: + pass + + middleware = GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issues")}, + handlers=Handlers(), + initial_groups=["issues"], + ) + message = CallToolRequestParams(name="create_issue", arguments={}) + context = MiddlewareContext(message=message) + + mock_call_next = AsyncMock(return_value="success") + result = await middleware.on_call_tool(context, mock_call_next) + + mock_call_next.assert_called_once() + assert result == "success" + + @pytest.mark.asyncio + async def test_disabled_tool_raises_error(self) -> None: + class Handlers: + @in_group("issues") + async def create_issue(self) -> None: + pass + + middleware = GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issues")}, + handlers=Handlers(), + ) + message = CallToolRequestParams(name="create_issue", arguments={}) + context = MiddlewareContext(message=message) + + with pytest.raises(ToolError) as exc_info: + await middleware.on_call_tool(context, AsyncMock()) + + assert "not available" in str(exc_info.value) + assert "enable_tools" in str(exc_info.value) + + @pytest.mark.asyncio + async def test_error_includes_group_hint(self) -> None: + class Handlers: + @in_group("issues") + async def create_issue(self) -> None: + pass + + middleware = GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issues")}, + handlers=Handlers(), + ) + message = CallToolRequestParams(name="create_issue", arguments={}) + context = MiddlewareContext(message=message) + + with pytest.raises(ToolError) as exc_info: + await middleware.on_call_tool(context, AsyncMock()) + + assert "enable_tools(groups=['issues'])" in str(exc_info.value) + + +class TestToolsListChangedNotification: + @pytest.mark.asyncio + async def test_sent_on_enable(self) -> None: + middleware = GroupsMiddleware(groups={"issues": GroupDefinition(description="Issues")}) + mock_session = MagicMock() + mock_session.send_tool_list_changed = AsyncMock() + mock_fastmcp_context = MagicMock() + mock_fastmcp_context.session = mock_session + + message = CallToolRequestParams(name="enable_tools", arguments={"groups": ["issues"]}) + context = MiddlewareContext(message=message, fastmcp_context=mock_fastmcp_context) + + await middleware.on_call_tool(context, AsyncMock()) + mock_session.send_tool_list_changed.assert_called_once() + + @pytest.mark.asyncio + async def test_sent_on_disable(self) -> None: + middleware = GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issues")}, + initial_groups=["issues"], + ) + mock_session = MagicMock() + mock_session.send_tool_list_changed = AsyncMock() + mock_fastmcp_context = MagicMock() + mock_fastmcp_context.session = mock_session + + message = CallToolRequestParams(name="disable_tools", arguments={"groups": ["issues"]}) + context = MiddlewareContext(message=message, fastmcp_context=mock_fastmcp_context) + + await middleware.on_call_tool(context, AsyncMock()) + mock_session.send_tool_list_changed.assert_called_once() + + @pytest.mark.asyncio + async def test_not_sent_when_no_change(self) -> None: + middleware = GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issues")}, + initial_groups=["issues"], + ) + mock_session = MagicMock() + mock_session.send_tool_list_changed = AsyncMock() + mock_fastmcp_context = MagicMock() + mock_fastmcp_context.session = mock_session + + message = CallToolRequestParams(name="enable_tools", arguments={"groups": ["issues"]}) + context = MiddlewareContext(message=message, fastmcp_context=mock_fastmcp_context) + + await middleware.on_call_tool(context, AsyncMock()) + mock_session.send_tool_list_changed.assert_not_called() + + +class TestDynamicDescriptions: + def test_enable_tools_shows_root_groups(self) -> None: + middleware = GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issue tracking"), + "repo": GroupDefinition(description="Repository management"), + } + ) + desc = middleware._build_enable_tools_description() + assert "issues: Issue tracking" in desc + assert "repo: Repository management" in desc + + def test_enable_tools_shows_enabled_status(self) -> None: + middleware = GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issue tracking")}, + initial_groups=["issues"], + ) + desc = middleware._build_enable_tools_description() + assert "(enabled)" in desc + + def test_enable_tools_shows_max_tools(self) -> None: + middleware = GroupsMiddleware( + groups={"issues": GroupDefinition(description="Issue tracking")}, + max_tools=10, + ) + desc = middleware._build_enable_tools_description() + assert "Max tools limit: 10" in desc + + def test_enable_tools_shows_children_after_parent_enabled(self) -> None: + middleware = GroupsMiddleware( + groups={ + "comms": GroupDefinition(description="Communications"), + "email": GroupDefinition(description="Email", parent="comms"), + } + ) + assert "email" not in middleware._build_enable_tools_description() + + middleware._enable_group("comms") + assert "email: Email" in middleware._build_enable_tools_description() + + def test_disable_tools_shows_enabled(self) -> None: + middleware = GroupsMiddleware( + groups={ + "issues": GroupDefinition(description="Issue tracking"), + "repo": GroupDefinition(description="Repository"), + }, + initial_groups=["issues"], + ) + desc = middleware._build_disable_tools_description() + assert "issues: Issue tracking" in desc + assert "repo" not in desc + + def test_disable_tools_empty_when_none_enabled(self) -> None: + middleware = GroupsMiddleware(groups={"issues": GroupDefinition(description="Issue tracking")}) + desc = middleware._build_disable_tools_description() + assert "No groups currently enabled" in desc diff --git a/tests/unit/test_proxy.py b/tests/unit/test_proxy.py index fadbfbf..123687b 100644 --- a/tests/unit/test_proxy.py +++ b/tests/unit/test_proxy.py @@ -1,7 +1,7 @@ """Tests for proxy server with todo support.""" import pytest -from fastmcp import FastMCP +from fastmcp import Client, FastMCP from wags.proxy import create_proxy @@ -51,11 +51,13 @@ def test_tool() -> str: assert "TodoWrite" in proxy.instructions assert "Task Management" in proxy.instructions - # Should have todo tools available - tools = await proxy._tool_manager.get_tools() - assert "TodoWrite" in tools + # Should have todo tools available via client connection + async with Client(proxy) as client: + tools = await client.list_tools() + tool_names = {t.name for t in tools} + assert "TodoWrite" in tool_names # Should also have original tool - assert "test_tool" in tools + assert "test_tool" in tool_names def test_create_proxy_with_todos_rejects_instructions(self) -> None: """Test that enable_todos=True raises error if server has instructions.""" @@ -75,11 +77,13 @@ async def test_todo_tools_no_prefix(self) -> None: server = FastMCP("test-server") proxy = create_proxy(server, enable_todos=True) - tools = await proxy._tool_manager.get_tools() + async with Client(proxy) as client: + tools = await client.list_tools() + tool_names = {t.name for t in tools} # Tools should be TodoWrite, not todo_TodoWrite - assert "TodoWrite" in tools - assert "todo_TodoWrite" not in tools + assert "TodoWrite" in tool_names + assert "todo_TodoWrite" not in tool_names def test_custom_server_name(self) -> None: """Test creating proxy with custom name.""" diff --git a/uv.lock b/uv.lock index 3ed24cb..6bbe747 100644 --- a/uv.lock +++ b/uv.lock @@ -5,16 +5,19 @@ requires-python = ">=3.13.5, <3.14" [manifest] overrides = [ { name = "anthropic", specifier = ">=0.68.0" }, + { name = "fastapi", specifier = ">=0.121.0" }, { name = "google-genai", specifier = ">=1.33.0" }, { name = "mcp", git = "https://github.com/chughtapan/python-sdk.git?rev=wags-dev" }, { name = "mistralai", specifier = ">=1.7.0" }, { name = "openai", extras = ["aiohttp"], specifier = ">=1.108.0" }, { name = "python-dotenv", specifier = ">=1.1.0" }, + { name = "tiktoken", specifier = ">=0.12.0" }, + { name = "uvicorn", specifier = ">=0.35.0" }, ] [[package]] name = "a2a-sdk" -version = "0.3.11" +version = "0.3.22" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-api-core" }, @@ -23,9 +26,21 @@ dependencies = [ { name = "protobuf" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/2c/6eff205080a4fb3937745f0bab4ff58716cdcc524acd077a493612d34336/a2a_sdk-0.3.11.tar.gz", hash = "sha256:194a6184d3e5c1c5d8941eb64fb33c346df3ebbec754effed8403f253bedb085", size = 226923, upload-time = "2025-11-07T11:05:38.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/92/a3/76f2d94a32a1b0dc760432d893a09ec5ed31de5ad51b1ef0f9d199ceb260/a2a_sdk-0.3.22.tar.gz", hash = "sha256:77a5694bfc4f26679c11b70c7f1062522206d430b34bc1215cfbb1eba67b7e7d", size = 231535, upload-time = "2025-12-16T18:39:21.19Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/f9/3e633485a3f23f5b3e04a7f0d3e690ae918fd1252941e8107c7593d882f1/a2a_sdk-0.3.11-py3-none-any.whl", hash = "sha256:f57673d5f38b3e0eb7c5b57e7dc126404d02c54c90692395ab4fd06aaa80cc8f", size = 140381, upload-time = "2025-11-07T11:05:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/64/e8/f4e39fd1cf0b3c4537b974637143f3ebfe1158dad7232d9eef15666a81ba/a2a_sdk-0.3.22-py3-none-any.whl", hash = "sha256:b98701135bb90b0ff85d35f31533b6b7a299bf810658c1c65f3814a6c15ea385", size = 144347, upload-time = "2025-12-16T18:39:19.218Z" }, +] + +[[package]] +name = "agent-client-protocol" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/7c/12da39be4f73026fd9b02144df5f64d803488cf1439aa221b0edb7c305e3/agent_client_protocol-0.7.1.tar.gz", hash = "sha256:8d7031209e14c3f2f987e3b95e7d9c3286158e7b2af1bf43d6aae5b8a429249f", size = 66226, upload-time = "2025-12-28T13:58:57.012Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/48/48d2fb454f911147432cd779f548e188274e1700f1cbe0a258e78158331a/agent_client_protocol-0.7.1-py3-none-any.whl", hash = "sha256:4ffe999488f2b23db26f09becdfaa2aaae6529f0847a52bca61bc2c628001c0f", size = 53771, upload-time = "2025-12-28T13:58:55.967Z" }, ] [[package]] @@ -95,6 +110,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -267,35 +291,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" }, ] -[[package]] -name = "azure-core" -version = "1.36.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0a/c4/d4ff3bc3ddf155156460bff340bbe9533f99fac54ddea165f35a8619f162/azure_core-1.36.0.tar.gz", hash = "sha256:22e5605e6d0bf1d229726af56d9e92bc37b6e726b141a18be0b4d424131741b7", size = 351139, upload-time = "2025-10-15T00:33:49.083Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/3c/b90d5afc2e47c4a45f4bba00f9c3193b0417fad5ad3bb07869f9d12832aa/azure_core-1.36.0-py3-none-any.whl", hash = "sha256:fee9923a3a753e94a259563429f3644aaf05c486d45b1215d098115102d91d3b", size = 213302, upload-time = "2025-10-15T00:33:51.058Z" }, -] - -[[package]] -name = "azure-identity" -version = "1.25.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "cryptography" }, - { name = "msal" }, - { name = "msal-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/8d/1a6c41c28a37eab26dc85ab6c86992c700cd3f4a597d9ed174b0e9c69489/azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456", size = 279826, upload-time = "2025-10-06T20:30:02.194Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/7b/5652771e24fff12da9dde4c20ecf4682e606b104f26419d139758cc935a6/azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651", size = 191317, upload-time = "2025-10-06T20:30:04.251Z" }, -] - [[package]] name = "bcrypt" version = "4.3.0" @@ -346,6 +341,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, ] +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + [[package]] name = "beautifulsoup4" version = "4.14.2" @@ -600,6 +604,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, ] +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + [[package]] name = "cohere" version = "5.13.3" @@ -758,6 +771,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -866,16 +888,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/98/2c050dec90e295a524c9b65c4cb9e7c302386a296b2938710448cbd267d5/faker-37.12.0-py3-none-any.whl", hash = "sha256:afe7ccc038da92f2fbae30d8e16d19d91e92e242f8401ce9caf44de892bab4c4", size = 1975461, upload-time = "2025-10-24T15:19:55.739Z" }, ] +[[package]] +name = "fakeredis" +version = "2.33.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187, upload-time = "2025-12-16T19:45:52.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605, upload-time = "2025-12-16T19:45:51.08Z" }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa" }, +] + [[package]] name = "fast-agent-mcp" -version = "0.3.19" -source = { git = "https://github.com/chughtapan/fast-agent.git?rev=wags-dev#8a7120d3a70bcc3bffce7fe3c5aa98ac301b21ad" } +version = "0.4.31" +source = { git = "https://github.com/chughtapan/fast-agent.git?rev=wags-dev#b4ce2857d87eccc8b268f05e3e6d584f87777fc9" } dependencies = [ { name = "a2a-sdk" }, + { name = "agent-client-protocol" }, { name = "aiohttp" }, { name = "anthropic" }, - { name = "azure-identity" }, - { name = "boto3" }, { name = "deprecated" }, { name = "email-validator" }, { name = "fastapi" }, @@ -896,23 +935,25 @@ dependencies = [ { name = "python-frontmatter" }, { name = "pyyaml" }, { name = "rich" }, - { name = "tensorzero" }, - { name = "textual" }, + { name = "tiktoken" }, { name = "typer" }, + { name = "uvloop", marker = "sys_platform != 'win32'" }, + { name = "watchfiles" }, ] [[package]] name = "fastapi" -version = "0.115.12" +version = "0.128.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, ] [[package]] @@ -950,20 +991,25 @@ wheels = [ [[package]] name = "fastmcp" -version = "0.0.1.dev2132+8b22665" -source = { git = "https://github.com/chughtapan/fastmcp.git?rev=wags-dev#8b22665875775978b813a142491d833242931d58" } +version = "0.0.1.dev2649+5187c199" +source = { git = "https://github.com/chughtapan/fastmcp.git?rev=wags-dev#5187c199864b922ab1cc4ff0fa67aa3ef2d8c3d0" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, { name = "httpx" }, + { name = "jsonschema-path" }, { name = "mcp" }, - { name = "openapi-core" }, { name = "openapi-pydantic" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, { name = "pydantic", extra = ["email"] }, + { name = "pydocket" }, { name = "pyperclip" }, { name = "python-dotenv" }, { name = "rich" }, + { name = "uvicorn" }, + { name = "websockets" }, ] [[package]] @@ -1316,21 +1362,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/a2/0d269db0f6163be503775dc8b6a6fa15820cc9fdc866f6ba608d86b721f2/httplib2-0.31.0-py3-none-any.whl", hash = "sha256:b9cd78abea9b4e43a7714c6e0f8b6b8561a6fc1e95d5dbd367f5bf0ef35f5d24", size = 91148, upload-time = "2025-09-11T12:16:01.803Z" }, ] -[[package]] -name = "httptools" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, - { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, -] - [[package]] name = "httpx" version = "0.28.1" @@ -1479,15 +1510,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6c/0c/f37b6a241f0759b7653ffa7213889d89ad49a2b76eb2ddf3b57b2738c347/iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242", size = 7545, upload-time = "2023-10-03T00:25:32.304Z" }, ] -[[package]] -name = "isodate" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, -] - [[package]] name = "isort" version = "5.13.2" @@ -1738,26 +1760,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" }, ] -[[package]] -name = "lazy-object-proxy" -version = "1.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, - { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, - { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, - { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, - { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, - { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, - { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, - { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, - { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, -] - [[package]] name = "libcst" version = "1.8.6" @@ -1785,18 +1787,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/0b/4fd40607bc4807ec2b93b054594373d7fa3d31bb983789901afcb9bcebe9/libcst-1.8.6-cp313-cp313t-win_arm64.whl", hash = "sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42", size = 1985181, upload-time = "2025-11-03T22:32:50.597Z" }, ] -[[package]] -name = "linkify-it-py" -version = "2.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "uc-micro-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, -] - [[package]] name = "litellm" version = "1.79.2" @@ -1820,6 +1810,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/b8/2a429e05bb709fc297201b37c6e039a4a1dfbcfd04a7e032a75864ed12b8/litellm-1.79.2-py3-none-any.whl", hash = "sha256:0d4e0962cbc72be1e13f759b539294a56837f8ff81d3ad9ffa743d9b8b6461bd", size = 10396310, upload-time = "2025-11-08T23:59:05.165Z" }, ] +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, + { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, + { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, + { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, + { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, +] + [[package]] name = "lxml" version = "6.0.2" @@ -1858,11 +1867,6 @@ wheels = [ { 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.optional-dependencies] -linkify = [ - { name = "linkify-it-py" }, -] - [[package]] name = "markdownify" version = "1.2.2" @@ -1920,8 +1924,8 @@ wheels = [ [[package]] name = "mcp" -version = "0.0.1.dev649+7ad8253" -source = { git = "https://github.com/chughtapan/python-sdk.git?rev=wags-dev#7ad825381b7847e08df8e4e3646cf72af2e487e0" } +version = "0.0.1.dev739+7fabbc8" +source = { git = "https://github.com/chughtapan/python-sdk.git?rev=wags-dev#7fabbc890aa58dd77e197723a44aca523ebe5360" } dependencies = [ { name = "anyio" }, { name = "httpx" }, @@ -1934,7 +1938,9 @@ dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "sse-starlette" }, { name = "starlette" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn" }, ] [[package]] @@ -2004,24 +2010,12 @@ dependencies = [ { name = "schema" }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "tiktoken" }, - { name = "uvicorn", extra = ["standard"] }, + { name = "uvicorn" }, { name = "wikipedia-api" }, { name = "xai-sdk" }, { name = "yfinance" }, ] -[[package]] -name = "mdit-py-plugins" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, -] - [[package]] name = "mdurl" version = "0.1.2" @@ -2065,32 +2059,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] -[[package]] -name = "msal" -version = "1.34.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/c857c46d653e104019a84f22d4494f2119b4fe9f896c92b4b864b3b045cc/msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f", size = 153961, upload-time = "2025-09-22T23:05:48.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/dc/18d48843499e278538890dc709e9ee3dea8375f8be8e82682851df1b48b5/msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1", size = 116987, upload-time = "2025-09-22T23:05:47.294Z" }, -] - -[[package]] -name = "msal-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "msal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, -] - [[package]] name = "multidict" version = "6.7.0" @@ -2247,26 +2215,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/71/a8712a89502b95da64db6b0b31c12ac5039542ae8e31caddba369b6bd324/openai_agents-0.2.11-py3-none-any.whl", hash = "sha256:ed26f7bb2b08bd7607ae87eb7bcfcee8c8f4431da134252757b31120a68b9086", size = 179141, upload-time = "2025-09-03T22:16:03.823Z" }, ] -[[package]] -name = "openapi-core" -version = "0.19.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "isodate" }, - { name = "jsonschema" }, - { name = "jsonschema-path" }, - { name = "more-itertools" }, - { name = "openapi-schema-validator" }, - { name = "openapi-spec-validator" }, - { name = "parse" }, - { name = "typing-extensions" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/35/1acaa5f2fcc6e54eded34a2ec74b479439c4e469fc4e8d0e803fda0234db/openapi_core-0.19.5.tar.gz", hash = "sha256:421e753da56c391704454e66afe4803a290108590ac8fa6f4a4487f4ec11f2d3", size = 103264, upload-time = "2025-03-20T20:17:28.193Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/6f/83ead0e2e30a90445ee4fc0135f43741aebc30cca5b43f20968b603e30b6/openapi_core-0.19.5-py3-none-any.whl", hash = "sha256:ef7210e83a59394f46ce282639d8d26ad6fc8094aa904c9c16eb1bac8908911f", size = 106595, upload-time = "2025-03-20T20:17:26.77Z" }, -] - [[package]] name = "openapi-pydantic" version = "0.5.1" @@ -2279,126 +2227,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, ] -[[package]] -name = "openapi-schema-validator" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonschema" }, - { name = "jsonschema-specifications" }, - { name = "rfc3339-validator" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" }, -] - -[[package]] -name = "openapi-spec-validator" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonschema" }, - { name = "jsonschema-path" }, - { name = "lazy-object-proxy" }, - { name = "openapi-schema-validator" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" }, -] - [[package]] name = "opentelemetry-api" -version = "1.38.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, ] [[package]] name = "opentelemetry-distro" -version = "0.59b0" +version = "0.60b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/73/909d18e3d609c9f72fdfc441dbf2f33d26d29126088de5b3df30f4867f8a/opentelemetry_distro-0.59b0.tar.gz", hash = "sha256:a72703a514e1773d35d1ec01489a5fd1f1e7ce92e93cf459ba60f85b880d0099", size = 2583, upload-time = "2025-10-16T08:39:28.111Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/77/f0b1f2bcf451ec5bc443d53bc7437577c3fc8444b3eb0d416ac5f7558b7b/opentelemetry_distro-0.60b1.tar.gz", hash = "sha256:8b7326b83a55ff7b17bb92225a86e2736a004f6af7aff00cb5d87b2d8e5bc283", size = 2584, upload-time = "2025-12-11T13:36:39.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/a5/71d78732d30616b0b57cce416fa49e7f25ce57492eaf66d0b6864c1df35f/opentelemetry_distro-0.59b0-py3-none-any.whl", hash = "sha256:bbe568d84d801d7e1ead320c4521fc37a4c24b3b2cd49a64f6d8a3c10676cea4", size = 3346, upload-time = "2025-10-16T08:38:27.63Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp" -version = "1.38.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/c2/2d/16e3487ddde2dee702bd746dd41950a8789b846d22a1c7e64824aac5ebea/opentelemetry_exporter_otlp-1.38.0.tar.gz", hash = "sha256:2f55acdd475e4136117eff20fbf1b9488b1b0b665ab64407516e1ac06f9c3f9d", size = 6147, upload-time = "2025-10-16T08:35:52.53Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/8a/81cd252b16b7d95ec1147982b6af81c7932d23918b4c3b15372531242ddd/opentelemetry_exporter_otlp-1.38.0-py3-none-any.whl", hash = "sha256:bc6562cef229fac8887ed7109fc5abc52315f39d9c03fd487bb8b4ef8fbbc231", size = 7018, upload-time = "2025-10-16T08:35:32.995Z" }, + { url = "https://files.pythonhosted.org/packages/24/70/78a86531495040fcad9569d7daa630eca06d27d37c825a8aad448b7c1c5b/opentelemetry_distro-0.60b1-py3-none-any.whl", hash = "sha256:581104a786f5df252f4dfe725e0ae16337a26da902acb92d8b3e7aee29f0c76e", size = 3343, upload-time = "2025-12-11T13:35:28.462Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.38.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" }, + { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, ] [[package]] -name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.38.0" +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, - { name = "grpcio" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, + { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676, upload-time = "2025-10-16T08:35:53.83Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/04/2a08fa9c0214ae38880df01e8bfae12b067ec0793446578575e5080d6545/opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb", size = 17288, upload-time = "2025-12-11T13:32:42.029Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695, upload-time = "2025-10-16T08:35:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/95/f1/b27d3e2e003cd9a3592c43d099d2ed8d0a947c15281bf8463a256db0b46c/opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985", size = 19641, upload-time = "2025-12-11T13:32:22.248Z" }, ] [[package]] -name = "opentelemetry-exporter-otlp-proto-http" -version = "1.38.0" +name = "opentelemetry-exporter-prometheus" +version = "0.60b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "googleapis-common-protos" }, { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, + { name = "prometheus-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" } +sdist = { url = "https://files.pythonhosted.org/packages/14/39/7dafa6fff210737267bed35a8855b6ac7399b9e582b8cf1f25f842517012/opentelemetry_exporter_prometheus-0.60b1.tar.gz", hash = "sha256:a4011b46906323f71724649d301b4dc188aaa068852e814f4df38cc76eac616b", size = 14976, upload-time = "2025-12-11T13:32:42.944Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" }, + { url = "https://files.pythonhosted.org/packages/9b/0d/4be6bf5477a3eb3d917d2f17d3c0b6720cd6cb97898444a61d43cc983f5c/opentelemetry_exporter_prometheus-0.60b1-py3-none-any.whl", hash = "sha256:49f59178de4f4590e3cef0b8b95cf6e071aae70e1f060566df5546fad773b8fd", size = 13019, upload-time = "2025-12-11T13:32:23.974Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.59b0" +version = "0.60b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -2406,14 +2308,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/ed/9c65cd209407fd807fa05be03ee30f159bdac8d59e7ea16a8fe5a1601222/opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc", size = 31544, upload-time = "2025-10-16T08:39:31.959Z" } +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/f5/7a40ff3f62bfe715dad2f633d7f1174ba1a7dd74254c15b2558b3401262a/opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee", size = 33020, upload-time = "2025-10-16T08:38:31.463Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, ] [[package]] name = "opentelemetry-instrumentation-anthropic" -version = "0.47.5" +version = "0.50.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -2421,9 +2323,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-semantic-conventions-ai" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/85/a2fc2cdc7633aabe85f9c644f0c2b9824c4d0e6ad7da35dfdd1ef5b5dcce/opentelemetry_instrumentation_anthropic-0.47.5.tar.gz", hash = "sha256:087470be96bb00b2c9229aa3be1b177b5e81e3a7454988847f3f06418a23d106", size = 14682, upload-time = "2025-10-24T19:21:34.932Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/3e/a1101d8f79ad69666963598c52071b24336c309de4ed6d6f38e73142350c/opentelemetry_instrumentation_anthropic-0.50.1.tar.gz", hash = "sha256:d2c1a589d48bc056eb8424204a2a8b219854629278f851e9e7d4d18b8b788203", size = 14927, upload-time = "2025-12-16T08:26:53.058Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/d2/f65527bd53d759a8e003b8a325492613236801774771ac38964d3762554b/opentelemetry_instrumentation_anthropic-0.47.5-py3-none-any.whl", hash = "sha256:b08339bc396442ab4bf4034eeeed21976ea84b436572b46ec31e2753b449b89a", size = 18128, upload-time = "2025-10-24T19:20:57.104Z" }, + { url = "https://files.pythonhosted.org/packages/db/31/5ff0ec511c2f98d3f983a3942bd71d2bfcc9b0e122c9327bdf20895b9edd/opentelemetry_instrumentation_anthropic-0.50.1-py3-none-any.whl", hash = "sha256:d5c12497c01a10ba34412805cb6d970fbdb5fc5d2f73f95ac387df99d2c9e813", size = 18458, upload-time = "2025-12-16T08:26:15.927Z" }, ] [[package]] @@ -2443,23 +2345,22 @@ wheels = [ [[package]] name = "opentelemetry-instrumentation-mcp" -version = "0.47.5" +version = "0.50.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp" }, { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-semantic-conventions-ai" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/44/ede68cfc4c3d262dd82088763c7bebd019ef57438adc815c15e1aa82bf27/opentelemetry_instrumentation_mcp-0.47.5.tar.gz", hash = "sha256:dc873754a35dff09eff2737322afbd999da4517138a62e8f29ffd668e4d885e8", size = 8833, upload-time = "2025-10-24T19:21:46.284Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/53/d9409e7506571a4ed4247a6b1b9e4226c5a6675c98ddf3975388c8e6dcf9/opentelemetry_instrumentation_mcp-0.50.1.tar.gz", hash = "sha256:5e60597824140a149574e91c88a10161b5ca215f2339ca9ede2d374bf22ad901", size = 8724, upload-time = "2025-12-16T08:27:03.678Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/6e/b94f2e1916076fb29f95b588528a9ac24411bb4d14c813ebd32a9b8dc498/opentelemetry_instrumentation_mcp-0.47.5-py3-none-any.whl", hash = "sha256:f475e281ff159d8fe5f20463405ed902732259f0a35c31df91e5a288956b18ff", size = 10657, upload-time = "2025-10-24T19:21:13.285Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/817a6e861cb2ebd5df4e869a9c76c92a882305a4ca5e56b092725ed58ec0/opentelemetry_instrumentation_mcp-0.50.1-py3-none-any.whl", hash = "sha256:908fb716d12f51130dbc92ca70112e21fbe3f046d30cd408f7b29fabba06c4a4", size = 10519, upload-time = "2025-12-16T08:26:31.46Z" }, ] [[package]] name = "opentelemetry-instrumentation-openai" -version = "0.47.5" +version = "0.50.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -2467,48 +2368,48 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-semantic-conventions-ai" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e2/db/3786ddc4de92e9b44ef7416a3786549b28f8a797fdf90e0dd265e6f9fb4d/opentelemetry_instrumentation_openai-0.47.5.tar.gz", hash = "sha256:0073613d1b586111aa40098d44d6a910b4edbe5d8df455fe778e85f50814e421", size = 25409, upload-time = "2025-10-24T19:21:49.996Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/f4/2cd7698431474102e85466fe197ffa440d8309e59a1260b62d85602e472a/opentelemetry_instrumentation_openai-0.50.1.tar.gz", hash = "sha256:43eea552ca80cc31f0197fac3458b53c4dbc2cff8f80aa9aa9d3fe899dba9190", size = 32262, upload-time = "2025-12-16T08:27:06.986Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/a9/e9a029a97c3e2f77b05da3d0a246adcc008e1ad4224234dc21f18b4b3966/opentelemetry_instrumentation_openai-0.47.5-py3-none-any.whl", hash = "sha256:d18e69a512d5e05436d6e1f3436045949f3e2e1c1027bc9178dfd31cf31e11b0", size = 35273, upload-time = "2025-10-24T19:21:19.447Z" }, + { url = "https://files.pythonhosted.org/packages/ae/62/909c5d0674f24d36388ca0b2454d84f9e5b09765b0294ba5b9a8f6b7d9e7/opentelemetry_instrumentation_openai-0.50.1-py3-none-any.whl", hash = "sha256:ea0ca70f09f1bcfd6a188d5122327386386dcc9b85fbf7187dd34e38d32c126d", size = 43002, upload-time = "2025-12-16T08:26:36.569Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.38.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" }, + { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.38.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.59b0" +version = "0.60b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, ] [[package]] @@ -2611,15 +2512,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/2f/804f58f0b856ab3bf21617cccf5b39206e6c4c94c2cd227bde125ea6105f/parameterized-0.9.0-py2.py3-none-any.whl", hash = "sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b", size = 20475, upload-time = "2023-03-27T02:01:09.31Z" }, ] -[[package]] -name = "parse" -version = "1.20.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391, upload-time = "2024-06-11T04:41:57.34Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126, upload-time = "2024-06-11T04:41:55.057Z" }, -] - [[package]] name = "parso" version = "0.8.5" @@ -2656,6 +2548,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] +[[package]] +name = "pathvalidate" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, +] + [[package]] name = "peewee" version = "3.18.3" @@ -2789,6 +2690,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/f7/244a5b1dd298650e4092c501197dad45036b1c31309ad4d01af430071a0f/polyfactory-2.22.3-py3-none-any.whl", hash = "sha256:0bfd5fe2fb2e5db39ded6aee8e923d1961095d4ebb44185cceee4654cb85e0b1", size = 63715, upload-time = "2025-10-18T14:04:52.657Z" }, ] +[[package]] +name = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -2949,6 +2859,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] +[[package]] +name = "py-key-value-aio" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "py-key-value-shared" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, +] + +[package.optional-dependencies] +disk = [ + { name = "diskcache" }, + { name = "pathvalidate" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "py-key-value-shared" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, +] + [[package]] name = "pyasn1" version = "0.6.1" @@ -3089,6 +3040,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, ] +[[package]] +name = "pydocket" +version = "0.16.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "fakeredis", extra = ["lua"] }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-prometheus" }, + { name = "opentelemetry-instrumentation" }, + { name = "prometheus-client" }, + { name = "py-key-value-aio", extra = ["memory", "redis"] }, + { name = "python-json-logger" }, + { name = "redis" }, + { name = "rich" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/00/26befe5f58df7cd1aeda4a8d10bc7d1908ffd86b80fd995e57a2a7b3f7bd/pydocket-0.16.6.tar.gz", hash = "sha256:b96c96ad7692827214ed4ff25fcf941ec38371314db5dcc1ae792b3e9d3a0294", size = 299054, upload-time = "2026-01-09T22:09:15.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/3f/7483e5a6dc6326b6e0c640619b5c5bd1d6e3c20e54d58f5fb86267cef00e/pydocket-0.16.6-py3-none-any.whl", hash = "sha256:683d21e2e846aa5106274e7d59210331b242d7fb0dce5b08d3b82065663ed183", size = 67697, upload-time = "2026-01-09T22:09:13.436Z" }, +] + [[package]] name = "pyee" version = "13.0.0" @@ -3243,6 +3217,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/87/3c8da047b3ec5f99511d1b4d7a5bc72d4b98751c7e78492d14dc736319c5/python_frontmatter-1.1.0-py3-none-any.whl", hash = "sha256:335465556358d9d0e6c98bbeb69b1c969f2a4a21360587b9873bfc3b213407c1", size = 9834, upload-time = "2024-01-16T18:50:00.911Z" }, ] +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + [[package]] name = "python-multipart" version = "0.0.20" @@ -3465,18 +3448,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, ] -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, -] - [[package]] name = "rich" version = "14.2.0" @@ -3639,6 +3610,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "soupsieve" version = "2.8" @@ -3755,58 +3735,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, ] -[[package]] -name = "tensorzero" -version = "2025.11.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "typing-extensions" }, - { name = "uuid-utils" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/eb/0c7c86f9bcfa088515c881985c578ac65e1f32832c247ffd3e3a8467a56a/tensorzero-2025.11.2.tar.gz", hash = "sha256:dc592591ba2062947b173169fa9e3e4f19215b5a6a9a1d43551d4dd76092026a", size = 1880206, upload-time = "2025-11-06T23:34:54.368Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/e2/1ac23d047ce54f05ebeff5b7207f7c95a9bbfaecfb07f15f251c19c8fe06/tensorzero-2025.11.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0d038abba51387a5224bd4ce45ee5eb586d1dc121c5aef62cd093fb7bb48587b", size = 26308119, upload-time = "2025-11-06T23:34:46.789Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a7/feff19b3c7710b5e39237ea2b270b91195bea457131012fb081fa01e11c4/tensorzero-2025.11.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e0841cbbe8ad9a9504daecc2988f06cebf84c64f91b99e886abf7cdcafb5cb", size = 29181220, upload-time = "2025-11-06T23:34:41.323Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/208b063e11dbe866bc9edf8ac67d9ac913f86441ae3a70caf3c9c9aa9d71/tensorzero-2025.11.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3164514d780041229febaf726b332c5c6e688b0fad689164425b778b6a912818", size = 29614862, upload-time = "2025-11-06T23:34:44.12Z" }, - { url = "https://files.pythonhosted.org/packages/67/e0/7a23e779f8a418bd6401602ed7d15472f805be4f511aa00bfeebf1f2086d/tensorzero-2025.11.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:bdbb3af5c2714a444e03087453579179417d82c2ee120e82c683ea5d149965ac", size = 29412933, upload-time = "2025-11-06T23:34:49.477Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/9aec5c36eb4ad030f17797b33764969b608ba18ff7535d6e7901982907f3/tensorzero-2025.11.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:578d396eb7b7d3ab79b0b700fc2c9e3950c7a753030b4a85e13ca6311980e327", size = 30165454, upload-time = "2025-11-06T23:34:52.137Z" }, - { url = "https://files.pythonhosted.org/packages/27/10/d177e1c6c1df6ef9094fdff29aa4cb416aa70570ec8a0c9689fc976b2bbd/tensorzero-2025.11.2-cp39-abi3-win_amd64.whl", hash = "sha256:88c866a87d91547714061f8f9ae9af4823cd3d3560fc8f4a5c45b9961d22259c", size = 25162180, upload-time = "2025-11-06T23:34:57.119Z" }, -] - -[[package]] -name = "textual" -version = "6.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py", extra = ["linkify"] }, - { name = "mdit-py-plugins" }, - { name = "platformdirs" }, - { name = "pygments" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/90/59757aa887ddcea61428820274f1a2d1f986feb7880374a5420ab5d37132/textual-6.5.0.tar.gz", hash = "sha256:e5f152cdd47db48a635d23b839721bae4d0e8b6d855e3fede7285218289294e3", size = 1574116, upload-time = "2025-10-31T17:21:53.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/37/1deba011782a49ea249c73adcf703a39b0249ac9b0e17d1a2e4074df8d57/textual-6.5.0-py3-none-any.whl", hash = "sha256:c5505be7fe606b8054fb88431279885f88352bddca64832f6acd293ef7d9b54f", size = 711848, upload-time = "2025-10-31T17:21:51.134Z" }, -] - [[package]] name = "tiktoken" -version = "0.11.0" +version = "0.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/86/ad0155a37c4f310935d5ac0b1ccf9bdb635dcb906e0a9a26b616dd55825a/tiktoken-0.11.0.tar.gz", hash = "sha256:3c518641aee1c52247c2b97e74d8d07d780092af79d5911a6ab5e79359d9b06a", size = 37648, upload-time = "2025-08-08T23:58:08.495Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/cd/a9034bcee638716d9310443818d73c6387a6a96db93cbcb0819b77f5b206/tiktoken-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a5f3f25ffb152ee7fec78e90a5e5ea5b03b4ea240beed03305615847f7a6ace2", size = 1055339, upload-time = "2025-08-08T23:57:51.802Z" }, - { url = "https://files.pythonhosted.org/packages/f1/91/9922b345f611b4e92581f234e64e9661e1c524875c8eadd513c4b2088472/tiktoken-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dc6e9ad16a2a75b4c4be7208055a1f707c9510541d94d9cc31f7fbdc8db41d8", size = 997080, upload-time = "2025-08-08T23:57:53.442Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9d/49cd047c71336bc4b4af460ac213ec1c457da67712bde59b892e84f1859f/tiktoken-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a0517634d67a8a48fd4a4ad73930c3022629a85a217d256a6e9b8b47439d1e4", size = 1128501, upload-time = "2025-08-08T23:57:54.808Z" }, - { url = "https://files.pythonhosted.org/packages/52/d5/a0dcdb40dd2ea357e83cb36258967f0ae96f5dd40c722d6e382ceee6bba9/tiktoken-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fb4effe60574675118b73c6fbfd3b5868e5d7a1f570d6cc0d18724b09ecf318", size = 1182743, upload-time = "2025-08-08T23:57:56.307Z" }, - { url = "https://files.pythonhosted.org/packages/3b/17/a0fc51aefb66b7b5261ca1314afa83df0106b033f783f9a7bcbe8e741494/tiktoken-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94f984c9831fd32688aef4348803b0905d4ae9c432303087bae370dc1381a2b8", size = 1244057, upload-time = "2025-08-08T23:57:57.628Z" }, - { url = "https://files.pythonhosted.org/packages/50/79/bcf350609f3a10f09fe4fc207f132085e497fdd3612f3925ab24d86a0ca0/tiktoken-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2177ffda31dec4023356a441793fed82f7af5291120751dee4d696414f54db0c", size = 883901, upload-time = "2025-08-08T23:57:59.359Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, ] [[package]] @@ -3960,15 +3912,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, ] -[[package]] -name = "uc-micro-py" -version = "1.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, -] - [[package]] name = "uritemplate" version = "4.2.0" @@ -3987,48 +3930,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] -[[package]] -name = "uuid-utils" -version = "0.11.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/ef/b6c1fd4fee3b2854bf9d602530ab8b6624882e2691c15a9c4d22ea8c03eb/uuid_utils-0.11.1.tar.gz", hash = "sha256:7ef455547c2ccb712840b106b5ab006383a9bfe4125ba1c5ab92e47bcbf79b46", size = 19933, upload-time = "2025-10-02T13:32:09.526Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/f5/254d7ce4b3aa4a1a3a4f279e0cc74eec8b4d3a61641d8ffc6e983907f2ca/uuid_utils-0.11.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4bc8cf73c375b9ea11baf70caacc2c4bf7ce9bfd804623aa0541e5656f3dbeaf", size = 581019, upload-time = "2025-10-02T13:31:32.239Z" }, - { url = "https://files.pythonhosted.org/packages/68/e6/f7d14c4e1988d8beb3ac9bd773f370376c704925bdfb07380f5476bb2986/uuid_utils-0.11.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0d2cb3bcc6f5862d08a0ee868b18233bc63ba9ea0e85ea9f3f8e703983558eba", size = 294377, upload-time = "2025-10-02T13:31:34.01Z" }, - { url = "https://files.pythonhosted.org/packages/8e/40/847a9a0258e7a2a14b015afdaa06ee4754a2680db7b74bac159d594eeb18/uuid_utils-0.11.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:463400604f623969f198aba9133ebfd717636f5e34257340302b1c3ff685dc0f", size = 328070, upload-time = "2025-10-02T13:31:35.619Z" }, - { url = "https://files.pythonhosted.org/packages/44/0c/c5d342d31860c9b4f481ef31a4056825961f9b462d216555e76dcee580ea/uuid_utils-0.11.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aef66b935342b268c6ffc1796267a1d9e73135740a10fe7e4098e1891cbcc476", size = 333610, upload-time = "2025-10-02T13:31:37.058Z" }, - { url = "https://files.pythonhosted.org/packages/e1/4b/52edc023ffcb9ab9a4042a58974a79c39ba7a565e683f1fd9814b504cf13/uuid_utils-0.11.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd65c41b81b762278997de0d027161f27f9cc4058fa57bbc0a1aaa63a63d6d1a", size = 475669, upload-time = "2025-10-02T13:31:38.38Z" }, - { url = "https://files.pythonhosted.org/packages/59/81/ee55ee63264531bb1c97b5b6033ad6ec81b5cd77f89174e9aef3af3d8889/uuid_utils-0.11.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccfac9d5d7522d61accabb8c68448ead6407933415e67e62123ed6ed11f86510", size = 331946, upload-time = "2025-10-02T13:31:39.66Z" }, - { url = "https://files.pythonhosted.org/packages/cf/07/5d4be27af0e9648afa512f0d11bb6d96cb841dd6d29b57baa3fbf55fd62e/uuid_utils-0.11.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:003f48f05c01692d0c1f7e413d194e7299a1a364e0047a4eb904d3478b84eca1", size = 352920, upload-time = "2025-10-02T13:31:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/5b/48/a69dddd9727512b0583b87bfff97d82a8813b28fb534a183c9e37033cfef/uuid_utils-0.11.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a5c936042120bdc30d62f539165beaa4a6ba7e817a89e5409a6f06dc62c677a9", size = 509413, upload-time = "2025-10-02T13:31:42.547Z" }, - { url = "https://files.pythonhosted.org/packages/66/0d/1b529a3870c2354dd838d5f133a1cba75220242b0061f04a904ca245a131/uuid_utils-0.11.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:2e16dcdbdf4cd34ffb31ead6236960adb50e6c962c9f4554a6ecfdfa044c6259", size = 529454, upload-time = "2025-10-02T13:31:44.338Z" }, - { url = "https://files.pythonhosted.org/packages/bd/f2/04a3f77c85585aac09d546edaf871a4012052fb8ace6dbddd153b4d50f02/uuid_utils-0.11.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f8b21fed11b23134502153d652c77c3a37fa841a9aa15a4e6186d440a22f1a0e", size = 498084, upload-time = "2025-10-02T13:31:45.601Z" }, - { url = "https://files.pythonhosted.org/packages/89/08/538b380b4c4b220f3222c970930fe459cc37f1dfc6c8dc912568d027f17d/uuid_utils-0.11.1-cp39-abi3-win32.whl", hash = "sha256:72abab5ab27c1b914e3f3f40f910532ae242df1b5f0ae43f1df2ef2f610b2a8c", size = 174314, upload-time = "2025-10-02T13:31:47.269Z" }, - { url = "https://files.pythonhosted.org/packages/00/66/971ec830094ac1c7d46381678f7138c1805015399805e7dd7769c893c9c8/uuid_utils-0.11.1-cp39-abi3-win_amd64.whl", hash = "sha256:5ed9962f8993ef2fd418205f92830c29344102f86871d99b57cef053abf227d9", size = 179214, upload-time = "2025-10-02T13:31:48.344Z" }, -] - [[package]] name = "uvicorn" -version = "0.34.0" +version = "0.40.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568, upload-time = "2024-12-15T13:33:30.42Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315, upload-time = "2024-12-15T13:33:27.467Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, ] [[package]] @@ -4185,18 +4097,6 @@ wheels = [ { 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 = "werkzeug" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/32/af/d4502dc713b4ccea7175d764718d5183caf8d0867a4f0190d5d4a45cea49/werkzeug-3.1.1.tar.gz", hash = "sha256:8cd39dfbdfc1e051965f156163e2974e52c210f130810e9ad36858f0fd3edad4", size = 806453, upload-time = "2024-11-01T16:40:45.462Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/ea/c67e1dee1ba208ed22c06d1d547ae5e293374bfc43e0eb0ef5e262b68561/werkzeug-3.1.1-py3-none-any.whl", hash = "sha256:a71124d1ef06008baafa3d266c02f56e1836a5984afd6dd6c9230669d60d9fb5", size = 224371, upload-time = "2024-11-01T16:40:43.994Z" }, -] - [[package]] name = "wikipedia-api" version = "0.8.1"