Skip to content
Open
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div align="center">
c<div align="center">

![The Connect logo](https://raw.githubusercontent.com/connectrpc/connectrpc.com/12f7ad8e95c5f784700bc280708b27cd148d0cf1/public/img/logos/simple-connect.svg)

Expand Down Expand Up @@ -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
Expand Down
33 changes: 26 additions & 7 deletions conformance/test/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

# Needs to run before importing from connectrpc
import _cov_embed # noqa: F401
import sniffio
from _util import create_standard_streams
from gen.connectrpc.conformance.v1 import service_pb
from gen.connectrpc.conformance.v1.config_pb import Code as ConformanceCode, HTTPVersion
Expand Down Expand Up @@ -141,6 +142,16 @@ def _create_request_info(
return request_info


async def _sleep(seconds: float) -> None:
"""Sleep on whichever event loop the server runs the application on."""
if 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:
Expand All @@ -159,7 +170,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
Expand Down Expand Up @@ -215,7 +226,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

Expand Down Expand Up @@ -247,7 +258,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]
Expand All @@ -262,7 +273,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]
Expand Down Expand Up @@ -616,6 +627,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
Expand All @@ -640,6 +652,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
Expand Down Expand Up @@ -708,7 +721,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):
Expand Down Expand Up @@ -774,10 +787,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":
Expand Down
2 changes: 1 addition & 1 deletion conformance/test/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ dev = [
"pyvoy==1.2.0",
"ruff==0.16.9",
"tombi==1.5.7",
"trio==0.34.0",
"ty==0.0.84",
"types-grpcio==1.83.0.20260730",
"types-protobuf==7.35.1.20260906",
Expand Down
Loading
Loading