Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
ad0696c
1
Parent(s): 18f9037
Add pyright
Browse files- .github/workflows/{lint.yml → run-static.yml} +0 -0
- .pre-commit-config.yaml +5 -0
- pyproject.toml +13 -0
- src/fastmcp/cli/cli.py +6 -1
- src/fastmcp/resources/base.py +5 -13
- src/fastmcp/resources/templates.py +1 -1
- src/fastmcp/server.py +7 -6
- src/fastmcp/utilities/func_metadata.py +8 -3
- src/fastmcp/utilities/types.py +3 -1
- tests/test_cli.py +10 -2
- uv.lock +18 -1
.github/workflows/{lint.yml → run-static.yml}
RENAMED
|
File without changes
|
.pre-commit-config.yaml
CHANGED
|
@@ -18,3 +18,8 @@ repos:
|
|
| 18 |
- id: ruff-format
|
| 19 |
- id: ruff
|
| 20 |
args: [--fix, --exit-non-zero-on-fix]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
- id: ruff-format
|
| 19 |
- id: ruff
|
| 20 |
args: [--fix, --exit-non-zero-on-fix]
|
| 21 |
+
|
| 22 |
+
- repo: https://github.com/RobertCraigie/pyright-python
|
| 23 |
+
rev: v1.1.352
|
| 24 |
+
hooks:
|
| 25 |
+
- id: pyright
|
pyproject.toml
CHANGED
|
@@ -25,6 +25,7 @@ build-backend = "hatchling.build"
|
|
| 25 |
[project.optional-dependencies]
|
| 26 |
tests = [
|
| 27 |
"pre-commit",
|
|
|
|
| 28 |
"pytest>=8.3.3",
|
| 29 |
"pytest-asyncio>=0.23.5",
|
| 30 |
"pytest-flakefinder",
|
|
@@ -39,3 +40,15 @@ asyncio_default_fixture_loop_scope = "session"
|
|
| 39 |
|
| 40 |
[tool.hatch.version]
|
| 41 |
source = "vcs"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
[project.optional-dependencies]
|
| 26 |
tests = [
|
| 27 |
"pre-commit",
|
| 28 |
+
"pyright>=1.1.389",
|
| 29 |
"pytest>=8.3.3",
|
| 30 |
"pytest-asyncio>=0.23.5",
|
| 31 |
"pytest-flakefinder",
|
|
|
|
| 40 |
|
| 41 |
[tool.hatch.version]
|
| 42 |
source = "vcs"
|
| 43 |
+
|
| 44 |
+
[tool.pyright]
|
| 45 |
+
include = ["src"]
|
| 46 |
+
exclude = ["**/node_modules", "**/__pycache__", ".venv", ".git", "dist"]
|
| 47 |
+
pythonVersion = "3.10"
|
| 48 |
+
pythonPlatform = "Darwin"
|
| 49 |
+
typeCheckingMode = "basic"
|
| 50 |
+
reportMissingImports = true
|
| 51 |
+
reportMissingTypeStubs = false
|
| 52 |
+
useLibraryCodeForTypes = true
|
| 53 |
+
venvPath = "."
|
| 54 |
+
venv = ".venv"
|
src/fastmcp/cli/cli.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
| 2 |
|
| 3 |
import importlib.metadata
|
| 4 |
import importlib.util
|
|
|
|
| 5 |
import subprocess
|
| 6 |
import sys
|
| 7 |
from pathlib import Path
|
|
@@ -242,6 +243,7 @@ def dev(
|
|
| 242 |
[npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd,
|
| 243 |
check=True,
|
| 244 |
shell=shell,
|
|
|
|
| 245 |
)
|
| 246 |
sys.exit(process.returncode)
|
| 247 |
except subprocess.CalledProcessError as e:
|
|
@@ -423,7 +425,10 @@ def install(
|
|
| 423 |
# Load from .env file if specified
|
| 424 |
if env_file:
|
| 425 |
try:
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
| 427 |
except Exception as e:
|
| 428 |
logger.error(f"Failed to load .env file: {e}")
|
| 429 |
sys.exit(1)
|
|
|
|
| 2 |
|
| 3 |
import importlib.metadata
|
| 4 |
import importlib.util
|
| 5 |
+
import os
|
| 6 |
import subprocess
|
| 7 |
import sys
|
| 8 |
from pathlib import Path
|
|
|
|
| 243 |
[npx_cmd, "@modelcontextprotocol/inspector"] + uv_cmd,
|
| 244 |
check=True,
|
| 245 |
shell=shell,
|
| 246 |
+
env=dict(os.environ.items()), # Convert to list of tuples for env update
|
| 247 |
)
|
| 248 |
sys.exit(process.returncode)
|
| 249 |
except subprocess.CalledProcessError as e:
|
|
|
|
| 425 |
# Load from .env file if specified
|
| 426 |
if env_file:
|
| 427 |
try:
|
| 428 |
+
env_values = dotenv.dotenv_values(env_file)
|
| 429 |
+
env_dict.update(
|
| 430 |
+
(k, str(v)) for k, v in env_values.items() if v is not None
|
| 431 |
+
)
|
| 432 |
except Exception as e:
|
| 433 |
logger.error(f"Failed to load .env file: {e}")
|
| 434 |
sys.exit(1)
|
src/fastmcp/resources/base.py
CHANGED
|
@@ -1,14 +1,14 @@
|
|
| 1 |
"""Base classes and interfaces for FastMCP resources."""
|
| 2 |
|
| 3 |
import abc
|
| 4 |
-
from typing import Union
|
| 5 |
|
| 6 |
from pydantic import (
|
| 7 |
AnyUrl,
|
| 8 |
BaseModel,
|
| 9 |
ConfigDict,
|
| 10 |
Field,
|
| 11 |
-
|
| 12 |
ValidationInfo,
|
| 13 |
field_validator,
|
| 14 |
)
|
|
@@ -19,8 +19,9 @@ class Resource(BaseModel, abc.ABC):
|
|
| 19 |
|
| 20 |
model_config = ConfigDict(validate_default=True)
|
| 21 |
|
| 22 |
-
|
| 23 |
-
|
|
|
|
| 24 |
name: str | None = Field(description="Name of the resource", default=None)
|
| 25 |
description: str | None = Field(
|
| 26 |
description="Description of the resource", default=None
|
|
@@ -31,15 +32,6 @@ class Resource(BaseModel, abc.ABC):
|
|
| 31 |
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
| 32 |
)
|
| 33 |
|
| 34 |
-
@field_validator("uri", mode="before")
|
| 35 |
-
def validate_uri(cls, uri: AnyUrl | str) -> AnyUrl:
|
| 36 |
-
if isinstance(uri, str):
|
| 37 |
-
# AnyUrl doesn't support triple-slashes, but files do ("file:///absolute/path")
|
| 38 |
-
if uri.startswith("file://"):
|
| 39 |
-
return FileUrl(uri)
|
| 40 |
-
return AnyUrl(uri)
|
| 41 |
-
return uri
|
| 42 |
-
|
| 43 |
@field_validator("name", mode="before")
|
| 44 |
@classmethod
|
| 45 |
def set_default_name(cls, name: str | None, info: ValidationInfo) -> str:
|
|
|
|
| 1 |
"""Base classes and interfaces for FastMCP resources."""
|
| 2 |
|
| 3 |
import abc
|
| 4 |
+
from typing import Union, Annotated
|
| 5 |
|
| 6 |
from pydantic import (
|
| 7 |
AnyUrl,
|
| 8 |
BaseModel,
|
| 9 |
ConfigDict,
|
| 10 |
Field,
|
| 11 |
+
UrlConstraints,
|
| 12 |
ValidationInfo,
|
| 13 |
field_validator,
|
| 14 |
)
|
|
|
|
| 19 |
|
| 20 |
model_config = ConfigDict(validate_default=True)
|
| 21 |
|
| 22 |
+
uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(
|
| 23 |
+
default=..., description="URI of the resource"
|
| 24 |
+
)
|
| 25 |
name: str | None = Field(description="Name of the resource", default=None)
|
| 26 |
description: str | None = Field(
|
| 27 |
description="Description of the resource", default=None
|
|
|
|
| 32 |
pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
|
| 33 |
)
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
@field_validator("name", mode="before")
|
| 36 |
@classmethod
|
| 37 |
def set_default_name(cls, name: str | None, info: ValidationInfo) -> str:
|
src/fastmcp/resources/templates.py
CHANGED
|
@@ -70,7 +70,7 @@ class ResourceTemplate(BaseModel):
|
|
| 70 |
result = await result
|
| 71 |
|
| 72 |
return FunctionResource(
|
| 73 |
-
uri=uri,
|
| 74 |
name=self.name,
|
| 75 |
description=self.description,
|
| 76 |
mime_type=self.mime_type,
|
|
|
|
| 70 |
result = await result
|
| 71 |
|
| 72 |
return FunctionResource(
|
| 73 |
+
uri=uri, # type: ignore
|
| 74 |
name=self.name,
|
| 75 |
description=self.description,
|
| 76 |
mime_type=self.mime_type,
|
src/fastmcp/server.py
CHANGED
|
@@ -23,6 +23,7 @@ from mcp.types import (
|
|
| 23 |
)
|
| 24 |
from mcp.types import (
|
| 25 |
Prompt as MCPPrompt,
|
|
|
|
| 26 |
)
|
| 27 |
from mcp.types import (
|
| 28 |
Resource as MCPResource,
|
|
@@ -159,7 +160,7 @@ class FastMCP:
|
|
| 159 |
|
| 160 |
async def call_tool(
|
| 161 |
self, name: str, arguments: dict
|
| 162 |
-
) -> Sequence[TextContent | ImageContent]:
|
| 163 |
"""Call a tool by name with arguments."""
|
| 164 |
context = self.get_context()
|
| 165 |
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
|
@@ -462,11 +463,11 @@ class FastMCP:
|
|
| 462 |
name=prompt.name,
|
| 463 |
description=prompt.description,
|
| 464 |
arguments=[
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
for arg in (prompt.arguments or [])
|
| 471 |
],
|
| 472 |
)
|
|
|
|
| 23 |
)
|
| 24 |
from mcp.types import (
|
| 25 |
Prompt as MCPPrompt,
|
| 26 |
+
PromptArgument as MCPPromptArgument,
|
| 27 |
)
|
| 28 |
from mcp.types import (
|
| 29 |
Resource as MCPResource,
|
|
|
|
| 160 |
|
| 161 |
async def call_tool(
|
| 162 |
self, name: str, arguments: dict
|
| 163 |
+
) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
|
| 164 |
"""Call a tool by name with arguments."""
|
| 165 |
context = self.get_context()
|
| 166 |
result = await self._tool_manager.call_tool(name, arguments, context=context)
|
|
|
|
| 463 |
name=prompt.name,
|
| 464 |
description=prompt.description,
|
| 465 |
arguments=[
|
| 466 |
+
MCPPromptArgument(
|
| 467 |
+
name=arg.name,
|
| 468 |
+
description=arg.description,
|
| 469 |
+
required=arg.required,
|
| 470 |
+
)
|
| 471 |
for arg in (prompt.arguments or [])
|
| 472 |
],
|
| 473 |
)
|
src/fastmcp/utilities/func_metadata.py
CHANGED
|
@@ -47,7 +47,7 @@ class FuncMetadata(BaseModel):
|
|
| 47 |
|
| 48 |
async def call_fn_with_arg_validation(
|
| 49 |
self,
|
| 50 |
-
fn: Callable | Awaitable,
|
| 51 |
fn_is_async: bool,
|
| 52 |
arguments_to_validate: dict[str, Any],
|
| 53 |
arguments_to_pass_directly: dict[str, Any] | None,
|
|
@@ -64,8 +64,12 @@ class FuncMetadata(BaseModel):
|
|
| 64 |
arguments_parsed_dict |= arguments_to_pass_directly or {}
|
| 65 |
|
| 66 |
if fn_is_async:
|
|
|
|
|
|
|
| 67 |
return await fn(**arguments_parsed_dict)
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
|
| 70 |
def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
|
| 71 |
"""Pre-parse data from JSON.
|
|
@@ -123,6 +127,7 @@ def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadat
|
|
| 123 |
sig = _get_typed_signature(func)
|
| 124 |
params = sig.parameters
|
| 125 |
dynamic_pydantic_model_params: dict[str, Any] = {}
|
|
|
|
| 126 |
for param in params.values():
|
| 127 |
if param.name.startswith("_"):
|
| 128 |
raise InvalidSignature(
|
|
@@ -153,7 +158,7 @@ def func_metadata(func: Callable, skip_names: Sequence[str] = ()) -> FuncMetadat
|
|
| 153 |
]
|
| 154 |
|
| 155 |
field_info = FieldInfo.from_annotated_attribute(
|
| 156 |
-
annotation,
|
| 157 |
param.default
|
| 158 |
if param.default is not inspect.Parameter.empty
|
| 159 |
else PydanticUndefined,
|
|
|
|
| 47 |
|
| 48 |
async def call_fn_with_arg_validation(
|
| 49 |
self,
|
| 50 |
+
fn: Callable[..., Any] | Awaitable[Any],
|
| 51 |
fn_is_async: bool,
|
| 52 |
arguments_to_validate: dict[str, Any],
|
| 53 |
arguments_to_pass_directly: dict[str, Any] | None,
|
|
|
|
| 64 |
arguments_parsed_dict |= arguments_to_pass_directly or {}
|
| 65 |
|
| 66 |
if fn_is_async:
|
| 67 |
+
if isinstance(fn, Awaitable):
|
| 68 |
+
return await fn
|
| 69 |
return await fn(**arguments_parsed_dict)
|
| 70 |
+
if isinstance(fn, Callable):
|
| 71 |
+
return fn(**arguments_parsed_dict)
|
| 72 |
+
raise TypeError("fn must be either Callable or Awaitable")
|
| 73 |
|
| 74 |
def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
|
| 75 |
"""Pre-parse data from JSON.
|
|
|
|
| 127 |
sig = _get_typed_signature(func)
|
| 128 |
params = sig.parameters
|
| 129 |
dynamic_pydantic_model_params: dict[str, Any] = {}
|
| 130 |
+
globalns = getattr(func, "__globals__", {})
|
| 131 |
for param in params.values():
|
| 132 |
if param.name.startswith("_"):
|
| 133 |
raise InvalidSignature(
|
|
|
|
| 158 |
]
|
| 159 |
|
| 160 |
field_info = FieldInfo.from_annotated_attribute(
|
| 161 |
+
_get_typed_annotation(annotation, globalns),
|
| 162 |
param.default
|
| 163 |
if param.default is not inspect.Parameter.empty
|
| 164 |
else PydanticUndefined,
|
src/fastmcp/utilities/types.py
CHANGED
|
@@ -47,7 +47,9 @@ class Image:
|
|
| 47 |
if self.path:
|
| 48 |
with open(self.path, "rb") as f:
|
| 49 |
data = base64.b64encode(f.read()).decode()
|
| 50 |
-
|
| 51 |
data = base64.b64encode(self.data).decode()
|
|
|
|
|
|
|
| 52 |
|
| 53 |
return ImageContent(type="image", data=data, mimeType=self._mime_type)
|
|
|
|
| 47 |
if self.path:
|
| 48 |
with open(self.path, "rb") as f:
|
| 49 |
data = base64.b64encode(f.read()).decode()
|
| 50 |
+
elif self.data is not None:
|
| 51 |
data = base64.b64encode(self.data).decode()
|
| 52 |
+
else:
|
| 53 |
+
raise ValueError("No image data available")
|
| 54 |
|
| 55 |
return ImageContent(type="image", data=data, mimeType=self._mime_type)
|
tests/test_cli.py
CHANGED
|
@@ -320,7 +320,11 @@ mcp = FastMCP("test", dependencies=["pandas", "numpy"])
|
|
| 320 |
x in deps_section for x in ["--with", "numpy", "--with", "pandas"]
|
| 321 |
)
|
| 322 |
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
else:
|
| 325 |
# same verification for unix, just with different command prefix
|
| 326 |
actual_cmd = mock_run.call_args_list[0][0][0]
|
|
@@ -342,7 +346,11 @@ mcp = FastMCP("test", dependencies=["pandas", "numpy"])
|
|
| 342 |
x in deps_section for x in ["--with", "numpy", "--with", "pandas"]
|
| 343 |
)
|
| 344 |
|
| 345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
|
| 347 |
|
| 348 |
def test_run_with_dependencies(mock_config, server_file):
|
|
|
|
| 320 |
x in deps_section for x in ["--with", "numpy", "--with", "pandas"]
|
| 321 |
)
|
| 322 |
|
| 323 |
+
# Verify subprocess call kwargs, allowing for environment variables
|
| 324 |
+
call_kwargs = mock_run.call_args_list[1][1]
|
| 325 |
+
assert call_kwargs["check"] is True
|
| 326 |
+
assert call_kwargs["shell"] is True
|
| 327 |
+
assert isinstance(call_kwargs["env"], dict)
|
| 328 |
else:
|
| 329 |
# same verification for unix, just with different command prefix
|
| 330 |
actual_cmd = mock_run.call_args_list[0][0][0]
|
|
|
|
| 346 |
x in deps_section for x in ["--with", "numpy", "--with", "pandas"]
|
| 347 |
)
|
| 348 |
|
| 349 |
+
# Verify subprocess call kwargs, allowing for environment variables
|
| 350 |
+
call_kwargs = mock_run.call_args_list[0][1]
|
| 351 |
+
assert call_kwargs["check"] is True
|
| 352 |
+
assert call_kwargs["shell"] is False
|
| 353 |
+
assert isinstance(call_kwargs["env"], dict)
|
| 354 |
|
| 355 |
|
| 356 |
def test_run_with_dependencies(mock_config, server_file):
|
uv.lock
CHANGED
|
@@ -228,7 +228,7 @@ wheels = [
|
|
| 228 |
|
| 229 |
[[package]]
|
| 230 |
name = "fastmcp"
|
| 231 |
-
version = "0.3.6.
|
| 232 |
source = { editable = "." }
|
| 233 |
dependencies = [
|
| 234 |
{ name = "httpx" },
|
|
@@ -245,6 +245,7 @@ dev = [
|
|
| 245 |
{ name = "ipython" },
|
| 246 |
{ name = "pdbpp" },
|
| 247 |
{ name = "pre-commit" },
|
|
|
|
| 248 |
{ name = "pytest" },
|
| 249 |
{ name = "pytest-asyncio" },
|
| 250 |
{ name = "pytest-flakefinder" },
|
|
@@ -253,6 +254,7 @@ dev = [
|
|
| 253 |
]
|
| 254 |
tests = [
|
| 255 |
{ name = "pre-commit" },
|
|
|
|
| 256 |
{ name = "pytest" },
|
| 257 |
{ name = "pytest-asyncio" },
|
| 258 |
{ name = "pytest-flakefinder" },
|
|
@@ -271,6 +273,8 @@ requires-dist = [
|
|
| 271 |
{ name = "pre-commit", marker = "extra == 'tests'" },
|
| 272 |
{ name = "pydantic", specifier = ">=2.5.3,<3.0.0" },
|
| 273 |
{ name = "pydantic-settings", specifier = ">=2.6.1" },
|
|
|
|
|
|
|
| 274 |
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.3" },
|
| 275 |
{ name = "pytest", marker = "extra == 'tests'", specifier = ">=8.3.3" },
|
| 276 |
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.5" },
|
|
@@ -730,6 +734,19 @@ version = "0.9.0"
|
|
| 730 |
source = { registry = "https://pypi.org/simple" }
|
| 731 |
sdist = { url = "https://files.pythonhosted.org/packages/05/1b/ea40363be0056080454cdbabe880773c3c5bd66d7b13f0c8b8b8c8da1e0c/pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775", size = 48744 }
|
| 732 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 733 |
[[package]]
|
| 734 |
name = "pytest"
|
| 735 |
version = "8.3.3"
|
|
|
|
| 228 |
|
| 229 |
[[package]]
|
| 230 |
name = "fastmcp"
|
| 231 |
+
version = "0.3.6.dev5+g6a13ab9.d20241203"
|
| 232 |
source = { editable = "." }
|
| 233 |
dependencies = [
|
| 234 |
{ name = "httpx" },
|
|
|
|
| 245 |
{ name = "ipython" },
|
| 246 |
{ name = "pdbpp" },
|
| 247 |
{ name = "pre-commit" },
|
| 248 |
+
{ name = "pyright" },
|
| 249 |
{ name = "pytest" },
|
| 250 |
{ name = "pytest-asyncio" },
|
| 251 |
{ name = "pytest-flakefinder" },
|
|
|
|
| 254 |
]
|
| 255 |
tests = [
|
| 256 |
{ name = "pre-commit" },
|
| 257 |
+
{ name = "pyright" },
|
| 258 |
{ name = "pytest" },
|
| 259 |
{ name = "pytest-asyncio" },
|
| 260 |
{ name = "pytest-flakefinder" },
|
|
|
|
| 273 |
{ name = "pre-commit", marker = "extra == 'tests'" },
|
| 274 |
{ name = "pydantic", specifier = ">=2.5.3,<3.0.0" },
|
| 275 |
{ name = "pydantic-settings", specifier = ">=2.6.1" },
|
| 276 |
+
{ name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.389" },
|
| 277 |
+
{ name = "pyright", marker = "extra == 'tests'", specifier = ">=1.1.389" },
|
| 278 |
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.3" },
|
| 279 |
{ name = "pytest", marker = "extra == 'tests'", specifier = ">=8.3.3" },
|
| 280 |
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.5" },
|
|
|
|
| 734 |
source = { registry = "https://pypi.org/simple" }
|
| 735 |
sdist = { url = "https://files.pythonhosted.org/packages/05/1b/ea40363be0056080454cdbabe880773c3c5bd66d7b13f0c8b8b8c8da1e0c/pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775", size = 48744 }
|
| 736 |
|
| 737 |
+
[[package]]
|
| 738 |
+
name = "pyright"
|
| 739 |
+
version = "1.1.389"
|
| 740 |
+
source = { registry = "https://pypi.org/simple" }
|
| 741 |
+
dependencies = [
|
| 742 |
+
{ name = "nodeenv" },
|
| 743 |
+
{ name = "typing-extensions" },
|
| 744 |
+
]
|
| 745 |
+
sdist = { url = "https://files.pythonhosted.org/packages/72/4e/9a5ab8745e7606b88c2c7ca223449ac9d82a71fd5e31df47b453f2cb39a1/pyright-1.1.389.tar.gz", hash = "sha256:716bf8cc174ab8b4dcf6828c3298cac05c5ed775dda9910106a5dcfe4c7fe220", size = 21940 }
|
| 746 |
+
wheels = [
|
| 747 |
+
{ url = "https://files.pythonhosted.org/packages/1b/26/c288cabf8cfc5a27e1aa9e5029b7682c0f920b8074f45d22bf844314d66a/pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60", size = 18581 },
|
| 748 |
+
]
|
| 749 |
+
|
| 750 |
[[package]]
|
| 751 |
name = "pytest"
|
| 752 |
version = "8.3.3"
|