mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Upgrade ty to 0.0.39 (#4225)
This commit is contained in:
parent
53b20168c8
commit
8e66b0a47a
32 changed files with 243 additions and 119 deletions
|
|
@ -27,22 +27,26 @@ class ResourceCSP(BaseModel):
|
|||
|
||||
connect_domains: list[str] | None = Field(
|
||||
default=None,
|
||||
alias="connectDomains",
|
||||
validation_alias="connectDomains",
|
||||
serialization_alias="connectDomains",
|
||||
description="Origins allowed for fetch/XHR/WebSocket (connect-src)",
|
||||
)
|
||||
resource_domains: list[str] | None = Field(
|
||||
default=None,
|
||||
alias="resourceDomains",
|
||||
validation_alias="resourceDomains",
|
||||
serialization_alias="resourceDomains",
|
||||
description="Origins allowed for scripts, images, styles, fonts (script-src etc.)",
|
||||
)
|
||||
frame_domains: list[str] | None = Field(
|
||||
default=None,
|
||||
alias="frameDomains",
|
||||
validation_alias="frameDomains",
|
||||
serialization_alias="frameDomains",
|
||||
description="Origins allowed for nested iframes (frame-src)",
|
||||
)
|
||||
base_uri_domains: list[str] | None = Field(
|
||||
default=None,
|
||||
alias="baseUriDomains",
|
||||
validation_alias="baseUriDomains",
|
||||
serialization_alias="baseUriDomains",
|
||||
description="Allowed base URIs for the document (base-uri)",
|
||||
)
|
||||
|
||||
|
|
@ -69,7 +73,8 @@ class ResourcePermissions(BaseModel):
|
|||
)
|
||||
clipboard_write: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
alias="clipboardWrite",
|
||||
validation_alias="clipboardWrite",
|
||||
serialization_alias="clipboardWrite",
|
||||
description="Request clipboard-write access",
|
||||
)
|
||||
|
||||
|
|
@ -91,7 +96,8 @@ class AppConfig(BaseModel):
|
|||
|
||||
resource_uri: str | None = Field(
|
||||
default=None,
|
||||
alias="resourceUri",
|
||||
validation_alias="resourceUri",
|
||||
serialization_alias="resourceUri",
|
||||
description="URI of the UI resource (typically ui:// scheme). Tools only.",
|
||||
)
|
||||
visibility: list[Literal["app", "model"]] | None = Field(
|
||||
|
|
@ -107,7 +113,8 @@ class AppConfig(BaseModel):
|
|||
domain: str | None = Field(default=None, description="Domain for the iframe")
|
||||
prefers_border: bool | None = Field(
|
||||
default=None,
|
||||
alias="prefersBorder",
|
||||
validation_alias="prefersBorder",
|
||||
serialization_alias="prefersBorder",
|
||||
description="Whether the UI prefers a visible border",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ class FileUpload(FastMCPApp):
|
|||
H3(provider._title)
|
||||
with If(STATE.stored.length()):
|
||||
Badge(
|
||||
STATE.stored.length(), # ty:ignore[invalid-argument-type]
|
||||
STATE.stored.length(),
|
||||
variant="secondary",
|
||||
)
|
||||
|
||||
|
|
@ -337,8 +337,8 @@ class FileUpload(FastMCPApp):
|
|||
Row(gap=2, align="center"),
|
||||
Column(gap=0),
|
||||
):
|
||||
Small(Rx("$item.name")) # ty:ignore[invalid-argument-type]
|
||||
Muted(Rx("$item.type")) # ty:ignore[invalid-argument-type]
|
||||
Small(Rx("$item.name"))
|
||||
Muted(Rx("$item.type"))
|
||||
|
||||
Button(
|
||||
"Upload to Server",
|
||||
|
|
@ -356,7 +356,7 @@ class FileUpload(FastMCPApp):
|
|||
),
|
||||
],
|
||||
on_error=ShowToast(
|
||||
ERROR, # ty:ignore[invalid-argument-type]
|
||||
ERROR,
|
||||
variant="error",
|
||||
),
|
||||
),
|
||||
|
|
@ -377,12 +377,12 @@ class FileUpload(FastMCPApp):
|
|||
),
|
||||
):
|
||||
with Column(gap=0):
|
||||
Small(f.name) # ty:ignore[invalid-argument-type]
|
||||
Muted(f.uploaded_at) # ty:ignore[invalid-argument-type]
|
||||
Small(f.name)
|
||||
Muted(f.uploaded_at)
|
||||
with Row(gap=2):
|
||||
Badge(f.type, variant="secondary") # ty:ignore[invalid-argument-type]
|
||||
Badge(f.type, variant="secondary")
|
||||
Badge(
|
||||
f.size_display, # ty:ignore[invalid-argument-type]
|
||||
f.size_display,
|
||||
variant="outline",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ class FormInput(FastMCPApp):
|
|||
if provider._send_message:
|
||||
on_success_actions.insert(
|
||||
0,
|
||||
SendMessage(RESULT), # ty:ignore[invalid-argument-type]
|
||||
SendMessage(RESULT),
|
||||
)
|
||||
|
||||
from_model_kwargs: dict[str, Any] = {
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ class GenerativeUI(Provider):
|
|||
|
||||
resource_config = AppConfig(csp=csp)
|
||||
resource = TextResource(
|
||||
uri=_gen.RESOURCE_URI, # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
||||
uri=_gen.RESOURCE_URI, # type: ignore[arg-type]
|
||||
name="Prefab Generative Renderer",
|
||||
text=get_generative_renderer_html(),
|
||||
mime_type=UI_MIME_TYPE,
|
||||
|
|
|
|||
|
|
@ -1089,7 +1089,7 @@ def _build_picker_html(tools: list[dict[str, Any]]) -> str:
|
|||
"__json_args__": Rx("__json_args__"),
|
||||
}
|
||||
|
||||
on_error = ShowToast(Rx("$error"), variant="error") # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
||||
on_error = ShowToast(Rx("$error"), variant="error") # type: ignore[arg-type]
|
||||
|
||||
input_mode = f"_mode_{name}"
|
||||
_desc_max_lines = 10
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ async def run_command(
|
|||
await run_v1_server_async(server, host=host, port=port, transport=transport)
|
||||
return
|
||||
|
||||
kwargs = {}
|
||||
kwargs: dict[str, Any] = {}
|
||||
if transport is not None:
|
||||
kwargs["transport"] = transport
|
||||
# Resolve effective transport for the HTTP kwargs guard — transport
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ def create_elicitation_callback(
|
|||
f"{result.content!r}"
|
||||
)
|
||||
return MCPElicitResult(
|
||||
_meta=result.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
|
||||
_meta=result.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
action=result.action,
|
||||
content=content,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
|
||||
import uuid
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
||||
|
||||
import mcp.types
|
||||
import pydantic_core
|
||||
|
|
@ -155,6 +155,7 @@ class ClientPromptsMixin:
|
|||
|
||||
# Inject trace context into meta for propagation to server
|
||||
propagated_meta = inject_trace_context(meta)
|
||||
request_meta = cast(mcp.types.RequestParams.Meta | None, propagated_meta)
|
||||
|
||||
# If meta provided, use send_request for SEP-1686 task support
|
||||
if propagated_meta:
|
||||
|
|
@ -164,7 +165,7 @@ class ClientPromptsMixin:
|
|||
name=name,
|
||||
arguments=serialized_arguments,
|
||||
task=mcp.types.TaskMetadata(**task_dict) if task_dict else None,
|
||||
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias # ty:ignore[unknown-argument]
|
||||
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
|
||||
)
|
||||
)
|
||||
result = await self._await_with_session_monitoring(
|
||||
|
|
@ -276,6 +277,7 @@ class ClientPromptsMixin:
|
|||
# Per SEP-1686 final spec: client sends only ttl, server generates taskId
|
||||
# Inject trace context into meta for propagation to server
|
||||
propagated_meta = inject_trace_context(meta)
|
||||
request_meta = cast(mcp.types.RequestParams.Meta | None, propagated_meta)
|
||||
|
||||
# Serialize arguments for MCP protocol
|
||||
serialized_arguments: dict[str, str] | None = None
|
||||
|
|
@ -294,7 +296,7 @@ class ClientPromptsMixin:
|
|||
name=name,
|
||||
arguments=serialized_arguments,
|
||||
task=mcp.types.TaskMetadata(ttl=ttl),
|
||||
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias # ty:ignore[unknown-argument]
|
||||
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
|
||||
import uuid
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
||||
|
||||
import mcp.types
|
||||
from pydantic import AnyUrl, RootModel
|
||||
|
|
@ -218,6 +218,7 @@ class ClientResourcesMixin:
|
|||
|
||||
# Inject trace context into meta for propagation to server
|
||||
propagated_meta = inject_trace_context(meta)
|
||||
request_meta = cast(mcp.types.RequestParams.Meta | None, propagated_meta)
|
||||
|
||||
# If meta provided, use send_request for SEP-1686 task support
|
||||
if propagated_meta:
|
||||
|
|
@ -226,7 +227,7 @@ class ClientResourcesMixin:
|
|||
params=mcp.types.ReadResourceRequestParams(
|
||||
uri=uri,
|
||||
task=mcp.types.TaskMetadata(**task_dict) if task_dict else None,
|
||||
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias # ty:ignore[unknown-argument]
|
||||
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
|
||||
)
|
||||
)
|
||||
result = await self._await_with_session_monitoring(
|
||||
|
|
@ -340,6 +341,7 @@ class ClientResourcesMixin:
|
|||
# Per SEP-1686 final spec: client sends only ttl, server generates taskId
|
||||
# Inject trace context into meta for propagation to server
|
||||
propagated_meta = inject_trace_context(meta)
|
||||
request_meta = cast(mcp.types.RequestParams.Meta | None, propagated_meta)
|
||||
|
||||
if isinstance(uri, str):
|
||||
uri = AnyUrl(uri)
|
||||
|
|
@ -348,7 +350,7 @@ class ClientResourcesMixin:
|
|||
params=mcp.types.ReadResourceRequestParams(
|
||||
uri=uri,
|
||||
task=mcp.types.TaskMetadata(ttl=ttl),
|
||||
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias # ty:ignore[unknown-argument]
|
||||
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
|
||||
import uuid
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast, overload
|
||||
|
||||
import mcp.types
|
||||
from opentelemetry.trace import Status, StatusCode
|
||||
|
|
@ -342,6 +342,7 @@ class ClientToolsMixin:
|
|||
# Per SEP-1686 final spec: client sends only ttl, server generates taskId
|
||||
# Inject trace context into meta for propagation to server
|
||||
propagated_meta = inject_trace_context(meta)
|
||||
request_meta = cast(mcp.types.RequestParams.Meta | None, propagated_meta)
|
||||
|
||||
# Build request with task metadata
|
||||
request = mcp.types.CallToolRequest(
|
||||
|
|
@ -349,7 +350,7 @@ class ClientToolsMixin:
|
|||
name=name,
|
||||
arguments=arguments or {},
|
||||
task=mcp.types.TaskMetadata(ttl=ttl),
|
||||
_meta=propagated_meta, # type: ignore[unknown-argument] # pydantic alias # ty:ignore[unknown-argument]
|
||||
_meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -410,7 +410,7 @@ class ToolTask(Task["CallToolResult"]):
|
|||
mcp_result = mcp.types.CallToolResult(
|
||||
content=raw_result.content,
|
||||
structuredContent=raw_result.structured_content,
|
||||
_meta=raw_result.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
|
||||
_meta=raw_result.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
)
|
||||
result = await self._client._parse_call_tool_result(
|
||||
self._tool_name,
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ class StdioTransport(ClientTransport):
|
|||
cwd=self.cwd,
|
||||
log_file=self.log_file,
|
||||
# TODO(ty): remove when ty supports Unpack[TypedDict] inference
|
||||
session_kwargs=session_kwargs, # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
|
||||
session_kwargs=session_kwargs, # type: ignore[arg-type]
|
||||
ready_event=self._ready_event,
|
||||
stop_event=self._stop_event,
|
||||
session_future=session_future,
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ class MCPConfig(BaseModel):
|
|||
if file_path.exists() and (
|
||||
content := file_path.read_text(encoding="utf-8").strip()
|
||||
):
|
||||
return cls.model_validate_json(content) # ty: ignore[possibly-unresolved-reference]
|
||||
return cls.model_validate_json(content)
|
||||
|
||||
raise ValueError(f"No MCP servers defined in the config: {file_path}")
|
||||
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ class PromptResult(pydantic.BaseModel):
|
|||
return GetPromptResult(
|
||||
description=self.description,
|
||||
messages=mcp_messages,
|
||||
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
|
||||
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -229,7 +229,7 @@ class Prompt(FastMCPComponent):
|
|||
icons=overrides.get("icons", self.icons),
|
||||
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
"_meta", self.get_meta()
|
||||
), # ty:ignore[unknown-argument]
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ import json
|
|||
import warnings
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from types import MethodType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Literal,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
cast,
|
||||
overload,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
|
@ -484,8 +486,8 @@ def prompt(
|
|||
task=task,
|
||||
auth=auth,
|
||||
)
|
||||
target = fn.__func__ if hasattr(fn, "__func__") else fn
|
||||
target.__fastmcp__ = metadata
|
||||
target = fn.__func__ if isinstance(fn, staticmethod | MethodType) else fn
|
||||
cast(Any, target).__fastmcp__ = metadata
|
||||
return fn
|
||||
|
||||
def decorator(fn: F, prompt_name: str | None) -> F:
|
||||
|
|
|
|||
|
|
@ -107,14 +107,14 @@ class ResourceContent(pydantic.BaseModel):
|
|||
uri=AnyUrl(uri) if isinstance(uri, str) else uri,
|
||||
text=self.content,
|
||||
mimeType=self.mime_type or "text/plain",
|
||||
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
|
||||
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
)
|
||||
else:
|
||||
return mcp.types.BlobResourceContents(
|
||||
uri=AnyUrl(uri) if isinstance(uri, str) else uri,
|
||||
blob=base64.b64encode(self.content).decode(),
|
||||
mimeType=self.mime_type or "application/octet-stream",
|
||||
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
|
||||
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -211,7 +211,7 @@ class ResourceResult(pydantic.BaseModel):
|
|||
mcp_contents = [item.to_mcp_resource_contents(uri) for item in self.contents]
|
||||
return mcp.types.ReadResourceResult(
|
||||
contents=mcp_contents,
|
||||
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
|
||||
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -418,7 +418,7 @@ class Resource(FastMCPComponent):
|
|||
annotations=overrides.get("annotations", self.annotations),
|
||||
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
"_meta", self.get_meta()
|
||||
), # ty:ignore[unknown-argument]
|
||||
),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,16 @@ import inspect
|
|||
import warnings
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeVar, runtime_checkable
|
||||
from types import MethodType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Literal,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
cast,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
from mcp.types import Annotations, Icon
|
||||
from pydantic import AnyUrl
|
||||
|
|
@ -326,8 +335,8 @@ def resource(
|
|||
task=task,
|
||||
auth=auth,
|
||||
)
|
||||
target = fn.__func__ if hasattr(fn, "__func__") else fn
|
||||
target.__fastmcp__ = metadata
|
||||
target = fn.__func__ if isinstance(fn, staticmethod | MethodType) else fn
|
||||
cast(Any, target).__fastmcp__ = metadata
|
||||
return fn
|
||||
|
||||
def decorator(fn: F) -> F:
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ class ResourceTemplate(FastMCPComponent):
|
|||
annotations=overrides.get("annotations", self.annotations),
|
||||
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
"_meta", self.get_meta()
|
||||
), # ty:ignore[unknown-argument]
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ This implementation is based on:
|
|||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Literal
|
||||
from typing import Any, Literal
|
||||
|
||||
import httpx
|
||||
from key_value.aio.protocols import AsyncKeyValue
|
||||
|
|
@ -151,7 +151,7 @@ class OIDCConfiguration(BaseModel):
|
|||
strict: The strict flag for the configuration
|
||||
timeout_seconds: HTTP request timeout in seconds
|
||||
"""
|
||||
get_kwargs = {}
|
||||
get_kwargs: dict[str, Any] = {}
|
||||
if timeout_seconds is not None:
|
||||
get_kwargs["timeout"] = timeout_seconds
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
import weakref
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import AsyncExitStack
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import anyio
|
||||
import mcp.types
|
||||
|
|
@ -28,6 +28,7 @@ from fastmcp.apps.config import UI_EXTENSION_ID
|
|||
from fastmcp.utilities.logging import get_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp.server.middleware import CallNext
|
||||
from fastmcp.server.server import FastMCP
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
|
@ -131,7 +132,8 @@ class MiddlewareServerSession(ServerSession):
|
|||
|
||||
try:
|
||||
return await self.fastmcp._run_middleware(
|
||||
mw_context, call_original_handler
|
||||
mw_context,
|
||||
cast("CallNext[Any, Any]", call_original_handler),
|
||||
)
|
||||
except McpError as e:
|
||||
# McpError can be thrown from middleware in `on_initialize`
|
||||
|
|
@ -212,19 +214,22 @@ class LowLevelServer(_Server[LifespanResultT, RequestT]):
|
|||
experimental_capabilities or {},
|
||||
)
|
||||
|
||||
# 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
|
||||
# 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 {}
|
||||
existing_extensions_value = (capabilities.model_extra or {}).get("extensions")
|
||||
existing_extensions = (
|
||||
existing_extensions_value
|
||||
if isinstance(existing_extensions_value, dict)
|
||||
else {}
|
||||
)
|
||||
return capabilities.model_copy(
|
||||
update={
|
||||
"tasks": get_task_capabilities(),
|
||||
"extensions": {**existing_extensions, UI_EXTENSION_ID: {}},
|
||||
}
|
||||
)
|
||||
capabilities.extensions = {**existing_extensions, UI_EXTENSION_ID: {}}
|
||||
|
||||
return capabilities
|
||||
|
||||
async def run(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Awaitable, Sequence
|
||||
from collections.abc import Awaitable, Callable, Sequence
|
||||
from dataclasses import dataclass, field, replace
|
||||
from datetime import datetime, timezone
|
||||
from functools import partial
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
|
|
@ -76,6 +75,16 @@ def make_middleware_wrapper(
|
|||
return wrapper
|
||||
|
||||
|
||||
def make_handler_wrapper(
|
||||
handler: Callable[..., Awaitable[Any]],
|
||||
call_next: CallNext[Any, Any],
|
||||
) -> CallNext[Any, Any]:
|
||||
async def wrapper(context: MiddlewareContext[Any]) -> Any:
|
||||
return await handler(context, call_next=call_next)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class Middleware:
|
||||
"""Base class for FastMCP middleware with dispatching hooks."""
|
||||
|
||||
|
|
@ -99,29 +108,32 @@ class Middleware:
|
|||
|
||||
match context.method:
|
||||
case "initialize":
|
||||
handler = partial(self.on_initialize, call_next=handler)
|
||||
handler = make_handler_wrapper(self.on_initialize, handler)
|
||||
case "tools/call":
|
||||
handler = partial(self.on_call_tool, call_next=handler)
|
||||
handler = make_handler_wrapper(self.on_call_tool, handler)
|
||||
case "resources/read":
|
||||
handler = partial(self.on_read_resource, call_next=handler)
|
||||
handler = make_handler_wrapper(self.on_read_resource, handler)
|
||||
case "prompts/get":
|
||||
handler = partial(self.on_get_prompt, call_next=handler)
|
||||
handler = make_handler_wrapper(self.on_get_prompt, handler)
|
||||
case "tools/list":
|
||||
handler = partial(self.on_list_tools, call_next=handler)
|
||||
handler = make_handler_wrapper(self.on_list_tools, handler)
|
||||
case "resources/list":
|
||||
handler = partial(self.on_list_resources, call_next=handler)
|
||||
handler = make_handler_wrapper(self.on_list_resources, handler)
|
||||
case "resources/templates/list":
|
||||
handler = partial(self.on_list_resource_templates, call_next=handler)
|
||||
handler = make_handler_wrapper(
|
||||
self.on_list_resource_templates,
|
||||
handler,
|
||||
)
|
||||
case "prompts/list":
|
||||
handler = partial(self.on_list_prompts, call_next=handler)
|
||||
handler = make_handler_wrapper(self.on_list_prompts, handler)
|
||||
|
||||
match context.type:
|
||||
case "request":
|
||||
handler = partial(self.on_request, call_next=handler)
|
||||
handler = make_handler_wrapper(self.on_request, handler)
|
||||
case "notification":
|
||||
handler = partial(self.on_notification, call_next=handler)
|
||||
handler = make_handler_wrapper(self.on_notification, handler)
|
||||
|
||||
handler = partial(self.on_message, call_next=handler)
|
||||
handler = make_handler_wrapper(self.on_message, handler)
|
||||
|
||||
return handler
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ from __future__ import annotations
|
|||
from collections.abc import AsyncIterator, Sequence
|
||||
from contextlib import asynccontextmanager
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
from typing_extensions import Self
|
||||
|
||||
|
|
@ -44,7 +44,13 @@ if TYPE_CHECKING:
|
|||
from fastmcp.prompts.base import Prompt
|
||||
from fastmcp.resources.base import Resource
|
||||
from fastmcp.resources.template import ResourceTemplate
|
||||
from fastmcp.server.transforms import Transform
|
||||
from fastmcp.server.transforms import (
|
||||
GetPromptNext,
|
||||
GetResourceNext,
|
||||
GetResourceTemplateNext,
|
||||
GetToolNext,
|
||||
Transform,
|
||||
)
|
||||
from fastmcp.tools.base import Tool
|
||||
|
||||
|
||||
|
|
@ -166,12 +172,15 @@ class Provider:
|
|||
The tool if found (may be marked disabled), None if not found.
|
||||
"""
|
||||
|
||||
async def base(n: str, version: VersionSpec | None = None) -> Tool | None:
|
||||
async def base(n: str, *, version: VersionSpec | None = None) -> Tool | None:
|
||||
return await self._get_tool(n, version)
|
||||
|
||||
chain = base
|
||||
chain: GetToolNext = cast("GetToolNext", base)
|
||||
for transform in self.transforms:
|
||||
chain = partial(transform.get_tool, call_next=chain)
|
||||
chain = cast(
|
||||
"GetToolNext",
|
||||
partial(cast(Any, transform.get_tool), call_next=chain),
|
||||
)
|
||||
|
||||
return await chain(name, version=version)
|
||||
|
||||
|
|
@ -250,12 +259,17 @@ class Provider:
|
|||
The resource if found (may be marked disabled), None if not found.
|
||||
"""
|
||||
|
||||
async def base(u: str, version: VersionSpec | None = None) -> Resource | None:
|
||||
async def base(
|
||||
u: str, *, version: VersionSpec | None = None
|
||||
) -> Resource | None:
|
||||
return await self._get_resource(u, version)
|
||||
|
||||
chain = base
|
||||
chain: GetResourceNext = cast("GetResourceNext", base)
|
||||
for transform in self.transforms:
|
||||
chain = partial(transform.get_resource, call_next=chain)
|
||||
chain = cast(
|
||||
"GetResourceNext",
|
||||
partial(cast(Any, transform.get_resource), call_next=chain),
|
||||
)
|
||||
|
||||
return await chain(uri, version=version)
|
||||
|
||||
|
|
@ -286,13 +300,19 @@ class Provider:
|
|||
"""
|
||||
|
||||
async def base(
|
||||
u: str, version: VersionSpec | None = None
|
||||
u: str, *, version: VersionSpec | None = None
|
||||
) -> ResourceTemplate | None:
|
||||
return await self._get_resource_template(u, version)
|
||||
|
||||
chain = base
|
||||
chain: GetResourceTemplateNext = cast("GetResourceTemplateNext", base)
|
||||
for transform in self.transforms:
|
||||
chain = partial(transform.get_resource_template, call_next=chain)
|
||||
chain = cast(
|
||||
"GetResourceTemplateNext",
|
||||
partial(
|
||||
cast(Any, transform.get_resource_template),
|
||||
call_next=chain,
|
||||
),
|
||||
)
|
||||
|
||||
return await chain(uri, version=version)
|
||||
|
||||
|
|
@ -322,12 +342,15 @@ class Provider:
|
|||
The prompt if found (may be marked disabled), None if not found.
|
||||
"""
|
||||
|
||||
async def base(n: str, version: VersionSpec | None = None) -> Prompt | None:
|
||||
async def base(n: str, *, version: VersionSpec | None = None) -> Prompt | None:
|
||||
return await self._get_prompt(n, version)
|
||||
|
||||
chain = base
|
||||
chain: GetPromptNext = cast("GetPromptNext", base)
|
||||
for transform in self.transforms:
|
||||
chain = partial(transform.get_prompt, call_next=chain)
|
||||
chain = cast(
|
||||
"GetPromptNext",
|
||||
partial(cast(Any, transform.get_prompt), call_next=chain),
|
||||
)
|
||||
|
||||
return await chain(name, version=version)
|
||||
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ class ProxyResource(Resource):
|
|||
client_factory: ClientFactoryT,
|
||||
*,
|
||||
_cached_content: ResourceResult | None = None,
|
||||
**kwargs,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self._client_factory = client_factory
|
||||
|
|
@ -1105,7 +1105,7 @@ class ProxyClient(Client[ClientTransportT]):
|
|||
kwargs["log_handler"] = default_proxy_log_handler
|
||||
if "progress_handler" not in kwargs:
|
||||
kwargs["progress_handler"] = default_proxy_progress_handler
|
||||
super().__init__(**kwargs | {"transport": transport})
|
||||
super().__init__(transport=transport, **kwargs)
|
||||
|
||||
# Enable forwarding of inbound HTTP headers (e.g. authorization) to
|
||||
# the upstream server. This is only appropriate for proxy clients,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import secrets
|
|||
import warnings
|
||||
from collections.abc import (
|
||||
AsyncIterator,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Sequence,
|
||||
)
|
||||
|
|
@ -65,7 +64,7 @@ from fastmcp.resources.template import ResourceTemplate
|
|||
from fastmcp.server.auth import AuthCheck, AuthContext, AuthProvider, run_auth_checks
|
||||
from fastmcp.server.lifespan import Lifespan
|
||||
from fastmcp.server.low_level import LowLevelServer
|
||||
from fastmcp.server.middleware import Middleware, MiddlewareContext
|
||||
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
|
||||
from fastmcp.server.mixins import LifespanMixin, MCPOperationsMixin, TransportMixin
|
||||
from fastmcp.server.providers import LocalProvider, Provider
|
||||
from fastmcp.server.providers.aggregate import AggregateProvider
|
||||
|
|
@ -480,12 +479,21 @@ class FastMCP(
|
|||
async def _run_middleware(
|
||||
self,
|
||||
context: MiddlewareContext[Any],
|
||||
call_next: Callable[[MiddlewareContext[Any]], Awaitable[Any]],
|
||||
call_next: CallNext[Any, Any],
|
||||
) -> Any:
|
||||
"""Builds and executes the middleware chain."""
|
||||
chain = call_next
|
||||
for mw in reversed(self.middleware):
|
||||
chain = partial(mw, call_next=chain)
|
||||
next_chain: CallNext[Any, Any] = chain
|
||||
|
||||
async def wrapped(
|
||||
ctx: MiddlewareContext[Any],
|
||||
mw: Middleware = mw,
|
||||
call_next: CallNext[Any, Any] = next_chain,
|
||||
) -> Any:
|
||||
return await mw(ctx, call_next)
|
||||
|
||||
chain = cast(CallNext[Any, Any], wrapped)
|
||||
return await chain(context)
|
||||
|
||||
def add_middleware(self, middleware: Middleware) -> None:
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ async def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any:
|
|||
"io.modelcontextprotocol/related-task": {
|
||||
"taskId": client_task_id,
|
||||
}
|
||||
}, # ty:ignore[unknown-argument]
|
||||
},
|
||||
)
|
||||
|
||||
# Parse task key to get component key
|
||||
|
|
@ -359,12 +359,12 @@ async def tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any:
|
|||
mcp_result = mcp.types.CallToolResult(
|
||||
content=content,
|
||||
structuredContent=structured_content,
|
||||
_meta=related_task_meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
|
||||
_meta=related_task_meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
)
|
||||
else:
|
||||
mcp_result = mcp.types.CallToolResult(
|
||||
content=mcp_result,
|
||||
_meta=related_task_meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
|
||||
_meta=related_task_meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
)
|
||||
return mcp_result
|
||||
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ class ToolResult(BaseModel):
|
|||
structuredContent=self.structured_content,
|
||||
content=self.content,
|
||||
isError=self.is_error,
|
||||
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field # ty:ignore[unknown-argument]
|
||||
_meta=self.meta, # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
)
|
||||
if self.structured_content is None:
|
||||
return self.content
|
||||
|
|
@ -215,7 +215,7 @@ class Tool(FastMCPComponent):
|
|||
execution=overrides.get("execution", self.execution),
|
||||
_meta=overrides.get( # type: ignore[call-arg] # _meta is Pydantic alias for meta field
|
||||
"_meta", self.get_meta()
|
||||
), # ty:ignore[unknown-argument]
|
||||
),
|
||||
)
|
||||
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import inspect
|
|||
import warnings
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from types import MethodType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Annotated,
|
||||
|
|
@ -13,6 +14,7 @@ from typing import (
|
|||
Literal,
|
||||
Protocol,
|
||||
TypeVar,
|
||||
cast,
|
||||
overload,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
|
@ -517,8 +519,8 @@ def tool(
|
|||
auth=auth,
|
||||
run_in_thread=run_in_thread,
|
||||
)
|
||||
target = fn.__func__ if hasattr(fn, "__func__") else fn
|
||||
target.__fastmcp__ = metadata
|
||||
target = fn.__func__ if isinstance(fn, staticmethod | MethodType) else fn
|
||||
cast(Any, target).__fastmcp__ = metadata
|
||||
return fn
|
||||
|
||||
def decorator(fn: F, tool_name: str | None) -> F:
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ dev = [
|
|||
"pytest-timeout>=2.4.0",
|
||||
"pytest-xdist>=3.6.1",
|
||||
"ruff>=0.12.8",
|
||||
"ty>=0.0.29",
|
||||
"ty>=0.0.39",
|
||||
"prek>=0.2.12",
|
||||
"loq>=0.1.0a3",
|
||||
"opentelemetry-exporter-otlp-proto-grpc>=1.39.0",
|
||||
|
|
@ -143,6 +143,9 @@ exclude = ["**/node_modules", "**/__pycache__", ".venv", ".git", "dist"]
|
|||
[tool.ty.environment]
|
||||
python-version = "3.10"
|
||||
|
||||
[tool.ty.analysis]
|
||||
replace-imports-with-any = ["prefab_ui.**"]
|
||||
|
||||
[tool.ty.rules]
|
||||
division-by-zero = "warn"
|
||||
possibly-missing-attribute = "warn"
|
||||
|
|
|
|||
|
|
@ -101,6 +101,23 @@ class TestPromptDecorator:
|
|||
result = cast(DecoratedPrompt, analyze)("Python")
|
||||
assert result == "Please analyze: Python"
|
||||
|
||||
async def test_staticmethod_metadata_is_available_on_unwrapped_function(self):
|
||||
"""@prompt should attach metadata where staticmethod access can find it."""
|
||||
|
||||
class MyClass:
|
||||
@prompt(name="custom-static-prompt")
|
||||
@staticmethod
|
||||
def my_prompt() -> str:
|
||||
return "hello"
|
||||
|
||||
mcp = FastMCP("Test")
|
||||
mcp.add_prompt(MyClass.my_prompt)
|
||||
|
||||
async with Client(mcp) as client:
|
||||
prompts = await client.list_prompts()
|
||||
|
||||
assert any(p.name == "custom-static-prompt" for p in prompts)
|
||||
|
||||
def test_prompt_rejects_classmethod_decorator(self):
|
||||
"""@prompt should reject classmethod-decorated functions."""
|
||||
with pytest.raises(TypeError, match="classmethod"):
|
||||
|
|
|
|||
|
|
@ -101,6 +101,23 @@ class TestResourceDecorator:
|
|||
result = cast(DecoratedResource, get_config)()
|
||||
assert result == {"setting": "value"}
|
||||
|
||||
async def test_staticmethod_metadata_is_available_on_unwrapped_function(self):
|
||||
"""@resource should attach metadata where staticmethod access can find it."""
|
||||
|
||||
class MyClass:
|
||||
@resource("config://static")
|
||||
@staticmethod
|
||||
def get_config() -> str:
|
||||
return "hello"
|
||||
|
||||
mcp = FastMCP("Test")
|
||||
mcp.add_resource(MyClass.get_config)
|
||||
|
||||
async with Client(mcp) as client:
|
||||
resources = await client.list_resources()
|
||||
|
||||
assert any(str(r.uri) == "config://static" for r in resources)
|
||||
|
||||
def test_resource_rejects_classmethod_decorator(self):
|
||||
"""@resource should reject classmethod-decorated functions."""
|
||||
|
||||
|
|
|
|||
|
|
@ -152,6 +152,19 @@ class TestToolDecorator:
|
|||
result = cast(DecoratedTool, greet)("World")
|
||||
assert result == "Hello, World!"
|
||||
|
||||
def test_staticmethod_metadata_is_available_on_unwrapped_function(self):
|
||||
"""@tool should attach metadata where staticmethod access can find it."""
|
||||
|
||||
class MyClass:
|
||||
@tool(name="custom-static-tool")
|
||||
@staticmethod
|
||||
def my_method() -> str:
|
||||
return "hello"
|
||||
|
||||
created_tool = FunctionTool.from_function(MyClass.my_method)
|
||||
|
||||
assert created_tool.name == "custom-static-tool"
|
||||
|
||||
def test_tool_rejects_classmethod_decorator(self):
|
||||
"""@tool should reject classmethod-decorated functions."""
|
||||
with pytest.raises(TypeError, match="classmethod"):
|
||||
|
|
|
|||
41
uv.lock
generated
41
uv.lock
generated
|
|
@ -10,7 +10,7 @@ resolution-markers = [
|
|||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-05-15T22:43:18.553853Z"
|
||||
exclude-newer = "2026-05-16T14:51:14.412224Z"
|
||||
exclude-newer-span = "P1W"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
|
|
@ -938,7 +938,7 @@ dev = [
|
|||
{ name = "pytest-timeout", specifier = ">=2.4.0" },
|
||||
{ name = "pytest-xdist", specifier = ">=3.6.1" },
|
||||
{ name = "ruff", specifier = ">=0.12.8" },
|
||||
{ name = "ty", specifier = ">=0.0.29" },
|
||||
{ name = "ty", specifier = ">=0.0.39" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3150,26 +3150,27 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.29"
|
||||
version = "0.0.39"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/d5/853561de49fae38c519e905b2d8da9c531219608f1fccc47a0fc2c896980/ty-0.0.29.tar.gz", hash = "sha256:e7936cca2f691eeda631876c92809688dbbab68687c3473f526cd83b6a9228d8", size = 5469221, upload-time = "2026-04-05T15:01:21.328Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/8d/7b5c74dc287fbcb37bae9853cec13bf44717c1735298500e4aeba31579a9/ty-0.0.39.tar.gz", hash = "sha256:f750277e76a01ecd86185960eca73823c26a53c51103568d56d4d904575159fd", size = 5702365, upload-time = "2026-05-22T21:09:56.403Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/03/b7/911f9962115acfa24e3b2ec9d4992dd994c38e8769e1b1d7680bb4d28a51/ty-0.0.29-py3-none-linux_armv6l.whl", hash = "sha256:b8a40955f7660d3eaceb0d964affc81b790c0765e7052921a5f861ff8a471c30", size = 10568206, upload-time = "2026-04-05T15:01:19.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/c3/fcae2167d4c77a97269f92f11d1b43b03617f81de1283d5d05b43432110c/ty-0.0.29-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6b6849adae15b00bbe2d3c5b078967dcb62eba37d38936b8eeb4c81a82d2e3b8", size = 10442530, upload-time = "2026-04-05T15:01:28.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/33/5a6bfa240cfcb9c36046ae2459fa9ea23238d20130d8656ff5ac4d6c012a/ty-0.0.29-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dcdd9b17209788152f7b7ea815eda07989152325052fe690013537cc7904ce49", size = 9915735, upload-time = "2026-04-05T15:01:10.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/1e/318f45fae232118e81a6306c30f50de42c509c412128d5bd231eab699ffb/ty-0.0.29-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d8ed4789bae78ffaf94462c0d25589a734cab0366b86f2bbcb1bb90e1a7a169", size = 10419748, upload-time = "2026-04-05T15:01:32.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/a8/5687872e2ab5a0f7dd4fd8456eac31e9381ad4dc74961f6f29965ad4dd91/ty-0.0.29-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91ec374b8565e0ad0900011c24641ebbef2da51adbd4fb69ff3280c8a7eceb02", size = 10394738, upload-time = "2026-04-05T15:01:06.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/68/015d118097eeb95e6a44c4abce4c0a28b7b9dfb3085b7f0ee48e4f099633/ty-0.0.29-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:298a8d5faa2502d3810bbbb47a030b9455495b9921594206043c785dd61548cf", size = 10910613, upload-time = "2026-04-05T15:01:17.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/01/47ce3c6c53e0670eadbe80756b167bf80ed6681d1ba57cfde2e8065a13d1/ty-0.0.29-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c8fba1a3524c6109d1e020d92301c79d41bf442fa8d335b9fa366239339cb70", size = 11475750, upload-time = "2026-04-05T15:01:30.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/cf/e361845b1081c9264ad5b7c963231bab03f2666865a9f2a115c4233f2137/ty-0.0.29-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c48adf88a70d264128c39ee922ed14a947817fced1e93c08c1a89c9244edcde", size = 11190055, upload-time = "2026-04-05T15:01:12.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/12/0fb0857e9a62cb11586e9a712103877bbf717f5fb570d16634408cfdefee/ty-0.0.29-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ce0a7a0e96bc7b42518cd3a1a6a6298ef64ff40ca4614355c1aa807059b5c6f", size = 11020539, upload-time = "2026-04-05T15:01:37.022Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/36/5a26753802083f80cd125db6c4348ad42b3c982ec36e718e0bf4c18f75e5/ty-0.0.29-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6ac86a05b4a3731d45365ab97780acc7b8146fa62fccb3cbe94fe6546c67a97", size = 10396399, upload-time = "2026-04-05T15:01:26.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/e6/b4e75b5752239ab3ab400f19faef4dbef81d05aab5d3419fda0c062a3765/ty-0.0.29-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6bbbf53141af0f3150bf288d716263f1a3550054e4b3551ca866d38192ba9891", size = 10421461, upload-time = "2026-04-05T15:01:08.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/21/1084b5b609f9abed62070ec0b31c283a403832a6310c8bbc208bd45ee1e6/ty-0.0.29-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1c9e06b770c1d0ff5efc51e34312390db31d53fcf3088163f413030b42b74f84", size = 10599187, upload-time = "2026-04-05T15:01:23.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/a1/ce19a2ca717bbcc1ee11378aba52ef70b6ce5b87245162a729d9fdc2360f/ty-0.0.29-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0307fe37e3f000ef1a4ae230bbaf511508a78d24a5e51b40902a21b09d5e6037", size = 11121198, upload-time = "2026-04-05T15:01:15.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/6b/f1430b279af704321566ce7ec2725d3d8258c2f815ebd93e474c64cd4543/ty-0.0.29-py3-none-win32.whl", hash = "sha256:7a2a898217960a825f8bc0087e1fdbaf379606175e98f9807187221d53a4a8ed", size = 9995331, upload-time = "2026-04-05T15:01:01.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/ef/3ef01c17785ff9a69378465c7d0faccd48a07b163554db0995e5d65a5a23/ty-0.0.29-py3-none-win_amd64.whl", hash = "sha256:fc1294200226b91615acbf34e0a9ad81caf98c081e9c6a912a31b0a7b603bc3f", size = 11023644, upload-time = "2026-04-05T15:01:04.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/55/87280a994d6a2d2647c65e12abbc997ed49835794366153c04c4d9304d76/ty-0.0.29-py3-none-win_arm64.whl", hash = "sha256:f9794bbd1bb3ce13f78c191d0c89ae4c63f52c12b6daa0c6fe220b90d019d12c", size = 10428165, upload-time = "2026-04-05T15:01:34.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/17/9b89802c26d12d0f7a27bc25d4066d941d42891e8898f9f26499f0067e32/ty-0.0.39-py3-none-linux_armv6l.whl", hash = "sha256:c1bb7ac70f1f7d70cc6655fd96558039e4562b10f489fa49c7ebfd5fcee73ad1", size = 11360431, upload-time = "2026-05-22T21:09:18.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/c6/663ded50e823dbf9fb9d002eca46b7cb1fb2c72b744b84f22ce732a0ee0b/ty-0.0.39-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3435b64c1e59c14c9aa39c20cc018823937cd38d55db853e74d95b8f420569b0", size = 11096281, upload-time = "2026-05-22T21:09:15.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ae/5d38ba9a6456ff4c78d212cf464fd8b9a25d8118465197b0b2dc891c0b19/ty-0.0.39-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f136377ce46c73677701a9e1ad730bf72f699bcec046e422eb79d0886cac3ab", size = 10529674, upload-time = "2026-05-22T21:09:46.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/6f/43638cb8106445d3c8817256a0731cde9dd7b6a53ae2e881294bc1930ca3/ty-0.0.39-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36b65fb0cc17f03e851d40e210d420be94ab8bc52d041328ad1e45f616036a61", size = 11055561, upload-time = "2026-05-22T21:09:36.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/17/95e62cf4458527ce78dc386eba18f8b10c3fb64cd8c9e7e59b262ff6029d/ty-0.0.39-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4967967bfadf3860ff84c3fccdbaec8edf8aa20d0d727521084733d853de6657", size = 11127185, upload-time = "2026-05-22T21:09:31.395Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/c0/93666c213db5c71ab1b1f1a0db5f66bf8c7c0e0b0bf59859f5da8f0b3c36/ty-0.0.39-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e10ecb1297099ddf9a1f054f8bd921d1863ce85fb819a3c96ed27865a1ba6ed", size = 11608459, upload-time = "2026-05-22T21:09:12.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/85/3b26585afc8b50230d6464bb0642feef4fab3f847e38b1f0ffa971a81446/ty-0.0.39-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9b19cca70e465d71b0510656343883d62372bbe74b7845cae7c0e701d6d5264b", size = 12177101, upload-time = "2026-05-22T21:09:40.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/4a/1039e4f6afc576dc1c3a4d22a6478904a1ad3766597cd0b93c077ab9dfce/ty-0.0.39-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:56c6704b01b9b3d80ff26b2918423b742516d1e469bef830e9254dcedc9185bf", size = 11827815, upload-time = "2026-05-22T21:09:49.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/c5/4688652870e350a76a8157f7ffb59ad54f37d5d10725aa7076f66ac94ec8/ty-0.0.39-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b7840ff46764b6a6757f4ade1cd0530fc3e8a0b435ca93e7602360e4cb90b6", size = 11694429, upload-time = "2026-05-22T21:09:21.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/72/8a1c4e823bb5bdc935a1c8140e100304e36a68a4139592f170aa9736fdb7/ty-0.0.39-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c62a3a87ce26b50819f0dbf03bd95f23f19eeb87bbc7aa732ec64277c77f1aa", size = 11869846, upload-time = "2026-05-22T21:09:28.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/9f/cf982457b861ae22d657c5dcdbc631199f7f90264279db1d17230dfbc3ff/ty-0.0.39-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f8c34bc81a9c3516e49904e9d8330aac385377cca98390193ea02b903a40fcf0", size = 11029763, upload-time = "2026-05-22T21:09:06.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/c9/95b64f6d43ae6e8f0b7e13dacf9c196d35819af22b1924171fba31383156/ty-0.0.39-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:66f5ab11586a64e79cb692ad685ee5469325c31b5f30bd3554f52f36dbe28cc4", size = 11146761, upload-time = "2026-05-22T21:09:10.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/69/0a89cfb06f7632a05bf56c78e0affb4a40f81759e275376cea75c9c5abe9/ty-0.0.39-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e8d89732bcbbcb091f439e556dfc4932f198b118b47d5b85212c60662099670e", size = 11281843, upload-time = "2026-05-22T21:09:34.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/53/64c4a27067a46643fea2b3fcf21a8a2f838d91a65ffdd14f2e82945b9538/ty-0.0.39-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:eceb6c91dcd05a231119f82abdd9aa337513de23ca6ac990bc44f88791dc1799", size = 11792477, upload-time = "2026-05-22T21:09:24.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/e8/02f4dd4a12bcdbda0006f9c7ff3b99a4be06bd0d257d3bd4a5b66de074e6/ty-0.0.39-py3-none-win32.whl", hash = "sha256:891c3262314dbc80bf3e872634d23dd216306945daa9a9fcc206ce5ed21ac4c9", size = 10615377, upload-time = "2026-05-22T21:09:43.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/5a/aaeb22faa8d4dae90a287d4c3636c671edcff3b99be5f4fc8b79ad71eef6/ty-0.0.39-py3-none-win_amd64.whl", hash = "sha256:ba7f2d54452535419e90f6f03ff39282999e87b43c21c00559f6d7ad711a36d5", size = 11710711, upload-time = "2026-05-22T21:09:53.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/17/ae7339651bfcaa5f54698c8c70eaf5031baa400ecb67baec31d03a56cbd4/ty-0.0.39-py3-none-win_arm64.whl", hash = "sha256:eb4cf0fefbbfedf9a352597bb2431ebdcb7eb3a595c0f825f228e897a0ec285d", size = 11081409, upload-time = "2026-05-22T21:09:03.741Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue