add ty check (#205)

This commit is contained in:
Adrian Lyjak
2025-11-18 10:55:40 -05:00
committed by GitHub
parent 32ae78a947
commit f3072532e4
29 changed files with 130 additions and 46 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"llama-index-utils-workflow": patch
"llama-index-workflows": patch
---
Update typechecking to support ty
+7
View File
@@ -23,6 +23,13 @@ repos:
- id: ruff-check
args: [--fix, --exit-non-zero-on-fix]
- repo: local
hooks:
- id: ty
name: ty
language: system
entry: uv run ty check packages
exclude: ^examples/|^docs/
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
hooks:
@@ -501,7 +501,11 @@ def _process_tools_and_handoffs(
if agent.name == root_agent:
edges.append(DrawWorkflowEdge("user", root_agent))
for t in agent.tools or []:
fn_name = t.metadata.get_name() if isinstance(t, BaseTool) else t.__name__
if isinstance(t, BaseTool):
fn_name = t.metadata.get_name()
else:
# Fallback for non-BaseTool callables or objects without __name__
fn_name = getattr(t, "__name__", type(t).__name__)
node_id = f"{agent.name}_{fn_name}"
nodes.append(
DrawWorkflowNode(
@@ -215,13 +215,14 @@ class Context(Generic[MODEL_T]):
# Initialize a runtime plugin (asyncio-based by default)
runtime: WorkflowRuntime = plugin or self._plugin.new_runtime(str(uuid.uuid4()))
# Initialize the new broker implementation (broker2)
self._broker_run = WorkflowBroker(
broker: WorkflowBroker[MODEL_T] = WorkflowBroker(
workflow=workflow,
context=self,
context=cast("Context[MODEL_T]", self),
runtime=runtime,
plugin=self._plugin,
)
return self._broker_run
self._broker_run = broker
return broker
def _workflow_run(
self,
@@ -31,7 +31,7 @@ class _Resource(Generic[T]):
def __init__(self, factory: Callable[..., T | Awaitable[T]], cache: bool) -> None:
self._factory = factory
self._is_async = inspect.iscoroutinefunction(factory)
self.name = factory.__qualname__
self.name = getattr(factory, "__qualname__", type(factory).__name__)
self.cache = cache
async def call(self) -> T:
@@ -9,16 +9,13 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import (
AsyncGenerator,
Callable,
Coroutine,
Generic,
Protocol,
TYPE_CHECKING,
cast,
)
from workflows.decorators import P, R
from workflows.events import Event, StopEvent
from workflows.runtime.types.internal_state import BrokerState
@@ -30,8 +27,8 @@ if TYPE_CHECKING:
@dataclass
class RegisteredWorkflow(Generic[P, R]):
workflow_function: Callable[P, R]
class RegisteredWorkflow:
workflow_function: ControlLoopFunction
steps: dict[str, StepWorkerFunction]
@@ -85,7 +85,7 @@ class Returns(Generic[R]):
step function has completed (including errors!)
"""
return_values: list[StepFunctionResult[R, Any]]
return_values: list[StepFunctionResult[R]]
class WaitingForEvent(Exception, Generic[EventType]):
@@ -177,6 +177,6 @@ StepFunctionResult = Union[
StepWorkerFailed[R],
AddCollectedEvent,
DeleteCollectedEvent,
AddWaiter[EventType],
AddWaiter[Event],
DeleteWaiter,
]
@@ -40,7 +40,7 @@ class StepWorkerFunction(Protocol, Generic[R]):
event: Event,
context: Context, # TODO - pass an identifier and re-hydrate from the plugin for distributed step workers
workflow: Workflow,
) -> Awaitable[list[StepFunctionResult[R, Any]]]: ...
) -> Awaitable[list[StepFunctionResult[R]]]: ...
async def partial(
@@ -81,7 +81,7 @@ def as_step_worker_function(func: Callable[P, Awaitable[R]]) -> StepWorkerFuncti
event: Event,
context: Context,
workflow: Workflow,
) -> list[StepFunctionResult[R, Any]]:
) -> list[StepFunctionResult[R]]:
returns = Returns[R](return_values=[])
token = StepWorkerStateContextVar.set(
@@ -17,7 +17,7 @@ events that can occur during workflow execution:
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Generic, Union
from typing import Generic, Union
from workflows.events import Event
from workflows.decorators import R
@@ -31,7 +31,7 @@ class TickStepResult(Generic[R]):
step_name: str
worker_id: int
event: Event
result: list[StepFunctionResult[R, Any]]
result: list[StepFunctionResult[R]]
@dataclass(frozen=True)
@@ -55,6 +55,9 @@ def run_server() -> None:
)
sys.exit(1)
# At this point, a WorkflowServer instance is guaranteed to exist
assert server is not None
host = os.environ.get("WORKFLOWS_PY_SERVER_HOST", "0.0.0.0")
port = int(os.environ.get("WORKFLOWS_PY_SERVER_PORT", 8080))
uvicorn.run(server.app, host=host, port=port)
@@ -9,7 +9,7 @@ import json
import logging
from importlib.metadata import version
from pathlib import Path
from typing import Any, AsyncGenerator, Callable, Awaitable
from typing import Any, AsyncGenerator, Callable, Awaitable, cast
from datetime import datetime, timezone
from llama_index_instrumentation.dispatcher import instrument_tags
@@ -90,7 +90,7 @@ class WorkflowServer:
self._middleware = middleware or [
Middleware(
CORSMiddleware,
CORSMiddleware, # type: ignore[arg-type]
# regex echoes the origin header back, which some browsers require (rather than "*") when credentials are required
allow_origin_regex=".*",
allow_methods=["*"],
@@ -1036,12 +1036,14 @@ class WorkflowServer:
"cancelled",
}
status_in = (
list(set(allowed_status_values).intersection(status_values))
status_in: list[Status] | None = (
cast(
list[Status],
list(set(allowed_status_values).intersection(status_values)),
)
if status_values is not None
else None
)
persistent_handlers = await self._workflow_store.query(
HandlerQuery(status_in=status_in, workflow_name_in=workflow_name_in)
)
@@ -93,6 +93,9 @@ class Workflow(metaclass=WorkflowMeta):
- [RetryPolicy][workflows.retry_policy.RetryPolicy]
"""
# Populated by the metaclass; declared here for type checkers.
_step_functions: dict[str, StepFunction]
def __init__(
self,
timeout: float | None = 45.0,
@@ -5,7 +5,7 @@ from httpx import ASGITransport, AsyncClient
from workflows.protocol.serializable_events import EventEnvelopeWithMetadata
from workflows.server.server import WorkflowServer
from workflows.client import WorkflowClient
from .client_workflows import (
from .client_workflows import ( # type: ignore[import]
greeting_wf,
crashing_wf,
InputEvent,
@@ -31,7 +31,11 @@ from workflows.events import (
from workflows.testing import WorkflowTestRunner
from workflows.workflow import Workflow
from ..conftest import AnotherTestEvent, LastEvent, OneTestEvent
from ..conftest import ( # type: ignore[import]
AnotherTestEvent,
LastEvent,
OneTestEvent,
)
@pytest.mark.asyncio
@@ -36,7 +36,7 @@ from workflows.runtime.workflow_registry import workflow_registry
from workflows.runtime.types.ticks import TickAddEvent, TickCancelRun
from workflows.retry_policy import ConstantDelayRetryPolicy
from .conftest import MockRuntimePlugin
from .conftest import MockRuntimePlugin # type: ignore[import]
class IntermediateEvent(Event):
@@ -17,6 +17,7 @@ def test_no_file_path_argument(capsys: Any) -> None:
with pytest.raises(SystemExit) as exc_info:
run_server()
assert isinstance(exc_info.value, SystemExit)
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert (
@@ -30,6 +31,7 @@ def test_nonexistent_file(capsys: Any) -> None:
with pytest.raises(SystemExit) as exc_info:
run_server()
assert isinstance(exc_info.value, SystemExit)
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert "Error: File '/nonexistent/file.py' not found" in captured.err
@@ -44,6 +46,7 @@ def test_directory_instead_of_file(capsys: Any, tmp_path: Path) -> None:
with pytest.raises(SystemExit) as exc_info:
run_server()
assert isinstance(exc_info.value, SystemExit)
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert f"Error: '{test_dir}' is not a file" in captured.err
@@ -63,6 +66,7 @@ another_variable = 42
with pytest.raises(SystemExit) as exc_info:
run_server()
assert isinstance(exc_info.value, SystemExit)
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert (
@@ -157,6 +161,7 @@ def invalid_syntax(
with pytest.raises(SystemExit) as exc_info:
run_server()
assert isinstance(exc_info.value, SystemExit)
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert "Error loading or running server:" in captured.err
@@ -172,6 +177,7 @@ def test_spec_creation_failure(capsys: Any, tmp_path: Path) -> None:
with pytest.raises(SystemExit) as exc_info:
run_server()
assert isinstance(exc_info.value, SystemExit)
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert "Unable to get spec from module" in captured.err
@@ -21,7 +21,7 @@ def test_stop_event_round_trip() -> None:
dumped = handler.model_dump(mode="python")
restored = PersistentHandler(**dumped)
assert isinstance(restored.result, StopEvent)
assert cast(StopEvent, restored.result).result == 1
assert restored.result.result == 1
def test_legacy_result_dict_is_coerced_to_stop_event() -> None:
@@ -31,13 +31,13 @@ def test_legacy_result_dict_is_coerced_to_stop_event() -> None:
)
assert isinstance(handler.result, StopEvent)
assert cast(StopEvent, handler.result).result == legacy_payload
assert handler.result.result == legacy_payload
dumped = handler.model_dump(mode="python")
restored = PersistentHandler(**dumped)
assert isinstance(restored.result, StopEvent)
assert cast(StopEvent, restored.result).result == legacy_payload
assert restored.result.result == legacy_payload
class MyStop(StopEvent):
@@ -55,10 +55,10 @@ def test_stop_event_subclass_round_trip() -> None:
restored = PersistentHandler(**dumped)
assert isinstance(restored.result, MyStop)
assert cast(MyStop, restored.result).result == payload
assert restored.result.result == payload
def test_converts_to_stop_event() -> None:
handler = PersistentHandler(**_base_handler_kwargs(), result=123) # type: ignore[arg-type]
assert isinstance(handler.result, StopEvent)
assert cast(StopEvent, handler.result).result == 123
assert handler.result.result == 123
@@ -15,7 +15,7 @@ from httpx import ASGITransport, AsyncClient, Response
from workflows.events import StopEvent, StartEvent
from .util import wait_for_passing
from .util import wait_for_passing # type: ignore[import]
from workflows import Context, step
from workflows.server import WorkflowServer
from workflows.server.abstract_workflow_store import HandlerQuery, PersistentHandler
@@ -24,7 +24,7 @@ from datetime import datetime
# Prepare the event to send
from workflows.context.serializers import JsonSerializer
from .conftest import ExternalEvent
from .conftest import ExternalEvent # type: ignore[import]
from workflows.server.memory_workflow_store import MemoryWorkflowStore
from llama_index_instrumentation.dispatcher import active_instrument_tags
@@ -17,8 +17,11 @@ from workflows import Workflow
from workflows.events import StopEvent
from workflows.server import WorkflowServer
from workflows.client.client import WorkflowClient
from .conftest import ExternalEvent, RequestedExternalEvent
from .util import wait_for_passing
from .conftest import ( # type: ignore[import]
ExternalEvent,
RequestedExternalEvent,
)
from .util import wait_for_passing # type: ignore[import]
def _get_free_port() -> int:
@@ -6,7 +6,10 @@ from typing import AsyncGenerator
from httpx import AsyncClient, ASGITransport
from .conftest import ExternalEvent, RequestedExternalEvent
from .conftest import ( # type: ignore[import]
ExternalEvent,
RequestedExternalEvent,
)
from workflows.events import Event, InternalDispatchEvent, StopEvent
from workflows.server import WorkflowServer
from workflows import Context
@@ -14,7 +17,7 @@ from workflows.workflow import Workflow
from workflows.server.abstract_workflow_store import HandlerQuery, PersistentHandler
from workflows.server.memory_workflow_store import MemoryWorkflowStore
from .util import wait_for_passing
from .util import wait_for_passing # type: ignore[import]
@pytest.fixture
@@ -23,6 +23,7 @@ async def wait_for_passing(
if last_exception:
raise last_exception
else:
func_name = getattr(func, "__name__", repr(func))
raise TimeoutError(
f"Function {func.__name__} timed out after {max_duration} seconds"
f"Function {func_name} timed out after {max_duration} seconds"
)
@@ -12,6 +12,11 @@ from workflows.events import Event, StartEvent, StopEvent
from workflows.resource import Resource, ResourceManager
from workflows.workflow import Workflow
# Global counters used in resource workflow tests
cc: int
cc1: int
cc2: int
class SecondEvent(Event):
msg: str = Field(description="A message")
@@ -13,7 +13,7 @@ from workflows.errors import WorkflowRuntimeError, WorkflowTimeoutError
from workflows.events import Event, StartEvent, StopEvent
from workflows.workflow import Workflow
from .conftest import OneTestEvent
from .conftest import OneTestEvent # type: ignore[import]
class StreamingWorkflow(Workflow):
@@ -20,7 +20,10 @@ from workflows.utils import (
validate_step_signature,
)
from .conftest import AnotherTestEvent, OneTestEvent
from .conftest import ( # type: ignore[import]
AnotherTestEvent,
OneTestEvent,
)
def test_validate_step_signature_of_method() -> None:
@@ -9,7 +9,7 @@ import logging
import pickle
import threading
import weakref
from typing import Any, Callable, Union
from typing import Any, Callable, Union, cast
from unittest import mock
from llama_index_instrumentation.dispatcher import active_instrument_tags
@@ -36,7 +36,7 @@ from workflows.runtime.types.ticks import TickAddEvent
from workflows.testing import WorkflowTestRunner
from workflows.workflow import Workflow
from .conftest import (
from .conftest import ( # type: ignore[import]
AnotherTestEvent,
DummyWorkflow,
LastEvent,
@@ -374,9 +374,10 @@ def test_workflow_disable_validation() -> None:
raise ValueError("The step raised an error!")
w = DummyWorkflow(disable_validation=True)
w._get_steps = mock.MagicMock() # type:ignore
w._get_steps = mock.MagicMock() # type: ignore[assignment]
mock_get_steps = cast(mock.MagicMock, w._get_steps)
w._validate()
w._get_steps.assert_not_called()
mock_get_steps.assert_not_called()
@pytest.mark.asyncio
@@ -872,7 +873,10 @@ async def test_workflow_instances_garbage_collected_after_completion() -> None:
for _ in range(10):
wf = TinyWorkflow()
refs.append(weakref.ref(wf))
wf_ref: weakref.ReferenceType[Workflow] = cast(
weakref.ReferenceType[Workflow], weakref.ref(wf)
)
refs.append(wf_ref)
await WorkflowTestRunner(wf).run()
# Drop strong reference before next iteration
del wf
@@ -13,7 +13,7 @@ from workflows.events import (
from workflows.workflow import Workflow
from workflows.testing import WorkflowTestRunner
from .conftest import OneTestEvent
from .conftest import OneTestEvent # type: ignore[import]
class PostponedAnnotationsWorkflow(Workflow):
+2 -1
View File
@@ -5,7 +5,8 @@ build-backend = "uv_build"
[dependency-groups]
dev = [
"pytest>=8.4.2",
"pytest-cov>=7.0.0"
"pytest-cov>=7.0.0",
"ty>=0.0.1a26"
]
[project]
@@ -5,8 +5,12 @@ from pathlib import Path
from typing import Optional
from packaging.version import Version
import sys
import tomllib
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
class VersionMismatchError(ValueError):
Generated
+27
View File
@@ -3933,6 +3933,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/85/a4ff8758c66f1fc32aa5e9a145908394bf9cf1c79ffd1113cfdeb77e74e4/trove_classifiers-2025.9.11.17-py3-none-any.whl", hash = "sha256:5d392f2d244deb1866556457d6f3516792124a23d1c3a463a2e8668a5d1c15dd", size = 14158, upload-time = "2025-09-11T17:07:49.886Z" },
]
[[package]]
name = "ty"
version = "0.0.1a26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/39/39/b4b4ecb6ca6d7e937fa56f0b92a8f48d7719af8fe55bdbf667638e9f93e2/ty-0.0.1a26.tar.gz", hash = "sha256:65143f8efeb2da1644821b710bf6b702a31ddcf60a639d5a576db08bded91db4", size = 4432154, upload-time = "2025-11-10T18:02:30.142Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/6a/661833ecacc4d994f7e30a7f1307bfd3a4a91392a6b03fb6a018723e75b8/ty-0.0.1a26-py3-none-linux_armv6l.whl", hash = "sha256:09208dca99bb548e9200136d4d42618476bfe1f4d2066511f2c8e2e4dfeced5e", size = 9173869, upload-time = "2025-11-10T18:01:46.012Z" },
{ url = "https://files.pythonhosted.org/packages/66/a8/32ea50f064342de391a7267f84349287e2f1c2eb0ad4811d6110916179d6/ty-0.0.1a26-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:91d12b66c91a1b82e698a2aa73fe043a1a9da83ff0dfd60b970500bee0963b91", size = 8973420, upload-time = "2025-11-10T18:01:49.32Z" },
{ url = "https://files.pythonhosted.org/packages/d1/f6/6659d55940cd5158a6740ae46a65be84a7ee9167738033a9b1259c36eef5/ty-0.0.1a26-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5bc6dfcea5477c81ad01d6a29ebc9bfcbdb21c34664f79c9e1b84be7aa8f289", size = 8528888, upload-time = "2025-11-10T18:01:51.511Z" },
{ url = "https://files.pythonhosted.org/packages/79/c9/4cbe7295013cc412b4f100b509aaa21982c08c59764a2efa537ead049345/ty-0.0.1a26-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40e5d15635e9918924138e8d3fb1cbf80822dfb8dc36ea8f3e72df598c0c4bea", size = 8801867, upload-time = "2025-11-10T18:01:53.888Z" },
{ url = "https://files.pythonhosted.org/packages/ed/b3/25099b219a6444c4b29f175784a275510c1cd85a23a926d687ab56915027/ty-0.0.1a26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86dc147ed0790c7c8fd3f0d6c16c3c5135b01e99c440e89c6ca1e0e592bb6682", size = 8975519, upload-time = "2025-11-10T18:01:56.231Z" },
{ url = "https://files.pythonhosted.org/packages/73/3e/3ad570f4f592cb1d11982dd2c426c90d2aa9f3d38bf77a7e2ce8aa614302/ty-0.0.1a26-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbe0e07c9d5e624edfc79a468f2ef191f9435581546a5bb6b92713ddc86ad4a6", size = 9331932, upload-time = "2025-11-10T18:01:58.476Z" },
{ url = "https://files.pythonhosted.org/packages/04/fa/62c72eead0302787f9cc0d613fc671107afeecdaf76ebb04db8f91bb9f7e/ty-0.0.1a26-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0dcebbfe9f24b43d98a078f4a41321ae7b08bea40f5c27d81394b3f54e9f7fb5", size = 9921353, upload-time = "2025-11-10T18:02:00.749Z" },
{ url = "https://files.pythonhosted.org/packages/6c/1f/3b329c4b60d878704e09eb9d05467f911f188e699961c044b75932893e0a/ty-0.0.1a26-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0901b75afc7738224ffc98bbc8ea03a20f167a2a83a4b23a6550115e8b3ddbc6", size = 9700800, upload-time = "2025-11-10T18:02:03.544Z" },
{ url = "https://files.pythonhosted.org/packages/92/24/13fcba20dd86a7c3f83c814279aa3eb6a29c5f1b38a3b3a4a0fd22159189/ty-0.0.1a26-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4788f34d384c132977958d76fef7f274f8d181b22e33933c4d16cff2bb5ca3b9", size = 9728289, upload-time = "2025-11-10T18:02:06.386Z" },
{ url = "https://files.pythonhosted.org/packages/40/7a/798894ff0b948425570b969be35e672693beeb6b852815b7340bc8de1575/ty-0.0.1a26-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b98851c11c560ce63cd972ed9728aa079d9cf40483f2cdcf3626a55849bfe107", size = 9279735, upload-time = "2025-11-10T18:02:09.425Z" },
{ url = "https://files.pythonhosted.org/packages/1a/54/71261cc1b8dc7d3c4ad92a83b4d1681f5cb7ea5965ebcbc53311ae8c6424/ty-0.0.1a26-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c20b4625a20059adecd86fe2c4df87cd6115fea28caee45d3bdcf8fb83d29510", size = 8767428, upload-time = "2025-11-10T18:02:11.956Z" },
{ url = "https://files.pythonhosted.org/packages/8e/07/b248b73a640badba2b301e6845699b7dd241f40a321b9b1bce684d440f70/ty-0.0.1a26-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d9909e96276f8d16382d285db92ae902174cae842aa953003ec0c06642db2f8a", size = 9009170, upload-time = "2025-11-10T18:02:14.878Z" },
{ url = "https://files.pythonhosted.org/packages/f8/35/ec8353f2bb7fd2f41bca6070b29ecb58e2de9af043e649678b8c132d5439/ty-0.0.1a26-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a76d649ceefe9baa9bbae97d217bee076fd8eeb2a961f66f1dff73cc70af4ac8", size = 9119215, upload-time = "2025-11-10T18:02:18.329Z" },
{ url = "https://files.pythonhosted.org/packages/70/48/db49fe1b7e66edf90dc285869043f99c12aacf7a99c36ee760e297bac6d5/ty-0.0.1a26-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0ee0f6366bcf70fae114e714d45335cacc8daa936037441e02998a9110b7a29", size = 9398655, upload-time = "2025-11-10T18:02:21.031Z" },
{ url = "https://files.pythonhosted.org/packages/10/f8/d869492bdbb21ae8cf4c99b02f20812bbbf49aa187cfeb387dfaa03036a8/ty-0.0.1a26-py3-none-win32.whl", hash = "sha256:86689b90024810cac7750bf0c6e1652e4b4175a9de7b82b8b1583202aeb47287", size = 8645669, upload-time = "2025-11-10T18:02:23.23Z" },
{ url = "https://files.pythonhosted.org/packages/b4/18/8a907575d2b335afee7556cb92233ebb5efcefe17752fc9dcab21cffb23b/ty-0.0.1a26-py3-none-win_amd64.whl", hash = "sha256:829e6e6dbd7d9d370f97b2398b4804552554bdcc2d298114fed5e2ea06cbc05c", size = 9442975, upload-time = "2025-11-10T18:02:25.68Z" },
{ url = "https://files.pythonhosted.org/packages/e9/22/af92dcfdd84b78dd97ac6b7154d6a763781f04a400140444885c297cc213/ty-0.0.1a26-py3-none-win_arm64.whl", hash = "sha256:b8f431c784d4cf5b4195a3521b2eca9c15902f239b91154cb920da33f943c62b", size = 8958958, upload-time = "2025-11-10T18:02:28.071Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
@@ -4110,6 +4135,7 @@ dependencies = [
dev = [
{ name = "pytest" },
{ name = "pytest-cov" },
{ name = "ty" },
]
[package.metadata]
@@ -4125,6 +4151,7 @@ requires-dist = [
dev = [
{ name = "pytest", specifier = ">=8.4.2" },
{ name = "pytest-cov", specifier = ">=7.0.0" },
{ name = "ty", specifier = ">=0.0.1a26" },
]
[[package]]