diff --git a/README.md b/README.md index 42f814b..c9e3715 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Connect already works with your current tech stack: ## Features -- **Servers:** WSGI and ASGI-ready, use with any [compatible server](https://connectrpc.com/docs/python/deployment/) +- **Servers:** WSGI and ASGI-ready (including trio), use with any [compatible server](https://connectrpc.com/docs/python/deployment/) - **Clients:** Lightweight sync and async clients, backed by `pyqwest` - **Protocols:** Supports Connect, gRPC, and gRPC-Web (HTTP/1.1 and HTTP/2) - **Type safety:** Fully type-annotated generated code diff --git a/conformance/test/server.py b/conformance/test/server.py index 1ef0287..d8104f5 100644 --- a/conformance/test/server.py +++ b/conformance/test/server.py @@ -141,6 +141,21 @@ def _create_request_info( return request_info +async def _sleep(seconds: float) -> None: + """Sleep on whichever event loop the server runs the application on.""" + try: + # trio depends on sniffio, so without it only asyncio can be running. + import sniffio # noqa: PLC0415 + except ModuleNotFoundError: + sniffio = None + if sniffio is not None and sniffio.current_async_library() == "trio": + import trio # noqa: PLC0415 + + await trio.sleep(seconds) + else: + await asyncio.sleep(seconds) + + async def _handle_unary_response( definition: UnaryResponseDefinition, reqs: list[Any], res: RES, ctx: RequestContext ) -> RES: @@ -159,7 +174,7 @@ async def _handle_unary_response( case _: response_data = b"" if definition.response_delay_ms: - await asyncio.sleep(definition.response_delay_ms / 1000.0) + await _sleep(definition.response_delay_ms / 1000.0) res.payload = ConformancePayload(request_info=request_info, data=response_data) return res @@ -215,7 +230,7 @@ async def server_stream( res.payload.request_info = request_info res.payload.data = res_data if definition.response_delay_ms: - await asyncio.sleep(definition.response_delay_ms / 1000.0) + await _sleep(definition.response_delay_ms / 1000.0) sent_message = True yield res @@ -247,7 +262,7 @@ async def bidi_stream( if not definition or res_idx >= len(definition.response_data): break if definition.response_delay_ms: - await asyncio.sleep(definition.response_delay_ms / 1000.0) + await _sleep(definition.response_delay_ms / 1000.0) res = BidiStreamResponse() res.payload = ConformancePayload() res.payload.data = definition.response_data[res_idx] @@ -262,7 +277,7 @@ async def bidi_stream( request_info = _create_request_info(ctx, requests) for i in range(res_idx, len(definition.response_data)): if definition.response_delay_ms: - await asyncio.sleep(definition.response_delay_ms / 1000.0) + await _sleep(definition.response_delay_ms / 1000.0) res = BidiStreamResponse() res.payload = ConformancePayload() res.payload.data = definition.response_data[i] @@ -619,6 +634,7 @@ async def serve_pyvoy( keyfile: str | None, cafile: str | None, port_future: asyncio.Future[int], + loop: Literal["trio"] | None = None, ): tls_cert = Path(certfile) if certfile else None tls_key = Path(keyfile) if keyfile else None @@ -643,6 +659,7 @@ async def start(): tls_key=tls_key, tls_cert=tls_cert, tls_ca_cert=tls_ca_cert, + loop=loop, ) as server: if ( request.http_version == HTTPVersion.HTTP_VERSION_3 @@ -711,7 +728,7 @@ def _find_free_port(): Mode = Literal["sync", "async"] -Server = Literal["granian", "gunicorn", "hypercorn", "pyvoy", "uvicorn"] +Server = Literal["granian", "gunicorn", "hypercorn", "pyvoy", "pyvoy-trio", "uvicorn"] class Args(argparse.Namespace): @@ -777,10 +794,16 @@ async def main() -> None: request, args.mode, certfile, keyfile, cafile, port_future ) ) - case "pyvoy": + case "pyvoy" | "pyvoy-trio": serve_task = asyncio.create_task( serve_pyvoy( - request, args.mode, certfile, keyfile, cafile, port_future + request, + args.mode, + certfile, + keyfile, + cafile, + port_future, + loop="trio" if args.server == "pyvoy-trio" else None, ) ) case "uvicorn": diff --git a/conformance/test/test_server.py b/conformance/test/test_server.py index 06f2ce5..0b90977 100644 --- a/conformance/test/test_server.py +++ b/conformance/test/test_server.py @@ -76,7 +76,7 @@ def test_server_sync(server: str, cov: Coverage) -> None: pytest.fail(f"\n{result.stdout}\n{result.stderr}") -@pytest.mark.parametrize("server", ["pyvoy", "uvicorn"]) +@pytest.mark.parametrize("server", ["pyvoy", "pyvoy-trio", "uvicorn"]) def test_server_async(server: str, cov: Coverage) -> None: args = maybe_patch_args_with_debug( [sys.executable, _server_py_path, "--mode", "async", "--server", server] diff --git a/pyproject.toml b/pyproject.toml index 20cc44a..b86c60d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ dev = [ "pyvoy==1.2.0", "ruff==0.16.10", "tombi==1.7.2", + "trio==0.34.0", "ty==0.0.84", "types-grpcio==1.84.0.20260928", "types-protobuf==7.35.1.20260906", diff --git a/src/connectrpc/_server_async.py b/src/connectrpc/_server_async.py index 83b7bb7..238685c 100644 --- a/src/connectrpc/_server_async.py +++ b/src/connectrpc/_server_async.py @@ -5,7 +5,7 @@ import functools import inspect from abc import ABC, abstractmethod -from asyncio import CancelledError, Event, create_task, sleep +from asyncio import CancelledError, Event, create_task, get_running_loop, sleep from dataclasses import replace from http import HTTPStatus from typing import TYPE_CHECKING, Generic, TypeVar, cast @@ -25,6 +25,7 @@ MetadataInterceptorsRun, ServerStreamInterceptor, UnaryInterceptor, + _aclose, resolve_interceptors, split_leading_metadata_interceptors, ) @@ -54,10 +55,12 @@ Mapping, Sequence, ) + from contextlib import AbstractAsyncContextManager, AbstractContextManager from asgiref.typing import ASGIReceiveCallable, ASGISendCallable, HTTPScope, Scope from . import _server_shared + from ._server_trio import TrioLoop from .compression import Compression else: ASGIReceiveCallable = "asgiref.typing.ASGIReceiveCallable" @@ -266,6 +269,7 @@ async def _handle_unary_connect( send: ASGISendCallable, ctx: RequestContext, ) -> None: + loop = _current_loop() accept_encoding = headers.get("accept-encoding", "") compression = negotiate_compression(accept_encoding, self._compressions) @@ -283,9 +287,20 @@ async def _handle_unary_connect( response_data = await endpoint.function(request, ctx) except Exception as e: # noqa: BLE001 # re-raised after ending the run error = e + except BaseException as e: + if loop.is_cancellation(e): + # Save the error for sending to the client, then let the cancellation reach the + # app server. + with loop.async_cleanup(): + error = await metadata_run.end( + ConnectError(Code.CANCELED, "Request was cancelled") + ) + assert error is not None # noqa: S101 + await self._handle_error(error, ctx, None, send) + raise finally: # End the run before sending the response so on_end can still modify - # response metadata. + # response metadata. This is a no-op if the run already ended. error = await metadata_run.end(error) if error is not None: raise error @@ -399,6 +414,7 @@ async def _handle_stream( headers: Headers, ctx: _server_shared.RequestContext, ) -> None: + loop = _current_loop() req_compression, resp_compression = protocol.negotiate_stream_compression( headers, self._compressions ) @@ -407,7 +423,9 @@ async def _handle_stream( metadata_run = MetadataInterceptorsRun(self._metadata_interceptors, ctx) error: Exception | None = None + cleanup: AbstractContextManager[object] = contextlib.nullcontext() sent_headers = False + request_stream: AsyncGenerator[_REQ] | None = None try: await metadata_run.start() if not req_compression: @@ -421,10 +439,12 @@ async def _handle_stream( codec, req_compression, self._read_max_bytes, + loop, ) - disconnect_detected: Event | None = None - monitor_task = None + watch: AbstractAsyncContextManager[Callable[[], bool] | None] = ( + contextlib.nullcontext(None) + ) match endpoint: case EndpointUnary(): @@ -446,85 +466,91 @@ async def _handle_stream( # The request has been fully consumed; monitor receive() for a # client disconnect so we can stop streaming promptly. - disconnect_detected = Event() - - async def _watch_for_disconnect() -> None: - while True: - msg = await receive() - if msg["type"] == "http.disconnect": - disconnect_detected.set() - return - - monitor_task = create_task(_watch_for_disconnect()) + watch = loop.watch_for_disconnect(receive) case EndpointBidiStream(): response_stream = endpoint.function(request_stream, ctx) try: - async for message in response_stream: - if disconnect_detected is not None and disconnect_detected.is_set(): - raise ConnectError(Code.CANCELED, "Client disconnected") - # Don't send headers until the first message to allow logic a chance to add - # response headers. + # The watcher stops before the generator is closed so a throwing + # generator finally-block doesn't leak the task. + async with watch as disconnected: + async for message in response_stream: + if disconnected is not None and disconnected(): + raise ConnectError(Code.CANCELED, "Client disconnected") + # Don't send headers until the first message to allow logic a + # chance to add response headers. + if not sent_headers: + await _send_stream_response_headers( + send, protocol, codec, resp_compression.name(), ctx + ) + sent_headers = True + + body = writer.write(message) + await send( + { + "type": "http.response.body", + "body": body, + "more_body": True, + } + ) + except BaseException as e: + await _close_response_stream(response_stream, loop, e) + raise + await _close_response_stream(response_stream, loop, None) + except Exception as e: # noqa: BLE001 # invoking user callback + error = e + except BaseException as e: + if loop.is_cancellation(e): + # Save the error for sending to the client, then let the cancellation reach the + # app server. + error = ConnectError(Code.CANCELED, "Request was cancelled") + cleanup = loop.async_cleanup() + raise + finally: + try: + with cleanup: + # End the run before ending the response so on_end can still + # modify response trailers. This is a no-op if the run already + # ended. + error = await metadata_run.end(error) + end_message = writer.end( + ctx.response_trailers, + ConnectWireError.from_exception(error) if error else None, + ) if not sent_headers: + # Exception before any response message is returned await _send_stream_response_headers( send, protocol, codec, resp_compression.name(), ctx ) - sent_headers = True - - body = writer.write(message) - await send( - {"type": "http.response.body", "body": body, "more_body": True} - ) + if isinstance(end_message, bytes): + await send( + { + "type": "http.response.body", + "body": end_message, + "more_body": False, + } + ) + else: + await send( + { + "type": "http.response.body", + "body": b"", + "more_body": False, + } + ) + await send( + { + "type": "http.response.trailers", + "headers": [ + (k.encode(), v.encode()) + for k, v in end_message.allitems() + ], + "more_trailers": False, + } + ) finally: - # Cancel the monitor first so a throwing generator finally-block - # doesn't leak the task. - if monitor_task is not None: - monitor_task.cancel() - with contextlib.suppress(CancelledError): - await monitor_task - # Explicitly close the stream so that any generator finally-blocks - # run promptly (Python defers async-generator cleanup to GC otherwise). - aclose = getattr(response_stream, "aclose", None) - if aclose is not None: - await aclose() - except CancelledError as e: - raise ConnectError(Code.CANCELED, "Request was cancelled") from e - except Exception as e: # noqa: BLE001 # invoking user callback - error = e - finally: - # End the run before ending the response so on_end can still modify - # response trailers. This is a no-op if the run already ended. - error = await metadata_run.end(error) - end_message = writer.end( - ctx.response_trailers, - ConnectWireError.from_exception(error) if error else None, - ) - if not sent_headers: - # Exception before any response message is returned - await _send_stream_response_headers( - send, protocol, codec, resp_compression.name(), ctx - ) - if isinstance(end_message, bytes): - await send( - { - "type": "http.response.body", - "body": end_message, - "more_body": False, - } - ) - else: - await send( - {"type": "http.response.body", "body": b"", "more_body": False} - ) - await send( - { - "type": "http.response.trailers", - "headers": [ - (k.encode(), v.encode()) for k, v in end_message.allitems() - ], - "more_trailers": False, - } - ) + if request_stream is not None: + await request_stream.aclose() if error and not isinstance(error, ConnectError): raise error @@ -590,6 +616,59 @@ def _resolve_endpoints(self, service: _SVC) -> Mapping[str, Endpoint]: return resolved_endpoints +class AsyncioLoop: + """The stream handler's event loop operations under asyncio.""" + + async def checkpoint(self) -> None: + """Give the loop a chance to deliver a cancellation.""" + await sleep(0) + + def is_cancellation(self, error: BaseException) -> bool: + """Whether the error is a loop cancellation.""" + return isinstance(error, CancelledError) + + def async_cleanup(self) -> AbstractContextManager[object]: + """Return a context in which cleanup can await after a cancellation.""" + # asyncio only delivers cancellation once so async logic works as normal during cleanup + return contextlib.nullcontext() + + @contextlib.asynccontextmanager + async def watch_for_disconnect( + self, receive: ASGIReceiveCallable + ) -> AsyncGenerator[Callable[[], bool]]: + """Watch receive() for a client disconnect.""" + disconnected = Event() + + async def watch() -> None: + while True: + msg = await receive() + if msg["type"] == "http.disconnect": + disconnected.set() + return + + task = create_task(watch()) + try: + yield disconnected.is_set + finally: + task.cancel() + with contextlib.suppress(CancelledError): + await task + + +_ASYNCIO_LOOP = AsyncioLoop() + + +def _current_loop() -> AsyncioLoop | TrioLoop: + try: + get_running_loop() + except RuntimeError: + # Not asyncio, so trio: the only other event loop that ASGI servers run. + from ._server_trio import TrioLoop # noqa: PLC0415 + + return TrioLoop() + return _ASYNCIO_LOOP + + async def _send_stream_response_headers( send: ASGISendCallable, protocol: ServerProtocol, @@ -614,25 +693,44 @@ async def _send_stream_response_headers( ) +async def _close_response_stream( + stream: AsyncIterator[object], + loop: AsyncioLoop | TrioLoop, + error: BaseException | None, +) -> None: + """Close the handler's generator, so its finally-blocks run promptly. + + While a cancellation propagates the generator cannot await, so it is given + the loop's cleanup context and any error it raises is dropped. + """ + if error is not None and loop.is_cancellation(error): + with loop.async_cleanup(), contextlib.suppress(Exception): + await _aclose(stream) + return + await _aclose(stream) + + async def _request_stream( receive: ASGIReceiveCallable, request_class: type[_REQ], codec: Codec, compression: Compression, - read_max_bytes: int | None = None, -) -> AsyncIterator[_REQ]: + read_max_bytes: int | None, + loop: AsyncioLoop | TrioLoop, +) -> AsyncGenerator[_REQ]: + body = _read_body(receive) try: with EnvelopeReader( request_class, codec, compression, read_max_bytes ) as reader: - async for chunk in _read_body(receive): + async for chunk in body: for message in reader.feed(chunk): yield message # Check for cancellation each message. While this seems heavyweight, # conformance tests require it. - await sleep(0) - except CancelledError as e: - raise ConnectError(Code.CANCELED, "Request was cancelled") from e + await loop.checkpoint() + finally: + await _aclose(body) async def _read_body(receive: ASGIReceiveCallable) -> AsyncIterator[bytes]: diff --git a/src/connectrpc/_server_trio.py b/src/connectrpc/_server_trio.py new file mode 100644 index 0000000..3ebd95d --- /dev/null +++ b/src/connectrpc/_server_trio.py @@ -0,0 +1,90 @@ +"""The stream handler's event loop operations under trio. + +Imported the first time a request runs under trio, so that applications on +asyncio never import trio. +""" + +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING + +import trio +import trio.lowlevel + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Callable + + from asgiref.typing import ASGIReceiveCallable + +# Seconds allowed for each cleanup step that runs after a cancellation: closing +# the handler's generator, then the on_end hooks and the end of the response. A +# cancelled request therefore delays the app server by at most twice this. The +# value is a guess at how long cleanup that does not wait on the client can +# take, and is unavoidable to support cleanup on cancellation with trio. +_CLEANUP_TIMEOUT = 1.0 + + +class TrioLoop: + """See _server_async.AsyncioLoop for what each operation is for.""" + + async def checkpoint(self) -> None: + await trio.lowlevel.checkpoint() + + def is_cancellation(self, error: BaseException) -> bool: + if isinstance(error, trio.Cancelled): + return True + # A cancelled nursery raises its cancellation in an exception group, + # beside any error its tasks raised while they were being cancelled. + # TODO: Use ExceptionGroup when updating Python floor to 3.11. + exceptions = getattr(error, "exceptions", None) + return isinstance(exceptions, tuple) and any( + self.is_cancellation(e) for e in exceptions + ) + + def async_cleanup(self) -> trio.CancelScope: + # Once the app server has cancelled the request, every checkpoint raises + # Cancelled again, so async cleanup can only run in a cancel scope shielded + # from that. Since the cleanup logic itself can't be canceled, we + # apply a deadline to prevent hangs. + scope = trio.move_on_after(_CLEANUP_TIMEOUT) + scope.shield = True + return scope + + @contextlib.asynccontextmanager + async def watch_for_disconnect( + self, receive: ASGIReceiveCallable + ) -> AsyncGenerator[Callable[[], bool]]: + disconnected = trio.Event() + + async def watch() -> None: + # An error raised in a nursery cancels the rest of it, so an error + # from receive() would end the response. + with contextlib.suppress(Exception): + while True: + msg = await receive() + if msg["type"] == "http.disconnect": + disconnected.set() + return + + error: BaseException | None = None + try: + async with trio.open_nursery() as nursery: + nursery.start_soon(watch) + try: + yield disconnected.is_set + except BaseException as e: # noqa: BLE001 + # Raised after the nursery exits; raised inside, the nursery + # would wrap it in an ExceptionGroup. + error = e + finally: + nursery.cancel_scope.cancel() + except BaseException: + # A nursery whose enclosing scope was cancelled raises a group of + # its own cancellations, which would drop an error that the + # handler raised beside the cancellation. + if error is not None and self.is_cancellation(error): + raise error from None + raise + if error is not None: + raise error diff --git a/test/test_roundtrip.py b/test/test_roundtrip.py index 9a09820..532fcfb 100644 --- a/test/test_roundtrip.py +++ b/test/test_roundtrip.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import json import random import struct import sys @@ -31,6 +32,8 @@ from asgiref.typing import HTTPDisconnectEvent, HTTPRequestEvent, HTTPScope + from connectrpc.request import RequestContext + @pytest.mark.parametrize("proto_json", [False, True]) @pytest.mark.parametrize("compression_name", ["gzip", "br", "zstd", "identity"]) @@ -656,3 +659,102 @@ async def send(message): assert generator_closed.is_set(), ( "generator should be closed after client disconnect" ) + + +@pytest.mark.parametrize("unary", [False, True], ids=["stream", "unary"]) +@pytest.mark.asyncio +async def test_server_cancelled(unary: bool) -> None: + """A cancelled request is answered with CANCELED, then the cancellation propagates.""" + events: list[str] = [] + blocked = asyncio.Event() + + class BlockedHaberdasher(Haberdasher): + async def make_hat(self, request, _ctx): + await self._block() + return Hat(size=request.inches) + + async def make_similar_hats(self, request, _ctx): + yield Hat(size=request.inches) + await self._block() + + async def _block(self): + try: + blocked.set() + await asyncio.Event().wait() + finally: + events.append("handler cleaned up") + + class Recorder: + async def on_start(self, ctx: RequestContext) -> None: # noqa: ARG002 + return None + + async def on_end( + self, _token: None, _ctx: RequestContext, error: Exception | None, / + ) -> None: + await asyncio.sleep(0) + code = error.code.value if isinstance(error, ConnectError) else error + events.append(f"on_end {code}") + + app = HaberdasherASGIApplication(BlockedHaberdasher(), interceptors=[Recorder()]) + request_bytes = Size(inches=10).to_binary() + if unary: + method, content_type, request_body = ( + "MakeHat", + b"application/proto", + request_bytes, + ) + else: + method, content_type = "MakeSimilarHats", b"application/connect+proto" + request_body = struct.pack(">BI", 0, len(request_bytes)) + request_bytes + sent_request = False + sent: list[dict] = [] + + async def receive() -> HTTPRequestEvent: + nonlocal sent_request + if not sent_request: + sent_request = True + return {"type": "http.request", "body": request_body, "more_body": False} + await asyncio.Event().wait() + raise AssertionError + + async def send(message): + sent.append(message) + + scope: HTTPScope = { + "type": "http", + "asgi": {"spec_version": "2.0", "version": "3.0"}, + "http_version": "1.1", + "method": "POST", + "scheme": "http", + "path": f"/connectrpc.example.Haberdasher/{method}", + "raw_path": f"/connectrpc.example.Haberdasher/{method}".encode(), + "query_string": b"", + "root_path": "", + "headers": [(b"content-type", content_type)], + "client": None, + "server": None, + "extensions": None, + } + + task = asyncio.create_task(app(scope, receive, send)) + await blocked.wait() + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + + assert events == ["handler cleaned up", "on_end canceled"] + error = {"code": "canceled", "message": "Request was cancelled"} + if unary: + assert [m["type"] for m in sent] == [ + "http.response.start", + "http.response.body", + ] + assert sent[0]["status"] == 499 + assert json.loads(sent[1]["body"]) == error + else: + assert [m["type"] for m in sent] == ["http.response.start"] + [ + "http.response.body" + ] * 2 + end = sent[-1]["body"] + assert end[0] & 0b10, "last body is the end-of-stream message" + assert json.loads(end[5:]) == {"error": error} diff --git a/test/test_server_trio.py b/test/test_server_trio.py new file mode 100644 index 0000000..a40665a --- /dev/null +++ b/test/test_server_trio.py @@ -0,0 +1,748 @@ +"""The ASGI application served under trio. + +Hypercorn's trio worker serves the application on a background thread, and the +sync client calls it over pyqwest's SyncHTTPTransport. An ASGI wrapper around +the application records what it sends and can cancel it, as the server does +when it shuts down with the request in flight. +""" + +from __future__ import annotations + +import gc +import gzip +import json +import subprocess +import sys +import threading +from concurrent.futures import Future, ThreadPoolExecutor +from functools import partial +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import pytest + +pytest.importorskip("trio") + +import trio +import trio.lowlevel +from hypercorn.config import Config +from hypercorn.trio import serve +from pyqwest import HTTPVersion, SyncClient, SyncHTTPTransport + +from connectrpc import _server_trio +from connectrpc.code import Code +from connectrpc.errors import ConnectError + +from .connectrpc.example.haberdasher_connect import ( + Haberdasher, + HaberdasherASGIApplication, + HaberdasherClientSync, +) +from .connectrpc.example.haberdasher_pb import Hat, Size + +if TYPE_CHECKING: + from collections.abc import AsyncIterator, Callable, Iterator + + from connectrpc.request import RequestContext + +# An envelope is a byte of flags and four bytes of length, then the message. +ENVELOPE_HEADER_LEN = 5 +COMPRESSED = 1 +END_STREAM = 2 + +CLEANUP_TIMEOUT = 0.2 + +CANCELLED = {"code": "canceled", "message": "Request was cancelled"} + + +class TrioHaberdasher(Haberdasher): + async def make_hat(self, request: Size, _ctx: RequestContext[Size, Hat], /) -> Hat: + # Fails unless the handler runs under trio. + await trio.lowlevel.checkpoint() + if request.inches < 0: + raise ConnectError(Code.INVALID_ARGUMENT, "inches must not be negative") + return Hat(size=request.inches, color="blue", name="bowler") + + async def make_flexible_hat( + self, request: AsyncIterator[Size], _ctx: RequestContext[Size, Hat], / + ) -> Hat: + total = 0 + async for size in request: + total += size.inches + return Hat(size=total, color="red", name="flexible") + + async def make_similar_hats( + self, request: Size, _ctx: RequestContext[Size, Hat], / + ) -> AsyncIterator[Hat]: + for i in range(3): + await trio.lowlevel.checkpoint() + if request.inches < 0 and i == 1: + raise ConnectError(Code.RESOURCE_EXHAUSTED, "no more hats") + yield Hat(size=request.inches + i, color="green", name=f"hat{i}") + + async def make_various_hats( + self, request: AsyncIterator[Size], _ctx: RequestContext[Size, Hat], / + ) -> AsyncIterator[Hat]: + async for size in request: + yield Hat(size=size.inches, color="black", name="echo") + + +class Wrapper: + """Wraps the application served by hypercorn. + + Records what the application sends, can block a send, and can cancel the + application from the test's thread. + """ + + def __init__(self) -> None: + self.app: HaberdasherASGIApplication = HaberdasherASGIApplication( + TrioHaberdasher() + ) + self.token: trio.lowlevel.TrioToken | None = None + self._in_flight = 0 + self.idle = threading.Event() + self.idle.set() + self.reset() + + def reset(self) -> None: + # A request finishes after the client has its response, so one from the + # previous test may still be running. + assert self.idle.wait(10) + self.sent: list[dict[str, Any]] = [] + self.on_send: Callable[[dict[str, Any]], Any] | None = None + self.raised: BaseException | None = None + self.cancelled_at = 0.0 + self.finished_at = 0.0 + self.done = threading.Event() + self.blocked = threading.Event() + self._scope: trio.CancelScope | None = None + + async def __call__(self, scope: Any, receive: Any, send: Any) -> None: + if scope["type"] != "http": + await self.app(scope, receive, send) + return + self._in_flight += 1 + self.idle.clear() + + async def recording_send(message: Any) -> None: + self.sent.append(message) + if self.on_send is not None: + await self.on_send(message) + await send(message) + + try: + with trio.CancelScope() as self._scope: + await self.app(scope, receive, recording_send) + except BaseException as e: + self.raised = e + raise + finally: + self.finished_at = trio.current_time() + self.done.set() + self._in_flight -= 1 + if self._in_flight == 0: + self.idle.set() + + def cancel(self) -> None: + def cancel_on_loop() -> None: + assert self._scope is not None + self.cancelled_at = trio.current_time() + self._scope.cancel() + + assert self.token is not None + trio.from_thread.run_sync(cancel_on_loop, trio_token=self.token) + + def types(self) -> list[str]: + return [m["type"].removeprefix("http.response.") for m in self.sent] + + def bodies(self) -> list[bytes]: + return [m["body"] for m in self.sent if m["type"] == "http.response.body"] + + def end_error(self) -> dict[str, str] | None: + """The error of a Connect stream's end message, or None for a success.""" + (end,) = [body for body in self.bodies() if body and body[0] & END_STREAM] + data = end[ENVELOPE_HEADER_LEN:] + if end[0] & COMPRESSED: + data = gzip.decompress(data) + return json.loads(data).get("error") + + def error(self) -> dict[str, str] | None: + """The error of the response: a unary error response, or a stream's end.""" + if self.sent[0]["status"] != 200: + return json.loads(self.bodies()[0]) + return self.end_error() + + async def block_on_first_message(self, message: dict[str, Any]) -> None: + if message["type"] == "http.response.body" and not self.blocked.is_set(): + self.blocked.set() + await trio.sleep_forever() + + async def block_in_nursery_on_first_message(self, message: dict[str, Any]) -> None: + """Block inside a nursery, as a server's send() may.""" + async with trio.open_nursery() as nursery: + nursery.start_soon(self.block_on_first_message, message) + + +@pytest.fixture(scope="module") +def transport() -> Iterator[SyncHTTPTransport]: + with SyncHTTPTransport(http_version=HTTPVersion.HTTP1) as transport: + yield transport + + +@pytest.fixture(scope="module") +def server() -> Iterator[tuple[str, Wrapper]]: + wrapper = Wrapper() + stop = threading.Event() + started: Future[str] = Future() + + async def stopped() -> None: + await trio.to_thread.run_sync(stop.wait, abandon_on_cancel=True) + + async def main() -> None: + wrapper.token = trio.lowlevel.current_trio_token() + config = Config() + config.bind = ["127.0.0.1:0"] + async with trio.open_nursery() as nursery: + binds = await nursery.start( + partial(serve, wrapper, config, shutdown_trigger=stopped) + ) + started.set_result(binds[0]) + + def run() -> None: + try: + trio.run(main) + except BaseException as e: + if not started.done(): + started.set_exception(e) + raise + + thread = threading.Thread(target=run, daemon=True) + thread.start() + try: + yield started.result(timeout=10), wrapper + finally: + stop.set() + thread.join(10) + assert not thread.is_alive(), "server did not stop" + + +@pytest.fixture +def url(server: tuple[str, Wrapper]) -> str: + return server[0] + + +@pytest.fixture +def wrapper(server: tuple[str, Wrapper]) -> Iterator[Wrapper]: + wrapper = server[1] + wrapper.reset() + yield wrapper + wrapper.app = HaberdasherASGIApplication(TrioHaberdasher()) + + +@pytest.fixture +def client(url: str, transport: SyncHTTPTransport) -> Iterator[HaberdasherClientSync]: + with HaberdasherClientSync(url, http_client=SyncClient(transport)) as client: + yield client + + +@pytest.fixture +def executor() -> Iterator[ThreadPoolExecutor]: + with ThreadPoolExecutor() as executor: + yield executor + + +@pytest.fixture +def cleanup_timeout(monkeypatch: pytest.MonkeyPatch) -> float: + monkeypatch.setattr(_server_trio, "_CLEANUP_TIMEOUT", CLEANUP_TIMEOUT) + return CLEANUP_TIMEOUT + + +def sizes() -> Iterator[Size]: + for inches in (1, 2, 3): + yield Size(inches=inches) + + +def call(client: HaberdasherClientSync, method: str) -> Any: + """Call the method with one request message, consuming any response stream.""" + match method: + case "MakeHat": + return client.make_hat(Size(inches=10)) + case "MakeFlexibleHat": + return client.make_flexible_hat(iter([Size(inches=10)])) + case "MakeSimilarHats": + return list(client.make_similar_hats(Size(inches=10))) + case "MakeVariousHats": + return list(client.make_various_hats(iter([Size(inches=10)]))) + case _: + raise ValueError(method) + + +def test_unary(client: HaberdasherClientSync) -> None: + assert client.make_hat(Size(inches=10)) == Hat(size=10, color="blue", name="bowler") + + +def test_unary_error(client: HaberdasherClientSync) -> None: + with pytest.raises(ConnectError) as exc_info: + client.make_hat(Size(inches=-1)) + assert exc_info.value.code == Code.INVALID_ARGUMENT + + +def test_client_stream(client: HaberdasherClientSync) -> None: + assert client.make_flexible_hat(sizes()).size == 6 + + +def test_server_stream(client: HaberdasherClientSync) -> None: + hats = list(client.make_similar_hats(Size(inches=10))) + assert [hat.size for hat in hats] == [10, 11, 12] + + +def test_server_stream_error(client: HaberdasherClientSync) -> None: + hats = [] + with pytest.raises(ConnectError) as exc_info: + hats.extend(client.make_similar_hats(Size(inches=-5))) + assert exc_info.value.code == Code.RESOURCE_EXHAUSTED + assert [hat.size for hat in hats] == [-5] + + +def test_bidi_stream(client: HaberdasherClientSync) -> None: + hats = list(client.make_various_hats(sizes())) + assert [hat.size for hat in hats] == [1, 2, 3] + + +class Recorder: + """A metadata interceptor whose on_end awaits before it finishes.""" + + def __init__(self, events: list[str], seconds: float = 0) -> None: + self._events = events + self._seconds = seconds + + async def on_start(self, ctx: RequestContext) -> None: # noqa: ARG002 + return None + + async def on_end( + self, _token: None, _ctx: RequestContext, error: Exception | None, / + ) -> None: + code = error.code.value if isinstance(error, ConnectError) else error + self._events.append(f"on_end started {code}") + await trio.sleep(self._seconds) + self._events.append("on_end finished") + + +class EndlessHats(Haberdasher): + """Streams hats until it is closed, and awaits while it cleans up.""" + + def __init__( + self, + events: list[str], + cleanup_error: Exception | None = None, + cleanup_seconds: float = 0, + ) -> None: + self._events = events + self._cleanup_error = cleanup_error + self._cleanup_seconds = cleanup_seconds + + async def make_similar_hats( + self, request: Size, _ctx: RequestContext[Size, Hat], / + ) -> AsyncIterator[Hat]: + try: + while True: + yield Hat(size=request.inches) + await trio.sleep(0.01) + finally: + await self._clean_up() + + async def make_various_hats( + self, request: AsyncIterator[Size], _ctx: RequestContext[Size, Hat], / + ) -> AsyncIterator[Hat]: + try: + async for size in request: + yield Hat(size=size.inches) + finally: + await self._clean_up() + + async def _clean_up(self) -> None: + await trio.sleep(self._cleanup_seconds) + self._events.append("handler cleaned up") + if self._cleanup_error is not None: + raise self._cleanup_error + + +@pytest.mark.parametrize( + ("cleanup_error", "end_error"), + [ + (None, {"code": "canceled", "message": "Client disconnected"}), + ( + ConnectError(Code.ABORTED, "cleanup failed"), + {"code": "aborted", "message": "cleanup failed"}, + ), + ], + ids=["clean", "cleanup-error"], +) +def test_disconnect_ends_server_stream( + url: str, + wrapper: Wrapper, + cleanup_error: Exception | None, + end_error: dict[str, str], +) -> None: + events: list[str] = [] + wrapper.app = HaberdasherASGIApplication( + EndlessHats(events, cleanup_error), interceptors=[Recorder(events)] + ) + # The client's own connection, so that closing it disconnects the request. + with ( + SyncHTTPTransport(http_version=HTTPVersion.HTTP1) as transport, + HaberdasherClientSync(url, http_client=SyncClient(transport)) as client, + ): + hats = client.make_similar_hats(Size(inches=10)) + assert [next(hats).size for _ in range(3)] == [10, 10, 10] + del hats + gc.collect() + + assert wrapper.done.wait(10) + assert wrapper.end_error() == end_error + assert events == [ + "handler cleaned up", + f"on_end started {end_error['code']}", + "on_end finished", + ] + + +@pytest.mark.parametrize( + ("method", "cleanup_error", "send_in_nursery"), + [ + ("MakeSimilarHats", None, False), + ("MakeSimilarHats", ConnectError(Code.ABORTED, "cleanup failed"), False), + ("MakeVariousHats", ConnectError(Code.ABORTED, "cleanup failed"), False), + ("MakeVariousHats", None, True), + ], +) +def test_cancelled_while_sending( + client: HaberdasherClientSync, + wrapper: Wrapper, + executor: ThreadPoolExecutor, + method: str, + cleanup_error: Exception | None, + send_in_nursery: bool, +) -> None: + events: list[str] = [] + wrapper.app = HaberdasherASGIApplication( + EndlessHats(events, cleanup_error), interceptors=[Recorder(events)] + ) + wrapper.on_send = ( + wrapper.block_in_nursery_on_first_message + if send_in_nursery + else wrapper.block_on_first_message + ) + + response = executor.submit(call, client, method) + assert wrapper.blocked.wait(10) + wrapper.cancel() + assert wrapper.done.wait(10) + response.exception(10) + + assert wrapper.raised is None + assert events == [ + "handler cleaned up", + "on_end started canceled", + "on_end finished", + ] + assert wrapper.types() == ["start", "body", "body"] + assert wrapper.sent[1]["more_body"] + assert wrapper.end_error() == CANCELLED + + +class BlockedHats(Haberdasher): + """Blocks forever; records when it is cancelled.""" + + def __init__( + self, + blocked: threading.Event, + events: list[str], + *, + in_nursery: bool, + cleanup_error: Exception | None = None, + ): + self._blocked = blocked + self._events = events + self._in_nursery = in_nursery + self._cleanup_error = cleanup_error + + async def make_hat(self, request: Size, _ctx: RequestContext[Size, Hat], /) -> Hat: + await self._block() + return Hat(size=request.inches) + + async def make_flexible_hat( + self, _request: AsyncIterator[Size], _ctx: RequestContext[Size, Hat], / + ) -> Hat: + await self._block() + return Hat() + + async def make_similar_hats( + self, _request: Size, _ctx: RequestContext[Size, Hat], / + ) -> AsyncIterator[Hat]: + await self._block() + yield Hat() + + async def make_various_hats( + self, _request: AsyncIterator[Size], _ctx: RequestContext[Size, Hat], / + ) -> AsyncIterator[Hat]: + await self._block() + yield Hat() + + async def _block(self) -> None: + try: + self._blocked.set() + if self._in_nursery: + # A cancelled nursery raises the cancellation in a group. + async with trio.open_nursery() as nursery: + nursery.start_soon(trio.sleep_forever) + nursery.start_soon(self._fail_when_cancelled) + else: + await trio.sleep_forever() + finally: + self._events.append("handler cleaned up") + + async def _fail_when_cancelled(self) -> None: + try: + await trio.sleep_forever() + finally: + if self._cleanup_error is not None: + raise self._cleanup_error + + +@pytest.mark.parametrize( + ("method", "in_nursery"), + [ + ("MakeHat", False), + ("MakeHat", True), + ("MakeFlexibleHat", True), + ("MakeSimilarHats", True), + ("MakeVariousHats", True), + ], +) +def test_cancelled_in_handler( + client: HaberdasherClientSync, + wrapper: Wrapper, + executor: ThreadPoolExecutor, + method: str, + in_nursery: bool, +) -> None: + events: list[str] = [] + blocked = threading.Event() + wrapper.app = HaberdasherASGIApplication( + BlockedHats(blocked, events, in_nursery=in_nursery), + interceptors=[Recorder(events)], + ) + + response = executor.submit(call, client, method) + assert blocked.wait(10) + wrapper.cancel() + assert wrapper.done.wait(10) + response.exception(10) + + assert wrapper.raised is None + assert events == [ + "handler cleaned up", + "on_end started canceled", + "on_end finished", + ] + assert wrapper.types() == ["start", "body"] + assert wrapper.error() == CANCELLED + + +def leaves(error: BaseException) -> list[BaseException]: + nested = getattr(error, "exceptions", None) + if nested is None: + return [error] + return [leaf for e in nested for leaf in leaves(e)] + + +@pytest.mark.parametrize( + "method", ["MakeHat", "MakeFlexibleHat", "MakeSimilarHats", "MakeVariousHats"] +) +def test_cancelled_in_handler_whose_cleanup_fails( + client: HaberdasherClientSync, + wrapper: Wrapper, + executor: ThreadPoolExecutor, + method: str, +) -> None: + """A cancellation beside a handler error is handled as a cancellation. + + The client is told CANCELED, and the handler's error still escapes the + application for the server to log. + """ + events: list[str] = [] + blocked = threading.Event() + cleanup_error = OSError("cleanup failed") + wrapper.app = HaberdasherASGIApplication( + BlockedHats(blocked, events, in_nursery=True, cleanup_error=cleanup_error), + interceptors=[Recorder(events)], + ) + + response = executor.submit(call, client, method) + assert blocked.wait(10) + wrapper.cancel() + assert wrapper.done.wait(10) + response.exception(10) + + assert wrapper.raised is not None + assert leaves(wrapper.raised) == [cleanup_error] + assert events == [ + "handler cleaned up", + "on_end started canceled", + "on_end finished", + ] + assert wrapper.types() == ["start", "body"] + assert wrapper.error() == CANCELLED + + +def test_handler_cleanup_after_cancellation_is_bounded( + client: HaberdasherClientSync, + wrapper: Wrapper, + executor: ThreadPoolExecutor, + cleanup_timeout: float, +) -> None: + events: list[str] = [] + wrapper.app = HaberdasherASGIApplication(EndlessHats(events, cleanup_seconds=60)) + wrapper.on_send = wrapper.block_on_first_message + + response = executor.submit(call, client, "MakeSimilarHats") + assert wrapper.blocked.wait(10) + wrapper.cancel() + assert wrapper.done.wait(10) + response.exception(10) + + assert events == [] + assert cleanup_timeout <= wrapper.finished_at - wrapper.cancelled_at < 5 + + +def test_on_end_after_cancellation_is_bounded( + client: HaberdasherClientSync, + wrapper: Wrapper, + executor: ThreadPoolExecutor, + cleanup_timeout: float, +) -> None: + events: list[str] = [] + wrapper.app = HaberdasherASGIApplication( + EndlessHats(events), interceptors=[Recorder(events, seconds=60)] + ) + wrapper.on_send = wrapper.block_on_first_message + + response = executor.submit(call, client, "MakeSimilarHats") + assert wrapper.blocked.wait(10) + wrapper.cancel() + assert wrapper.done.wait(10) + response.exception(10) + + assert events == ["handler cleaned up", "on_end started canceled"] + assert cleanup_timeout <= wrapper.finished_at - wrapper.cancelled_at < 5 + + +@pytest.mark.parametrize("method", ["MakeHat", "MakeSimilarHats"]) +def test_on_end_is_not_bounded_without_cancellation( + client: HaberdasherClientSync, wrapper: Wrapper, cleanup_timeout: float, method: str +) -> None: + events: list[str] = [] + wrapper.app = HaberdasherASGIApplication( + TrioHaberdasher(), interceptors=[Recorder(events, seconds=2 * cleanup_timeout)] + ) + + call(client, method) + + assert events == ["on_end started None", "on_end finished"] + + +def test_handler_timeout_on_request_stream( + client: HaberdasherClientSync, wrapper: Wrapper, executor: ThreadPoolExecutor +) -> None: + timed_out: list[bool] = [] + + class Service(Haberdasher): + async def make_flexible_hat( + self, request: AsyncIterator[Size], _ctx: RequestContext[Size, Hat], / + ) -> Hat: + total = 0 + with trio.move_on_after(0.5) as scope: + async for size in request: + total += size.inches + timed_out.append(scope.cancelled_caught) + return Hat(size=total) + + wrapper.app = HaberdasherASGIApplication(Service()) + release = threading.Event() + + def stalling_sizes() -> Iterator[Size]: + # One message, then the client stalls without ending the body. + yield Size(inches=7) + release.wait(10) + + response = executor.submit(client.make_flexible_hat, stalling_sizes()) + assert wrapper.done.wait(10) + release.set() + + assert timed_out == [True] + assert response.result(10) == Hat(size=7) + + +def test_bidi_handler_returns_early( + client: HaberdasherClientSync, wrapper: Wrapper +) -> None: + """The request stream is closed by the server, not the garbage collector.""" + + class FirstSizeOnly(Haberdasher): + async def make_various_hats( + self, request: AsyncIterator[Size], _ctx: RequestContext[Size, Hat], / + ) -> AsyncIterator[Hat]: + async for size in request: + yield Hat(size=size.inches) + return + + wrapper.app = HaberdasherASGIApplication(FirstSizeOnly()) + + hats = list(client.make_various_hats(sizes())) + + assert [hat.size for hat in hats] == [1] + + +SERVER_STREAM_ON_ASYNCIO = """ +import asyncio +import sys + +from pyqwest import Client +from pyqwest.testing import ASGITransport + +from test.connectrpc.example.haberdasher_connect import ( + Haberdasher, + HaberdasherASGIApplication, + HaberdasherClient, +) +from test.connectrpc.example.haberdasher_pb import Hat, Size + + +class Service(Haberdasher): + async def make_similar_hats(self, request, ctx): + yield Hat(size=request.inches) + + +async def main(): + transport = ASGITransport(HaberdasherASGIApplication(Service())) + async with HaberdasherClient("http://localhost", http_client=Client(transport)) as client: + hats = [hat async for hat in client.make_similar_hats(Size(inches=10))] + assert hats == [Hat(size=10)], hats + + +asyncio.run(main()) +loaded = sorted(name for name in sys.modules if name.split(".")[0] == "trio") +assert not loaded, loaded +""" + + +def test_asyncio_does_not_import_trio() -> None: + result = subprocess.run( + [sys.executable, "-c", SERVER_STREAM_ON_ASYNCIO], + cwd=Path(__file__).parents[1], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr diff --git a/uv.lock b/uv.lock index 2c4ae6b..360b7ee 100644 --- a/uv.lock +++ b/uv.lock @@ -41,6 +41,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/1b/54f4ad77cd8a584fa70746c47df988e002cf1ee1eba43364d46f87803647/asgiref-3.12.1-py3-none-any.whl", hash = "sha256:fe386d1c2bff7259ea95929266d12a8cf9a8b5a1c2598402967d8792e7a7c094", size = 25478, upload-time = "2026-07-14T09:56:16.926Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "backports-asyncio-runner" version = "1.2.0" @@ -134,6 +143,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/ac/65748603c46ab40b85fdf03dbfbb4c5812c2f0d4ea5457184732b55e9877/buf_bin-1.73.0-py3-none-win_arm64.whl", hash = "sha256:ef4bd08ff1af592046059f6866c0e91cf8898cde4bd18873b1cf8afac182df76", size = 14816019, upload-time = "2026-09-11T14:05:01.499Z" }, ] +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/7f/040f9e163e4acac3ee3d85b02d00b2576e7ca980d8785f0a3a5f1a9bf7f5/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", size = 174578, upload-time = "2026-08-03T21:19:27.338Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0b/644a2ec1a4eaba49c2939410bb1eb1d25b09d6d0582f5d2f95c537043725/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", size = 185082, upload-time = "2026-08-03T21:19:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + [[package]] name = "click" version = "8.5.0" @@ -189,6 +238,7 @@ dev = [ { name = "pyvoy" }, { name = "ruff" }, { name = "tombi" }, + { name = "trio" }, { name = "ty" }, { name = "types-grpcio" }, { name = "types-protobuf" }, @@ -234,6 +284,7 @@ dev = [ { name = "pyvoy", specifier = "==1.2.0" }, { name = "ruff", specifier = "==0.16.10" }, { name = "tombi", specifier = "==1.7.2" }, + { name = "trio", specifier = "==0.34.0" }, { name = "ty", specifier = "==0.0.84" }, { name = "types-grpcio", specifier = "==1.84.0.20260928" }, { name = "types-protobuf", specifier = "==7.35.1.20260906" }, @@ -618,11 +669,26 @@ wheels = [ name = "griffelib" version = "2.3.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] sdist = { url = "https://files.pythonhosted.org/packages/27/af/018c10bc9edd42b6ef6db2e96b09542050d5253f9b195e74bc910b2d13ab/griffelib-2.3.0.tar.gz", hash = "sha256:7b0952caf5bca6afa4bb5ee8c6a2d183fe3f21b62efc5f6c7243cb2b26d2d115", size = 234534, upload-time = "2026-09-04T15:08:17.472Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/41/63/e876e789525063c840ccfa8857febdabd6523bcef9ce7eb979b9305ea895/griffelib-2.3.0-py3-none-any.whl", hash = "sha256:1b8f9cd525681c26b1d6d574faa1371651e8459ca51d209684f50b8096ae06e0", size = 169423, upload-time = "2026-09-04T15:08:12.956Z" }, ] +[[package]] +name = "griffelib" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/2b/27/b55f1a5278918be765fb2fd8b20966bc72bbdd3f789f031937cceea7834a/griffelib-2.3.2.tar.gz", hash = "sha256:df00c7a0dee3d86268d76788997a1859272cb1fb7b865658e043d2c0c3d52e60", size = 234729, upload-time = "2026-10-06T09:54:37.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/e5/0ae74c83c1cab2c14daadf28bd0143eb14bb81503834af987b66badc7b61/griffelib-2.3.2-py3-none-any.whl", hash = "sha256:8e710afededd5607f95bf3c8ccc175ee306e7084459faf27b9f9f44ef2a7a274", size = 169420, upload-time = "2026-10-06T09:54:32.038Z" }, +] + [[package]] name = "grpcio" version = "1.84.0" @@ -846,11 +912,11 @@ wheels = [ [[package]] name = "iniconfig" -version = "2.3.0" +version = "2.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, ] [[package]] @@ -1127,7 +1193,8 @@ name = "mkdocstrings-python" version = "2.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "griffelib" }, + { name = "griffelib", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "griffelib", version = "2.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "mkdocs-autorefs" }, { name = "mkdocstrings" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, @@ -1231,6 +1298,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/48/d982ebfca471152ec6d454853290d4352392bdcb1593142ec1e1b8cf0b28/opentelemetry_util_http-0.66b0-py3-none-any.whl", hash = "sha256:7f5e581e94f023b30995f700e41ce319cec82bd542ff1c1c7c9b56d6b10bb687", size = 8385, upload-time = "2026-09-25T12:35:42.28Z" }, ] +[[package]] +name = "outcome" +version = "1.3.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, +] + [[package]] name = "packaging" version = "26.3" @@ -1260,11 +1339,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.12.3" +version = "4.12.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/42/23/4a86fc741c38c5b69792a4ef954b281afa69bea9f083f881de1b0d23bc07/platformdirs-4.12.3.tar.gz", hash = "sha256:427fc0bb321ae0c5b037fa03238ca74820437be162e78b4848c4d4055b9b766c", size = 59538, upload-time = "2026-10-03T23:40:46.595Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/a1/d5f9002a70298c64a789779077d8dd90c10aa1f47fe40c86802df874f2a6/platformdirs-4.12.4.tar.gz", hash = "sha256:63743c02414e755de4e31b8f68125c1407495b86c5a006e203c01ff8b9924250", size = 60010, upload-time = "2026-10-07T23:30:32.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/9b/6ce1ead737fe496611bda600c263b9a11ae7bd8f41bb13f9bd7e0a2c37a4/platformdirs-4.12.3-py3-none-any.whl", hash = "sha256:080f3b39423b5abfca9a23d84c4e9795f54d395cd8459867a8ded44084fcd5f8", size = 32493, upload-time = "2026-10-03T23:40:45.104Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ba/223e00b885e960edd5d4b4d178c88acce019bd5b83786093681b8c393492/platformdirs-4.12.4-py3-none-any.whl", hash = "sha256:78bfb9db2a8471ed7eebe3c3c932da413911042994e699b384fbb4493fa872d7", size = 32596, upload-time = "2026-10-07T23:30:30.825Z" }, ] [[package]] @@ -1402,6 +1481,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/05/53533263fce2309f22397bde30200239b452156114926e143cee625f4a6b/protoc_gen_py-0.6.0-py3-none-any.whl", hash = "sha256:97557d104ad0d98e1927be91a9fde875d63f7d64574f7ace9b5d02ffe9285222", size = 12434, upload-time = "2026-09-30T06:45:39.83Z" }, ] +[[package]] +name = "pycparser" +version = "3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/d3/eb1d3bc30dda12f7e69640ae2ac8cb10240b71fb73024ad528b7d2ae73da/pycparser-3.1.tar.gz", hash = "sha256:b3fc6dec06a8b2fefa0ed4ff92285306a5e3be9987bc5603c9edbdc4e492418f", size = 105946, upload-time = "2026-10-08T12:26:34.349Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/ce/b3ae9ee0324d991c860187be2a6ee436d27a3d02397eaddbb101ec901f3d/pycparser-3.1-py3-none-any.whl", hash = "sha256:f09d358c840bd147b79e55f2bc494f18ea869dc897f5852a8f5766b74f787882", size = 48709, upload-time = "2026-10-08T12:26:32.97Z" }, +] + [[package]] name = "pygments" version = "2.21.0" @@ -1706,6 +1794,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "starlette" version = "1.7.0" @@ -1756,56 +1862,92 @@ wheels = [ [[package]] name = "tomli" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, - { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, - { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, - { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, - { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, - { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, - { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, - { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, - { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, - { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, - { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, - { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, - { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, - { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, - { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, - { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, - { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, - { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, - { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, - { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, - { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, - { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, - { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, - { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, - { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, - { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, - { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, - { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, - { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, - { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, - { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6", size = 17662, upload-time = "2026-10-07T12:23:37.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/ab99b60ee52acd949684febabc3005d0045d0f66bebd9cdebd67372d26dd/tomli-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545", size = 163901, upload-time = "2026-10-07T12:22:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/bc/00/ee01b7ed4579180fff07142d290257f25ba786f23f3ec6005f620933c2f5/tomli-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef", size = 163756, upload-time = "2026-10-07T12:22:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/72/c2/4efebf65372f6583185f79799312109dddb61102d47e5c33dcfd1a297aca/tomli-2.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b", size = 268038, upload-time = "2026-10-07T12:22:18.135Z" }, + { url = "https://files.pythonhosted.org/packages/53/07/5850468e925d898abb36038666f9c333a94d2a223e802a8ba5b6d319d23f/tomli-2.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56", size = 276422, upload-time = "2026-10-07T12:22:19.567Z" }, + { url = "https://files.pythonhosted.org/packages/b4/87/f293984cdcf83c054196d4fd3dad44fc68ae55b4b8c44bc76cef360c3150/tomli-2.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1", size = 272616, upload-time = "2026-10-07T12:22:20.794Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ce/db582886b3c1219d3fec93ebd669332482e5aee7a91e0f7838d84f2d1759/tomli-2.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885", size = 276593, upload-time = "2026-10-07T12:22:22.12Z" }, + { url = "https://files.pythonhosted.org/packages/bf/72/7619b87dea4261fc27dd7b54c4461c129c1f7d9bb7ba3aec89c797a431b8/tomli-2.5.0-cp311-cp311-win32.whl", hash = "sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e", size = 101830, upload-time = "2026-10-07T12:22:23.651Z" }, + { url = "https://files.pythonhosted.org/packages/1e/74/220106da34502304b6751a2a9b8a9fbca6c3fd47e737a2e2e3da7c61c9db/tomli-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8", size = 112742, upload-time = "2026-10-07T12:22:24.972Z" }, + { url = "https://files.pythonhosted.org/packages/27/99/7d9c8b41837a7773613e169504147375c157a290167aa59ad74a085f521f/tomli-2.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980", size = 109332, upload-time = "2026-10-07T12:22:26.117Z" }, + { url = "https://files.pythonhosted.org/packages/52/ed/7baa86f87493646a594de388c7c1c40a39dd0461f7e9c0359cbeefc91fe8/tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df", size = 164854, upload-time = "2026-10-07T12:22:27.444Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b1/44c0341f2224397855723c7a8a39f718ea6fcbcc3dacc66e5aeca0f334e3/tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b", size = 164074, upload-time = "2026-10-07T12:22:28.679Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e2d5b7d3fba47adedb23de616c16d428ea076c79a3d8e1d95d649ffe197e/tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0", size = 274274, upload-time = "2026-10-07T12:22:29.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/90/6090e706ff27a6f89f4a40578e3324b95c3cd8c4150868aabf33a8f414c3/tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6", size = 286435, upload-time = "2026-10-07T12:22:31.297Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a2c40768df16c408f22430afb0a73e9d7e5f79c950884954649d1146b74d/tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc", size = 278119, upload-time = "2026-10-07T12:22:32.601Z" }, + { url = "https://files.pythonhosted.org/packages/12/25/3c0cb485b98e9cfac495629b1c93c87ccf0b72fbe9d2689fd8fe62c6d5a3/tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7", size = 286177, upload-time = "2026-10-07T12:22:33.745Z" }, + { url = "https://files.pythonhosted.org/packages/77/8b/0144c65f0e37e51c18d04ae15c21b19431c165002d0131fe9aa8b0b8b1e8/tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2", size = 102760, upload-time = "2026-10-07T12:22:34.887Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/5d6d8f42fc9a05fce69354e00ff256484192f5f2fc9a2165718fa0de61ec/tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7", size = 112722, upload-time = "2026-10-07T12:22:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/65/df18032218db0fb9b769fb23c8039a051f15c811993995ea04c350273a32/tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea", size = 109534, upload-time = "2026-10-07T12:22:37.296Z" }, + { url = "https://files.pythonhosted.org/packages/42/e5/51736d70da209350969e15aca5c5ab6e2ce1ea87a0a892a6c13aec172a86/tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea", size = 163328, upload-time = "2026-10-07T12:22:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/086f80dab4ab497602644274e6dea7ec5dd0b4e262e443a8ad3bb7edee2d/tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043", size = 162246, upload-time = "2026-10-07T12:22:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/aa/eb/3ecc94459f3635c92321f4e7bde571323fdb2267c50e19e3188a281eae3b/tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0", size = 272655, upload-time = "2026-10-07T12:22:41.08Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d7/494fd1f0c37a621f1ad9975c2efadb523e8101f144ed6edb2e7fe64738f2/tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b", size = 283595, upload-time = "2026-10-07T12:22:42.222Z" }, + { url = "https://files.pythonhosted.org/packages/70/51/bb8d62b1317e6640866f6949b2d5855e5300f2c99d46de1cd245570bba65/tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066", size = 276253, upload-time = "2026-10-07T12:22:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/f4/f46bd7f0763cd47de2db697dca9257c6a4adfd1a93b018cc75c8190ed5a8/tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b", size = 283582, upload-time = "2026-10-07T12:22:44.983Z" }, + { url = "https://files.pythonhosted.org/packages/ac/03/70f2bcb2923a6db37818d917e124270a7f4cfd38ea576f5aa753a91c0ef5/tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68", size = 102628, upload-time = "2026-10-07T12:22:46.508Z" }, + { url = "https://files.pythonhosted.org/packages/dc/98/d52024bb5b0ff68b4f0d276d867f634c84a67319a7e9f6b7708a37742333/tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc", size = 113301, upload-time = "2026-10-07T12:22:47.647Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f2/540db3a70572a8c23a28aba3e9c358ce0ffffbafc990905c1343aa265b31/tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84", size = 109744, upload-time = "2026-10-07T12:22:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/caf6b307766eb9567664a8707e9d6be5fcc0e8903f18781c6677a60d80c7/tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105", size = 162899, upload-time = "2026-10-07T12:22:50.088Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/68cfce773a2733a49c74f99d627fb461bd990756860099eac25617889585/tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646", size = 162080, upload-time = "2026-10-07T12:22:51.558Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b2/e5bb8651fdad593f670501a7d718b1a7f73f064d44dea15e04c04dfef45d/tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b", size = 273380, upload-time = "2026-10-07T12:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d2/9e2d7f8b1dfe0e2b34c245986ebd55c4c553ea4ce6c47c443b332673253f/tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75", size = 283228, upload-time = "2026-10-07T12:22:54.173Z" }, + { url = "https://files.pythonhosted.org/packages/ba/df/ec7b876b7b1a2718bd74a3743c076fff565b04029ba33e8f61fac262739f/tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb", size = 277189, upload-time = "2026-10-07T12:22:55.342Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7b/e192d9eed0b9cb80da799f4d77052297fb9a2c3cc9b19f571f56ea88add6/tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3", size = 283632, upload-time = "2026-10-07T12:22:56.735Z" }, + { url = "https://files.pythonhosted.org/packages/84/50/ff94454e75461d75623e47401ed323d65c10aab8fe9033242c20cd2fdf32/tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b", size = 103535, upload-time = "2026-10-07T12:22:58.084Z" }, + { url = "https://files.pythonhosted.org/packages/54/0b/bdacf05f963bd6026ebf6eeb0beda847d1d60e03e440725c64a4e08a0afd/tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a", size = 114621, upload-time = "2026-10-07T12:22:59.2Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/53f438fa6ae4f9d4ed0ddde3e7242b3bdc34b48c8f9948b72b9e9b127676/tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3", size = 111572, upload-time = "2026-10-07T12:23:00.479Z" }, + { url = "https://files.pythonhosted.org/packages/b9/20/1f88f19427d380a40e90a770e087489eaafe4aeee070ae88ed2bbec00acd/tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4", size = 171814, upload-time = "2026-10-07T12:23:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/cbe5079c9f9a54b9b3e27fc82f08f3cb36edee75561679f53d2380c801d6/tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d", size = 171324, upload-time = "2026-10-07T12:23:03.18Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/1d53fd3b0f1cb3ba542e345ec32c26aefdddc4e829e4f3429af8a4f27782/tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9", size = 297441, upload-time = "2026-10-07T12:23:04.345Z" }, + { url = "https://files.pythonhosted.org/packages/66/d9/0800acb6a111686f764c1b91ef15cc42a20a66a46013bb42220f1d2c61c1/tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f", size = 307476, upload-time = "2026-10-07T12:23:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/30a8f3cd51b5bec37f04744bad0b0dc6160df84aad4f27b0e9283d66f221/tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374", size = 296113, upload-time = "2026-10-07T12:23:07.202Z" }, + { url = "https://files.pythonhosted.org/packages/ab/18/0b9ffc597e69c5a1e20a7823cb60d54b39a9f54e91edcb8574f022186758/tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442", size = 307725, upload-time = "2026-10-07T12:23:08.508Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c7/18f8baae0b5607a60e8e19b4a7fedee43a8ff6458e3896dcbbadeeac9c22/tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03", size = 108546, upload-time = "2026-10-07T12:23:09.956Z" }, + { url = "https://files.pythonhosted.org/packages/72/34/4cca9739254130627bde87500b3f2b512154fe2f278efa7e2a5e10ad4bcb/tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1", size = 117814, upload-time = "2026-10-07T12:23:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/afa530d47dd80a78fce43beac6bc6e00f84558eafcffbc6f37b21e80d056/tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0", size = 115188, upload-time = "2026-10-07T12:23:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc", size = 162775, upload-time = "2026-10-07T12:23:13.941Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276", size = 161406, upload-time = "2026-10-07T12:23:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52", size = 273855, upload-time = "2026-10-07T12:23:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7", size = 284910, upload-time = "2026-10-07T12:23:18.166Z" }, + { url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391", size = 277723, upload-time = "2026-10-07T12:23:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859", size = 285115, upload-time = "2026-10-07T12:23:20.698Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb", size = 103475, upload-time = "2026-10-07T12:23:21.941Z" }, + { url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5", size = 114589, upload-time = "2026-10-07T12:23:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd", size = 111493, upload-time = "2026-10-07T12:23:24.233Z" }, + { url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57", size = 171380, upload-time = "2026-10-07T12:23:25.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd", size = 170553, upload-time = "2026-10-07T12:23:26.855Z" }, + { url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01", size = 294428, upload-time = "2026-10-07T12:23:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f", size = 304909, upload-time = "2026-10-07T12:23:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a", size = 293220, upload-time = "2026-10-07T12:23:30.608Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142", size = 305705, upload-time = "2026-10-07T12:23:32.181Z" }, + { url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5", size = 108432, upload-time = "2026-10-07T12:23:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571", size = 117281, upload-time = "2026-10-07T12:23:34.648Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7", size = 115069, upload-time = "2026-10-07T12:23:35.77Z" }, + { url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b", size = 14765, upload-time = "2026-10-07T12:23:36.875Z" }, +] + +[[package]] +name = "trio" +version = "0.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "outcome" }, + { name = "sniffio" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/dc/a2d25ed73ad49cfd79bf18d262577c3731c98e382284e28d522f49a0df35/trio-0.34.0.tar.gz", hash = "sha256:63b9485408bdfdde544fced107045a8c0086cdc4bd0ef2f797b9e0dd111b964b", size = 607457, upload-time = "2026-08-11T00:33:42.198Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/1f/555f1364bed52a92a864181962b77f1b15adadeacf23b86105324363e461/trio-0.34.0-py3-none-any.whl", hash = "sha256:6c7c9f49917694dcdcd5f67abd168df5599eca480d61f29854d17a61a75c2f05", size = 511840, upload-time = "2026-08-11T00:33:40.552Z" }, ] [[package]] @@ -1976,22 +2118,22 @@ wheels = [ [[package]] name = "winloop" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/19/f11a72d288887c9ed6f4b033040231a73caf361ddbdaab92a9dc34af9ced/winloop-0.7.0.tar.gz", hash = "sha256:dcdea8aa687711e0a41cbe6280cb0957e2cb1f5dcbc51e65138503ea3a70ff7a", size = 2690490, upload-time = "2026-09-30T16:12:03.432Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/b9/3d71dd3d00b45145c90ff0f8a756a4e490dbb3fd9850a8aac20c5c7f9f5e/winloop-0.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:d61e1f6f372f64c1b08126b32e4dd4bde099d55c5a4f50ac54e6740c0cab2ed4", size = 656617, upload-time = "2026-09-30T16:11:46.145Z" }, - { url = "https://files.pythonhosted.org/packages/49/cd/c6cba2b0f5fbb79cc02cded8baefe93ee3891eb84738ca8c95a7256208a0/winloop-0.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:a159055cf2685bf77e42d62b3388a5b5dab3b36c8b35d3f4c6a354f987a59bdd", size = 592803, upload-time = "2026-09-30T16:11:47.715Z" }, - { url = "https://files.pythonhosted.org/packages/c2/fe/1fa36fb7ee8ba148dbadb5dbd61ac92a2526f8809ddf2eca8ba2020b24e2/winloop-0.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f4ee09e36190bc77d820d793fe76d5ace48f83d9f8e39adde0231d0a0366011", size = 664893, upload-time = "2026-09-30T16:11:48.914Z" }, - { url = "https://files.pythonhosted.org/packages/6c/78/6835e22f756ecd1e5dd00cd85b803b94e8c8683c40a3dc71a3c9e0cdc07e/winloop-0.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:324f9f6293432ed52f09f8729f7576ff038d08754310b32ea7fcb433f9b03616", size = 594661, upload-time = "2026-09-30T16:11:50.27Z" }, - { url = "https://files.pythonhosted.org/packages/24/4c/e7334c45b8a8a126a1cd97d5098ef45a5d0011fe2f8cc89a8612fe396fd5/winloop-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c76d88db62d4246e7812cbdd28e9c628917104af0062e1add857cdb6857257c", size = 687185, upload-time = "2026-09-30T16:11:51.315Z" }, - { url = "https://files.pythonhosted.org/packages/05/03/7c6a8ab38a30607415b067395edc3e0d128091222da5b5f0fd66f153b6cb/winloop-0.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:23185a1c414cd327754671b982ac52bd10d2090b30139cf74e3168c30c9d1bf2", size = 593976, upload-time = "2026-09-30T16:11:52.58Z" }, - { url = "https://files.pythonhosted.org/packages/93/05/6b1aa396f83a9c9b04089aa01606f0c55acc1ecfbacad032491e63baae28/winloop-0.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:af9a18e9f22dece1d9161a6fe3d6597e8429cb29d98b2fccb914f7699371b19d", size = 686139, upload-time = "2026-09-30T16:11:53.925Z" }, - { url = "https://files.pythonhosted.org/packages/2a/00/a6fd76635e3726cb37047c3548820ae0af7991740d4f28899362841f1ead/winloop-0.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:552c901afead131739c3f1a7a219fe2fcf3b8e1a0428b92f3530950997961acd", size = 593763, upload-time = "2026-09-30T16:11:54.924Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/f19225e0ea9f95102e1b6a6893db76ab2f345ef0bf92b8d986c67927929e/winloop-0.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:ff5d7552f7ef189d99f3097ce4e0bc17c002e3ced1e01fcbcdebfeff050a717b", size = 696488, upload-time = "2026-09-30T16:11:56.032Z" }, - { url = "https://files.pythonhosted.org/packages/51/22/2709911d2ff28154657c60a9119c4224347087edcc41f730631ecd4b2f8a/winloop-0.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:7edb554a69d2d315e0713142cd831001f2f1fb9a796688077bd7b651d3bbb2f6", size = 606668, upload-time = "2026-09-30T16:11:57.458Z" }, - { url = "https://files.pythonhosted.org/packages/3b/57/33754ab6a97b65e4112123b275848635e7cd480933c05638e4d6cc258a37/winloop-0.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:81ea78f879f7a59f58c9f6b97a3fd1b249f9c6b4576705e05be520da05fb1172", size = 751964, upload-time = "2026-09-30T16:11:58.517Z" }, - { url = "https://files.pythonhosted.org/packages/69/43/4e87aff10d190e126dd574ac38103e8b10749d7de66dd6442443199b467f/winloop-0.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a1d646722537504fff6f7d39f73ed70d7a8ce7505c3751cfac3c798d6d6b3904", size = 652194, upload-time = "2026-09-30T16:11:59.75Z" }, +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/66/0a8c65d4bb4504945aba3cc399588e06e1ea9a503cb6fd70f4b68c1f3b4d/winloop-0.7.1.tar.gz", hash = "sha256:2ea92f36493477427ca3c7f4cb84a45f47f5feb3559dfe4b660d8e8068171a47", size = 2706597, upload-time = "2026-10-05T16:58:58.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/d8/ff1d165dfce5da41c493fe893fdbabbe0bcb582735e0a21f3f06c7b0c5ef/winloop-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:c94f574049aa1b35e8c6babe805f9c5bc73e7d3752b6a7696f0403cec98108a4", size = 665005, upload-time = "2026-10-05T16:58:40.002Z" }, + { url = "https://files.pythonhosted.org/packages/41/65/a66f68c8123193247d53e4c3c70eb0ac2d05bb7dbe56959527e8a3fa0b6c/winloop-0.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:9089b64e6afd297716290671852bf6ec0b70bef2a336613765271fe85832a256", size = 599807, upload-time = "2026-10-05T16:58:41.379Z" }, + { url = "https://files.pythonhosted.org/packages/12/47/69650d40470ea5014f971f0bbf1c249e783a6048b7bb6d04782f0d032c16/winloop-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:19154f8be70bc9ce9455a9ca26d8ecd48cc6cf432b7ccbf57238c84db29ba53a", size = 673395, upload-time = "2026-10-05T16:58:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d2/abb25356ff35882dccdc5c558e2a78cef05b62e24d10edb397a7c3b4b445/winloop-0.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae1808cfbc608e92577cef81131674d93efed548090995760fd1cd721aa50dbb", size = 602395, upload-time = "2026-10-05T16:58:44.447Z" }, + { url = "https://files.pythonhosted.org/packages/45/a7/e67a4e6156c159bd4ae3eaab1075914c32c2ee2b8fe0ff1894c2119a2a67/winloop-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:9bb416a2f0fee52dcdda3137358c56b48bfb63c443e11232d4c04353b56d39b0", size = 695292, upload-time = "2026-10-05T16:58:45.654Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f0/1f7c199da23ff8d9e8fcbafade3ad11dcb1005c58eac93991255c262fe06/winloop-0.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:6f2f90622542382bf9304b5ec9d252102f444bc3b445e24515250d7c62d62702", size = 600770, upload-time = "2026-10-05T16:58:46.782Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/7c14e1ec68d14b731bfc6d0480fce2a2c88e3248465b594161a28a9ed2e9/winloop-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:0aa944cb845ba5d6420fe53733feb3fb474600b298202ec2bd7cb03f46502331", size = 694427, upload-time = "2026-10-05T16:58:47.878Z" }, + { url = "https://files.pythonhosted.org/packages/36/6a/f14211f8b7b907dfdb4da8e99cf1a99695405cc8c24793d9835f0d396189/winloop-0.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:20464fde92ecbcda14a590d4d88275abf19eca99ed3ae787e0dee1e054be51d5", size = 600196, upload-time = "2026-10-05T16:58:49.236Z" }, + { url = "https://files.pythonhosted.org/packages/89/71/435fe9292708ae750057d10717026c71ff8f3f98b73fd7a2e4ccb5d3fa2d/winloop-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:86a78413b3ae9c6d6c5f9cc0a16a0ab7c48040df7952321f095b8b9b72ec653d", size = 703598, upload-time = "2026-10-05T16:58:50.578Z" }, + { url = "https://files.pythonhosted.org/packages/d3/11/e5dea8140388a27ac6c57787bbf1f758bfadff410fc2cb797db0a51a188b/winloop-0.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:db04854c148a1ead62cbf63394d320dbb4c14fe7d33738012deb3ad5ecf41eef", size = 612769, upload-time = "2026-10-05T16:58:52.24Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/c3ec01a05d100bc47734815ee39020d5ee3678731125a02c2b46922b4e4e/winloop-0.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:d1d3cf643c6f652aa3be42168df90d086508853fc40c58b3d1b3397577ffa667", size = 760188, upload-time = "2026-10-05T16:58:53.587Z" }, + { url = "https://files.pythonhosted.org/packages/04/52/715f8ec02883145aa17f8a47f6bf878acc28db194ffcb3e99eb7bd270ff9/winloop-0.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2c6c1306da9b998e167930f9b0f7f8e3e6c9c75f6a666277a53ad9fa3afa694c", size = 660528, upload-time = "2026-10-05T16:58:54.671Z" }, ] [[package]]