Merge branch 'main' into claude/issue-3011-20260128-0658

This commit is contained in:
Bill Easton 2026-01-31 14:55:13 -06:00 committed by GitHub
commit c7eb47183d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 2958 additions and 646 deletions

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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:

View file

@ -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:

View file

@ -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"
]

View file

@ -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` | ✗ |
<Note>
`offline_access` is automatically included to obtain refresh tokens. FastMCP manages token refreshing automatically.
</Note>
<Info>
**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.
</Info>

View file

@ -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.
<Tip>
**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.

View file

@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx"
<LocalFocusTip />
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

View file

@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx"
<LocalFocusTip />
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.
<Note>
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.

View file

@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx"
<LocalFocusTip />
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

View file

@ -10,7 +10,7 @@ import { LocalFocusTip } from "/snippets/local-focus.mdx"
<LocalFocusTip />
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

178
docs/integrations/goose.mdx Normal file
View file

@ -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"
<LocalFocusTip />
[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
<VersionBadge version="3.0.0" />
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
```
<Note>
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.
</Note>
#### 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.
<Warning>
**`uvx` (from `uv`) must be installed and available in your system PATH**. Goose uses `uvx` to run Python-based extensions in isolated environments.
</Warning>
## 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.

View file

@ -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`
<VersionBadge version="2.9.0" />

View file

@ -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.

View file

@ -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"
]
}
}

View file

@ -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"

View file

@ -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 = """\
<!DOCTYPE html>
<html>
<head>
<meta name="color-scheme" content="light dark">
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
background: transparent;
}
body {
display: flex;
justify-content: center;
align-items: center;
height: 340px;
width: 340px;
}
img {
width: 300px;
height: 300px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div id="qr"></div>
<script type="module">
import { App } from "https://unpkg.com/@modelcontextprotocol/ext-apps@0.4.0/app-with-deps";
const app = new App({ name: "QR View", version: "1.0.0" });
app.ontoolresult = ({ content }) => {
const img = content?.find(c => c.type === 'image');
if (img) {
const qrDiv = document.getElementById('qr');
qrDiv.innerHTML = '';
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif'];
const mimeType = allowedTypes.includes(img.mimeType) ? img.mimeType : 'image/png';
const image = document.createElement('img');
image.src = `data:${mimeType};base64,${img.data}`;
image.alt = "QR Code";
qrDiv.appendChild(image);
}
};
function handleHostContextChanged(ctx) {
if (ctx.safeAreaInsets) {
document.body.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
document.body.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
document.body.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
document.body.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
}
}
app.onhostcontextchanged = handleHostContextChanged;
await app.connect();
const ctx = app.getHostContext();
if (ctx) {
handleHostContextChanged(ctx);
}
</script>
</body>
</html>"""
@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()

View file

@ -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]]

View file

@ -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")

View file

@ -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(

View file

@ -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 <spec>
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)

View file

@ -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

View file

@ -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)

View file

@ -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(

View file

@ -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,

View file

@ -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

View file

@ -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(),

156
src/fastmcp/server/apps.py Normal file
View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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."""

View file

@ -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(

View file

@ -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]:

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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")

298
tests/cli/test_goose.py Normal file
View file

@ -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"),
("test<calc", "test%3Ccalc"),
("test>calc", "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

View file

@ -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:

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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})

525
tests/test_apps.py Normal file
View file

@ -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 "<html>hello</html>"
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 "<html>hello</html>"
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 "<html>hello</html>"
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 "<html>standalone</html>"
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"<html>{view}</html>"
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 "<html><body>Hello</body></html>"
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 "<html><body>Hello</body></html>"
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 "<html>secure</html>"
@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 "<html>app</html>"
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"
]

509
uv.lock generated
View file

@ -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]]