diff --git a/.github/workflows/martian-issue-triage.yml b/.github/workflows/martian-issue-triage.yml index 87a5ec85f..276c7142e 100644 --- a/.github/workflows/martian-issue-triage.yml +++ b/.github/workflows/martian-issue-triage.yml @@ -57,6 +57,7 @@ jobs: 2. You will identify the issue type (bug/feature/question) up front and tailor the Recommendation (e.g., for questions: answer directly + links; for bugs: point to failing tests/lines). 3. You will avoid speculation and only assert facts that are deeply rooted (traceable) to the codebase, language/framework conventions, related issues, related pull requests, etc. 4. The main branch of the repository has been cloned locally, but changes will not be accepted and you are not allowed to make pull requests or other changes. You can search the local repository for relevant code. You will use the available MCP Server tools identify related issues and pull requests (search_issues and search_pull_requests) and you can use search_code to look at the code in relevant dependent packages. For example, you can use search_code to look at the underlying SDK `https://github.com/modelcontextprotocol/python-sdk` to see how it implements a certain class or function relevant to the issue at hand. + 5. You cannot modify GitHub Workflows directly, you will have to create the updated workflow in a `github` folder and tell the maintainer to relocate it for you. # Getting Started 1. Call the generate_agents_md tool to get a high-level summary of the project you're working in diff --git a/.github/workflows/run-static.yml b/.github/workflows/run-static.yml index 5a15b23d4..7acc3aac6 100644 --- a/.github/workflows/run-static.yml +++ b/.github/workflows/run-static.yml @@ -29,7 +29,8 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-uv + - name: Setup uv + uses: ./.github/actions/setup-uv with: resolution: locked diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 9cfb0cfa0..7799e7aa2 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -38,14 +38,17 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-uv + - name: Setup uv + uses: ./.github/actions/setup-uv with: python-version: ${{ matrix.python-version }} resolution: locked - - uses: ./.github/actions/run-pytest + - name: Run unit tests + uses: ./.github/actions/run-pytest - - uses: ./.github/actions/run-pytest + - name: Run client process tests + uses: ./.github/actions/run-pytest with: test-type: client_process @@ -57,13 +60,16 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-uv + - name: Setup uv (lowest-direct) + uses: ./.github/actions/setup-uv with: resolution: lowest-direct - - uses: ./.github/actions/run-pytest + - name: Run unit tests + uses: ./.github/actions/run-pytest - - uses: ./.github/actions/run-pytest + - name: Run client process tests + uses: ./.github/actions/run-pytest with: test-type: client_process @@ -75,11 +81,13 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-uv + - name: Setup uv + uses: ./.github/actions/setup-uv with: resolution: locked - - uses: ./.github/actions/run-pytest + - name: Run integration tests + uses: ./.github/actions/run-pytest with: test-type: integration env: diff --git a/.github/workflows/run-upgrade-checks.yml b/.github/workflows/run-upgrade-checks.yml index 5cd0558ff..ba391d767 100644 --- a/.github/workflows/run-upgrade-checks.yml +++ b/.github/workflows/run-upgrade-checks.yml @@ -32,7 +32,8 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-uv + - name: Setup uv (upgrade) + uses: ./.github/actions/setup-uv with: resolution: upgrade @@ -57,14 +58,17 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-uv + - name: Setup uv (upgrade) + uses: ./.github/actions/setup-uv with: python-version: ${{ matrix.python-version }} resolution: upgrade - - uses: ./.github/actions/run-pytest + - name: Run unit tests + uses: ./.github/actions/run-pytest - - uses: ./.github/actions/run-pytest + - name: Run client process tests + uses: ./.github/actions/run-pytest with: test-type: client_process @@ -76,11 +80,13 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-uv + - name: Setup uv (upgrade) + uses: ./.github/actions/setup-uv with: resolution: upgrade - - uses: ./.github/actions/run-pytest + - name: Run integration tests + uses: ./.github/actions/run-pytest with: test-type: integration env: diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx index 5bd8676a3..974d34009 100644 --- a/docs/development/v3-notes/v3-features.mdx +++ b/docs/development/v3-notes/v3-features.mdx @@ -4,7 +4,100 @@ title: v3.0 Feature Tracking This document tracks major features in FastMCP v3.0 for release notes preparation. -## Provider-Based Architecture +## 3.0.0beta2 + +### CLI: Expanded Reload File Watching + +The `--reload` flag now watches a comprehensive set of file types, making it suitable for MCP apps with frontend bundles ([#3028](https://github.com/jlowin/fastmcp/pull/3028)). Previously limited to `.py` files, it now watches JavaScript, TypeScript, HTML, CSS, config files, and media assets. + +### CLI: fastmcp install stdio + +The new `fastmcp install stdio` command generates full `uv run` commands for running FastMCP servers over stdio ([#3032](https://github.com/jlowin/fastmcp/pull/3032)). + +```bash +# Generate command for a server +fastmcp install stdio server.py + +# Outputs: +# uv run --directory /path/to/project fastmcp run server.py +``` + +The command automatically detects the project directory and generates the appropriate `uv run` invocation, making it easy to integrate FastMCP servers with MCP clients. + +### MCP Apps (SDK Compatibility) + +Support for [MCP Apps](https://modelcontextprotocol.io/specification/2025-06-18/server/apps) — the spec extension that lets MCP servers deliver interactive UIs via sandboxed iframes. Extension negotiation, typed UI metadata on tools and resources, and the `ui://` resource scheme. No component DSL, renderer, or `FastMCPApp` class yet — those are future phases. + +**Registering tools with UI metadata:** + +```python +from fastmcp import FastMCP +from fastmcp.server.apps import ToolUI, ResourceUI, ResourceCSP, ResourcePermissions + +mcp = FastMCP("My Server") + +# Register the HTML bundle as a ui:// resource with CSP +@mcp.resource( + "ui://my-app/view.html", + ui=ResourceUI( + csp=ResourceCSP(resource_domains=["https://unpkg.com"]), + permissions=ResourcePermissions(clipboard_write={}), + ), +) +def app_html() -> str: + from pathlib import Path + return Path("./dist/index.html").read_text() + +# Tool with UI — clients render an iframe alongside the result +@mcp.tool(ui=ToolUI(resource_uri="ui://my-app/view.html")) +async def list_users() -> list[dict]: + return [{"id": "1", "name": "Alice"}] + +# App-only tool — visible to the UI but hidden from the model +@mcp.tool(ui=ToolUI(resource_uri="ui://my-app/view.html", visibility=["app"])) +async def delete_user(id: str) -> dict: + return {"deleted": True} +``` + +The `ui=` parameter accepts either a typed model (`ToolUI`, `ResourceUI`) or a raw dict for forward compatibility. It merges into `meta["ui"]` — alongside any other metadata you set. + +**`ui://` resources** automatically get the correct MIME type (`text/html;profile=mcp-app`) unless you override it explicitly. + +**Extension negotiation**: The server advertises `io.modelcontextprotocol/ui` in `capabilities.extensions`. UI metadata (`_meta.ui`) always flows through to clients — the MCP Apps spec assigns visibility enforcement to the host, not the server. Tools can check whether the connected client supports a given extension at runtime via `ctx.client_supports_extension()`: + +```python +from fastmcp import Context +from fastmcp.server.apps import ToolUI, UI_EXTENSION_ID + +@mcp.tool(ui=ToolUI(resource_uri="ui://dashboard")) +async def dashboard(ctx: Context) -> dict: + data = compute_dashboard() + if ctx.client_supports_extension(UI_EXTENSION_ID): + return data + return {"summary": format_text(data)} +``` + +**Key details:** +- `ToolUI` fields: `resource_uri`, `visibility`, `csp`, `permissions`, `domain`, `prefers_border` (all optional except for typical usage of `resource_uri`) +- `ResourceUI` fields: `csp`, `permissions`, `domain`, `prefers_border` — metadata for the resource itself when it's a UI bundle +- `csp` accepts a `ResourceCSP` model with structured domain lists: `connect_domains`, `resource_domains`, `frame_domains`, `base_uri_domains` +- `permissions` accepts a `ResourcePermissions` model: `camera`, `microphone`, `geolocation`, `clipboard_write` (each set to `{}` to request) +- Both models use `extra="allow"` for forward compatibility with future spec additions +- Models use Pydantic aliases for wire format (`resourceUri`, `prefersBorder`, `connectDomains`, `clipboardWrite`) +- Resource metadata (including CSP/permissions) is propagated to `resources/read` response content items so hosts can read it when rendering the iframe +- `ctx.client_supports_extension(id)` is a general-purpose method — works for any extension, not just MCP Apps +- `structuredContent` in tool results already works via `ToolResult` — MCP Apps clients use this to pass data into the iframe +- The server does not strip `_meta.ui` for non-UI clients; per the spec, visibility enforcement is the host's responsibility + +**Future phases** will add a component DSL for building UIs declaratively, an in-repo renderer, and a `FastMCPApp` class. + +Implementation: `src/fastmcp/server/apps.py` (models and constants), with integration points in `server.py` (decorator parameters), `low_level.py` (extension advertisement), and `context.py` (`client_supports_extension` method). + +--- + +## 3.0.0beta1 + +### Provider-Based Architecture v3.0 introduces a provider-based component system that replaces v2's static-only registration ([#2622](https://github.com/jlowin/fastmcp/pull/2622)). Providers dynamically source tools, resources, templates, and prompts at runtime. @@ -201,7 +294,7 @@ Documentation: `docs/servers/transforms/resources-as-tools.mdx`, `docs/servers/t --- -## Session-Scoped State +### Session-Scoped State v3.0 changes context state from request-scoped to session-scoped. State now persists across multiple tool calls within the same MCP session. @@ -232,7 +325,7 @@ Documentation: `docs/servers/context.mdx` --- -## Visibility System +### Visibility System Components can be enabled/disabled using the visibility system. Each `enable()` or `disable()` call adds a stateless Visibility transform that marks components via internal metadata. Later transforms override earlier ones. @@ -263,7 +356,7 @@ Works at both server and provider level. Supports: - **Override semantics**: Later transforms override earlier marks (enable after disable = enabled) - **Transform ordering**: Visibility transforms are injected at the point you call them, so component state is known -### Per-Session Visibility +#### Per-Session Visibility Server-level visibility changes affect all connected clients. For per-session control, use `Context` methods that apply rules only to the current session ([#2917](https://github.com/jlowin/fastmcp/pull/2917)): @@ -304,7 +397,7 @@ Documentation: `docs/servers/visibility.mdx` --- -## Component Versioning +### Component Versioning v3.0 introduces versioning support for tools, resources, and prompts. Components can declare a version, and when multiple versions of the same component exist, the highest version is automatically exposed to clients. @@ -422,11 +515,11 @@ The `@` is always present (even for unversioned components) to enable unambiguou --- -## Type-Safe Canonical Results +### Type-Safe Canonical Results v3.0 introduces type-safe result classes that provide explicit control over component responses while supporting MCP runtime metadata: `ToolResult` ([#2736](https://github.com/jlowin/fastmcp/pull/2736)), `ResourceResult` ([#2734](https://github.com/jlowin/fastmcp/pull/2734)), and `PromptResult` ([#2738](https://github.com/jlowin/fastmcp/pull/2738)). -### ToolResult +#### ToolResult `ToolResult` (`src/fastmcp/tools/tool.py:79`) provides structured tool responses: @@ -447,7 +540,7 @@ Fields: - `structured_content`: Dict matching tool's output schema - `meta`: Runtime metadata passed to MCP as `_meta` -### ResourceResult +#### ResourceResult `ResourceResult` (`src/fastmcp/resources/resource.py:117`) provides structured resource responses: @@ -467,7 +560,7 @@ def get_items() -> ResourceResult: Accepts strings, bytes, or `list[ResourceContent]` for flexible content handling. -### PromptResult +#### PromptResult `PromptResult` (`src/fastmcp/prompts/prompt.py:109`) provides structured prompt responses: @@ -487,7 +580,7 @@ def conversation() -> PromptResult: --- -## Background Tasks (SEP-1686) +### Background Tasks (SEP-1686) v3.0 implements MCP SEP-1686 for background task execution via Docket integration. @@ -520,7 +613,7 @@ Requires Docket server for task scheduling and result polling. --- -## Decorators Return Functions +### Decorators Return Functions v3.0 changes what decorators (`@tool`, `@resource`, `@prompt`) return ([#2856](https://github.com/jlowin/fastmcp/pull/2856)). Decorators now return the original function unchanged, rather than transforming it into a component object. @@ -552,7 +645,7 @@ Environment variable: `FASTMCP_DECORATOR_MODE=object` --- -## CLI Auto-Reload +### CLI Auto-Reload The `--reload` flag enables file watching with automatic server restarts for development ([#2816](https://github.com/jlowin/fastmcp/pull/2816)). @@ -581,7 +674,7 @@ fastmcp dev server.py # Includes --reload by default --- -## Component Authorization +### Component Authorization v3.0 introduces callable-based authorization for tools, resources, and prompts ([#2855](https://github.com/jlowin/fastmcp/pull/2855)). @@ -634,7 +727,7 @@ STDIO transport bypasses all auth checks (no OAuth concept). --- -## FileSystemProvider +### FileSystemProvider v3.0 introduces `FileSystemProvider`, a fundamentally different approach to organizing MCP servers. Instead of importing a server instance and decorating functions with `@server.tool`, you use standalone decorators in separate files and let the provider discover them. @@ -672,7 +765,7 @@ Documentation: [FileSystemProvider](/servers/providers/filesystem) --- -## SkillsProvider +### SkillsProvider v3.0 introduces `SkillsProvider` for exposing agent skills as MCP resources ([#2944](https://github.com/jlowin/fastmcp/pull/2944)). Skills are directories containing instructions and supporting files that teach AI assistants how to perform tasks—used by Claude Code, Cursor, VS Code Copilot, and other AI coding tools. @@ -715,7 +808,7 @@ Documentation: [Skills Provider](/servers/providers/skills) --- -## OpenTelemetry Tracing +### OpenTelemetry Tracing v3.0 adds OpenTelemetry instrumentation for observability into server and client operations ([#2869](https://github.com/jlowin/fastmcp/pull/2869)). @@ -743,7 +836,7 @@ Documentation: [Telemetry](/servers/telemetry) --- -## Pagination +### Pagination v3.0 adds pagination support for list operations when servers expose many components ([#2903](https://github.com/jlowin/fastmcp/pull/2903)). @@ -769,7 +862,7 @@ Documentation: [Pagination](/servers/pagination) --- -## Composable Lifespans +### Composable Lifespans Lifespans can be combined with the `|` operator for modular setup/teardown ([#2828](https://github.com/jlowin/fastmcp/pull/2828)): @@ -810,7 +903,7 @@ Documentation: [Lifespan](/servers/lifespan) --- -## Tool Timeout +### Tool Timeout Tools can limit foreground execution time with a `timeout` parameter ([#2872](https://github.com/jlowin/fastmcp/pull/2872)): @@ -827,7 +920,7 @@ Note: This timeout applies to foreground execution only. Background tasks (`task --- -## PingMiddleware +### PingMiddleware Sends periodic server-to-client pings to keep long-lived connections alive ([#2838](https://github.com/jlowin/fastmcp/pull/2838)): @@ -843,7 +936,7 @@ The middleware starts a background ping task on first message from each session, --- -## Context.transport Property +### Context.transport Property Tools can detect which transport is active ([#2850](https://github.com/jlowin/fastmcp/pull/2850)): @@ -863,7 +956,7 @@ Returns `Literal["stdio", "sse", "streamable-http"]` when running, or `None` out --- -## Automatic Threadpool for Sync Functions +### Automatic Threadpool for Sync Functions Synchronous tools, resources, and prompts now automatically run in a threadpool, preventing event loop blocking during concurrent requests ([#2865](https://github.com/jlowin/fastmcp/pull/2865)): @@ -880,7 +973,7 @@ Three concurrent calls now execute in parallel (~10s) rather than sequentially ( --- -## CLI Update Notifications +### CLI Update Notifications The CLI notifies users when a newer FastMCP version is available on PyPI ([#2840](https://github.com/jlowin/fastmcp/pull/2840)). @@ -893,11 +986,11 @@ The CLI notifies users when a newer FastMCP version is available on PyPI ([#2840 --- -## Deprecated Features +### Deprecated Features These emit deprecation warnings but continue to work. -### Mount Prefix Parameter +#### Mount Prefix Parameter The `prefix` parameter for `mount()` renamed to `namespace`: @@ -909,7 +1002,7 @@ main.mount(subserver, prefix="api") main.mount(subserver, namespace="api") ``` -### Tag Filtering Init Parameters +#### Tag Filtering Init Parameters `FastMCP(include_tags=..., exclude_tags=...)` deprecated. Use `enable()`/`disable()` methods: @@ -922,11 +1015,11 @@ mcp = FastMCP("server") mcp.disable(tags={"internal"}) ``` -### Tool Serializer Parameter +#### Tool Serializer Parameter The `tool_serializer` parameter on `FastMCP` is deprecated. Return `ToolResult` for explicit serialization control. -### Tool Transformation Methods +#### Tool Transformation Methods `add_tool_transformation()`, `remove_tool_transformation()`, and `tool_transformations` constructor parameter are deprecated. Use `add_transform(ToolTransform({...}))` instead: @@ -941,13 +1034,13 @@ mcp.add_transform(ToolTransform({"name": config})) --- -## Breaking Changes +### Breaking Changes -### WSTransport Removed +#### WSTransport Removed The deprecated `WSTransport` client transport has been removed ([#2826](https://github.com/jlowin/fastmcp/pull/2826)). Use `StreamableHttpTransport` instead. -### Decorators Return Functions +#### Decorators Return Functions Decorators (`@tool`, `@resource`, `@prompt`) now return the original function instead of component objects. Code that treats the decorated function as a `FunctionTool`, `FunctionResource`, or `FunctionPrompt` will break. @@ -971,7 +1064,7 @@ greet("World") # "Hello, World!" Set `FASTMCP_DECORATOR_MODE=object` or `fastmcp.settings.decorator_mode = "object"` for v2 behavior. -### Component Enable/Disable Moved to Server/Provider +#### Component Enable/Disable Moved to Server/Provider The `enabled` field and `enable()`/`disable()` methods removed from component objects: @@ -984,7 +1077,7 @@ tool.disable() server.disable(names={"my_tool"}, components=["tool"]) ``` -### Component Lookup Methods +#### Component Lookup Methods Server lookup and listing methods have updated signatures: @@ -1001,7 +1094,7 @@ tools = await server.get_tools() tool = next((t for t in tools if t.name == "my_tool"), None) ``` -### Prompt Return Types +#### Prompt Return Types Prompt functions now use `Message` instead of `mcp.types.PromptMessage`: @@ -1021,7 +1114,7 @@ def my_prompt() -> Message: return Message("Hello") # role defaults to "user" ``` -### Auth Provider Environment Variables Removed +#### Auth Provider Environment Variables Removed Auth providers no longer auto-load from environment variables ([#2752](https://github.com/jlowin/fastmcp/pull/2752)): @@ -1039,13 +1132,13 @@ auth = GitHubProvider( See `docs/development/v3-notes/auth-provider-env-vars.mdx` for rationale. -### Server Banner Environment Variable +#### Server Banner Environment Variable `FASTMCP_SHOW_CLI_BANNER` → `FASTMCP_SHOW_SERVER_BANNER` ([#2771](https://github.com/jlowin/fastmcp/pull/2771)) Now applies to all server startup methods, not just the CLI. -### Context State Methods Are Async +#### Context State Methods Are Async `ctx.set_state()` and `ctx.get_state()` are now async and session-scoped: diff --git a/docs/docs.json b/docs/docs.json index a21316504..70b6d2173 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -254,6 +254,7 @@ "integrations/claude-desktop", "integrations/cursor", "integrations/gemini-cli", + "integrations/goose", "integrations/mcp-json-configuration" ] }, @@ -307,6 +308,7 @@ "python-sdk/fastmcp-cli-install-claude_desktop", "python-sdk/fastmcp-cli-install-cursor", "python-sdk/fastmcp-cli-install-gemini_cli", + "python-sdk/fastmcp-cli-install-goose", "python-sdk/fastmcp-cli-install-mcp_json", "python-sdk/fastmcp-cli-install-shared" ] diff --git a/docs/integrations/azure.mdx b/docs/integrations/azure.mdx index 4f6a67341..836b5a4ae 100644 --- a/docs/integrations/azure.mdx +++ b/docs/integrations/azure.mdx @@ -125,7 +125,7 @@ auth_provider = AzureProvider( # identifier_uri defaults to api://{client_id} # identifier_uri="api://your-api-id", # Optional: request additional upstream scopes in the authorize request - # additional_authorize_scopes=["User.Read", "offline_access", "openid", "email"], + # additional_authorize_scopes=["User.Read", "openid", "email"], # redirect_path="/auth/callback" # Default value, customize if needed # base_authority="login.microsoftonline.us" # For Azure Government (default: login.microsoftonline.com) ) @@ -183,6 +183,10 @@ FastMCP automatically prefixes `required_scopes` with your `identifier_uri` (e.g | `User.Read` | `User.Read` | ✗ | | `Mail.Send` | `Mail.Send` | ✗ | + +`offline_access` is automatically included to obtain refresh tokens. FastMCP manages token refreshing automatically. + + **Why aren't `additional_authorize_scopes` validated?** Azure issues separate tokens per resource. The access token FastMCP receives is for *your API*—Graph scopes aren't in its `scp` claim. To call Graph APIs, your server uses the upstream Azure token in an on-behalf-of (OBO) flow. diff --git a/docs/integrations/chatgpt.mdx b/docs/integrations/chatgpt.mdx index d674a21e2..92ddb7404 100644 --- a/docs/integrations/chatgpt.mdx +++ b/docs/integrations/chatgpt.mdx @@ -5,7 +5,7 @@ description: Connect FastMCP servers to ChatGPT in Chat and Deep Research modes icon: message-smile --- -ChatGPT supports MCP servers through remote HTTP connections in two modes: **Chat mode** for interactive conversations and **Deep Research mode** for comprehensive information retrieval. +[ChatGPT](https://chatgpt.com/) supports MCP servers through remote HTTP connections in two modes: **Chat mode** for interactive conversations and **Deep Research mode** for comprehensive information retrieval. **Developer Mode Required for Chat Mode**: To use MCP servers in regular ChatGPT conversations, you must first enable Developer Mode in your ChatGPT settings. This feature is available for ChatGPT Pro, Team, Enterprise, and Edu users. diff --git a/docs/integrations/claude-code.mdx b/docs/integrations/claude-code.mdx index 2b04c10a8..8098ff51e 100644 --- a/docs/integrations/claude-code.mdx +++ b/docs/integrations/claude-code.mdx @@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx" -Claude Code supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers. +[Claude Code](https://docs.anthropic.com/en/docs/claude-code) supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers. ## Requirements diff --git a/docs/integrations/claude-desktop.mdx b/docs/integrations/claude-desktop.mdx index 598e4be5f..4478bcc37 100644 --- a/docs/integrations/claude-desktop.mdx +++ b/docs/integrations/claude-desktop.mdx @@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx" -Claude Desktop supports MCP servers through local STDIO connections and remote servers (beta), allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers. +[Claude Desktop](https://www.claude.com/download) supports MCP servers through local STDIO connections and remote servers (beta), allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers. Remote MCP server support is currently in beta and available for users on Claude Pro, Max, Team, and Enterprise plans (as of June 2025). Most users will still need to use local STDIO connections. diff --git a/docs/integrations/cursor.mdx b/docs/integrations/cursor.mdx index 5b98cbd1a..da0744ee0 100644 --- a/docs/integrations/cursor.mdx +++ b/docs/integrations/cursor.mdx @@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx" -Cursor supports MCP servers through multiple transport methods including STDIO, SSE, and Streamable HTTP, allowing you to extend Cursor's AI assistant with custom tools, resources, and prompts from your FastMCP servers. +[Cursor](https://www.cursor.com/) supports MCP servers through multiple transport methods including STDIO, SSE, and Streamable HTTP, allowing you to extend Cursor's AI assistant with custom tools, resources, and prompts from your FastMCP servers. ## Requirements diff --git a/docs/integrations/gemini-cli.mdx b/docs/integrations/gemini-cli.mdx index 591ab4d5d..10613fb1b 100644 --- a/docs/integrations/gemini-cli.mdx +++ b/docs/integrations/gemini-cli.mdx @@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx" -Gemini CLI supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Gemini's capabilities with custom tools, resources, and prompts from your FastMCP servers. +[Gemini CLI](https://geminicli.com/) supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Gemini's capabilities with custom tools, resources, and prompts from your FastMCP servers. ## Requirements diff --git a/docs/integrations/goose.mdx b/docs/integrations/goose.mdx new file mode 100644 index 000000000..fc2ff8e39 --- /dev/null +++ b/docs/integrations/goose.mdx @@ -0,0 +1,178 @@ +--- +title: Goose 🤝 FastMCP +sidebarTitle: Goose +description: Install and use FastMCP servers in Goose +icon: message-smile +--- + +import { VersionBadge } from "/snippets/version-badge.mdx" +import { LocalFocusTip } from "/snippets/local-focus.mdx" + + + +[Goose](https://block.github.io/goose/) is an open-source AI agent from Block that supports MCP servers as extensions. FastMCP can install your server directly into Goose using its deeplink protocol — one command opens Goose with an install dialog ready to go. + +## Requirements + +This integration uses Goose's deeplink protocol to register your server as a STDIO extension running via `uvx`. You must have Goose installed on your system for the deeplink to open automatically. + +For remote deployments, configure your FastMCP server with HTTP transport and add it to Goose directly using `goose configure` or the config file. + +## Create a Server + +The examples in this guide will use the following simple dice-rolling server, saved as `server.py`. + +```python server.py +import random +from fastmcp import FastMCP + +mcp = FastMCP(name="Dice Roller") + +@mcp.tool +def roll_dice(n_dice: int) -> list[int]: + """Roll `n_dice` 6-sided dice and return the results.""" + return [random.randint(1, 6) for _ in range(n_dice)] + +if __name__ == "__main__": + mcp.run() +``` + +## Install the Server + +### FastMCP CLI + + +The easiest way to install a FastMCP server in Goose is using the `fastmcp install goose` command. This generates a `goose://` deeplink and opens it, prompting Goose to install the server. + +```bash +fastmcp install goose server.py +``` + +The install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file: + +```bash +# These are equivalent if your server object is named 'mcp' +fastmcp install goose server.py +fastmcp install goose server.py:mcp + +# Use explicit object name if your server has a different name +fastmcp install goose server.py:my_custom_server +``` + +Under the hood, the generated command uses `uvx` to run your server in an isolated environment. Goose requires `uvx` rather than `uv run`, so the install produces a command like: + +```bash +uvx --with pandas fastmcp run /path/to/server.py +``` + +#### Dependencies + +Use the `--with` flag to specify additional packages your server needs: + +```bash +fastmcp install goose server.py --with pandas --with requests +``` + +Alternatively, you can use a `fastmcp.json` configuration file (recommended): + +```json fastmcp.json +{ + "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json", + "source": { + "path": "server.py", + "entrypoint": "mcp" + }, + "environment": { + "dependencies": ["pandas", "requests"] + } +} +``` + +#### Python Version + +Use `--python` to specify which Python version your server should use: + +```bash +fastmcp install goose server.py --python 3.11 +``` + + +The Goose install uses `uvx`, which does not support `--project`, `--with-requirements`, or `--with-editable`. If you need these options, use `fastmcp install mcp-json` to generate a full configuration and add it to Goose manually. + + +#### Environment Variables + +Goose's deeplink protocol does not support environment variables. If your server needs them (like API keys), you have two options: + +1. **Configure after install**: Run `goose configure` and add environment variables to the extension. +2. **Manual config**: Use `fastmcp install mcp-json` to generate the full configuration, then add it to `~/.config/goose/config.yaml` with the `envs` field. + +### Manual Configuration + +For more control, you can manually edit Goose's configuration file at `~/.config/goose/config.yaml`: + +```yaml +extensions: + dice-roller: + name: Dice Roller + cmd: uvx + args: [fastmcp, run, /path/to/server.py] + enabled: true + type: stdio + timeout: 300 +``` + +#### Dependencies + +When manually configuring, add packages using `--with` flags in the args: + +```yaml +extensions: + dice-roller: + name: Dice Roller + cmd: uvx + args: [--with, pandas, --with, requests, fastmcp, run, /path/to/server.py] + enabled: true + type: stdio + timeout: 300 +``` + +#### Environment Variables + +Environment variables can be specified in the `envs` field: + +```yaml +extensions: + weather-server: + name: Weather Server + cmd: uvx + args: [fastmcp, run, /path/to/weather_server.py] + enabled: true + envs: + API_KEY: your-api-key + DEBUG: "true" + type: stdio + timeout: 300 +``` + +You can also use `goose configure` to add extensions interactively, which prompts for environment variables. + + +**`uvx` (from `uv`) must be installed and available in your system PATH**. Goose uses `uvx` to run Python-based extensions in isolated environments. + + +## Using the Server + +Once your server is installed, you can start using your FastMCP server with Goose. + +Try asking Goose something like: + +> "Roll some dice for me" + +Goose will automatically detect your `roll_dice` tool and use it to fulfill your request, returning something like: + +> 🎲 Here are your dice rolls: 4, 6, 4 +> +> You rolled 3 dice with a total of 14! + +Goose can now access all the tools, resources, and prompts you've defined in your FastMCP server. diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx index 7fd4cce4f..8662e26e5 100644 --- a/docs/patterns/cli.mdx +++ b/docs/patterns/cli.mdx @@ -300,13 +300,19 @@ Install a MCP server in MCP client applications. FastMCP currently supports the - **Claude Code** - Installs via Claude Code's built-in MCP management system - **Claude Desktop** - Installs via direct configuration file modification - **Cursor** - Installs via deeplink that opens Cursor for user confirmation +- **Gemini CLI** - Installs via Gemini CLI's built-in MCP management system +- **Goose** - Installs via deeplink that opens Goose for user confirmation (uses `uvx`) - **MCP JSON** - Generates standard MCP JSON configuration for manual use +- **Stdio** - Outputs the shell command to run a server over stdio transport ```bash fastmcp install claude-code server.py fastmcp install claude-desktop server.py fastmcp install cursor server.py +fastmcp install gemini-cli server.py +fastmcp install goose server.py fastmcp install mcp-json server.py +fastmcp install stdio server.py ``` Note that for security reasons, MCP clients usually run every server in a completely isolated environment. Therefore, all dependencies must be explicitly specified using the `--with` and/or `--with-editable` options (following `uv` conventions) or by attaching them to your server in code via the `dependencies` parameter. You should not assume that the MCP server will have access to your local environment. @@ -380,6 +386,9 @@ fastmcp install cursor server.py --env API_KEY=secret --env DEBUG=true # Install with environment file fastmcp install cursor server.py --env-file .env +# Install in Goose (uses uvx deeplink) +fastmcp install goose server.py --with pandas + # Install with specific Python version fastmcp install claude-desktop server.py --python 3.11 @@ -394,6 +403,15 @@ fastmcp install mcp-json server.py --name "My Server" --with pandas # Copy JSON configuration to clipboard fastmcp install mcp-json server.py --copy + +# Output the stdio command for running a server +fastmcp install stdio server.py + +# Output the stdio command from a fastmcp.json (includes configured dependencies) +fastmcp install stdio fastmcp.json + +# Copy the stdio command to clipboard +fastmcp install stdio server.py --copy ``` ### MCP JSON Generation @@ -436,6 +454,38 @@ To use this configuration with your MCP client, you'll typically need to add it | ------ | ---- | ----------- | | Copy to Clipboard | `--copy` | Copy configuration to clipboard instead of printing to stdout | +### Stdio Command + +The `stdio` subcommand outputs the shell command an MCP host uses to start your server over stdio transport. Use it when you need a ready-to-paste `uv run --with fastmcp fastmcp run ...` command for a tool or script without a dedicated install target. + +```bash +# Print the command to stdout +fastmcp install stdio server.py + +# Output: uv run --with fastmcp fastmcp run /absolute/path/to/server.py +``` + +When you pass a `fastmcp.json`, FastMCP automatically includes dependencies from the configuration: + +```bash +fastmcp install stdio fastmcp.json + +# Output: uv run --with fastmcp --with pillow --with 'qrcode[pil]>=8.0' fastmcp run /absolute/path/to/qr_server.py +``` + +Use `--copy` to send the command directly to your clipboard: + +```bash +fastmcp install stdio server.py --copy +# ✓ Command copied to clipboard +``` + +**Options specific to stdio:** + +| Option | Flag | Description | +| ------ | ---- | ----------- | +| Copy to Clipboard | `--copy` | Copy command to clipboard instead of printing to stdout | + ## `fastmcp inspect` diff --git a/examples/apps/qr_server/README.md b/examples/apps/qr_server/README.md new file mode 100644 index 000000000..f2c9ebf1d --- /dev/null +++ b/examples/apps/qr_server/README.md @@ -0,0 +1,35 @@ +# QR Code MCP App + +An MCP App server that generates QR codes with an interactive viewer UI. Ported from the [ext-apps QR server example](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server) to demonstrate FastMCP's MCP Apps support. + +## What it demonstrates + +- Linking a tool to a `ui://` resource via `ToolUI` +- Serving embedded HTML with the `@modelcontextprotocol/ext-apps` JS SDK from CDN +- Declaring CSP resource domains via `ResourceCSP` +- Returning `ImageContent` (base64 PNG) from a tool + +## Setup + +```bash +cd examples/apps/qr_server +uv sync +``` + +## Usage + +```bash +uv run python qr_server.py +``` + +Or install it into an MCP client: + +```bash +fastmcp install stdio fastmcp.json +``` + +## How it works + +The server registers one tool (`generate_qr`) and one resource (`ui://qr-server/view.html`). The tool generates a QR code as a base64 PNG image. The resource serves an HTML page that uses the MCP Apps JS SDK to receive the tool result and display the image in a sandboxed iframe. + +The HTML loads the ext-apps SDK from unpkg, so the resource declares `csp=ResourceCSP(resource_domains=["https://unpkg.com"])` to allow the host to set the appropriate Content-Security-Policy. diff --git a/examples/apps/qr_server/fastmcp.json b/examples/apps/qr_server/fastmcp.json new file mode 100644 index 000000000..7e74faa31 --- /dev/null +++ b/examples/apps/qr_server/fastmcp.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json", + "source": { + "path": "qr_server.py" + }, + "environment": { + "dependencies": [ + "fastmcp", + "qrcode[pil]>=8.0", + "pillow" + ] + } +} diff --git a/examples/apps/qr_server/pyproject.toml b/examples/apps/qr_server/pyproject.toml new file mode 100644 index 000000000..815ea0a75 --- /dev/null +++ b/examples/apps/qr_server/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "fastmcp-app-examples" +version = "0.1.0" +description = "MCP App examples for FastMCP" +requires-python = ">=3.10" +dependencies = [ + "fastmcp", + "qrcode[pil]>=8.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/apps/qr_server/qr_server.py b/examples/apps/qr_server/qr_server.py new file mode 100644 index 000000000..8478cd557 --- /dev/null +++ b/examples/apps/qr_server/qr_server.py @@ -0,0 +1,170 @@ +"""QR Code MCP App Server — generates QR codes with an interactive view UI. + +Demonstrates MCP Apps with FastMCP: +- Tool linked to a ui:// resource via ToolUI +- HTML resource with CSP metadata for CDN-loaded dependencies +- Embedded HTML using the @modelcontextprotocol/ext-apps JS SDK +- ImageContent return type for binary data +- Both stdio and HTTP transport modes + +Based on https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server + +Setup (from examples/apps/): + uv sync + +Usage: + uv run python qr_server.py # HTTP mode (port 3001) + uv run python qr_server.py --stdio # stdio mode for MCP clients +""" + +from __future__ import annotations + +import base64 +import io + +import qrcode # type: ignore[import-untyped] +from mcp import types + +from fastmcp import FastMCP +from fastmcp.server.apps import ResourceCSP, ResourceUI, ToolUI +from fastmcp.tools import ToolResult + +VIEW_URI: str = "ui://qr-server/view.html" + +mcp: FastMCP = FastMCP("QR Code Server") + +EMBEDDED_VIEW_HTML: str = """\ + + + + + + + +
+ + +""" + + +@mcp.tool(ui=ToolUI(resource_uri=VIEW_URI)) +def generate_qr( + text: str = "https://gofastmcp.com", + box_size: int = 10, + border: int = 4, + error_correction: str = "M", + fill_color: str = "black", + back_color: str = "white", +) -> ToolResult: + """Generate a QR code from text. + + Args: + text: The text/URL to encode + box_size: Size of each box in pixels (default: 10) + border: Border size in boxes (default: 4) + error_correction: Error correction level - L(7%), M(15%), Q(25%), H(30%) + fill_color: Foreground color (hex like #FF0000 or name like red) + back_color: Background color (hex like #FFFFFF or name like white) + """ + error_levels = { + "L": qrcode.constants.ERROR_CORRECT_L, + "M": qrcode.constants.ERROR_CORRECT_M, + "Q": qrcode.constants.ERROR_CORRECT_Q, + "H": qrcode.constants.ERROR_CORRECT_H, + } + + if box_size <= 0: + raise ValueError("box_size must be > 0") + if border < 0: + raise ValueError("border must be >= 0") + + error_key = error_correction.upper() + if error_key not in error_levels: + raise ValueError(f"error_correction must be one of: {', '.join(error_levels)}") + + qr = qrcode.QRCode( + version=1, + error_correction=error_levels[error_key], + box_size=box_size, + border=border, + ) + qr.add_data(text) + qr.make(fit=True) + + img = qr.make_image(fill_color=fill_color, back_color=back_color) + buffer = io.BytesIO() + img.save(buffer, format="PNG") + b64 = base64.b64encode(buffer.getvalue()).decode() + return ToolResult( + content=[types.ImageContent(type="image", data=b64, mimeType="image/png")] + ) + + +@mcp.resource( + VIEW_URI, + ui=ResourceUI(csp=ResourceCSP(resource_domains=["https://unpkg.com"])), +) +def view() -> str: + """Interactive QR code viewer — renders tool results as images.""" + return EMBEDDED_VIEW_HTML + + +if __name__ == "__main__": + mcp.run() diff --git a/examples/testing_demo/uv.lock b/examples/testing_demo/uv.lock index e728133e2..1ad5e3870 100644 --- a/examples/testing_demo/uv.lock +++ b/examples/testing_demo/uv.lock @@ -1255,11 +1255,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.20" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] diff --git a/src/fastmcp/cli/install/__init__.py b/src/fastmcp/cli/install/__init__.py index 9b3e3960d..35b8d832d 100644 --- a/src/fastmcp/cli/install/__init__.py +++ b/src/fastmcp/cli/install/__init__.py @@ -6,7 +6,9 @@ from .claude_code import claude_code_command from .claude_desktop import claude_desktop_command from .cursor import cursor_command from .gemini_cli import gemini_cli_command +from .goose import goose_command from .mcp_json import mcp_json_command +from .stdio import stdio_command # Create a cyclopts app for install subcommands install_app = cyclopts.App( @@ -19,4 +21,6 @@ install_app.command(claude_code_command, name="claude-code") install_app.command(claude_desktop_command, name="claude-desktop") install_app.command(cursor_command, name="cursor") install_app.command(gemini_cli_command, name="gemini-cli") +install_app.command(goose_command, name="goose") install_app.command(mcp_json_command, name="mcp-json") +install_app.command(stdio_command, name="stdio") diff --git a/src/fastmcp/cli/install/cursor.py b/src/fastmcp/cli/install/cursor.py index 88693ec38..0358c3261 100644 --- a/src/fastmcp/cli/install/cursor.py +++ b/src/fastmcp/cli/install/cursor.py @@ -1,12 +1,10 @@ """Cursor integration for FastMCP install using Cyclopts.""" import base64 -import os -import subprocess import sys from pathlib import Path from typing import Annotated -from urllib.parse import quote, urlparse +from urllib.parse import quote import cyclopts from rich import print @@ -15,6 +13,7 @@ from fastmcp.mcp_config import StdioMCPServer, update_config_file from fastmcp.utilities.logging import get_logger from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment +from .shared import open_deeplink as _shared_open_deeplink from .shared import process_common_args logger = get_logger(__name__) @@ -46,7 +45,7 @@ def generate_cursor_deeplink( def open_deeplink(deeplink: str) -> bool: - """Attempt to open a deeplink URL using the system's default handler. + """Attempt to open a Cursor deeplink URL using the system's default handler. Args: deeplink: The deeplink URL to open @@ -54,21 +53,7 @@ def open_deeplink(deeplink: str) -> bool: Returns: True if the command succeeded, False otherwise """ - parsed = urlparse(deeplink) - if parsed.scheme != "cursor": - logger.warning(f"Invalid deeplink scheme: {parsed.scheme}") - return False - - try: - if sys.platform == "darwin": # macOS - subprocess.run(["open", deeplink], check=True, capture_output=True) - elif sys.platform == "win32": # Windows - os.startfile(deeplink) - else: # Linux and others - subprocess.run(["xdg-open", deeplink], check=True, capture_output=True) - return True - except (subprocess.CalledProcessError, FileNotFoundError, OSError): - return False + return _shared_open_deeplink(deeplink, expected_scheme="cursor") def install_cursor_workspace( diff --git a/src/fastmcp/cli/install/goose.py b/src/fastmcp/cli/install/goose.py new file mode 100644 index 000000000..16161dcf1 --- /dev/null +++ b/src/fastmcp/cli/install/goose.py @@ -0,0 +1,209 @@ +"""Goose integration for FastMCP install using Cyclopts.""" + +import re +import sys +from pathlib import Path +from typing import Annotated +from urllib.parse import quote + +import cyclopts +from rich import print + +from fastmcp.utilities.logging import get_logger + +from .shared import open_deeplink, process_common_args + +logger = get_logger(__name__) + + +def _slugify(name: str) -> str: + """Convert a display name to a URL-safe identifier. + + Lowercases, replaces non-alphanumeric runs with hyphens, + and strips leading/trailing hyphens. + """ + slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") + return slug or "fastmcp-server" + + +def generate_goose_deeplink( + name: str, + command: str, + args: list[str], + *, + description: str = "MCP server installed via FastMCP", +) -> str: + """Generate a Goose deeplink for installing an MCP extension. + + Args: + name: Human-readable display name for the extension. + command: The executable command (e.g. "uv"). + args: Arguments to the command. + description: Short description shown in Goose. + + Returns: + A goose://extension?... deeplink URL. + """ + extension_id = _slugify(name) + + params: list[str] = [f"cmd={quote(command, safe='')}"] + for arg in args: + params.append(f"arg={quote(arg, safe='')}") + params.append(f"id={quote(extension_id, safe='')}") + params.append(f"name={quote(name, safe='')}") + params.append(f"description={quote(description, safe='')}") + + return f"goose://extension?{'&'.join(params)}" + + +def _build_uvx_command( + server_spec: str, + *, + python_version: str | None = None, + with_packages: list[str] | None = None, +) -> list[str]: + """Build a uvx command for running a FastMCP server. + + Goose requires uvx (not uv run) as the command. The uvx format is: + uvx [--with pkg] [--python X] fastmcp run + + uvx automatically infers that the `fastmcp` command comes from the + `fastmcp` package, so --from is not needed. + """ + args: list[str] = ["uvx"] + + if python_version: + args.extend(["--python", python_version]) + + for pkg in sorted(set(with_packages or [])): + if pkg != "fastmcp": + args.extend(["--with", pkg]) + + args.extend(["fastmcp", "run", server_spec]) + return args + + +def install_goose( + file: Path, + server_object: str | None, + name: str, + *, + with_packages: list[str] | None = None, + python_version: str | None = None, +) -> bool: + """Install FastMCP server in Goose via deeplink. + + Args: + file: Path to the server file. + server_object: Optional server object name (for :object suffix). + name: Name for the extension in Goose. + with_packages: Optional list of additional packages to install. + python_version: Optional Python version to use. + + Returns: + True if installation was successful, False otherwise. + """ + if server_object: + server_spec = f"{file.resolve()}:{server_object}" + else: + server_spec = str(file.resolve()) + + full_command = _build_uvx_command( + server_spec, + python_version=python_version, + with_packages=with_packages, + ) + + deeplink = generate_goose_deeplink( + name=name, + command=full_command[0], + args=full_command[1:], + ) + + print(f"[blue]Opening Goose to install '{name}'[/blue]") + + if open_deeplink(deeplink, expected_scheme="goose"): + print("[green]Goose should now open with the installation dialog[/green]") + return True + else: + print( + "[red]Could not open Goose automatically.[/red]\n" + f"[blue]Please copy this link and open it in Goose: {deeplink}[/blue]" + ) + return False + + +async def goose_command( + server_spec: str, + *, + server_name: Annotated[ + str | None, + cyclopts.Parameter( + name=["--name", "-n"], + help="Custom name for the extension in Goose", + ), + ] = None, + with_packages: Annotated[ + list[str] | None, + cyclopts.Parameter( + "--with", + help="Additional packages to install (can be used multiple times)", + ), + ] = None, + env_vars: Annotated[ + list[str] | None, + cyclopts.Parameter( + "--env", + help="Environment variables in KEY=VALUE format (can be used multiple times)", + ), + ] = None, + env_file: Annotated[ + Path | None, + cyclopts.Parameter( + "--env-file", + help="Load environment variables from .env file", + ), + ] = None, + python: Annotated[ + str | None, + cyclopts.Parameter( + "--python", + help="Python version to use (e.g., 3.10, 3.11)", + ), + ] = None, +) -> None: + """Install an MCP server in Goose. + + Uses uvx to run the server. Environment variables are not included + in the deeplink; use `fastmcp install mcp-json` to generate a full + config for manual installation. + + Args: + server_spec: Python file to install, optionally with :object suffix + """ + with_packages = with_packages or [] + env_vars = env_vars or [] + + if env_vars or env_file: + print( + "[red]Goose deeplinks cannot include environment variables.[/red]\n" + "[yellow]Use `fastmcp install mcp-json` to generate a config, then add it " + "to your Goose config file with env vars: " + "https://block.github.io/goose/docs/getting-started/using-extensions/#config-entry[/yellow]" + ) + sys.exit(1) + + file, server_object, name, with_packages, _env_dict = await process_common_args( + server_spec, server_name, with_packages, env_vars, env_file + ) + + success = install_goose( + file=file, + server_object=server_object, + name=name, + with_packages=with_packages, + python_version=python, + ) + + if not success: + sys.exit(1) diff --git a/src/fastmcp/cli/install/shared.py b/src/fastmcp/cli/install/shared.py index 8e183562a..fe980dce6 100644 --- a/src/fastmcp/cli/install/shared.py +++ b/src/fastmcp/cli/install/shared.py @@ -1,8 +1,11 @@ """Shared utilities for install commands.""" import json +import os +import subprocess import sys from pathlib import Path +from urllib.parse import urlparse from dotenv import dotenv_values from pydantic import ValidationError @@ -42,6 +45,7 @@ async def process_common_args( env_vars = env_vars or [] # Create MCPServerConfig from server_spec config = None + config_path: Path | None = None if server_spec.endswith(".json"): config_path = Path(server_spec).resolve() if not config_path.exists(): @@ -76,7 +80,14 @@ async def process_common_args( # Extract file and server_object from the source # The FileSystemSource handles parsing path:object syntax - file = Path(config.source.path).resolve() + source_path = Path(config.source.path).expanduser() + # If loaded from a JSON config, resolve relative paths against the config's directory + if not source_path.is_absolute() and config_path is not None: + file = (config_path.parent / source_path).resolve() + else: + file = source_path.resolve() + # Update the source path so load_server() resolves correctly + config.source.path = str(file) server_object = ( config.source.entrypoint if hasattr(config.source, "entrypoint") else None ) @@ -91,14 +102,21 @@ async def process_common_args( }, ) - # Try to import server to get its name and dependencies + # Verify the resolved file actually exists + if not file.is_file(): + print(f"[red]Server file not found: {file}[/red]") + sys.exit(1) + + # Try to import server to get its name and dependencies. + # load_server() resolves paths against cwd, which may differ from our + # config-relative resolution, so we catch SystemExit from its file check. name = server_name server = None if not name: try: server = await config.source.load_server() name = server.name - except (ImportError, ModuleNotFoundError) as e: + except (ImportError, ModuleNotFoundError, SystemExit) as e: logger.debug( "Could not import server (likely missing dependencies), using file name", extra={"error": str(e)}, @@ -125,3 +143,32 @@ async def process_common_args( env_dict[key] = value return file, server_object, name, with_packages, env_dict + + +def open_deeplink(url: str, *, expected_scheme: str) -> bool: + """Attempt to open a deeplink URL using the system's default handler. + + Args: + url: The deeplink URL to open. + expected_scheme: The URL scheme to validate (e.g. "cursor", "goose"). + + Returns: + True if the command succeeded, False otherwise. + """ + parsed = urlparse(url) + if parsed.scheme != expected_scheme: + logger.warning( + f"Invalid deeplink scheme: {parsed.scheme}, expected {expected_scheme}" + ) + return False + + try: + if sys.platform == "darwin": + subprocess.run(["open", url], check=True, capture_output=True) + elif sys.platform == "win32": + os.startfile(url) + else: + subprocess.run(["xdg-open", url], check=True, capture_output=True) + return True + except (subprocess.CalledProcessError, FileNotFoundError, OSError): + return False diff --git a/src/fastmcp/cli/install/stdio.py b/src/fastmcp/cli/install/stdio.py new file mode 100644 index 000000000..31c89c1dc --- /dev/null +++ b/src/fastmcp/cli/install/stdio.py @@ -0,0 +1,156 @@ +"""Stdio command generation for FastMCP install using Cyclopts.""" + +import builtins +import shlex +import sys +from pathlib import Path +from typing import Annotated + +import cyclopts +import pyperclip +from rich import print as rich_print + +from fastmcp.utilities.logging import get_logger +from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment + +from .shared import process_common_args + +logger = get_logger(__name__) + + +def install_stdio( + file: Path, + server_object: str | None, + *, + with_editable: list[Path] | None = None, + with_packages: list[str] | None = None, + copy: bool = False, + python_version: str | None = None, + with_requirements: Path | None = None, + project: Path | None = None, +) -> bool: + """Generate the stdio command for running a FastMCP server. + + Args: + file: Path to the server file + server_object: Optional server object name (for :object suffix) + with_editable: Optional list of directories to install in editable mode + with_packages: Optional list of additional packages to install + copy: If True, copy to clipboard instead of printing to stdout + python_version: Optional Python version to use + with_requirements: Optional requirements file to install from + project: Optional project directory to run within + + Returns: + True if generation was successful, False otherwise + """ + try: + env_config = UVEnvironment( + python=python_version, + dependencies=(with_packages or []) + ["fastmcp"], + requirements=with_requirements, + project=project, + editable=with_editable, + ) + # Build server spec from parsed components + if server_object: + server_spec = f"{file.resolve()}:{server_object}" + else: + server_spec = str(file.resolve()) + + # Build the full command + full_command = env_config.build_command(["fastmcp", "run", server_spec]) + command_str = shlex.join(full_command) + + if copy: + pyperclip.copy(command_str) + rich_print("[green]✓ Command copied to clipboard[/green]") + else: + builtins.print(command_str) + + return True + + except (OSError, ValueError, pyperclip.PyperclipException) as e: + rich_print(f"[red]Failed to generate stdio command: {e}[/red]") + return False + + +async def stdio_command( + server_spec: str, + *, + server_name: Annotated[ + str | None, + cyclopts.Parameter( + name=["--name", "-n"], + help="Custom name for the server (used for dependency resolution)", + ), + ] = None, + with_editable: Annotated[ + list[Path] | None, + cyclopts.Parameter( + "--with-editable", + help="Directory with pyproject.toml to install in editable mode (can be used multiple times)", + ), + ] = None, + with_packages: Annotated[ + list[str] | None, + cyclopts.Parameter( + "--with", help="Additional packages to install (can be used multiple times)" + ), + ] = None, + copy: Annotated[ + bool, + cyclopts.Parameter( + "--copy", + help="Copy command to clipboard instead of printing to stdout", + ), + ] = False, + python: Annotated[ + str | None, + cyclopts.Parameter( + "--python", + help="Python version to use (e.g., 3.10, 3.11)", + ), + ] = None, + with_requirements: Annotated[ + Path | None, + cyclopts.Parameter( + "--with-requirements", + help="Requirements file to install dependencies from", + ), + ] = None, + project: Annotated[ + Path | None, + cyclopts.Parameter( + "--project", + help="Run the command within the given project directory", + ), + ] = None, +) -> None: + """Generate the stdio command for running a FastMCP server. + + Outputs the shell command that an MCP host would use to start this server + over stdio transport. Useful for manual configuration or debugging. + + Args: + server_spec: Python file to run, optionally with :object suffix + """ + with_editable = with_editable or [] + with_packages = with_packages or [] + file, server_object, _name, packages, _env_dict = await process_common_args( + server_spec, server_name, with_packages, [], None + ) + + success = install_stdio( + file=file, + server_object=server_object, + with_editable=with_editable, + with_packages=packages, + copy=copy, + python_version=python, + with_requirements=with_requirements, + project=project, + ) + + if not success: + sys.exit(1) diff --git a/src/fastmcp/cli/run.py b/src/fastmcp/cli/run.py index ece664b72..64fbf0d06 100644 --- a/src/fastmcp/cli/run.py +++ b/src/fastmcp/cli/run.py @@ -25,6 +25,57 @@ logger = get_logger("cli.run") TransportType = Literal["stdio", "http", "sse", "streamable-http"] LogLevelType = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] +# File extensions to watch for reload +WATCHED_EXTENSIONS: set[str] = { + # Python + ".py", + # JavaScript/TypeScript + ".js", + ".ts", + ".jsx", + ".tsx", + # Markup/Content + ".html", + ".md", + ".mdx", + ".txt", + ".xml", + # Styles + ".css", + ".scss", + ".sass", + ".less", + # Data/Config + ".json", + ".yaml", + ".yml", + ".toml", + # Framework-specific + ".vue", + ".svelte", + # GraphQL + ".graphql", + ".gql", + # Images + ".svg", + ".png", + ".jpg", + ".jpeg", + ".gif", + ".ico", + ".webp", + # Media + ".mp3", + ".mp4", + ".wav", + ".webm", + # Fonts + ".woff", + ".woff2", + ".ttf", + ".eot", +} + def is_url(path: str) -> bool: """Check if a string is a URL.""" @@ -231,9 +282,9 @@ async def run_v1_server_async( await server.run_sse_async() -def _python_file_filter(change: Change, path: str) -> bool: - """Filter for Python files only.""" - return path.endswith(".py") +def _watch_filter(_change: Change, path: str) -> bool: + """Filter for files that should trigger reload.""" + return any(path.endswith(ext) for ext in WATCHED_EXTENSIONS) async def _terminate_process(process: asyncio.subprocess.Process) -> None: @@ -300,7 +351,7 @@ async def run_with_reload( # Watch for either: file changes OR process death watch_task = asyncio.create_task( - anext(aiter(awatch(*watch_paths, watch_filter=_python_file_filter))) + anext(aiter(awatch(*watch_paths, watch_filter=_watch_filter))) ) wait_task = asyncio.create_task(process.wait()) shutdown_task = asyncio.create_task(shutdown_event.wait()) @@ -331,7 +382,7 @@ async def run_with_reload( # Wait for file change or shutdown (avoid hot loop on crash) watch_task = asyncio.create_task( - anext(aiter(awatch(*watch_paths, watch_filter=_python_file_filter))) + anext(aiter(awatch(*watch_paths, watch_filter=_watch_filter))) ) shutdown_task = asyncio.create_task(shutdown_event.wait()) done, pending = await asyncio.wait( diff --git a/src/fastmcp/resources/function_resource.py b/src/fastmcp/resources/function_resource.py index b7fa5bb7a..c6a881dab 100644 --- a/src/fastmcp/resources/function_resource.py +++ b/src/fastmcp/resources/function_resource.py @@ -14,6 +14,7 @@ from pydantic import AnyUrl import fastmcp from fastmcp.decorators import resolve_task_config from fastmcp.resources.resource import Resource, ResourceResult +from fastmcp.server.apps import resolve_ui_mime_type from fastmcp.server.dependencies import ( transform_context_annotations, without_injected_parameters, @@ -180,6 +181,9 @@ class FunctionResource(Resource): # Wrap fn to handle dependency resolution internally wrapped_fn = without_injected_parameters(fn) + # Apply ui:// MIME default, then fall back to text/plain + resolved_mime = resolve_ui_mime_type(metadata.uri, metadata.mime_type) + return cls( fn=wrapped_fn, uri=uri_obj, @@ -188,7 +192,7 @@ class FunctionResource(Resource): title=metadata.title, description=metadata.description or inspect.getdoc(fn), icons=metadata.icons, - mime_type=metadata.mime_type or "text/plain", + mime_type=resolved_mime or "text/plain", tags=metadata.tags or set(), annotations=metadata.annotations, meta=metadata.meta, diff --git a/src/fastmcp/resources/resource.py b/src/fastmcp/resources/resource.py index 6b23468e4..2aaca1863 100644 --- a/src/fastmcp/resources/resource.py +++ b/src/fastmcp/resources/resource.py @@ -307,12 +307,27 @@ class Resource(FastMCPComponent): 2. In tasks_result_handler() to convert Docket task results to ResourceResult Handles ResourceResult passthrough and converts raw values using - ResourceResult's normalization. + ResourceResult's normalization. When the raw value is a plain + string or bytes, the resource's own ``mime_type`` is forwarded so + that ``ui://`` resources (and others with non-default MIME types) + don't fall back to ``text/plain``. + + The resource's component-level ``meta`` (e.g. ``ui`` metadata for + MCP Apps CSP/permissions) is propagated to each content item so + that hosts can read it from the ``resources/read`` response. """ if isinstance(raw_value, ResourceResult): return raw_value - # ResourceResult.__init__ handles all normalization + # For plain str/bytes returns, wrap in ResourceContent with the + # resource's MIME type and component meta so the wire response + # carries the correct type and metadata (e.g. CSP for MCP Apps). + if isinstance(raw_value, (str, bytes)): + return ResourceResult( + [ResourceContent(raw_value, mime_type=self.mime_type, meta=self.meta)] + ) + + # ResourceResult.__init__ handles all other normalization return ResourceResult(raw_value) @overload diff --git a/src/fastmcp/resources/template.py b/src/fastmcp/resources/template.py index 1ef6c7e18..02836e398 100644 --- a/src/fastmcp/resources/template.py +++ b/src/fastmcp/resources/template.py @@ -22,6 +22,7 @@ from pydantic import ( ) from fastmcp.resources.resource import Resource, ResourceResult +from fastmcp.server.apps import resolve_ui_mime_type from fastmcp.server.dependencies import ( transform_context_annotations, without_injected_parameters, @@ -567,6 +568,9 @@ class FunctionResourceTemplate(ResourceTemplate): # Use validate_call on wrapper for runtime type coercion fn = validate_call(wrapper_fn) + # Apply ui:// MIME default, then fall back to text/plain + resolved_mime = resolve_ui_mime_type(uri_template, mime_type) + return cls( uri_template=uri_template, name=func_name, @@ -574,7 +578,7 @@ class FunctionResourceTemplate(ResourceTemplate): title=title, description=description, icons=icons, - mime_type=mime_type or "text/plain", + mime_type=resolved_mime or "text/plain", fn=fn, parameters=parameters, tags=tags or set(), diff --git a/src/fastmcp/server/apps.py b/src/fastmcp/server/apps.py new file mode 100644 index 000000000..566938b98 --- /dev/null +++ b/src/fastmcp/server/apps.py @@ -0,0 +1,156 @@ +"""MCP Apps support — extension negotiation and typed UI metadata models. + +Provides constants and Pydantic models for the MCP Apps extension +(io.modelcontextprotocol/ui), enabling tools and resources to carry +UI metadata for clients that support interactive app rendering. +""" + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, Field + +UI_EXTENSION_ID = "io.modelcontextprotocol/ui" +UI_MIME_TYPE = "text/html;profile=mcp-app" + + +class ResourceCSP(BaseModel): + """Content Security Policy for MCP App resources. + + Declares which external origins the app is allowed to connect to or + load resources from. Hosts use these declarations to build the + ``Content-Security-Policy`` header for the sandboxed iframe. + """ + + connect_domains: list[str] | None = Field( + default=None, + alias="connectDomains", + description="Origins allowed for fetch/XHR/WebSocket (connect-src)", + ) + resource_domains: list[str] | None = Field( + default=None, + alias="resourceDomains", + description="Origins allowed for scripts, images, styles, fonts (script-src etc.)", + ) + frame_domains: list[str] | None = Field( + default=None, + alias="frameDomains", + description="Origins allowed for nested iframes (frame-src)", + ) + base_uri_domains: list[str] | None = Field( + default=None, + alias="baseUriDomains", + description="Allowed base URIs for the document (base-uri)", + ) + + model_config = {"populate_by_name": True, "extra": "allow"} + + +class ResourcePermissions(BaseModel): + """Iframe sandbox permissions for MCP App resources. + + Each field, when set (typically to ``{}``), requests that the host + grant the corresponding Permission Policy feature to the sandboxed + iframe. Hosts MAY honour these; apps should use JS feature detection + as a fallback. + """ + + camera: dict[str, Any] | None = Field( + default=None, description="Request camera access" + ) + microphone: dict[str, Any] | None = Field( + default=None, description="Request microphone access" + ) + geolocation: dict[str, Any] | None = Field( + default=None, description="Request geolocation access" + ) + clipboard_write: dict[str, Any] | None = Field( + default=None, + alias="clipboardWrite", + description="Request clipboard-write access", + ) + + model_config = {"populate_by_name": True, "extra": "allow"} + + +class ToolUI(BaseModel): + """Typed ``_meta.ui`` for tools — links a tool to its UI resource. + + All fields use ``exclude_none`` serialization so only explicitly-set + values appear on the wire. Aliases match the MCP Apps wire format + (camelCase). + """ + + resource_uri: str | None = Field( + default=None, + alias="resourceUri", + description="URI of the UI resource (typically ui:// scheme)", + ) + visibility: list[str] | None = Field( + default=None, + description="Where this tool is visible: 'app', 'model', or both", + ) + csp: ResourceCSP | None = Field( + default=None, description="Content Security Policy for the app iframe" + ) + permissions: ResourcePermissions | None = Field( + default=None, description="Iframe sandbox permissions" + ) + domain: str | None = Field(default=None, description="Domain for the iframe") + prefers_border: bool | None = Field( + default=None, + alias="prefersBorder", + description="Whether the UI prefers a visible border", + ) + + model_config = {"populate_by_name": True} + + +class ResourceUI(BaseModel): + """Typed ``_meta.ui`` for resources — rendering hints for UI-capable clients.""" + + csp: ResourceCSP | None = Field( + default=None, description="Content Security Policy for the app iframe" + ) + permissions: ResourcePermissions | None = Field( + default=None, description="Iframe sandbox permissions" + ) + domain: str | None = Field(default=None, description="Domain for the iframe") + prefers_border: bool | None = Field( + default=None, + alias="prefersBorder", + description="Whether the UI prefers a visible border", + ) + + model_config = {"populate_by_name": True} + + +def ui_to_meta_dict(ui: ToolUI | ResourceUI | dict[str, Any]) -> dict[str, Any]: + """Convert a UI model or dict to the wire-format dict for ``meta["ui"]``.""" + if isinstance(ui, (ToolUI, ResourceUI)): + return ui.model_dump(by_alias=True, exclude_none=True) + return ui + + +def resolve_ui_mime_type(uri: str, explicit_mime_type: str | None) -> str | None: + """Return the appropriate MIME type for a resource URI. + + For ``ui://`` scheme resources, defaults to ``UI_MIME_TYPE`` when no + explicit MIME type is provided. This ensures UI resources are correctly + identified regardless of how they're registered (via FastMCP.resource, + the standalone @resource decorator, or resource templates). + + Args: + uri: The resource URI string + explicit_mime_type: The MIME type explicitly provided by the user + + Returns: + The resolved MIME type (explicit value, UI default, or None) + """ + if explicit_mime_type is not None: + return explicit_mime_type + # Case-insensitive scheme check per RFC 3986 + if uri.lower().startswith("ui://"): + return UI_MIME_TYPE + return None diff --git a/src/fastmcp/server/auth/oauth_proxy/proxy.py b/src/fastmcp/server/auth/oauth_proxy/proxy.py index 4938932f8..e1a24720f 100644 --- a/src/fastmcp/server/auth/oauth_proxy/proxy.py +++ b/src/fastmcp/server/auth/oauth_proxy/proxy.py @@ -978,6 +978,20 @@ class OAuthProxy(OAuthProvider, ConsentMixin): # Refresh Token Flow # ------------------------------------------------------------------------- + def _prepare_scopes_for_token_exchange(self, scopes: list[str]) -> list[str]: + """Prepare scopes for initial token exchange (auth code -> tokens). + + Override this method to provide scopes during the authorization + code exchange. Some providers (like Azure) require scopes to be sent. + + Args: + scopes: Scopes from the authorization request + + Returns: + List of scopes to send, or empty list to omit scope parameter + """ + return scopes + def _prepare_scopes_for_upstream_refresh(self, scopes: list[str]) -> list[str]: """Prepare scopes for upstream token refresh request. @@ -1532,6 +1546,13 @@ class OAuthProxy(OAuthProvider, ConsentMixin): txn_id, ) + # Allow providers to specify scope for token exchange + exchange_scopes = self._prepare_scopes_for_token_exchange( + transaction.get("scopes") or [] + ) + if exchange_scopes: + token_params["scope"] = " ".join(exchange_scopes) + # Add any extra token parameters configured for this proxy if self._extra_token_params: token_params.update(self._extra_token_params) diff --git a/src/fastmcp/server/auth/providers/azure.py b/src/fastmcp/server/auth/providers/azure.py index e57b9c04a..36fb8975a 100644 --- a/src/fastmcp/server/auth/providers/azure.py +++ b/src/fastmcp/server/auth/providers/azure.py @@ -132,9 +132,10 @@ class AzureProvider(OAuthProxy): - NOT validated on tokens - NOT advertised to MCP clients - Used to request additional permissions from Azure (e.g., Graph API access) - Example: ["User.Read", "Mail.Read", "offline_access"] + Example: ["User.Read", "Mail.Read"] These scopes allow your FastMCP server to call Microsoft Graph APIs using the upstream Azure token, but MCP clients are unaware of them. + Note: "offline_access" is automatically included to obtain refresh tokens. allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients. If None (default), all URIs are allowed. If empty list, no URIs are allowed. client_storage: Storage backend for OAuth state (client registrations, encrypted tokens). @@ -150,15 +151,19 @@ class AzureProvider(OAuthProxy): """ # Parse scopes if provided as string parsed_required_scopes = parse_scopes(required_scopes) - parsed_additional_scopes = ( - parse_scopes(additional_authorize_scopes) + parsed_additional_scopes: list[str] = ( + parse_scopes(additional_authorize_scopes) or [] if additional_authorize_scopes else [] ) + # Always include offline_access to get refresh tokens from Azure + if "offline_access" not in parsed_additional_scopes: + parsed_additional_scopes = [*parsed_additional_scopes, "offline_access"] + # Apply defaults self.identifier_uri = identifier_uri or f"api://{client_id}" - self.additional_authorize_scopes = parsed_additional_scopes + self.additional_authorize_scopes: list[str] = parsed_additional_scopes # Always validate tokens against the app's API client ID using JWT issuer = f"https://{base_authority}/{tenant_id}/v2.0" @@ -193,6 +198,8 @@ class AzureProvider(OAuthProxy): token_endpoint = f"https://{base_authority}/{tenant_id}/oauth2/v2.0/token" # Initialize OAuth proxy with Azure endpoints + # Remember there's hooks called, such as _prepare_scopes_for_token_exchange + # and _prepare_scopes_for_upstream_refresh super().__init__( upstream_authorization_endpoint=authorization_endpoint, upstream_token_endpoint=token_endpoint, @@ -206,7 +213,6 @@ class AzureProvider(OAuthProxy): client_storage=client_storage, jwt_signing_key=jwt_signing_key, require_authorization_consent=require_authorization_consent, - # Advertise full scopes including OIDC (even though we only validate non-OIDC) valid_scopes=parsed_required_scopes, ) @@ -318,16 +324,37 @@ class AzureProvider(OAuthProxy): # Let parent build the URL with prefixed scopes return super()._build_upstream_authorize_url(txn_id, modified_transaction) + def _prepare_scopes_for_token_exchange(self, scopes: list[str]) -> list[str]: + """Prepare scopes for Azure authorization code exchange. + + Azure requires scopes during token exchange (AADSTS28003 error if missing). + Azure only allows ONE resource per token request (AADSTS28000), so we only + include scopes for this API plus OIDC scopes. + + Args: + scopes: Scopes from the authorization request (unprefixed) + + Returns: + List of scopes for Azure token endpoint + """ + # Prefix scopes for this API + prefixed_scopes = self._prefix_scopes_for_azure(scopes or []) + + # Add OIDC scopes only (not other API scopes) to avoid AADSTS28000 + if self.additional_authorize_scopes: + prefixed_scopes.extend( + s for s in self.additional_authorize_scopes if s in OIDC_SCOPES + ) + + deduplicated = list(dict.fromkeys(prefixed_scopes)) + logger.debug("Token exchange scopes: %s", deduplicated) + return deduplicated + def _prepare_scopes_for_upstream_refresh(self, scopes: list[str]) -> list[str]: """Prepare scopes for Azure token refresh. - Azure requires: - 1. Fully-qualified custom scopes (e.g., "api://xxx/read" not "read") - 2. Microsoft Graph scopes (e.g., "User.Read", "openid") sent as-is - 3. Additional scopes from provider config (additional_authorize_scopes) - - This method transforms base client scopes for Azure while keeping them - unprefixed in storage to prevent accumulation. + Azure requires fully-qualified scopes and only allows ONE resource per + token request (AADSTS28000). We include scopes for this API plus OIDC scopes. Args: scopes: Base scopes from RefreshToken (unprefixed, e.g., ["read"]) @@ -338,22 +365,19 @@ class AzureProvider(OAuthProxy): logger.debug("Base scopes from storage: %s", scopes) # Filter out any additional_authorize_scopes that may have been stored - # (they shouldn't be in storage, but clean them up if they are) additional_scopes_set = set(self.additional_authorize_scopes or []) base_scopes = [s for s in scopes if s not in additional_scopes_set] - # Prefix base scopes with identifier_uri for Azure using shared helper + # Prefix base scopes with identifier_uri for Azure prefixed_scopes = self._prefix_scopes_for_azure(base_scopes) - # Add additional scopes (Graph + OIDC) for the Azure request - # These are NOT stored in RefreshToken, only sent to Azure + # Add OIDC scopes only (not other API scopes) to avoid AADSTS28000 if self.additional_authorize_scopes: - prefixed_scopes.extend(self.additional_authorize_scopes) + prefixed_scopes.extend( + s for s in self.additional_authorize_scopes if s in OIDC_SCOPES + ) - # Deduplicate while preserving order (in case older tokens have duplicates) - # Use dict.fromkeys() for O(n) deduplication with order preservation deduplicated_scopes = list(dict.fromkeys(prefixed_scopes)) - logger.debug("Scopes for Azure token endpoint: %s", deduplicated_scopes) return deduplicated_scopes diff --git a/src/fastmcp/server/context.py b/src/fastmcp/server/context.py index 0fe505b16..9a5eb3087 100644 --- a/src/fastmcp/server/context.py +++ b/src/fastmcp/server/context.py @@ -33,6 +33,7 @@ from fastmcp.server.elicitation import ( handle_elicit_accept, parse_elicit_response_type, ) +from fastmcp.server.low_level import MiddlewareServerSession from fastmcp.server.sampling import SampleStep, SamplingResult, SamplingTool from fastmcp.server.sampling.run import ( sample_impl, @@ -455,6 +456,33 @@ class Context: """ return _current_transport.get() + def client_supports_extension(self, extension_id: str) -> bool: + """Check whether the connected client supports a given MCP extension. + + Inspects the ``extensions`` extra field on ``ClientCapabilities`` + sent by the client during initialization. + + Returns ``False`` when no session is available (e.g., outside a + request context) or when the client did not advertise the extension. + + Example:: + + from fastmcp.server.apps import UI_EXTENSION_ID + + @mcp.tool + async def my_tool(ctx: Context) -> str: + if ctx.client_supports_extension(UI_EXTENSION_ID): + return "UI-capable client" + return "text-only client" + """ + rc = self.request_context + if rc is None: + return False + session = rc.session + if not isinstance(session, MiddlewareServerSession): + return False + return session.client_supports_extension(extension_id) + @property def client_id(self) -> str | None: """Get the client ID if available.""" diff --git a/src/fastmcp/server/low_level.py b/src/fastmcp/server/low_level.py index a51d0b8bf..2bc004ff3 100644 --- a/src/fastmcp/server/low_level.py +++ b/src/fastmcp/server/low_level.py @@ -24,6 +24,7 @@ from mcp.shared.message import SessionMessage from mcp.shared.session import RequestResponder from pydantic import AnyUrl +from fastmcp.server.apps import UI_EXTENSION_ID from fastmcp.utilities.logging import get_logger if TYPE_CHECKING: @@ -49,6 +50,25 @@ class MiddlewareServerSession(ServerSession): raise RuntimeError("FastMCP instance is no longer available") return fastmcp + def client_supports_extension(self, extension_id: str) -> bool: + """Check if the connected client supports a given MCP extension. + + Inspects the ``extensions`` extra field on ``ClientCapabilities`` + sent by the client during initialization. + """ + client_params = self._client_params + if client_params is None: + return False + caps = client_params.capabilities + if caps is None: + return False + # ClientCapabilities uses extra="allow" — extensions is an extra field + extras = caps.model_extra or {} + extensions: dict[str, Any] | None = extras.get("extensions") + if not extensions: + return False + return extension_id in extensions + async def _received_request( self, responder: RequestResponder[mcp.types.ClientRequest, mcp.types.ServerResult], @@ -188,6 +208,15 @@ class LowLevelServer(_Server[LifespanResultT, RequestT]): # Set tasks as a first-class field (not experimental) per SEP-1686 capabilities.tasks = get_task_capabilities() + # Advertise MCP Apps extension support (io.modelcontextprotocol/ui) + # Uses the same extra-field pattern as tasks above — ServerCapabilities + # has extra="allow" so this survives serialization. + # Merge with any existing extensions to avoid clobbering other features. + existing_extensions: dict[str, Any] = ( + getattr(capabilities, "extensions", None) or {} + ) + capabilities.extensions = {**existing_extensions, UI_EXTENSION_ID: {}} + return capabilities async def run( diff --git a/src/fastmcp/server/middleware/error_handling.py b/src/fastmcp/server/middleware/error_handling.py index fc0e2a1c8..5b235e804 100644 --- a/src/fastmcp/server/middleware/error_handling.py +++ b/src/fastmcp/server/middleware/error_handling.py @@ -78,7 +78,9 @@ class ErrorHandlingMiddleware(Middleware): except Exception as callback_error: self.logger.error(f"Error in error callback: {callback_error}") - def _transform_error(self, error: Exception) -> Exception: + def _transform_error( + self, error: Exception, context: MiddlewareContext + ) -> Exception: """Transform non-MCP errors to proper MCP errors.""" if isinstance(error, McpError): return error @@ -94,9 +96,13 @@ class ErrorHandlingMiddleware(Middleware): ErrorData(code=-32602, message=f"Invalid params: {error!s}") ) elif error_type in (FileNotFoundError, KeyError, NotFoundError): - return McpError( - ErrorData(code=-32001, message=f"Resource not found: {error!s}") - ) + # MCP spec defines -32002 specifically for resource not found + method = context.method or "" + if method.startswith("resources/"): + return McpError( + ErrorData(code=-32002, message=f"Resource not found: {error!s}") + ) + return McpError(ErrorData(code=-32001, message=f"Not found: {error!s}")) elif error_type is PermissionError: return McpError( ErrorData(code=-32000, message=f"Permission denied: {error!s}") @@ -119,7 +125,7 @@ class ErrorHandlingMiddleware(Middleware): self._log_error(error, context) # Transform and re-raise - transformed_error = self._transform_error(error) + transformed_error = self._transform_error(error, context) raise transformed_error from error def get_error_stats(self) -> dict[str, int]: diff --git a/src/fastmcp/server/mixins/mcp_operations.py b/src/fastmcp/server/mixins/mcp_operations.py index e39d73ec7..60537147d 100644 --- a/src/fastmcp/server/mixins/mcp_operations.py +++ b/src/fastmcp/server/mixins/mcp_operations.py @@ -154,6 +154,7 @@ class MCPOperationsMixin: tools = _dedupe_with_versions(list(await server.list_tools()), lambda t: t.name) sdk_tools = [tool.to_mcp_tool(name=tool.name) for tool in tools] + # SDK may pass None for internal cache refresh despite type hint cursor = ( request.params.cursor if request is not None and request.params else None @@ -177,6 +178,7 @@ class MCPOperationsMixin: sdk_resources = [ resource.to_mcp_resource(uri=str(resource.uri)) for resource in resources ] + cursor = request.params.cursor if request.params else None page, next_cursor = _apply_pagination( sdk_resources, cursor, server._list_page_size @@ -334,9 +336,15 @@ class MCPOperationsMixin: return result return result.to_mcp_result(uri) except DisabledError as e: - raise NotFoundError(f"Unknown resource: {str(uri)!r}") from e - except NotFoundError: - raise + raise McpError( + mcp.types.ErrorData( + code=-32002, message=f"Resource not found: {str(uri)!r}" + ) + ) from e + except NotFoundError as e: + raise McpError( + mcp.types.ErrorData(code=-32002, message=f"Resource not found: {e}") + ) from e async def _get_prompt_mcp( self, name: str, arguments: dict[str, Any] | None diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 48a2fdf93..a09b58a6e 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -58,6 +58,12 @@ from fastmcp.prompts.function_prompt import FunctionPrompt from fastmcp.prompts.prompt import PromptResult from fastmcp.resources.resource import Resource, ResourceResult from fastmcp.resources.template import ResourceTemplate +from fastmcp.server.apps import ( + ResourceUI, + ToolUI, + resolve_ui_mime_type, + ui_to_meta_dict, +) from fastmcp.server.auth import AuthContext, AuthProvider, run_auth_checks from fastmcp.server.dependencies import get_access_token from fastmcp.server.lifespan import Lifespan @@ -1370,6 +1376,7 @@ class FastMCP( annotations: ToolAnnotations | dict[str, Any] | None = None, exclude_args: list[str] | None = None, meta: dict[str, Any] | None = None, + ui: ToolUI | dict[str, Any] | None = None, task: bool | TaskConfig | None = None, timeout: float | None = None, auth: AuthCheckCallable | list[AuthCheckCallable] | None = None, @@ -1390,6 +1397,7 @@ class FastMCP( annotations: ToolAnnotations | dict[str, Any] | None = None, exclude_args: list[str] | None = None, meta: dict[str, Any] | None = None, + ui: ToolUI | dict[str, Any] | None = None, task: bool | TaskConfig | None = None, timeout: float | None = None, auth: AuthCheckCallable | list[AuthCheckCallable] | None = None, @@ -1409,6 +1417,7 @@ class FastMCP( annotations: ToolAnnotations | dict[str, Any] | None = None, exclude_args: list[str] | None = None, meta: dict[str, Any] | None = None, + ui: ToolUI | dict[str, Any] | None = None, task: bool | TaskConfig | None = None, timeout: float | None = None, auth: AuthCheckCallable | list[AuthCheckCallable] | None = None, @@ -1465,6 +1474,11 @@ class FastMCP( server.tool(my_function, name="custom_name") ``` """ + # Merge UI metadata into meta["ui"] before passing to provider + if ui is not None: + meta = dict(meta) if meta else {} + meta["ui"] = ui_to_meta_dict(ui) + # Delegate to LocalProvider with server-level defaults result = self._local_provider.tool( name_or_fn, @@ -1523,6 +1537,7 @@ class FastMCP( tags: set[str] | None = None, annotations: Annotations | dict[str, Any] | None = None, meta: dict[str, Any] | None = None, + ui: ResourceUI | dict[str, Any] | None = None, task: bool | TaskConfig | None = None, auth: AuthCheckCallable | list[AuthCheckCallable] | None = None, ) -> Callable[[AnyFunction], Resource | ResourceTemplate | AnyFunction]: @@ -1577,6 +1592,22 @@ class FastMCP( return f"Weather for {city}: {data}" ``` """ + # Catch incorrect decorator usage early (before any processing) + if not isinstance(uri, str): + raise TypeError( + "The @resource decorator was used incorrectly. " + "It requires a URI as the first argument. " + "Use @resource('uri') instead of @resource" + ) + + # Apply default MIME type for ui:// scheme resources + mime_type = resolve_ui_mime_type(uri, mime_type) + + # Merge UI metadata into meta["ui"] before passing to provider + if ui is not None: + meta = dict(meta) if meta else {} + meta["ui"] = ui_to_meta_dict(ui) + # Delegate to LocalProvider with server-level defaults inner_decorator = self._local_provider.resource( uri, diff --git a/src/fastmcp/utilities/skills.py b/src/fastmcp/utilities/skills.py index 3c7e3c9bf..a84728fe3 100644 --- a/src/fastmcp/utilities/skills.py +++ b/src/fastmcp/utilities/skills.py @@ -2,11 +2,14 @@ from __future__ import annotations +import base64 import json from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING +import mcp.types + if TYPE_CHECKING: from fastmcp.client import Client @@ -101,7 +104,7 @@ async def get_skill_manifest(client: Client, skill_name: str) -> SkillManifest: raise ValueError(f"Could not read manifest for skill: {skill_name}") content = result[0] - if hasattr(content, "text"): + if isinstance(content, mcp.types.TextResourceContents): try: manifest_data = json.loads(content.text) except json.JSONDecodeError as e: @@ -197,12 +200,9 @@ async def download_skill( file_path.parent.mkdir(parents=True, exist_ok=True) # Write content - if hasattr(content, "text"): + if isinstance(content, mcp.types.TextResourceContents): file_path.write_text(content.text) - elif hasattr(content, "blob"): - # Handle base64-encoded binary content - import base64 - + elif isinstance(content, mcp.types.BlobResourceContents): file_path.write_bytes(base64.b64decode(content.blob)) else: # Skip unknown content types diff --git a/tests/cli/test_cursor.py b/tests/cli/test_cursor.py index 7e63e7b9f..476b678ef 100644 --- a/tests/cli/test_cursor.py +++ b/tests/cli/test_cursor.py @@ -181,7 +181,7 @@ class TestOpenDeeplink: """Test opening deeplink on Windows.""" with patch("sys.platform", "win32"): with patch( - "fastmcp.cli.install.cursor.os.startfile", create=True + "fastmcp.cli.install.shared.os.startfile", create=True ) as mock_startfile: result = open_deeplink("cursor://test") @@ -251,7 +251,7 @@ class TestOpenDeeplink: """Test handling of OSError on Windows.""" with patch("sys.platform", "win32"): with patch( - "fastmcp.cli.install.cursor.os.startfile", create=True + "fastmcp.cli.install.shared.os.startfile", create=True ) as mock_startfile: mock_startfile.side_effect = OSError("File not found") result = open_deeplink("cursor://test") diff --git a/tests/cli/test_goose.py b/tests/cli/test_goose.py new file mode 100644 index 000000000..6108eeff6 --- /dev/null +++ b/tests/cli/test_goose.py @@ -0,0 +1,298 @@ +from pathlib import Path +from unittest.mock import patch +from urllib.parse import parse_qs, unquote, urlparse + +import pytest + +from fastmcp.cli.install.goose import ( + _build_uvx_command, + _slugify, + generate_goose_deeplink, + goose_command, + install_goose, +) + + +class TestSlugify: + def test_simple_name(self): + assert _slugify("My Server") == "my-server" + + def test_special_characters(self): + assert _slugify("my_server (v2.0)") == "my-server-v2-0" + + def test_already_slugified(self): + assert _slugify("my-server") == "my-server" + + def test_empty_string(self): + assert _slugify("") == "fastmcp-server" + + def test_only_special_chars(self): + assert _slugify("!!!") == "fastmcp-server" + + def test_consecutive_hyphens_collapsed(self): + assert _slugify("a---b") == "a-b" + + def test_leading_trailing_stripped(self): + assert _slugify("--hello--") == "hello" + + +class TestBuildUvxCommand: + def test_basic(self): + cmd = _build_uvx_command("server.py") + assert cmd == ["uvx", "fastmcp", "run", "server.py"] + + def test_with_python_version(self): + cmd = _build_uvx_command("server.py", python_version="3.11") + assert cmd == [ + "uvx", + "--python", + "3.11", + "fastmcp", + "run", + "server.py", + ] + + def test_with_packages(self): + cmd = _build_uvx_command("server.py", with_packages=["numpy", "pandas"]) + assert "--with" in cmd + assert "numpy" in cmd + assert "pandas" in cmd + + def test_fastmcp_not_in_with(self): + cmd = _build_uvx_command("server.py", with_packages=["fastmcp", "numpy"]) + # fastmcp is the command itself, so it shouldn't appear in --with + with_indices = [i for i, v in enumerate(cmd) if v == "--with"] + with_values = [cmd[i + 1] for i in with_indices] + assert "fastmcp" not in with_values + + def test_packages_sorted_and_deduplicated(self): + cmd = _build_uvx_command( + "server.py", with_packages=["pandas", "numpy", "pandas"] + ) + with_indices = [i for i, v in enumerate(cmd) if v == "--with"] + with_values = [cmd[i + 1] for i in with_indices] + assert with_values == ["numpy", "pandas"] + + def test_server_spec_with_object(self): + cmd = _build_uvx_command("server.py:app") + assert cmd[-1] == "server.py:app" + + +class TestGooseDeeplinkGeneration: + def test_basic_deeplink(self): + deeplink = generate_goose_deeplink( + name="test-server", + command="uvx", + args=["fastmcp", "run", "server.py"], + ) + assert deeplink.startswith("goose://extension?") + parsed = urlparse(deeplink) + params = parse_qs(parsed.query) + assert params["cmd"] == ["uvx"] + assert params["name"] == ["test-server"] + assert params["id"] == ["test-server"] + + def test_special_characters_in_name(self): + deeplink = generate_goose_deeplink( + name="my server (test)", + command="uvx", + args=["fastmcp", "run", "server.py"], + ) + assert "name=my%20server%20%28test%29" in deeplink + parsed = urlparse(deeplink) + params = parse_qs(parsed.query) + assert params["id"] == ["my-server-test"] + + def test_url_injection_protection(self): + deeplink = generate_goose_deeplink( + name="test&evil=true", + command="uvx", + args=["fastmcp", "run", "server.py"], + ) + assert "name=test%26evil%3Dtrue" in deeplink + parsed = urlparse(deeplink) + params = parse_qs(parsed.query) + assert params["name"] == ["test&evil=true"] + + def test_dangerous_characters_encoded(self): + dangerous_names = [ + ("test|calc", "test%7Ccalc"), + ("test;calc", "test%3Bcalc"), + ("testcalc", "test%3Ecalc"), + ("test`calc", "test%60calc"), + ("test$calc", "test%24calc"), + ("test'calc", "test%27calc"), + ('test"calc', "test%22calc"), + ("test calc", "test%20calc"), + ("test#anchor", "test%23anchor"), + ("test?query=val", "test%3Fquery%3Dval"), + ] + for dangerous_name, expected_encoded in dangerous_names: + deeplink = generate_goose_deeplink( + name=dangerous_name, command="uvx", args=["fastmcp", "run", "server.py"] + ) + assert f"name={expected_encoded}" in deeplink, ( + f"Failed to encode {dangerous_name}" + ) + + def test_custom_description(self): + deeplink = generate_goose_deeplink( + name="my-server", + command="uvx", + args=["fastmcp", "run", "server.py"], + description="My custom MCP server", + ) + parsed = urlparse(deeplink) + params = parse_qs(parsed.query) + assert params["description"] == ["My custom MCP server"] + + def test_args_with_special_characters(self): + deeplink = generate_goose_deeplink( + name="test", + command="uvx", + args=[ + "--with", + "numpy>=1.20", + "fastmcp", + "run", + "server.py:MyApp", + ], + ) + parsed = urlparse(deeplink) + params = parse_qs(parsed.query) + assert "numpy>=1.20" in params["arg"] + assert "server.py:MyApp" in params["arg"] + + def test_empty_args(self): + deeplink = generate_goose_deeplink(name="simple", command="python", args=[]) + parsed = urlparse(deeplink) + params = parse_qs(parsed.query) + assert "arg" not in params + assert params["cmd"] == ["python"] + + def test_command_with_path(self): + deeplink = generate_goose_deeplink( + name="test", + command="/usr/local/bin/uvx", + args=["fastmcp", "run", "server.py"], + ) + parsed = urlparse(deeplink) + params = parse_qs(parsed.query) + assert params["cmd"] == ["/usr/local/bin/uvx"] + + +class TestInstallGoose: + @patch("fastmcp.cli.install.goose.open_deeplink") + @patch("fastmcp.cli.install.goose.print") + def test_success(self, mock_print, mock_open): + mock_open.return_value = True + result = install_goose( + file=Path("/path/to/server.py"), + server_object=None, + name="test-server", + ) + assert result is True + mock_open.assert_called_once() + call_url = mock_open.call_args[0][0] + assert call_url.startswith("goose://extension?") + assert mock_open.call_args[1] == {"expected_scheme": "goose"} + + @patch("fastmcp.cli.install.goose.open_deeplink") + @patch("fastmcp.cli.install.goose.print") + def test_success_uses_uvx(self, mock_print, mock_open): + mock_open.return_value = True + install_goose( + file=Path("/path/to/server.py"), + server_object=None, + name="test-server", + ) + call_url = mock_open.call_args[0][0] + parsed = urlparse(call_url) + params = parse_qs(parsed.query) + assert params["cmd"] == ["uvx"] + assert "fastmcp" in params["arg"] + + @patch("fastmcp.cli.install.goose.open_deeplink") + @patch("fastmcp.cli.install.goose.print") + def test_failure(self, mock_print, mock_open): + mock_open.return_value = False + result = install_goose( + file=Path("/path/to/server.py"), + server_object=None, + name="test-server", + ) + assert result is False + + @patch("fastmcp.cli.install.goose.open_deeplink") + @patch("fastmcp.cli.install.goose.print") + def test_with_server_object(self, mock_print, mock_open): + mock_open.return_value = True + install_goose( + file=Path("/path/to/server.py"), + server_object="app", + name="test-server", + ) + call_url = mock_open.call_args[0][0] + parsed = urlparse(call_url) + params = parse_qs(parsed.query) + args = params["arg"] + assert any("server.py:app" in unquote(a) for a in args) + + @patch("fastmcp.cli.install.goose.open_deeplink") + @patch("fastmcp.cli.install.goose.print") + def test_with_packages(self, mock_print, mock_open): + mock_open.return_value = True + install_goose( + file=Path("/path/to/server.py"), + server_object=None, + name="test-server", + with_packages=["numpy", "pandas"], + ) + call_url = mock_open.call_args[0][0] + parsed = urlparse(call_url) + params = parse_qs(parsed.query) + args = params["arg"] + assert "numpy" in args + assert "pandas" in args + + @patch("fastmcp.cli.install.goose.open_deeplink") + @patch("fastmcp.cli.install.goose.print") + def test_fallback_message_on_failure(self, mock_print, mock_open): + mock_open.return_value = False + install_goose( + file=Path("/path/to/server.py"), + server_object=None, + name="test-server", + ) + fallback_calls = [ + call + for call in mock_print.call_args_list + if "copy this link" in str(call).lower() or "goose://" in str(call) + ] + assert len(fallback_calls) > 0 + + +class TestGooseCommand: + @patch("fastmcp.cli.install.goose.install_goose") + @patch("fastmcp.cli.install.goose.process_common_args") + async def test_basic(self, mock_process, mock_install): + mock_process.return_value = (Path("server.py"), None, "test-server", [], {}) + mock_install.return_value = True + await goose_command("server.py") + mock_install.assert_called_once_with( + file=Path("server.py"), + server_object=None, + name="test-server", + with_packages=[], + python_version=None, + ) + + @patch("fastmcp.cli.install.goose.install_goose") + @patch("fastmcp.cli.install.goose.process_common_args") + async def test_failure_exits(self, mock_process, mock_install): + mock_process.return_value = (Path("server.py"), None, "test-server", [], {}) + mock_install.return_value = False + with pytest.raises(SystemExit) as exc_info: + await goose_command("server.py") + assert exc_info.value.code == 1 diff --git a/tests/cli/test_install.py b/tests/cli/test_install.py index 6cffefa78..81fde3c1c 100644 --- a/tests/cli/test_install.py +++ b/tests/cli/test_install.py @@ -1,6 +1,7 @@ from pathlib import Path from fastmcp.cli.install import install_app +from fastmcp.cli.install.stdio import install_stdio class TestInstallApp: @@ -25,7 +26,9 @@ class TestInstallApp: install_app.parse_args(["claude-desktop", "--help"]) install_app.parse_args(["cursor", "--help"]) install_app.parse_args(["gemini-cli", "--help"]) + install_app.parse_args(["goose", "--help"]) install_app.parse_args(["mcp-json", "--help"]) + install_app.parse_args(["stdio", "--help"]) except SystemExit: # Help commands exit with 0, that's expected pass @@ -163,6 +166,53 @@ class TestCursorInstall: assert bound.arguments["server_name"] == "test-server" +class TestGooseInstall: + """Test goose install command.""" + + def test_goose_basic(self): + """Test basic goose install command parsing.""" + command, bound, _ = install_app.parse_args( + ["goose", "server.py", "--name", "test-server"] + ) + + assert command is not None + assert bound.arguments["server_spec"] == "server.py" + assert bound.arguments["server_name"] == "test-server" + + def test_goose_with_options(self): + """Test goose install with various options.""" + command, bound, _ = install_app.parse_args( + [ + "goose", + "server.py", + "--name", + "test-server", + "--with", + "package1", + "--with", + "package2", + "--env", + "VAR1=value1", + ] + ) + + assert bound.arguments["with_packages"] == ["package1", "package2"] + assert bound.arguments["env_vars"] == ["VAR1=value1"] + + def test_goose_with_python(self): + """Test goose install with --python option.""" + command, bound, _ = install_app.parse_args( + [ + "goose", + "server.py", + "--python", + "3.11", + ] + ) + + assert bound.arguments["python"] == "3.11" + + class TestMcpJsonInstall: """Test mcp-json install command.""" @@ -185,6 +235,74 @@ class TestMcpJsonInstall: assert bound.arguments["copy"] is True +class TestStdioInstall: + """Test stdio install command.""" + + def test_stdio_basic(self): + """Test basic stdio install command parsing.""" + command, bound, _ = install_app.parse_args(["stdio", "server.py"]) + + assert command is not None + assert bound.arguments["server_spec"] == "server.py" + + def test_stdio_with_copy(self): + """Test stdio install with copy to clipboard option.""" + command, bound, _ = install_app.parse_args(["stdio", "server.py", "--copy"]) + + assert bound.arguments["copy"] is True + + def test_stdio_with_packages(self): + """Test stdio install with additional packages.""" + command, bound, _ = install_app.parse_args( + ["stdio", "server.py", "--with", "requests", "--with", "httpx"] + ) + + assert bound.arguments["with_packages"] == ["requests", "httpx"] + + def test_install_stdio_generates_command(self, tmp_path: Path): + """Test that install_stdio produces a shell command containing fastmcp run.""" + server_file = tmp_path / "server.py" + server_file.write_text("# placeholder") + + # Capture stdout + import io + import sys + + captured = io.StringIO() + old_stdout = sys.stdout + sys.stdout = captured + try: + result = install_stdio(file=server_file, server_object=None) + finally: + sys.stdout = old_stdout + + assert result is True + output = captured.getvalue() + assert "fastmcp" in output + assert "run" in output + assert str(server_file.resolve()) in output + + def test_install_stdio_with_object(self, tmp_path: Path): + """Test that install_stdio includes the :object suffix.""" + server_file = tmp_path / "server.py" + server_file.write_text("# placeholder") + + import io + import sys + + captured = io.StringIO() + old_stdout = sys.stdout + sys.stdout = captured + try: + result = install_stdio(file=server_file, server_object="app") + finally: + sys.stdout = old_stdout + + assert result is True + output = captured.getvalue() + assert f"{server_file.resolve()}:app" in output + + class TestGeminiCliInstall: """Test gemini-cli install command.""" @@ -253,6 +371,8 @@ class TestInstallCommandParsing: ["claude-desktop", "server.py"], ["cursor", "server.py"], ["gemini-cli", "server.py"], + ["goose", "server.py"], + ["stdio", "server.py"], ] for cmd_args in commands_to_test: @@ -267,6 +387,12 @@ class TestInstallCommandParsing: assert command is not None assert bound.arguments["server_spec"] == "server.py" + def test_stdio_minimal(self): + """Test that stdio works with minimal arguments.""" + command, bound, _ = install_app.parse_args(["stdio", "server.py"]) + assert command is not None + assert bound.arguments["server_spec"] == "server.py" + def test_python_option(self): """Test --python option for all install commands.""" commands_to_test = [ @@ -274,7 +400,9 @@ class TestInstallCommandParsing: ["claude-desktop", "server.py", "--python", "3.11"], ["cursor", "server.py", "--python", "3.11"], ["gemini-cli", "server.py", "--python", "3.11"], + ["goose", "server.py", "--python", "3.11"], ["mcp-json", "server.py", "--python", "3.11"], + ["stdio", "server.py", "--python", "3.11"], ] for cmd_args in commands_to_test: @@ -290,6 +418,7 @@ class TestInstallCommandParsing: ["cursor", "server.py", "--with-requirements", "requirements.txt"], ["gemini-cli", "server.py", "--with-requirements", "requirements.txt"], ["mcp-json", "server.py", "--with-requirements", "requirements.txt"], + ["stdio", "server.py", "--with-requirements", "requirements.txt"], ] for cmd_args in commands_to_test: @@ -305,6 +434,7 @@ class TestInstallCommandParsing: ["cursor", "server.py", "--project", "/path/to/project"], ["gemini-cli", "server.py", "--project", "/path/to/project"], ["mcp-json", "server.py", "--project", "/path/to/project"], + ["stdio", "server.py", "--project", "/path/to/project"], ] for cmd_args in commands_to_test: diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index 90582b684..184ea1dde 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -605,24 +605,93 @@ mcp = fastmcp.FastMCP("TestServer") class TestReloadFunctionality: """Test reload functionality.""" - def test_python_file_filter_accepts_py_files(self): - """Test that Python file filter accepts .py files.""" + def test_watch_filter_accepts_watched_extensions(self): + """Test that watch filter accepts common source file extensions.""" from watchfiles import Change - from fastmcp.cli.run import _python_file_filter + from fastmcp.cli.run import _watch_filter - assert _python_file_filter(Change.modified, "/path/to/file.py") is True - assert _python_file_filter(Change.added, "server.py") is True - assert _python_file_filter(Change.deleted, "/some/dir/module.py") is True + # Python + assert _watch_filter(Change.modified, "/path/to/file.py") is True + assert _watch_filter(Change.added, "server.py") is True + # JavaScript/TypeScript + assert _watch_filter(Change.modified, "/path/to/file.js") is True + assert _watch_filter(Change.modified, "/path/to/file.ts") is True + assert _watch_filter(Change.modified, "/path/to/file.jsx") is True + assert _watch_filter(Change.modified, "/path/to/file.tsx") is True + # Markup/Content + assert _watch_filter(Change.modified, "/path/to/file.html") is True + assert _watch_filter(Change.modified, "/path/to/file.md") is True + assert _watch_filter(Change.modified, "/path/to/file.txt") is True + # Styles + assert _watch_filter(Change.modified, "/path/to/file.css") is True + assert _watch_filter(Change.modified, "/path/to/file.scss") is True + # Data/Config + assert _watch_filter(Change.modified, "/path/to/file.json") is True + assert _watch_filter(Change.modified, "/path/to/file.yaml") is True + # Images + assert _watch_filter(Change.modified, "/path/to/file.png") is True + assert _watch_filter(Change.modified, "/path/to/file.svg") is True - def test_python_file_filter_rejects_non_py_files(self): - """Test that Python file filter rejects non-.py files.""" + def test_watch_filter_rejects_unwatched_extensions(self): + """Test that watch filter rejects files not in the watched set.""" from watchfiles import Change - from fastmcp.cli.run import _python_file_filter + from fastmcp.cli.run import _watch_filter - assert _python_file_filter(Change.modified, "/path/to/file.txt") is False - assert _python_file_filter(Change.modified, "/path/to/file.js") is False - assert _python_file_filter(Change.modified, "/path/to/file.pyc") is False - assert _python_file_filter(Change.modified, "/path/to/.py") is True # Edge case - assert _python_file_filter(Change.modified, "Dockerfile") is False + assert _watch_filter(Change.modified, "/path/to/file.pyc") is False + assert _watch_filter(Change.modified, "/path/to/file.pyo") is False + assert _watch_filter(Change.modified, "Dockerfile") is False + assert _watch_filter(Change.modified, "/path/to/file.lock") is False + assert _watch_filter(Change.modified, "/path/to/.gitignore") is False + + def test_all_watched_extensions_are_accepted(self): + """Test that every extension in WATCHED_EXTENSIONS is accepted.""" + from watchfiles import Change + + from fastmcp.cli.run import WATCHED_EXTENSIONS, _watch_filter + + for ext in WATCHED_EXTENSIONS: + path = f"/path/to/file{ext}" + assert _watch_filter(Change.modified, path) is True, ( + f"Expected {ext} to be watched" + ) + + def test_watched_extensions_includes_frontend_types(self): + """Verify WATCHED_EXTENSIONS contains the expected frontend file types.""" + from fastmcp.cli.run import WATCHED_EXTENSIONS + + # Core frontend extensions that must be present + expected = { + # Python + ".py", + # JavaScript/TypeScript + ".js", + ".ts", + ".jsx", + ".tsx", + # Markup + ".html", + ".md", + ".mdx", + ".xml", + # Styles + ".css", + ".scss", + ".sass", + ".less", + # Data/Config + ".json", + ".yaml", + ".yml", + ".toml", + # Images + ".png", + ".jpg", + ".svg", + # Media + ".mp4", + ".mp3", + } + for ext in expected: + assert ext in WATCHED_EXTENSIONS, f"Expected {ext} in WATCHED_EXTENSIONS" diff --git a/tests/server/auth/providers/test_azure.py b/tests/server/auth/providers/test_azure.py index e3fb13931..165d3229b 100644 --- a/tests/server/auth/providers/test_azure.py +++ b/tests/server/auth/providers/test_azure.py @@ -48,6 +48,36 @@ class TestAzureProvider: assert provider._redirect_path == "/auth/callback" # Azure provider defaults are set but we can't easily verify them without accessing internals + def test_offline_access_automatically_included(self): + """Test that offline_access is automatically added to get refresh tokens.""" + # Without specifying offline_access + provider = AzureProvider( + client_id="test_client", + client_secret="test_secret", + tenant_id="test-tenant", + base_url="https://myserver.com", + required_scopes=["read"], + jwt_signing_key="test-secret", + ) + + assert "offline_access" in provider.additional_authorize_scopes + + def test_offline_access_not_duplicated(self): + """Test that offline_access is not duplicated if already specified.""" + provider = AzureProvider( + client_id="test_client", + client_secret="test_secret", + tenant_id="test-tenant", + base_url="https://myserver.com", + required_scopes=["read"], + additional_authorize_scopes=["User.Read", "offline_access"], + jwt_signing_key="test-secret", + ) + + # Should appear exactly once + assert provider.additional_authorize_scopes.count("offline_access") == 1 + assert "User.Read" in provider.additional_authorize_scopes + def test_oauth_endpoints_configured_correctly(self): """Test that OAuth endpoints are configured correctly.""" provider = AzureProvider( @@ -414,7 +444,8 @@ class TestAzureProvider: assert "api://my-api/read" in result assert "api://my-api/write" in result - assert len(result) == 2 + assert "offline_access" in result # Auto-included for refresh tokens + assert len(result) == 3 def test_prepare_scopes_for_upstream_refresh_already_prefixed(self): """Test that already-prefixed scopes remain unchanged.""" @@ -435,10 +466,15 @@ class TestAzureProvider: assert "api://my-api/read" in result assert "api://other-api/admin" in result - assert len(result) == 2 + assert "offline_access" in result # Auto-included for refresh tokens + assert len(result) == 3 def test_prepare_scopes_for_upstream_refresh_with_additional_scopes(self): - """Test that additional_authorize_scopes are added during token refresh.""" + """Test that only OIDC scopes from additional_authorize_scopes are added. + + Azure only allows ONE resource per token request (AADSTS28000), so + non-OIDC scopes like User.Read are excluded from refresh requests. + """ provider = AzureProvider( client_id="test_client", client_secret="test_secret", @@ -447,7 +483,7 @@ class TestAzureProvider: identifier_uri="api://my-api", required_scopes=["read"], additional_authorize_scopes=[ - "User.Read", + "User.Read", # Not OIDC - excluded "openid", "profile", "offline_access", @@ -455,16 +491,16 @@ class TestAzureProvider: jwt_signing_key="test-secret", ) - # Base scopes should be prefixed, additional scopes appended + # Base scopes should be prefixed, only OIDC scopes appended result = provider._prepare_scopes_for_upstream_refresh(["read", "write"]) assert "api://my-api/read" in result assert "api://my-api/write" in result - assert "User.Read" in result + assert "User.Read" not in result # Not OIDC, excluded assert "openid" in result assert "profile" in result assert "offline_access" in result - assert len(result) == 6 + assert len(result) == 5 def test_prepare_scopes_for_upstream_refresh_filters_duplicate_additional_scopes( self, @@ -482,15 +518,17 @@ class TestAzureProvider: ) # If additional scopes were accidentally stored, they should be filtered - # to prevent accumulation + # User.Read is not OIDC so won't be added result = provider._prepare_scopes_for_upstream_refresh( ["read", "User.Read", "openid"] ) - # Should have: api://my-api/read (prefixed) + User.Read + openid (added once) + # Should have: api://my-api/read (prefixed) + openid + offline_access (OIDC scopes) + # User.Read is filtered from storage AND not added (not OIDC) assert "api://my-api/read" in result - assert result.count("User.Read") == 1 + assert "User.Read" not in result # Not OIDC assert result.count("openid") == 1 + assert "offline_access" in result # Auto-included and is OIDC assert len(result) == 3 def test_prepare_scopes_for_upstream_refresh_mixed_scopes(self): @@ -502,7 +540,7 @@ class TestAzureProvider: base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], - additional_authorize_scopes=["User.Read"], + additional_authorize_scopes=["openid"], # OIDC scope jwt_signing_key="test-secret", ) @@ -514,8 +552,9 @@ class TestAzureProvider: assert "api://my-api/read" in result assert "api://other-api/admin" in result # Already prefixed, unchanged assert "api://my-api/write" in result - assert "User.Read" in result - assert len(result) == 4 + assert "openid" in result + assert "offline_access" in result # Auto-included + assert len(result) == 5 def test_prepare_scopes_for_upstream_refresh_scope_with_slash(self): """Test that scopes containing '/' are not prefixed.""" @@ -552,12 +591,13 @@ class TestAzureProvider: jwt_signing_key="test-secret", ) - # Empty scopes should still add additional_authorize_scopes + # Empty scopes should still add OIDC scopes (not User.Read) result = provider._prepare_scopes_for_upstream_refresh([]) - assert "User.Read" in result + assert "User.Read" not in result # Not OIDC assert "openid" in result - assert len(result) == 2 + assert "offline_access" in result # Auto-included + assert len(result) == 2 # Only OIDC scopes: openid + offline_access def test_prepare_scopes_for_upstream_refresh_no_additional_scopes(self): """Test behavior when no additional_authorize_scopes are configured.""" @@ -571,12 +611,13 @@ class TestAzureProvider: jwt_signing_key="test-secret", ) - # Should only prefix base scopes, no additional scopes added + # Should prefix base scopes, plus auto-added offline_access result = provider._prepare_scopes_for_upstream_refresh(["read", "write"]) assert "api://my-api/read" in result assert "api://my-api/write" in result - assert len(result) == 2 + assert "offline_access" in result # Auto-included + assert len(result) == 3 def test_prepare_scopes_for_upstream_refresh_deduplicates_scopes(self): """Test that duplicate scopes are deduplicated while preserving order.""" @@ -587,23 +628,24 @@ class TestAzureProvider: base_url="https://myserver.com", identifier_uri="api://my-api", required_scopes=["read"], - additional_authorize_scopes=["User.Read", "openid"], + additional_authorize_scopes=["openid", "profile"], # OIDC scopes only jwt_signing_key="test-secret", ) - # Test with duplicate base scopes and duplicate additional scopes + # Test with duplicate base scopes result = provider._prepare_scopes_for_upstream_refresh( - ["read", "write", "read", "User.Read", "openid"] + ["read", "write", "read", "openid"] ) - # Should have deduplicated results in order + # Should have deduplicated results in order (OIDC scopes added, offline_access auto-added) assert result == [ "api://my-api/read", "api://my-api/write", - "User.Read", "openid", + "profile", + "offline_access", ] - assert len(result) == 4 + assert len(result) == 5 def test_prepare_scopes_for_upstream_refresh_deduplicates_prefixed_variants(self): """Test that both prefixed and unprefixed variants are deduplicated.""" @@ -625,8 +667,9 @@ class TestAzureProvider: # Should deduplicate - first occurrence wins (api://my-api/read from "read") assert "api://my-api/read" in result assert "api://my-api/write" in result - # Should only have 2 items (read processed twice, but deduplicated) - assert len(result) == 2 + assert "offline_access" in result # Auto-included + # Should have 3 items (read deduplicated, plus offline_access) + assert len(result) == 3 assert result.count("api://my-api/read") == 1 @@ -809,242 +852,134 @@ class TestOIDCScopeHandling: assert "api://my-api/profile" not in result -class TestAzureExtractUpstreamClaims: - """Tests for Azure provider's _extract_upstream_claims method.""" +class TestAzureTokenExchangeScopes: + """Tests for Azure provider's token exchange scope handling. - @staticmethod - def create_test_jwt(claims: dict) -> str: - """Create a test JWT token with the given claims.""" - import base64 - import json + Azure requires scopes to be sent during the authorization code exchange. + The provider overrides _prepare_scopes_for_token_exchange to return + properly prefixed scopes. + """ - header = base64.urlsafe_b64encode( - json.dumps({"alg": "RS256", "typ": "JWT"}).encode() - ).rstrip(b"=") - payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).rstrip(b"=") - signature = base64.urlsafe_b64encode(b"fake-signature").rstrip(b"=") - return f"{header.decode()}.{payload.decode()}.{signature.decode()}" - - async def test_extract_claims_from_azure_jwt(self): - """Test that Azure identity claims are extracted from access token.""" + def test_prepare_scopes_returns_prefixed_scopes(self): + """Test that _prepare_scopes_for_token_exchange returns prefixed scopes.""" provider = AzureProvider( client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", base_url="https://myserver.com", - required_scopes=["read"], + identifier_uri="api://my-api", + required_scopes=["read", "write"], jwt_signing_key="test-secret", ) - azure_jwt = self.create_test_jwt( - { - "sub": "user-subject-id", - "oid": "user-object-id", - "tid": "tenant-id-123", - "azp": "client-app-id", - "name": "Test User", - "given_name": "Test", - "family_name": "User", - "preferred_username": "testuser@example.com", - "upn": "testuser@example.com", - "email": "test@example.com", - "roles": ["Admin", "Reader"], - "groups": ["group-1", "group-2"], - "exp": 9999999999, - "iat": 1234567890, - "iss": "https://login.microsoftonline.com/test-tenant/v2.0", - } - ) + scopes = provider._prepare_scopes_for_token_exchange(["read", "write"]) + assert len(scopes) > 0 + assert "api://my-api/read" in scopes + assert "api://my-api/write" in scopes - idp_tokens = { - "access_token": azure_jwt, - "token_type": "Bearer", - "expires_in": 3600, - } - - claims = await provider._extract_upstream_claims(idp_tokens) - - assert claims is not None - assert claims["sub"] == "user-subject-id" - assert claims["oid"] == "user-object-id" - assert claims["tid"] == "tenant-id-123" - assert claims["azp"] == "client-app-id" - assert claims["name"] == "Test User" - assert claims["given_name"] == "Test" - assert claims["family_name"] == "User" - assert claims["preferred_username"] == "testuser@example.com" - assert claims["upn"] == "testuser@example.com" - assert claims["email"] == "test@example.com" - assert claims["roles"] == ["Admin", "Reader"] - assert claims["groups"] == ["group-1", "group-2"] - - async def test_extract_claims_only_includes_identity_claims(self): - """Test that only identity claims are extracted, not all JWT claims.""" + def test_prepare_scopes_includes_additional_oidc_scopes(self): + """Test that _prepare_scopes_for_token_exchange includes OIDC scopes.""" provider = AzureProvider( client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", base_url="https://myserver.com", + identifier_uri="api://my-api", required_scopes=["read"], + additional_authorize_scopes=["openid", "profile", "offline_access"], jwt_signing_key="test-secret", ) - azure_jwt = self.create_test_jwt( - { - "sub": "user-id", - "oid": "object-id", - "name": "Test User", - "exp": 9999999999, - "iat": 1234567890, - "iss": "https://issuer.example.com", - "aud": "test-audience", - "nbf": 1234567890, - "scp": "read write", - "azp": "some-client", - } + scopes = provider._prepare_scopes_for_token_exchange(["read"]) + assert len(scopes) > 0 + assert "api://my-api/read" in scopes + assert "openid" in scopes + assert "profile" in scopes + assert "offline_access" in scopes + + def test_prepare_scopes_excludes_other_api_scopes(self): + """Test token exchange excludes other API scopes (Azure AADSTS28000). + + Azure only allows ONE resource per token exchange. Other API scopes + are requested during authorization but excluded from token exchange. + """ + provider = AzureProvider( + client_id="00000000-1111-2222-3333-444444444444", + client_secret="test_secret", + tenant_id="test-tenant", + base_url="https://myserver.com", + required_scopes=["user_impersonation"], + additional_authorize_scopes=[ + "openid", + "profile", + "offline_access", + "api://aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/user_impersonation", + "api://11111111-2222-3333-4444-555555555555/user_impersonation", + ], + jwt_signing_key="test-secret", ) - idp_tokens = {"access_token": azure_jwt} + scopes = provider._prepare_scopes_for_token_exchange(["user_impersonation"]) + assert len(scopes) > 0 + # Primary API scope should be prefixed with the provider's identifier_uri + assert "api://00000000-1111-2222-3333-444444444444/user_impersonation" in scopes + # OIDC scopes should be included + assert "openid" in scopes + assert "profile" in scopes + assert "offline_access" in scopes + # Other API scopes should NOT be included (Azure multi-resource limitation) + assert not any("api://aaaaaaaa" in s for s in scopes) + assert not any("api://11111111" in s for s in scopes) - claims = await provider._extract_upstream_claims(idp_tokens) - - # Only identity claims should be present - assert claims is not None - assert "sub" in claims - assert "oid" in claims - assert "name" in claims - assert "azp" in claims # azp is an identity claim we extract - # Standard JWT claims should NOT be extracted - assert "exp" not in claims - assert "iat" not in claims - assert "iss" not in claims - assert "aud" not in claims - assert "nbf" not in claims - assert "scp" not in claims - - async def test_extract_claims_returns_none_for_missing_access_token(self): - """Test that None is returned when access_token is missing.""" + def test_prepare_scopes_deduplicates_scopes(self): + """Test that duplicate scopes are deduplicated.""" provider = AzureProvider( client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", base_url="https://myserver.com", + identifier_uri="api://my-api", required_scopes=["read"], + additional_authorize_scopes=["api://my-api/read", "openid"], jwt_signing_key="test-secret", ) - idp_tokens = {"token_type": "Bearer", "expires_in": 3600} + # Pass a scope that will be prefixed to match one in additional_authorize_scopes + scopes = provider._prepare_scopes_for_token_exchange(["read"]) + assert len(scopes) > 0 + # Should be deduplicated - api://my-api/read appears only once + assert scopes.count("api://my-api/read") == 1 + assert "openid" in scopes - claims = await provider._extract_upstream_claims(idp_tokens) + def test_extra_token_params_does_not_contain_scope(self): + """Test that extra_token_params doesn't contain scope to avoid TypeError. - assert claims is None + Previously, Azure provider set extra_token_params={"scope": ...} during init. + This caused a TypeError in exchange_refresh_token because it passes both + scope=... AND **self._extra_token_params, resulting in: + "got multiple values for keyword argument 'scope'" - async def test_extract_claims_returns_none_for_opaque_token(self): - """Test that None is returned for opaque (non-JWT) tokens.""" + The fix uses the _prepare_scopes_for_token_exchange hook instead. + """ provider = AzureProvider( client_id="test_client", client_secret="test_secret", tenant_id="test-tenant", base_url="https://myserver.com", - required_scopes=["read"], + identifier_uri="api://my-api", + required_scopes=["read", "write"], + additional_authorize_scopes=["openid", "profile", "offline_access"], jwt_signing_key="test-secret", ) - idp_tokens = { - "access_token": "gho_opaque_token_not_a_jwt", # Not a JWT - "token_type": "Bearer", - } + # extra_token_params should NOT contain "scope" to avoid TypeError during refresh + assert "scope" not in provider._extra_token_params - claims = await provider._extract_upstream_claims(idp_tokens) + # Instead, scopes should be provided via the hook methods + exchange_scopes = provider._prepare_scopes_for_token_exchange(["read", "write"]) + assert len(exchange_scopes) > 0 - assert claims is None - - async def test_extract_claims_returns_none_for_malformed_jwt(self): - """Test that None is returned for malformed JWT tokens.""" - provider = AzureProvider( - client_id="test_client", - client_secret="test_secret", - tenant_id="test-tenant", - base_url="https://myserver.com", - required_scopes=["read"], - jwt_signing_key="test-secret", + refresh_scopes = provider._prepare_scopes_for_upstream_refresh( + ["read", "write"] ) - - # Only two parts (missing signature) - idp_tokens = {"access_token": "header.payload"} - - claims = await provider._extract_upstream_claims(idp_tokens) - - assert claims is None - - async def test_extract_claims_returns_none_for_invalid_base64(self): - """Test that None is returned for JWT with invalid base64.""" - provider = AzureProvider( - client_id="test_client", - client_secret="test_secret", - tenant_id="test-tenant", - base_url="https://myserver.com", - required_scopes=["read"], - jwt_signing_key="test-secret", - ) - - # Invalid base64 in payload - idp_tokens = {"access_token": "header.not-valid-base64!!!.signature"} - - claims = await provider._extract_upstream_claims(idp_tokens) - - assert claims is None - - async def test_extract_claims_returns_none_for_empty_identity_claims(self): - """Test that None is returned when no identity claims are present.""" - provider = AzureProvider( - client_id="test_client", - client_secret="test_secret", - tenant_id="test-tenant", - base_url="https://myserver.com", - required_scopes=["read"], - jwt_signing_key="test-secret", - ) - - # JWT with only standard claims, no identity claims - azure_jwt = self.create_test_jwt( - { - "exp": 9999999999, - "iat": 1234567890, - "iss": "https://issuer.example.com", - "aud": "test-audience", - } - ) - - idp_tokens = {"access_token": azure_jwt} - - claims = await provider._extract_upstream_claims(idp_tokens) - - assert claims is None - - async def test_extract_claims_partial_identity_claims(self): - """Test extraction when only some identity claims are present.""" - provider = AzureProvider( - client_id="test_client", - client_secret="test_secret", - tenant_id="test-tenant", - base_url="https://myserver.com", - required_scopes=["read"], - jwt_signing_key="test-secret", - ) - - # JWT with only sub and name - azure_jwt = self.create_test_jwt( - { - "sub": "user-id", - "name": "Test User", - "exp": 9999999999, - } - ) - - idp_tokens = {"access_token": azure_jwt} - - claims = await provider._extract_upstream_claims(idp_tokens) - - assert claims is not None - assert claims == {"sub": "user-id", "name": "Test User"} + assert len(refresh_scopes) > 0 diff --git a/tests/server/middleware/test_error_handling.py b/tests/server/middleware/test_error_handling.py index bf5188712..3bac578d7 100644 --- a/tests/server/middleware/test_error_handling.py +++ b/tests/server/middleware/test_error_handling.py @@ -101,87 +101,95 @@ class TestErrorHandlingMiddleware: assert "Error in error callback: callback error" in caplog.text - def test_transform_error_mcp_error(self): + def test_transform_error_mcp_error(self, mock_context): """Test that MCP errors are not transformed.""" middleware = ErrorHandlingMiddleware() from mcp.types import ErrorData error = McpError(ErrorData(code=-32001, message="test error")) - result = middleware._transform_error(error) + result = middleware._transform_error(error, mock_context) assert result is error - def test_transform_error_disabled(self): + def test_transform_error_disabled(self, mock_context): """Test error transformation when disabled.""" middleware = ErrorHandlingMiddleware(transform_errors=False) error = ValueError("test error") - result = middleware._transform_error(error) + result = middleware._transform_error(error, mock_context) assert result is error - def test_transform_error_value_error(self): + def test_transform_error_value_error(self, mock_context): """Test transforming ValueError.""" middleware = ErrorHandlingMiddleware() error = ValueError("test error") - result = middleware._transform_error(error) + result = middleware._transform_error(error, mock_context) assert isinstance(result, McpError) assert result.error.code == -32602 assert "Invalid params: test error" in result.error.message - def test_transform_error_file_not_found(self): - """Test transforming FileNotFoundError.""" + def test_transform_error_not_found_for_resource_method(self): + """Test that not-found errors use -32002 for resource methods.""" middleware = ErrorHandlingMiddleware() - error = FileNotFoundError("test error") + resource_context = MagicMock(spec=MiddlewareContext) + resource_context.method = "resources/read" - result = middleware._transform_error(error) + for error in [ + FileNotFoundError("test error"), + NotFoundError("test error"), + ]: + result = middleware._transform_error(error, resource_context) - assert isinstance(result, McpError) - assert result.error.code == -32001 - assert "Resource not found: test error" in result.error.message + assert isinstance(result, McpError) + assert result.error.code == -32002 + assert "Resource not found: test error" in result.error.message - def test_transform_error_not_found_error(self): - """Test transforming NotFoundError.""" + def test_transform_error_not_found_for_non_resource_method(self, mock_context): + """Test that not-found errors use -32001 for non-resource methods.""" middleware = ErrorHandlingMiddleware() - error = NotFoundError("test error") - result = middleware._transform_error(error) + for error in [ + FileNotFoundError("test error"), + NotFoundError("test error"), + ]: + result = middleware._transform_error(error, mock_context) - assert isinstance(result, McpError) - assert result.error.code == -32001 - assert "Resource not found: test error" in result.error.message + assert isinstance(result, McpError) + assert result.error.code == -32001 + assert "Not found: test error" in result.error.message - def test_transform_error_permission_error(self): + def test_transform_error_permission_error(self, mock_context): """Test transforming PermissionError.""" middleware = ErrorHandlingMiddleware() error = PermissionError("test error") - result = middleware._transform_error(error) + result = middleware._transform_error(error, mock_context) assert isinstance(result, McpError) assert result.error.code == -32000 assert "Permission denied: test error" in result.error.message - def test_transform_error_timeout_error(self): + def test_transform_error_timeout_error(self, mock_context): """Test transforming TimeoutError.""" middleware = ErrorHandlingMiddleware() error = TimeoutError("test error") - result = middleware._transform_error(error) + result = middleware._transform_error(error, mock_context) assert isinstance(result, McpError) assert result.error.code == -32000 assert "Request timeout: test error" in result.error.message - def test_transform_error_generic(self): + def test_transform_error_generic(self, mock_context): """Test transforming generic error.""" middleware = ErrorHandlingMiddleware() error = RuntimeError("test error") - result = middleware._transform_error(error) + result = middleware._transform_error(error, mock_context) assert isinstance(result, McpError) assert result.error.code == -32603 diff --git a/tests/server/test_dependencies.py b/tests/server/test_dependencies.py index 2be1313f2..7d4119c8e 100644 --- a/tests/server/test_dependencies.py +++ b/tests/server/test_dependencies.py @@ -67,7 +67,7 @@ async def test_depends_with_async_function(mcp: FastMCP): return 42 @mcp.tool() - async def greet_user(name: str, user_id: int = Depends(get_user_id)) -> str: # type: ignore[assignment] + async def greet_user(name: str, user_id: int = Depends(get_user_id)) -> str: return f"Hello {name}, your ID is {user_id}" result = await mcp.call_tool("greet_user", {"name": "Alice"}) @@ -198,7 +198,7 @@ async def test_sync_tool_with_async_dependency(mcp: FastMCP): return "loaded_config" @mcp.tool() - def process_data(value: int, config: str = Depends(fetch_config)) -> str: # type: ignore[assignment] + def process_data(value: int, config: str = Depends(fetch_config)) -> str: return f"Processing {value} with {config}" result = await mcp.call_tool("process_data", {"value": 100}) diff --git a/tests/test_apps.py b/tests/test_apps.py new file mode 100644 index 000000000..348eab36b --- /dev/null +++ b/tests/test_apps.py @@ -0,0 +1,525 @@ +"""Tests for MCP Apps Phase 1 — SDK compatibility. + +Covers UI metadata models, tool/resource registration with ``ui=``, +extension negotiation, and the ``Context.client_supports_extension`` method. +""" + +from __future__ import annotations + +from typing import Any + +from fastmcp import Client, FastMCP +from fastmcp.server.apps import ( + UI_EXTENSION_ID, + UI_MIME_TYPE, + ResourceCSP, + ResourcePermissions, + ResourceUI, + ToolUI, + ui_to_meta_dict, +) +from fastmcp.server.context import Context + +# --------------------------------------------------------------------------- +# Model serialization +# --------------------------------------------------------------------------- + + +class TestToolUI: + def test_serializes_with_aliases(self): + ui = ToolUI(resource_uri="ui://my-app/view.html", visibility=["app"]) + d = ui.model_dump(by_alias=True, exclude_none=True) + assert d == {"resourceUri": "ui://my-app/view.html", "visibility": ["app"]} + + def test_excludes_none_fields(self): + ui = ToolUI(resource_uri="ui://foo") + d = ui.model_dump(by_alias=True, exclude_none=True) + assert d == {"resourceUri": "ui://foo"} + + def test_all_fields(self): + ui = ToolUI( + resource_uri="ui://app", + visibility=["app", "model"], + csp=ResourceCSP(resource_domains=["https://cdn.example.com"]), + permissions=ResourcePermissions(camera={}, clipboard_write={}), + domain="example.com", + prefers_border=True, + ) + d = ui.model_dump(by_alias=True, exclude_none=True) + assert d == { + "resourceUri": "ui://app", + "visibility": ["app", "model"], + "csp": {"resourceDomains": ["https://cdn.example.com"]}, + "permissions": {"camera": {}, "clipboardWrite": {}}, + "domain": "example.com", + "prefersBorder": True, + } + + def test_populate_by_name(self): + ui = ToolUI(resource_uri="ui://app") + assert ui.resource_uri == "ui://app" + + +class TestResourceCSP: + def test_serializes_with_aliases(self): + csp = ResourceCSP( + connect_domains=["https://api.example.com"], + resource_domains=["https://cdn.example.com"], + ) + d = csp.model_dump(by_alias=True, exclude_none=True) + assert d == { + "connectDomains": ["https://api.example.com"], + "resourceDomains": ["https://cdn.example.com"], + } + + def test_excludes_none_fields(self): + csp = ResourceCSP(resource_domains=["https://unpkg.com"]) + d = csp.model_dump(by_alias=True, exclude_none=True) + assert d == {"resourceDomains": ["https://unpkg.com"]} + + def test_all_fields(self): + csp = ResourceCSP( + connect_domains=["https://api.example.com"], + resource_domains=["https://cdn.example.com"], + frame_domains=["https://embed.example.com"], + base_uri_domains=["https://base.example.com"], + ) + d = csp.model_dump(by_alias=True, exclude_none=True) + assert d == { + "connectDomains": ["https://api.example.com"], + "resourceDomains": ["https://cdn.example.com"], + "frameDomains": ["https://embed.example.com"], + "baseUriDomains": ["https://base.example.com"], + } + + def test_populate_by_name(self): + csp = ResourceCSP(connect_domains=["https://api.example.com"]) + assert csp.connect_domains == ["https://api.example.com"] + + def test_empty(self): + csp = ResourceCSP() + d = csp.model_dump(by_alias=True, exclude_none=True) + assert d == {} + + def test_extra_fields_preserved(self): + """Unknown CSP directives from future spec versions pass through.""" + csp = ResourceCSP( + resource_domains=["https://cdn.example.com"], + **{"workerDomains": ["https://worker.example.com"]}, + ) + d = csp.model_dump(by_alias=True, exclude_none=True) + assert d["resourceDomains"] == ["https://cdn.example.com"] + assert d["workerDomains"] == ["https://worker.example.com"] + + +class TestResourcePermissions: + def test_serializes_with_aliases(self): + perms = ResourcePermissions(microphone={}, clipboard_write={}) + d = perms.model_dump(by_alias=True, exclude_none=True) + assert d == {"microphone": {}, "clipboardWrite": {}} + + def test_excludes_none_fields(self): + perms = ResourcePermissions(camera={}) + d = perms.model_dump(by_alias=True, exclude_none=True) + assert d == {"camera": {}} + + def test_all_fields(self): + perms = ResourcePermissions( + camera={}, microphone={}, geolocation={}, clipboard_write={} + ) + d = perms.model_dump(by_alias=True, exclude_none=True) + assert d == { + "camera": {}, + "microphone": {}, + "geolocation": {}, + "clipboardWrite": {}, + } + + def test_populate_by_name(self): + perms = ResourcePermissions(clipboard_write={}) + assert perms.clipboard_write == {} + + def test_extra_fields_preserved(self): + """Unknown permissions from future spec versions pass through.""" + perms = ResourcePermissions(camera={}, **{"midi": {}}) + d = perms.model_dump(by_alias=True, exclude_none=True) + assert d["camera"] == {} + assert d["midi"] == {} + + def test_empty(self): + perms = ResourcePermissions() + d = perms.model_dump(by_alias=True, exclude_none=True) + assert d == {} + + +class TestResourceUI: + def test_serializes_with_aliases(self): + ui = ResourceUI( + prefers_border=True, + csp=ResourceCSP(resource_domains=["https://cdn.example.com"]), + ) + d = ui.model_dump(by_alias=True, exclude_none=True) + assert d == { + "prefersBorder": True, + "csp": {"resourceDomains": ["https://cdn.example.com"]}, + } + + def test_excludes_none_fields(self): + ui = ResourceUI() + d = ui.model_dump(by_alias=True, exclude_none=True) + assert d == {} + + def test_with_permissions(self): + ui = ResourceUI( + permissions=ResourcePermissions(microphone={}, clipboard_write={}), + ) + d = ui.model_dump(by_alias=True, exclude_none=True) + assert d == { + "permissions": {"microphone": {}, "clipboardWrite": {}}, + } + + +class TestUIToMetaDict: + def test_from_tool_ui(self): + ui = ToolUI(resource_uri="ui://app", visibility=["app"]) + result = ui_to_meta_dict(ui) + assert result["resourceUri"] == "ui://app" + assert result["visibility"] == ["app"] + + def test_from_resource_ui(self): + ui = ResourceUI(prefers_border=False) + result = ui_to_meta_dict(ui) + assert result == {"prefersBorder": False} + + def test_passthrough_for_dict(self): + raw: dict[str, Any] = {"resourceUri": "ui://app", "custom": "value"} + result = ui_to_meta_dict(raw) + assert result is raw + + +# --------------------------------------------------------------------------- +# Tool registration with ui= +# --------------------------------------------------------------------------- + + +class TestToolRegistrationWithUI: + async def test_tool_ui_model(self): + server = FastMCP("test") + + @server.tool(ui=ToolUI(resource_uri="ui://my-app/view.html")) + def my_tool() -> str: + return "hello" + + tools = list(await server.list_tools()) + assert len(tools) == 1 + assert tools[0].meta is not None + assert tools[0].meta["ui"]["resourceUri"] == "ui://my-app/view.html" + + async def test_tool_ui_dict(self): + server = FastMCP("test") + + @server.tool(ui={"resourceUri": "ui://foo", "visibility": ["app"]}) + def my_tool() -> str: + return "hello" + + tools = list(await server.list_tools()) + assert tools[0].meta is not None + assert tools[0].meta["ui"]["resourceUri"] == "ui://foo" + assert tools[0].meta["ui"]["visibility"] == ["app"] + + async def test_ui_merges_with_existing_meta(self): + server = FastMCP("test") + + @server.tool(meta={"custom": "data"}, ui=ToolUI(resource_uri="ui://app")) + def my_tool() -> str: + return "hello" + + tools = list(await server.list_tools()) + meta = tools[0].meta + assert meta is not None + assert meta["custom"] == "data" + assert meta["ui"]["resourceUri"] == "ui://app" + + async def test_ui_in_mcp_wire_format(self): + server = FastMCP("test") + + @server.tool(ui=ToolUI(resource_uri="ui://app", visibility=["app"])) + def my_tool() -> str: + return "hello" + + tools = list(await server.list_tools()) + mcp_tool = tools[0].to_mcp_tool() + assert mcp_tool.meta is not None + assert mcp_tool.meta["ui"]["resourceUri"] == "ui://app" + assert mcp_tool.meta["ui"]["visibility"] == ["app"] + + async def test_tool_without_ui_has_no_ui_meta(self): + server = FastMCP("test") + + @server.tool + def my_tool() -> str: + return "hello" + + tools = list(await server.list_tools()) + meta = tools[0].meta + assert meta is None or "ui" not in meta + + +# --------------------------------------------------------------------------- +# Resource registration with ui:// and ui= +# --------------------------------------------------------------------------- + + +class TestResourceWithUI: + async def test_ui_scheme_defaults_mime_type(self): + server = FastMCP("test") + + @server.resource("ui://my-app/view.html") + def app_html() -> str: + return "hello" + + resources = list(await server.list_resources()) + assert len(resources) == 1 + assert resources[0].mime_type == UI_MIME_TYPE + + async def test_explicit_mime_type_overrides_ui_default(self): + server = FastMCP("test") + + @server.resource("ui://my-app/view.html", mime_type="text/html") + def app_html() -> str: + return "hello" + + resources = list(await server.list_resources()) + assert resources[0].mime_type == "text/html" + + async def test_resource_ui_metadata(self): + server = FastMCP("test") + + @server.resource( + "ui://my-app/view.html", + ui=ResourceUI(prefers_border=True), + ) + def app_html() -> str: + return "hello" + + resources = list(await server.list_resources()) + assert resources[0].meta is not None + assert resources[0].meta["ui"]["prefersBorder"] is True + + async def test_non_ui_scheme_no_mime_default(self): + server = FastMCP("test") + + @server.resource("resource://data") + def data() -> str: + return "data" + + resources = list(await server.list_resources()) + assert resources[0].mime_type != UI_MIME_TYPE + + async def test_standalone_decorator_ui_scheme_defaults_mime_type(self): + """Test that the standalone @resource decorator also applies ui:// MIME default.""" + from fastmcp.resources import resource + + @resource("ui://standalone-app/view.html") + def standalone_app() -> str: + return "standalone" + + server = FastMCP("test") + server.add_resource(standalone_app) + + resources = list(await server.list_resources()) + assert len(resources) == 1 + assert resources[0].mime_type == UI_MIME_TYPE + + async def test_resource_template_ui_scheme_defaults_mime_type(self): + """Test that resource templates also apply ui:// MIME default.""" + server = FastMCP("test") + + @server.resource("ui://template-app/{view}") + def template_app(view: str) -> str: + return f"{view}" + + templates = list(await server.list_resource_templates()) + assert len(templates) == 1 + assert templates[0].mime_type == UI_MIME_TYPE + + +# --------------------------------------------------------------------------- +# Extension advertisement +# --------------------------------------------------------------------------- + + +class TestExtensionAdvertisement: + async def test_capabilities_include_ui_extension(self): + server = FastMCP("test") + + @server.tool + def my_tool() -> str: + return "hello" + + async with Client(server) as client: + init_result = client.initialize_result + extras = init_result.capabilities.model_extra or {} + extensions = extras.get("extensions", {}) + assert UI_EXTENSION_ID in extensions + + +# --------------------------------------------------------------------------- +# Context.client_supports_extension +# --------------------------------------------------------------------------- + + +class TestContextClientSupportsExtension: + async def test_returns_false_when_no_session(self): + server = FastMCP("test") + async with Context(fastmcp=server) as ctx: + assert ctx.client_supports_extension(UI_EXTENSION_ID) is False + + +# --------------------------------------------------------------------------- +# Integration — full client↔server round-trip +# --------------------------------------------------------------------------- + + +class TestIntegration: + async def test_tool_with_ui_roundtrip(self): + """UI metadata flows through to clients — no server-side stripping.""" + server = FastMCP("test") + + @server.tool(ui=ToolUI(resource_uri="ui://app/view.html", visibility=["app"])) + async def my_tool() -> dict[str, str]: + return {"result": "ok"} + + async with Client(server) as client: + tools = await client.list_tools() + assert len(tools) == 1 + # _meta.ui is preserved — the host decides what to do with it + meta = tools[0].meta + assert meta is not None + assert meta["ui"]["resourceUri"] == "ui://app/view.html" + assert meta["ui"]["visibility"] == ["app"] + + async def test_resource_with_ui_scheme_roundtrip(self): + server = FastMCP("test") + + @server.resource("ui://my-app/view.html") + def app_html() -> str: + return "Hello" + + async with Client(server) as client: + resources = await client.list_resources() + assert len(resources) == 1 + assert str(resources[0].uri) == "ui://my-app/view.html" + assert resources[0].mimeType == UI_MIME_TYPE + + async def test_ui_resource_read_preserves_mime_type(self): + """Reading a ui:// resource returns content with the correct MIME type.""" + server = FastMCP("test") + + @server.resource("ui://my-app/view.html") + def app_html() -> str: + return "Hello" + + async with Client(server) as client: + result = await client.read_resource_mcp("ui://my-app/view.html") + assert len(result.contents) == 1 + assert result.contents[0].mimeType == UI_MIME_TYPE + + async def test_ui_tool_callable(self): + """A tool registered with ui= is still callable normally.""" + server = FastMCP("test") + + @server.tool(ui=ToolUI(resource_uri="ui://app")) + async def greet(name: str) -> str: + return f"Hello, {name}!" + + async with Client(server) as client: + result = await client.call_tool("greet", {"name": "Alice"}) + assert any("Hello, Alice!" in str(c) for c in result.content) + + async def test_extension_and_tool_together(self): + """Server advertises extension AND tool has UI meta (stored on FastMCP Tool).""" + server = FastMCP("test") + + @server.tool(ui=ToolUI(resource_uri="ui://dashboard", visibility=["app"])) + def dashboard() -> str: + return "data" + + # Verify the stored FastMCP Tool still has full metadata + tools = list(await server.list_tools()) + assert tools[0].meta is not None + assert tools[0].meta["ui"]["resourceUri"] == "ui://dashboard" + + # Verify the server advertises the extension + async with Client(server) as client: + extras = client.initialize_result.capabilities.model_extra or {} + assert UI_EXTENSION_ID in extras.get("extensions", {}) + + async def test_csp_and_permissions_roundtrip(self): + """CSP and permissions metadata flows through to clients correctly.""" + server = FastMCP("test") + + @server.resource( + "ui://secure-app/view.html", + ui=ResourceUI( + csp=ResourceCSP( + resource_domains=["https://unpkg.com"], + connect_domains=["https://api.example.com"], + ), + permissions=ResourcePermissions(microphone={}, clipboard_write={}), + ), + ) + def secure_app() -> str: + return "secure" + + @server.tool( + ui=ToolUI( + resource_uri="ui://secure-app/view.html", + csp=ResourceCSP(resource_domains=["https://cdn.example.com"]), + permissions=ResourcePermissions(camera={}), + ) + ) + def secure_tool() -> str: + return "result" + + async with Client(server) as client: + # Verify resource metadata + resources = await client.list_resources() + assert len(resources) == 1 + meta = resources[0].meta + assert meta is not None + assert meta["ui"]["csp"]["resourceDomains"] == ["https://unpkg.com"] + assert meta["ui"]["csp"]["connectDomains"] == ["https://api.example.com"] + assert meta["ui"]["permissions"]["microphone"] == {} + assert meta["ui"]["permissions"]["clipboardWrite"] == {} + + # Verify tool metadata + tools = await client.list_tools() + assert len(tools) == 1 + tool_meta = tools[0].meta + assert tool_meta is not None + assert tool_meta["ui"]["csp"]["resourceDomains"] == [ + "https://cdn.example.com" + ] + assert tool_meta["ui"]["permissions"]["camera"] == {} + + async def test_resource_read_propagates_meta_to_content_items(self): + """resources/read must include _meta on content items so hosts can read CSP.""" + server = FastMCP("test") + + @server.resource( + "ui://csp-app/view.html", + ui=ResourceUI( + csp=ResourceCSP(resource_domains=["https://unpkg.com"]), + ), + ) + def app_view() -> str: + return "app" + + async with Client(server) as client: + read_result = await client.read_resource_mcp("ui://csp-app/view.html") + content_item = read_result.contents[0] + assert content_item.meta is not None + assert content_item.meta["ui"]["csp"]["resourceDomains"] == [ + "https://unpkg.com" + ] diff --git a/uv.lock b/uv.lock index 0217b7a35..d70b4e42c 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.13'", @@ -126,11 +126,11 @@ wheels = [ [[package]] name = "cachetools" -version = "6.2.4" +version = "6.2.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/91/d9ae9a66b01102a18cd16db0cf4cd54187ffe10f0865cc80071a4104fbb3/cachetools-6.2.6.tar.gz", hash = "sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6", size = 32363, upload-time = "2026-01-27T20:32:59.956Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/f458fa2c388e79dd9d8b9b0c99f1d31b568f27388f2fdba7bb66bbc0c6ed/cachetools-6.2.6-py3-none-any.whl", hash = "sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda", size = 11668, upload-time = "2026-01-27T20:32:58.527Z" }, ] [[package]] @@ -345,101 +345,101 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.1" +version = "7.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, - { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, - { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, - { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, - { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, - { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, - { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, - { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, - { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, - { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, - { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, - { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, - { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, - { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, - { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, - { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, - { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, - { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, - { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, - { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, - { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, - { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, - { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, - { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, - { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, - { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, - { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, - { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, - { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, - { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, - { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, - { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, - { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, - { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, - { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, - { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, - { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, - { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, - { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, - { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, - { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, - { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, - { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, - { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, - { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, - { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, - { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, - { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, + { url = "https://files.pythonhosted.org/packages/a4/2d/63e37369c8e81a643afe54f76073b020f7b97ddbe698c5c944b51b0a2bc5/coverage-7.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f4af3b01763909f477ea17c962e2cca8f39b350a4e46e3a30838b2c12e31b81b", size = 218842, upload-time = "2026-01-25T12:57:15.3Z" }, + { url = "https://files.pythonhosted.org/packages/57/06/86ce882a8d58cbcb3030e298788988e618da35420d16a8c66dac34f138d0/coverage-7.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:36393bd2841fa0b59498f75466ee9bdec4f770d3254f031f23e8fd8e140ffdd2", size = 219360, upload-time = "2026-01-25T12:57:17.572Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/70b0eb1ee19ca4ef559c559054c59e5b2ae4ec9af61398670189e5d276e9/coverage-7.13.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cc7573518b7e2186bd229b1a0fe24a807273798832c27032c4510f47ffdb896", size = 246123, upload-time = "2026-01-25T12:57:19.087Z" }, + { url = "https://files.pythonhosted.org/packages/35/fb/05b9830c2e8275ebc031e0019387cda99113e62bb500ab328bb72578183b/coverage-7.13.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca9566769b69a5e216a4e176d54b9df88f29d750c5b78dbb899e379b4e14b30c", size = 247930, upload-time = "2026-01-25T12:57:20.929Z" }, + { url = "https://files.pythonhosted.org/packages/81/aa/3f37858ca2eed4f09b10ca3c6ddc9041be0a475626cd7fd2712f4a2d526f/coverage-7.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c9bdea644e94fd66d75a6f7e9a97bb822371e1fe7eadae2cacd50fcbc28e4dc", size = 249804, upload-time = "2026-01-25T12:57:22.904Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/c904f40c56e60a2d9678a5ee8df3d906d297d15fb8bec5756c3b0a67e2df/coverage-7.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5bd447332ec4f45838c1ad42268ce21ca87c40deb86eabd59888859b66be22a5", size = 246815, upload-time = "2026-01-25T12:57:24.314Z" }, + { url = "https://files.pythonhosted.org/packages/41/91/ddc1c5394ca7fd086342486440bfdd6b9e9bda512bf774599c7c7a0081e0/coverage-7.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c79ad5c28a16a1277e1187cf83ea8dafdcc689a784228a7d390f19776db7c31", size = 247843, upload-time = "2026-01-25T12:57:26.544Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/cdff8f4cd33697883c224ea8e003e9c77c0f1a837dc41d95a94dd26aad67/coverage-7.13.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:76e06ccacd1fb6ada5d076ed98a8c6f66e2e6acd3df02819e2ee29fd637b76ad", size = 245850, upload-time = "2026-01-25T12:57:28.507Z" }, + { url = "https://files.pythonhosted.org/packages/f5/42/e837febb7866bf2553ab53dd62ed52f9bb36d60c7e017c55376ad21fbb05/coverage-7.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:49d49e9a5e9f4dc3d3dac95278a020afa6d6bdd41f63608a76fa05a719d5b66f", size = 246116, upload-time = "2026-01-25T12:57:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/09/b1/4a3f935d7df154df02ff4f71af8d61298d713a7ba305d050ae475bfbdde2/coverage-7.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed2bce0e7bfa53f7b0b01c722da289ef6ad4c18ebd52b1f93704c21f116360c8", size = 246720, upload-time = "2026-01-25T12:57:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/538a6fd44c515f1c5197a3f078094cbaf2ce9f945df5b44e29d95c864bff/coverage-7.13.2-cp310-cp310-win32.whl", hash = "sha256:1574983178b35b9af4db4a9f7328a18a14a0a0ce76ffaa1c1bacb4cc82089a7c", size = 221465, upload-time = "2026-01-25T12:57:33.511Z" }, + { url = "https://files.pythonhosted.org/packages/5e/09/4b63a024295f326ec1a40ec8def27799300ce8775b1cbf0d33b1790605c4/coverage-7.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:a360a8baeb038928ceb996f5623a4cd508728f8f13e08d4e96ce161702f3dd99", size = 222397, upload-time = "2026-01-25T12:57:34.927Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f0/3d3eac7568ab6096ff23791a526b0048a1ff3f49d0e236b2af6fb6558e88/coverage-7.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed75de7d1217cf3b99365d110975f83af0528c849ef5180a12fd91b5064df9d6", size = 219168, upload-time = "2026-01-25T12:58:23.376Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/f8b5cfeddbab95fdef4dcd682d82e5dcff7a112ced57a959f89537ee9995/coverage-7.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:97e596de8fa9bada4d88fde64a3f4d37f1b6131e4faa32bad7808abc79887ddc", size = 219537, upload-time = "2026-01-25T12:58:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e6/8d8e6e0c516c838229d1e41cadcec91745f4b1031d4db17ce0043a0423b4/coverage-7.13.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:68c86173562ed4413345410c9480a8d64864ac5e54a5cda236748031e094229f", size = 250528, upload-time = "2026-01-25T12:58:26.567Z" }, + { url = "https://files.pythonhosted.org/packages/8e/78/befa6640f74092b86961f957f26504c8fba3d7da57cc2ab7407391870495/coverage-7.13.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7be4d613638d678b2b3773b8f687537b284d7074695a43fe2fbbfc0e31ceaed1", size = 253132, upload-time = "2026-01-25T12:58:28.251Z" }, + { url = "https://files.pythonhosted.org/packages/9d/10/1630db1edd8ce675124a2ee0f7becc603d2bb7b345c2387b4b95c6907094/coverage-7.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f63ce526a96acd0e16c4af8b50b64334239550402fb1607ce6a584a6d62ce9", size = 254374, upload-time = "2026-01-25T12:58:30.294Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/0d9381647b1e8e6d310ac4140be9c428a0277330991e0c35bdd751e338a4/coverage-7.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:406821f37f864f968e29ac14c3fccae0fec9fdeba48327f0341decf4daf92d7c", size = 250762, upload-time = "2026-01-25T12:58:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5636dfc9a7c871ee8776af83ee33b4c26bc508ad6cee1e89b6419a366582/coverage-7.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ee68e5a4e3e5443623406b905db447dceddffee0dceb39f4e0cd9ec2a35004b5", size = 252502, upload-time = "2026-01-25T12:58:33.961Z" }, + { url = "https://files.pythonhosted.org/packages/02/2a/7ff2884d79d420cbb2d12fed6fff727b6d0ef27253140d3cdbbd03187ee0/coverage-7.13.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2ee0e58cca0c17dd9c6c1cdde02bb705c7b3fbfa5f3b0b5afeda20d4ebff8ef4", size = 250463, upload-time = "2026-01-25T12:58:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/ba51087db645b6c7261570400fc62c89a16278763f36ba618dc8657a187b/coverage-7.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e5bbb5018bf76a56aabdb64246b5288d5ae1b7d0dd4d0534fe86df2c2992d1c", size = 250288, upload-time = "2026-01-25T12:58:37.226Z" }, + { url = "https://files.pythonhosted.org/packages/03/07/44e6f428551c4d9faf63ebcefe49b30e5c89d1be96f6a3abd86a52da9d15/coverage-7.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a55516c68ef3e08e134e818d5e308ffa6b1337cc8b092b69b24287bf07d38e31", size = 252063, upload-time = "2026-01-25T12:58:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/c2/67/35b730ad7e1859dd57e834d1bc06080d22d2f87457d53f692fce3f24a5a9/coverage-7.13.2-cp313-cp313-win32.whl", hash = "sha256:5b20211c47a8abf4abc3319d8ce2464864fa9f30c5fcaf958a3eed92f4f1fef8", size = 221716, upload-time = "2026-01-25T12:58:40.484Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/e5fcf5a97c72f45fc14829237a6550bf49d0ab882ac90e04b12a69db76b4/coverage-7.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:14f500232e521201cf031549fb1ebdfc0a40f401cf519157f76c397e586c3beb", size = 222522, upload-time = "2026-01-25T12:58:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/25d7b2f946d239dd2d6644ca2cc060d24f97551e2af13b6c24c722ae5f97/coverage-7.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:9779310cb5a9778a60c899f075a8514c89fa6d10131445c2207fc893e0b14557", size = 221145, upload-time = "2026-01-25T12:58:45Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f7/080376c029c8f76fadfe43911d0daffa0cbdc9f9418a0eead70c56fb7f4b/coverage-7.13.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5a1e41ce5df6b547cbc3d3699381c9e2c2c369c67837e716ed0f549d48e", size = 219861, upload-time = "2026-01-25T12:58:46.586Z" }, + { url = "https://files.pythonhosted.org/packages/42/11/0b5e315af5ab35f4c4a70e64d3314e4eec25eefc6dec13be3a7d5ffe8ac5/coverage-7.13.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b01899e82a04085b6561eb233fd688474f57455e8ad35cd82286463ba06332b7", size = 220207, upload-time = "2026-01-25T12:58:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0c/0874d0318fb1062117acbef06a09cf8b63f3060c22265adaad24b36306b7/coverage-7.13.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:838943bea48be0e2768b0cf7819544cdedc1bbb2f28427eabb6eb8c9eb2285d3", size = 261504, upload-time = "2026-01-25T12:58:49.904Z" }, + { url = "https://files.pythonhosted.org/packages/83/5e/1cd72c22ecb30751e43a72f40ba50fcef1b7e93e3ea823bd9feda8e51f9a/coverage-7.13.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93d1d25ec2b27e90bcfef7012992d1f5121b51161b8bffcda756a816cf13c2c3", size = 263582, upload-time = "2026-01-25T12:58:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/9b/da/8acf356707c7a42df4d0657020308e23e5a07397e81492640c186268497c/coverage-7.13.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93b57142f9621b0d12349c43fc7741fe578e4bc914c1e5a54142856cfc0bf421", size = 266008, upload-time = "2026-01-25T12:58:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/41/41/ea1730af99960309423c6ea8d6a4f1fa5564b2d97bd1d29dda4b42611f04/coverage-7.13.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f06799ae1bdfff7ccb8665d75f8291c69110ba9585253de254688aa8a1ccc6c5", size = 260762, upload-time = "2026-01-25T12:58:55.372Z" }, + { url = "https://files.pythonhosted.org/packages/22/fa/02884d2080ba71db64fdc127b311db60e01fe6ba797d9c8363725e39f4d5/coverage-7.13.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f9405ab4f81d490811b1d91c7a20361135a2df4c170e7f0b747a794da5b7f23", size = 263571, upload-time = "2026-01-25T12:58:57.52Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6b/4083aaaeba9b3112f55ac57c2ce7001dc4d8fa3fcc228a39f09cc84ede27/coverage-7.13.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f9ab1d5b86f8fbc97a5b3cd6280a3fd85fef3b028689d8a2c00918f0d82c728c", size = 261200, upload-time = "2026-01-25T12:58:59.255Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d2/aea92fa36d61955e8c416ede9cf9bf142aa196f3aea214bb67f85235a050/coverage-7.13.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:f674f59712d67e841525b99e5e2b595250e39b529c3bda14764e4f625a3fa01f", size = 260095, upload-time = "2026-01-25T12:59:01.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ae/04ffe96a80f107ea21b22b2367175c621da920063260a1c22f9452fd7866/coverage-7.13.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c6cadac7b8ace1ba9144feb1ae3cb787a6065ba6d23ffc59a934b16406c26573", size = 262284, upload-time = "2026-01-25T12:59:02.802Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/6f354dcd7dfc41297791d6fb4e0d618acb55810bde2c1fd14b3939e05c2b/coverage-7.13.2-cp313-cp313t-win32.whl", hash = "sha256:14ae4146465f8e6e6253eba0cccd57423e598a4cb925958b240c805300918343", size = 222389, upload-time = "2026-01-25T12:59:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d5/080ad292a4a3d3daf411574be0a1f56d6dee2c4fdf6b005342be9fac807f/coverage-7.13.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9074896edd705a05769e3de0eac0a8388484b503b68863dd06d5e473f874fd47", size = 223450, upload-time = "2026-01-25T12:59:06.677Z" }, + { url = "https://files.pythonhosted.org/packages/88/96/df576fbacc522e9fb8d1c4b7a7fc62eb734be56e2cba1d88d2eabe08ea3f/coverage-7.13.2-cp313-cp313t-win_arm64.whl", hash = "sha256:69e526e14f3f854eda573d3cf40cffd29a1a91c684743d904c33dbdcd0e0f3e7", size = 221707, upload-time = "2026-01-25T12:59:08.363Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/1da9e51a0775634b04fcc11eb25c002fc58ee4f92ce2e8512f94ac5fc5bf/coverage-7.13.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:387a825f43d680e7310e6f325b2167dd093bc8ffd933b83e9aa0983cf6e0a2ef", size = 219213, upload-time = "2026-01-25T12:59:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b3caac3ebbd10230fea5a33012b27d19e999a17c9285c4228b4b2e35b7da/coverage-7.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0d7fea9d8e5d778cd5a9e8fc38308ad688f02040e883cdc13311ef2748cb40f", size = 219549, upload-time = "2026-01-25T12:59:13.638Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/e1cf7def1bdc72c1907e60703983a588f9558434a2ff94615747bd73c192/coverage-7.13.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e080afb413be106c95c4ee96b4fffdc9e2fa56a8bbf90b5c0918e5c4449412f5", size = 250586, upload-time = "2026-01-25T12:59:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ba/49/f54ec02ed12be66c8d8897270505759e057b0c68564a65c429ccdd1f139e/coverage-7.13.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7fc042ba3c7ce25b8a9f097eb0f32a5ce1ccdb639d9eec114e26def98e1f8a4", size = 253093, upload-time = "2026-01-25T12:59:17.491Z" }, + { url = "https://files.pythonhosted.org/packages/fb/5e/aaf86be3e181d907e23c0f61fccaeb38de8e6f6b47aed92bf57d8fc9c034/coverage-7.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0ba505e021557f7f8173ee8cd6b926373d8653e5ff7581ae2efce1b11ef4c27", size = 254446, upload-time = "2026-01-25T12:59:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/a5fa01460e2d75b0c853b392080d6829d3ca8b5ab31e158fa0501bc7c708/coverage-7.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7de326f80e3451bd5cc7239ab46c73ddb658fe0b7649476bc7413572d36cd548", size = 250615, upload-time = "2026-01-25T12:59:21.928Z" }, + { url = "https://files.pythonhosted.org/packages/86/0b/6d56315a55f7062bb66410732c24879ccb2ec527ab6630246de5fe45a1df/coverage-7.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abaea04f1e7e34841d4a7b343904a3f59481f62f9df39e2cd399d69a187a9660", size = 252452, upload-time = "2026-01-25T12:59:23.592Z" }, + { url = "https://files.pythonhosted.org/packages/30/19/9bc550363ebc6b0ea121977ee44d05ecd1e8bf79018b8444f1028701c563/coverage-7.13.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9f93959ee0c604bccd8e0697be21de0887b1f73efcc3aa73a3ec0fd13feace92", size = 250418, upload-time = "2026-01-25T12:59:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/580530a31ca2f0cc6f07a8f2ab5460785b02bb11bdf815d4c4d37a4c5169/coverage-7.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:13fe81ead04e34e105bf1b3c9f9cdf32ce31736ee5d90a8d2de02b9d3e1bcb82", size = 250231, upload-time = "2026-01-25T12:59:27.888Z" }, + { url = "https://files.pythonhosted.org/packages/e2/42/dd9093f919dc3088cb472893651884bd675e3df3d38a43f9053656dca9a2/coverage-7.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d6d16b0f71120e365741bca2cb473ca6fe38930bc5431c5e850ba949f708f892", size = 251888, upload-time = "2026-01-25T12:59:29.636Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0af4053e6e819774626e133c3d6f70fae4d44884bfc4b126cb647baee8d3/coverage-7.13.2-cp314-cp314-win32.whl", hash = "sha256:9b2f4714bb7d99ba3790ee095b3b4ac94767e1347fe424278a0b10acb3ff04fe", size = 221968, upload-time = "2026-01-25T12:59:31.424Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/5aff1e1f80d55862442855517bb8ad8ad3a68639441ff6287dde6a58558b/coverage-7.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4121a90823a063d717a96e0a0529c727fb31ea889369a0ee3ec00ed99bf6859", size = 222783, upload-time = "2026-01-25T12:59:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/de/20/09abafb24f84b3292cc658728803416c15b79f9ee5e68d25238a895b07d9/coverage-7.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:6873f0271b4a15a33e7590f338d823f6f66f91ed147a03938d7ce26efd04eee6", size = 221348, upload-time = "2026-01-25T12:59:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/b6/60/a3820c7232db63be060e4019017cd3426751c2699dab3c62819cdbcea387/coverage-7.13.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f61d349f5b7cd95c34017f1927ee379bfbe9884300d74e07cf630ccf7a610c1b", size = 219950, upload-time = "2026-01-25T12:59:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/fd/37/e4ef5975fdeb86b1e56db9a82f41b032e3d93a840ebaf4064f39e770d5c5/coverage-7.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a43d34ce714f4ca674c0d90beb760eb05aad906f2c47580ccee9da8fe8bfb417", size = 220209, upload-time = "2026-01-25T12:59:38.339Z" }, + { url = "https://files.pythonhosted.org/packages/54/df/d40e091d00c51adca1e251d3b60a8b464112efa3004949e96a74d7c19a64/coverage-7.13.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bff1b04cb9d4900ce5c56c4942f047dc7efe57e2608cb7c3c8936e9970ccdbee", size = 261576, upload-time = "2026-01-25T12:59:40.446Z" }, + { url = "https://files.pythonhosted.org/packages/c5/44/5259c4bed54e3392e5c176121af9f71919d96dde853386e7730e705f3520/coverage-7.13.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6ae99e4560963ad8e163e819e5d77d413d331fd00566c1e0856aa252303552c1", size = 263704, upload-time = "2026-01-25T12:59:42.346Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/ae9f005827abcbe2c70157459ae86053971c9fa14617b63903abbdce26d9/coverage-7.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e79a8c7d461820257d9aa43716c4efc55366d7b292e46b5b37165be1d377405d", size = 266109, upload-time = "2026-01-25T12:59:44.073Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c0/8e279c1c0f5b1eaa3ad9b0fb7a5637fc0379ea7d85a781c0fe0bb3cfc2ab/coverage-7.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:060ee84f6a769d40c492711911a76811b4befb6fba50abb450371abb720f5bd6", size = 260686, upload-time = "2026-01-25T12:59:45.804Z" }, + { url = "https://files.pythonhosted.org/packages/b2/47/3a8112627e9d863e7cddd72894171c929e94491a597811725befdcd76bce/coverage-7.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3bca209d001fd03ea2d978f8a4985093240a355c93078aee3f799852c23f561a", size = 263568, upload-time = "2026-01-25T12:59:47.929Z" }, + { url = "https://files.pythonhosted.org/packages/92/bc/7ea367d84afa3120afc3ce6de294fd2dcd33b51e2e7fbe4bbfd200f2cb8c/coverage-7.13.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6b8092aa38d72f091db61ef83cb66076f18f02da3e1a75039a4f218629600e04", size = 261174, upload-time = "2026-01-25T12:59:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/33/b7/f1092dcecb6637e31cc2db099581ee5c61a17647849bae6b8261a2b78430/coverage-7.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4a3158dc2dcce5200d91ec28cd315c999eebff355437d2765840555d765a6e5f", size = 260017, upload-time = "2026-01-25T12:59:51.463Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cd/f3d07d4b95fbe1a2ef0958c15da614f7e4f557720132de34d2dc3aa7e911/coverage-7.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3973f353b2d70bd9796cc12f532a05945232ccae966456c8ed7034cb96bbfd6f", size = 262337, upload-time = "2026-01-25T12:59:53.407Z" }, + { url = "https://files.pythonhosted.org/packages/e0/db/b0d5b2873a07cb1e06a55d998697c0a5a540dcefbf353774c99eb3874513/coverage-7.13.2-cp314-cp314t-win32.whl", hash = "sha256:79f6506a678a59d4ded048dc72f1859ebede8ec2b9a2d509ebe161f01c2879d3", size = 222749, upload-time = "2026-01-25T12:59:56.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/838a5394c082ac57d85f57f6aba53093b30d9089781df72412126505716f/coverage-7.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:196bfeabdccc5a020a57d5a368c681e3a6ceb0447d153aeccc1ab4d70a5032ba", size = 223857, upload-time = "2026-01-25T12:59:58.201Z" }, + { url = "https://files.pythonhosted.org/packages/44/d4/b608243e76ead3a4298824b50922b89ef793e50069ce30316a65c1b4d7ef/coverage-7.13.2-cp314-cp314t-win_arm64.whl", hash = "sha256:69269ab58783e090bfbf5b916ab3d188126e22d6070bbfc93098fdd474ef937c", size = 221881, upload-time = "2026-01-25T13:00:00.449Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] [package.optional-dependencies] @@ -449,72 +449,67 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.3" +version = "46.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, - { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, - { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, - { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, - { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, - { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" }, + { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" }, + { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" }, + { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" }, + { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" }, + { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" }, + { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" }, + { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" }, + { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" }, + { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" }, + { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" }, + { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" }, + { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" }, + { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" }, + { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" }, + { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" }, + { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" }, + { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" }, + { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" }, + { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" }, + { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" }, + { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" }, + { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" }, + { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" }, ] [[package]] name = "cyclopts" -version = "4.5.0" +version = "4.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -524,9 +519,9 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/7b/663f3285c1ac0e5d0854bd9db2c87caa6fa3d1a063185e3394a6cdca9151/cyclopts-4.5.0.tar.gz", hash = "sha256:717ac4235548b58d500baf7e688aa4d024caf0ee68f61a012ffd5e29db3099f9", size = 161980, upload-time = "2026-01-16T02:07:16.171Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/93/6085aa89c3fff78a5180987354538d72e43b0db27e66a959302d0c07821a/cyclopts-4.5.1.tar.gz", hash = "sha256:fadc45304763fd9f5d6033727f176898d17a1778e194436964661a005078a3dd", size = 162075, upload-time = "2026-01-25T15:23:54.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a3/2e00fececc34a99ae3a5d5702a5dd29c5371e4ed016647301a2b9bcc1976/cyclopts-4.5.0-py3-none-any.whl", hash = "sha256:305b9aa90a9cd0916f0a450b43e50ad5df9c252680731a0719edfb9b20381bf5", size = 199772, upload-time = "2026-01-16T02:07:14.707Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/996760c30f1302704af57c66ff2d723f7d656d0d0b93563b5528a51484bb/cyclopts-4.5.1-py3-none-any.whl", hash = "sha256:0642c93601e554ca6b7b9abd81093847ea4448b2616280f2a0952416574e8c7a", size = 199807, upload-time = "2026-01-25T15:23:55.219Z" }, ] [[package]] @@ -1262,17 +1257,17 @@ wheels = [ [[package]] name = "loq" -version = "0.1.0a6" +version = "0.1.0a7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d2/35/46a80e6cfa66f7631462abc17bd29c7a9778e5aaf0d84ea6c189d6e55762/loq-0.1.0a6.tar.gz", hash = "sha256:1c37d864a52972eb8d1f7189a4feae2e01a6860c040861f0c0c4b86995822c6b", size = 61090, upload-time = "2026-01-18T19:35:39.477Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/9f/27aacd5b1566ebbec1531ead263fa1f60899b8a7cd6aa1a710c34b05a5b8/loq-0.1.0a7.tar.gz", hash = "sha256:576f70f45d466accf6a4c1020e430f0fc008047eeae1889952bd981f9da12bf0", size = 63997, upload-time = "2026-01-22T04:42:14.476Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/77/ba6a542bcf618ee1ef2a075f02d83feebf6cc267be5afd6ab2d55b79dded/loq-0.1.0a6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:db1a52ee0f183e1df6baedd8262ee36cd17be60c790b402994c25ae02fcc5f5f", size = 1320055, upload-time = "2026-01-18T19:35:40.185Z" }, - { url = "https://files.pythonhosted.org/packages/73/14/c7befa473a2de39c17e89314e04fbe0df011ebd258e84e20dbf328921c6e/loq-0.1.0a6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:79260d6b1227bc7e6d2aad1a3a8569eaa273eab82a335b2e577b45f56b149291", size = 1243061, upload-time = "2026-01-18T19:35:35.643Z" }, - { url = "https://files.pythonhosted.org/packages/40/00/0e744f4843c01b9fc9a609fabf9ea0ecb3b27cddebddc1cdad5e69e874ae/loq-0.1.0a6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:255c0b331bb8832b7601f15b2e5e6336c04b844e3ff6c4f0aa9475d2a69097c0", size = 1263414, upload-time = "2026-01-18T19:35:41.205Z" }, - { url = "https://files.pythonhosted.org/packages/59/32/32f546280b0af2071ba2690d31363114ed524184dde75276f66b2f2d57e7/loq-0.1.0a6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:025c0369797327d49a0830cdf4ea0f9e29bd8c8d13965dd5064158ac88d5d073", size = 1357072, upload-time = "2026-01-18T19:35:36.879Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d6/08451ece6d4a93ccadf0e33f82b5b97bd43be499de30c0eaf8b4fd231876/loq-0.1.0a6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:adb55a119a86e931f628a0c100b4b0992da5564409838b27c4f8a57a5a12dfa4", size = 1303656, upload-time = "2026-01-18T19:35:37.904Z" }, - { url = "https://files.pythonhosted.org/packages/3e/34/d19f43ae998b5a32d68da2e3d169690d64d213c46f8e77a2207741b26f7f/loq-0.1.0a6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9828d3da2c950c883faa434d7c5b7ee51da3fedeb5372e0f998b13a28d287acf", size = 1421668, upload-time = "2026-01-18T19:35:34.471Z" }, - { url = "https://files.pythonhosted.org/packages/48/b5/8fc0d61c04e690585192bace9232a7a8ddf001d2895a441258566b7e6f71/loq-0.1.0a6-py3-none-win_amd64.whl", hash = "sha256:641abd7645343b8b51031265f495b2d8868a27a8d20de2f592c2db00299839dc", size = 1317924, upload-time = "2026-01-18T19:35:42.308Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e5/b55110b86951b184019bfe7290ee73115b932b3bb0d4428bbe20c68d2f31/loq-0.1.0a7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:47d40a9792397f8f100e7c2984d56227bd05c7f3d6817e8e26a7af88cbff0b26", size = 1336455, upload-time = "2026-01-22T04:42:23.812Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b9/519e83707c3819349e4497a4b30e9a814d155eb1317a25425cac73cb8045/loq-0.1.0a7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:178393fa15968082aafb54c24d2015eef7090b0668ca6715e3e3c390bdeeea46", size = 1259828, upload-time = "2026-01-22T04:42:21.951Z" }, + { url = "https://files.pythonhosted.org/packages/84/28/e9730ef5cf29baef96a015a514752d7142f48287ac7efc8bdd09794d2b28/loq-0.1.0a7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9527526e4cffb12c84207cd2a92ccfa90968e137908b4329d9f6e07ba0cb575", size = 1278762, upload-time = "2026-01-22T04:42:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/90/48/be790e0542fa660ec84f863e0440570442e167fc5c65d8ad36bd93e5f315/loq-0.1.0a7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ed89b64ab1de6be9936d274b95f946a1b6f052493e1a1f1db3aac70852b3e0c", size = 1374495, upload-time = "2026-01-22T04:42:16.588Z" }, + { url = "https://files.pythonhosted.org/packages/29/8b/74e6e39ac302ff722acd2c154f5f6b3c28f750f914e859158b1333b2330e/loq-0.1.0a7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e54a96e61c9aafebfa873e5a897132e611b457892f149809380625d4fa9c1147", size = 1318652, upload-time = "2026-01-22T04:42:19.229Z" }, + { url = "https://files.pythonhosted.org/packages/57/b2/ae6bae30259715db7d97c7c7f929d5a210dd2a59b2564a020899fba8a7e5/loq-0.1.0a7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a21ab864964823678b8478fef83f42c2634662f736cf8b552de09736f52ca91f", size = 1440332, upload-time = "2026-01-22T04:42:17.897Z" }, + { url = "https://files.pythonhosted.org/packages/6b/48/11ed2d491b353cb7c159398a3017a64a5c424c85979a2eb805ded6763739/loq-0.1.0a7-py3-none-win_amd64.whl", hash = "sha256:a5d0f98e9613ffab868aacc1e1277ca3f7d39158c55aa3d0e534865591592da7", size = 1334210, upload-time = "2026-01-22T04:42:20.748Z" }, ] [[package]] @@ -1375,7 +1370,7 @@ wheels = [ [[package]] name = "mcp" -version = "1.25.0" +version = "1.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1393,9 +1388,9 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, ] [[package]] @@ -1418,7 +1413,7 @@ wheels = [ [[package]] name = "openai" -version = "2.15.0" +version = "2.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1430,9 +1425,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383, upload-time = "2026-01-09T22:10:08.603Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/6c/e4c964fcf1d527fdf4739e7cc940c60075a4114d50d03871d5d5b1e13a88/openai-2.16.0.tar.gz", hash = "sha256:42eaa22ca0d8ded4367a77374104d7a2feafee5bd60a107c3c11b5243a11cd12", size = 629649, upload-time = "2026-01-27T23:28:02.579Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879, upload-time = "2026-01-09T22:10:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/16/83/0315bf2cfd75a2ce8a7e54188e9456c60cec6c0cf66728ed07bd9859ff26/openai-2.16.0-py3-none-any.whl", hash = "sha256:5f46643a8f42899a84e80c38838135d7038e7718333ce61396994f887b09a59b", size = 1068612, upload-time = "2026-01-27T23:28:00.356Z" }, ] [[package]] @@ -1531,11 +1526,11 @@ wheels = [ [[package]] name = "packaging" -version = "25.0" +version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -1655,45 +1650,45 @@ wheels = [ [[package]] name = "protobuf" -version = "6.33.4" +version = "6.33.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/b8/cda15d9d46d03d4aa3a67cb6bffe05173440ccf86a9541afaf7ac59a1b6b/protobuf-6.33.4.tar.gz", hash = "sha256:dc2e61bca3b10470c1912d166fe0af67bfc20eb55971dcef8dfa48ce14f0ed91", size = 444346, upload-time = "2026-01-12T18:33:40.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/be/24ef9f3095bacdf95b458543334d0c4908ccdaee5130420bf064492c325f/protobuf-6.33.4-cp310-abi3-win32.whl", hash = "sha256:918966612c8232fc6c24c78e1cd89784307f5814ad7506c308ee3cf86662850d", size = 425612, upload-time = "2026-01-12T18:33:29.656Z" }, - { url = "https://files.pythonhosted.org/packages/31/ad/e5693e1974a28869e7cd244302911955c1cebc0161eb32dfa2b25b6e96f0/protobuf-6.33.4-cp310-abi3-win_amd64.whl", hash = "sha256:8f11ffae31ec67fc2554c2ef891dcb561dae9a2a3ed941f9e134c2db06657dbc", size = 436962, upload-time = "2026-01-12T18:33:31.345Z" }, - { url = "https://files.pythonhosted.org/packages/66/15/6ee23553b6bfd82670207ead921f4d8ef14c107e5e11443b04caeb5ab5ec/protobuf-6.33.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2fe67f6c014c84f655ee06f6f66213f9254b3a8b6bda6cda0ccd4232c73c06f0", size = 427612, upload-time = "2026-01-12T18:33:32.646Z" }, - { url = "https://files.pythonhosted.org/packages/2b/48/d301907ce6d0db75f959ca74f44b475a9caa8fcba102d098d3c3dd0f2d3f/protobuf-6.33.4-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:757c978f82e74d75cba88eddec479df9b99a42b31193313b75e492c06a51764e", size = 324484, upload-time = "2026-01-12T18:33:33.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/1c/e53078d3f7fe710572ab2dcffd993e1e3b438ae71cfc031b71bae44fcb2d/protobuf-6.33.4-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c7c64f259c618f0bef7bee042075e390debbf9682334be2b67408ec7c1c09ee6", size = 339256, upload-time = "2026-01-12T18:33:35.231Z" }, - { url = "https://files.pythonhosted.org/packages/e8/8e/971c0edd084914f7ee7c23aa70ba89e8903918adca179319ee94403701d5/protobuf-6.33.4-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:3df850c2f8db9934de4cf8f9152f8dc2558f49f298f37f90c517e8e5c84c30e9", size = 323311, upload-time = "2026-01-12T18:33:36.305Z" }, - { url = "https://files.pythonhosted.org/packages/75/b1/1dc83c2c661b4c62d56cc081706ee33a4fc2835bd90f965baa2663ef7676/protobuf-6.33.4-py3-none-any.whl", hash = "sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc", size = 170532, upload-time = "2026-01-12T18:33:39.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, ] [[package]] name = "psutil" -version = "7.2.1" +version = "7.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, - { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, - { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, - { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, - { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, - { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, - { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, - { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, - { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, - { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, - { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, - { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, - { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, - { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, - { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, - { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] [[package]] @@ -1757,11 +1752,11 @@ wheels = [ [[package]] name = "pycparser" -version = "2.23" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] [[package]] @@ -1918,7 +1913,7 @@ wheels = [ [[package]] name = "pydocket" -version = "0.17.2" +version = "0.17.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle" }, @@ -1934,9 +1929,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/17/1fb6309e40bbee999c5d881b8213a1078968412d855e064a9a94cfb9eeef/pydocket-0.17.2.tar.gz", hash = "sha256:8f02c68952701eb1b3a70d439b76392d15f1eb9568d0bde6a69997ea5c79c89f", size = 329829, upload-time = "2026-01-26T16:07:56.217Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/65/eb3b4f7afac80308d74bab2a668b31f074524ff6fbc664a685c6ed7c8885/pydocket-0.17.3.tar.gz", hash = "sha256:8922b4ca5f3f428e69b7695b9b5a313bbedc3ce35c74045cadcd89f7c0e6ac2d", size = 329828, upload-time = "2026-01-27T01:08:06.514Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/74/4c9b70753d5721165047e6428ac239ca083118474794deaca5a27d0ab212/pydocket-0.17.2-py3-none-any.whl", hash = "sha256:f43743b84b4e3d614d99b0cad2deebab028104c217745406ecf9e1efb8926e04", size = 91628, upload-time = "2026-01-26T16:07:55.018Z" }, + { url = "https://files.pythonhosted.org/packages/ea/3b/29c69e4f88f5e5ea5e90e3cf93493cafb68bf9a2f625b916cc26ab1def89/pydocket-0.17.3-py3-none-any.whl", hash = "sha256:9ef2c6e855f52a3210acff300bcbcc45773d79295e2deddcc7aef7f67b2a5ba7", size = 91626, upload-time = "2026-01-27T01:08:05.085Z" }, ] [[package]] @@ -2195,11 +2190,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.21" +version = "0.0.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, ] [[package]] @@ -2340,15 +2335,15 @@ wheels = [ [[package]] name = "rich" -version = "14.2.0" +version = "14.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, + { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" }, ] [[package]] @@ -2488,28 +2483,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.13" +version = "0.14.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, - { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, - { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, - { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, - { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, - { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, - { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, - { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, - { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, - { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, - { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, ] [[package]] @@ -2682,26 +2677,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.13" +version = "0.0.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/dc/b607f00916f5a7c52860b84a66dc17bc6988e8445e96b1d6e175a3837397/ty-0.0.13.tar.gz", hash = "sha256:7a1d135a400ca076407ea30012d1f75419634160ed3b9cad96607bf2956b23b3", size = 4999183, upload-time = "2026-01-21T13:21:16.133Z" } +sdist = { url = "https://files.pythonhosted.org/packages/af/57/22c3d6bf95c2229120c49ffc2f0da8d9e8823755a1c3194da56e51f1cc31/ty-0.0.14.tar.gz", hash = "sha256:a691010565f59dd7f15cf324cdcd1d9065e010c77a04f887e1ea070ba34a7de2", size = 5036573, upload-time = "2026-01-27T00:57:31.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/df/3632f1918f4c0a33184f107efc5d436ab6da147fd3d3b94b3af6461efbf4/ty-0.0.13-py3-none-linux_armv6l.whl", hash = "sha256:1b2b8e02697c3a94c722957d712a0615bcc317c9b9497be116ef746615d892f2", size = 9993501, upload-time = "2026-01-21T13:21:26.628Z" }, - { url = "https://files.pythonhosted.org/packages/92/87/6a473ced5ac280c6ce5b1627c71a8a695c64481b99aabc798718376a441e/ty-0.0.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f15cdb8e233e2b5adfce673bb21f4c5e8eaf3334842f7eea3c70ac6fda8c1de5", size = 9860986, upload-time = "2026-01-21T13:21:24.425Z" }, - { url = "https://files.pythonhosted.org/packages/5d/9b/d89ae375cf0a7cd9360e1164ce017f8c753759be63b6a11ed4c944abe8c6/ty-0.0.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0819e89ac9f0d8af7a062837ce197f0461fee2fc14fd07e2c368780d3a397b73", size = 9350748, upload-time = "2026-01-21T13:21:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a6/9ad58518056fab344b20c0bb2c1911936ebe195318e8acc3bc45ac1c6b6b/ty-0.0.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de79f481084b7cc7a202ba0d7a75e10970d10ffa4f025b23f2e6b7324b74886", size = 9849884, upload-time = "2026-01-21T13:21:21.886Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c3/8add69095fa179f523d9e9afcc15a00818af0a37f2b237a9b59bc0046c34/ty-0.0.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4fb2154cff7c6e95d46bfaba283c60642616f20d73e5f96d0c89c269f3e1bcec", size = 9822975, upload-time = "2026-01-21T13:21:14.292Z" }, - { url = "https://files.pythonhosted.org/packages/a4/05/4c0927c68a0a6d43fb02f3f0b6c19c64e3461dc8ed6c404dde0efb8058f7/ty-0.0.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00be58d89337c27968a20d58ca553458608c5b634170e2bec82824c2e4cf4d96", size = 10294045, upload-time = "2026-01-21T13:21:30.505Z" }, - { url = "https://files.pythonhosted.org/packages/b4/86/6dc190838aba967557fe0bfd494c595d00b5081315a98aaf60c0e632aaeb/ty-0.0.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72435eade1fa58c6218abb4340f43a6c3ff856ae2dc5722a247d3a6dd32e9737", size = 10916460, upload-time = "2026-01-21T13:21:07.788Z" }, - { url = "https://files.pythonhosted.org/packages/04/40/9ead96b7c122e1109dfcd11671184c3506996bf6a649306ec427e81d9544/ty-0.0.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77a548742ee8f621d718159e7027c3b555051d096a49bb580249a6c5fc86c271", size = 10597154, upload-time = "2026-01-21T13:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7d/e832a2c081d2be845dc6972d0c7998914d168ccbc0b9c86794419ab7376e/ty-0.0.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da067c57c289b7cf914669704b552b6207c2cc7f50da4118c3e12388642e6b3f", size = 10410710, upload-time = "2026-01-21T13:21:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/31/e3/898be3a96237a32f05c4c29b43594dc3b46e0eedfe8243058e46153b324f/ty-0.0.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d1b50a01fffa140417fca5a24b658fbe0734074a095d5b6f0552484724474343", size = 9826299, upload-time = "2026-01-21T13:21:00.845Z" }, - { url = "https://files.pythonhosted.org/packages/bb/eb/db2d852ce0ed742505ff18ee10d7d252f3acfd6fc60eca7e9c7a0288a6d8/ty-0.0.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f33c46f52e5e9378378eca0d8059f026f3c8073ace02f7f2e8d079ddfe5207e", size = 9831610, upload-time = "2026-01-21T13:21:05.842Z" }, - { url = "https://files.pythonhosted.org/packages/9e/61/149f59c8abaddcbcbb0bd13b89c7741ae1c637823c5cf92ed2c644fcadef/ty-0.0.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:168eda24d9a0b202cf3758c2962cc295878842042b7eca9ed2965259f59ce9f2", size = 9978885, upload-time = "2026-01-21T13:21:10.306Z" }, - { url = "https://files.pythonhosted.org/packages/a0/cd/026d4e4af60a80918a8d73d2c42b8262dd43ab2fa7b28d9743004cb88d57/ty-0.0.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4917678b95dc8cb399cc459fab568ba8d5f0f33b7a94bf840d9733043c43f29", size = 10506453, upload-time = "2026-01-21T13:20:56.633Z" }, - { url = "https://files.pythonhosted.org/packages/63/06/8932833a4eca2df49c997a29afb26721612de8078ae79074c8fe87e17516/ty-0.0.13-py3-none-win32.whl", hash = "sha256:c1f2ec40daa405508b053e5b8e440fbae5fdb85c69c9ab0ee078f8bc00eeec3d", size = 9433482, upload-time = "2026-01-21T13:20:58.717Z" }, - { url = "https://files.pythonhosted.org/packages/aa/fd/e8d972d1a69df25c2cecb20ea50e49ad5f27a06f55f1f5f399a563e71645/ty-0.0.13-py3-none-win_amd64.whl", hash = "sha256:8b7b1ab9f187affbceff89d51076038363b14113be29bda2ddfa17116de1d476", size = 10319156, upload-time = "2026-01-21T13:21:03.266Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c2/05fdd64ac003a560d4fbd1faa7d9a31d75df8f901675e5bed1ee2ceeff87/ty-0.0.13-py3-none-win_arm64.whl", hash = "sha256:1c9630333497c77bb9bcabba42971b96ee1f36c601dd3dcac66b4134f9fa38f0", size = 9808316, upload-time = "2026-01-21T13:20:54.053Z" }, + { url = "https://files.pythonhosted.org/packages/99/cb/cc6d1d8de59beb17a41f9a614585f884ec2d95450306c173b3b7cc090d2e/ty-0.0.14-py3-none-linux_armv6l.whl", hash = "sha256:32cf2a7596e693094621d3ae568d7ee16707dce28c34d1762947874060fdddaa", size = 10034228, upload-time = "2026-01-27T00:57:53.133Z" }, + { url = "https://files.pythonhosted.org/packages/f3/96/dd42816a2075a8f31542296ae687483a8d047f86a6538dfba573223eaf9a/ty-0.0.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f971bf9805f49ce8c0968ad53e29624d80b970b9eb597b7cbaba25d8a18ce9a2", size = 9939162, upload-time = "2026-01-27T00:57:43.857Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b4/73c4859004e0f0a9eead9ecb67021438b2e8e5fdd8d03e7f5aca77623992/ty-0.0.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:45448b9e4806423523268bc15e9208c4f3f2ead7c344f615549d2e2354d6e924", size = 9418661, upload-time = "2026-01-27T00:58:03.411Z" }, + { url = "https://files.pythonhosted.org/packages/58/35/839c4551b94613db4afa20ee555dd4f33bfa7352d5da74c5fa416ffa0fd2/ty-0.0.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee94a9b747ff40114085206bdb3205a631ef19a4d3fb89e302a88754cbbae54c", size = 9837872, upload-time = "2026-01-27T00:57:23.718Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/bbecf7e2faa20c04bebd35fc478668953ca50ee5847ce23e08acf20ea119/ty-0.0.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6756715a3c33182e9ab8ffca2bb314d3c99b9c410b171736e145773ee0ae41c3", size = 9848819, upload-time = "2026-01-27T00:57:58.501Z" }, + { url = "https://files.pythonhosted.org/packages/be/60/3c0ba0f19c0f647ad9d2b5b5ac68c0f0b4dc899001bd53b3a7537fb247a2/ty-0.0.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89d0038a2f698ba8b6fec5cf216a4e44e2f95e4a5095a8c0f57fe549f87087c2", size = 10324371, upload-time = "2026-01-27T00:57:29.291Z" }, + { url = "https://files.pythonhosted.org/packages/24/32/99d0a0b37d0397b0a989ffc2682493286aa3bc252b24004a6714368c2c3d/ty-0.0.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c64a83a2d669b77f50a4957039ca1450626fb474619f18f6f8a3eb885bf7544", size = 10865898, upload-time = "2026-01-27T00:57:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/1a/88/30b583a9e0311bb474269cfa91db53350557ebec09002bfc3fb3fc364e8c/ty-0.0.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:242488bfb547ef080199f6fd81369ab9cb638a778bb161511d091ffd49c12129", size = 10555777, upload-time = "2026-01-27T00:58:05.853Z" }, + { url = "https://files.pythonhosted.org/packages/cd/a2/cb53fb6325dcf3d40f2b1d0457a25d55bfbae633c8e337bde8ec01a190eb/ty-0.0.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4790c3866f6c83a4f424fc7d09ebdb225c1f1131647ba8bdc6fcdc28f09ed0ff", size = 10412913, upload-time = "2026-01-27T00:57:38.834Z" }, + { url = "https://files.pythonhosted.org/packages/42/8f/f2f5202d725ed1e6a4e5ffaa32b190a1fe70c0b1a2503d38515da4130b4c/ty-0.0.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:950f320437f96d4ea9a2332bbfb5b68f1c1acd269ebfa4c09b6970cc1565bd9d", size = 9837608, upload-time = "2026-01-27T00:57:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/59a2a0521640c489dafa2c546ae1f8465f92956fede18660653cce73b4c5/ty-0.0.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a0ec3ee70d83887f86925bbc1c56f4628bd58a0f47f6f32ddfe04e1f05466df", size = 9884324, upload-time = "2026-01-27T00:57:46.786Z" }, + { url = "https://files.pythonhosted.org/packages/03/95/8d2a49880f47b638743212f011088552ecc454dd7a665ddcbdabea25772a/ty-0.0.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1a4e6b6da0c58b34415955279eff754d6206b35af56a18bb70eb519d8d139ef", size = 10033537, upload-time = "2026-01-27T00:58:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/4523b36f2ce69f92ccf783855a9e0ebbbd0f0bb5cdce6211ee1737159ed3/ty-0.0.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dc04384e874c5de4c5d743369c277c8aa73d1edea3c7fc646b2064b637db4db3", size = 10495910, upload-time = "2026-01-27T00:57:26.691Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/655beb51224d1bfd4f9ddc0bb209659bfe71ff141bcf05c418ab670698f0/ty-0.0.14-py3-none-win32.whl", hash = "sha256:b20e22cf54c66b3e37e87377635da412d9a552c9bf4ad9fc449fed8b2e19dad2", size = 9507626, upload-time = "2026-01-27T00:57:41.43Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d9/c569c9961760e20e0a4bc008eeb1415754564304fd53997a371b7cf3f864/ty-0.0.14-py3-none-win_amd64.whl", hash = "sha256:e312ff9475522d1a33186657fe74d1ec98e4a13e016d66f5758a452c90ff6409", size = 10437980, upload-time = "2026-01-27T00:57:36.422Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/186829654f5bfd9a028f6648e9caeb11271960a61de97484627d24443f91/ty-0.0.14-py3-none-win_arm64.whl", hash = "sha256:b6facdbe9b740cb2c15293a1d178e22ffc600653646452632541d01c36d5e378", size = 9885831, upload-time = "2026-01-27T00:57:49.747Z" }, ] [[package]] @@ -2868,11 +2863,11 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.2.14" +version = "0.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/6e/62daec357285b927e82263a81f3b4c1790215bc77c42530ce4a69d501a43/wcwidth-0.5.0.tar.gz", hash = "sha256:f89c103c949a693bf563377b2153082bf58e309919dfb7f27b04d862a0089333", size = 246585, upload-time = "2026-01-27T01:31:44.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/f2/3e/45583b67c2ff08ad5a582d316fcb2f11d6cf0a50c7707ac09d212d25bc98/wcwidth-0.5.0-py3-none-any.whl", hash = "sha256:1efe1361b83b0ff7877b81ba57c8562c99cf812158b778988ce17ec061095695", size = 93772, upload-time = "2026-01-27T01:31:43.432Z" }, ] [[package]]