Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 9 additions & 17 deletions connectrpc-grpcreflect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,7 @@ The services are standard Connect services that you use as normal.
```python
from typing import cast

from connectrpc_grpcreflect import (
ServerReflectionAlphaASGIApplication,
ServerReflectionAlphaService,
ServerReflectionASGIApplication,
ServerReflectionService,
)
from connectrpc_grpcreflect import reflection_apps
from starlette.applications import Starlette
from starlette.routing import Mount
from starlette.types import ASGIApp
Expand All @@ -25,25 +20,22 @@ from .gen.connectrpc.eliza.v1.eliza_connect import ElizaServiceASGIApplication
from .service import ElizaService

eliza_app = ElizaServiceASGIApplication(ElizaService())
reflection_app = ServerReflectionASGIApplication(
ServerReflectionService(eliza_pb.desc())
)
reflection_alpha_app = ServerReflectionAlphaASGIApplication(
ServerReflectionAlphaService(eliza_pb.desc())
)

app = Starlette(
routes=[
Mount(eliza_app.path, cast(ASGIApp, eliza_app)),
Mount(reflection_app.path, cast(ASGIApp, reflection_app)),
Mount(reflection_alpha_app.path, cast(ASGIApp, reflection_alpha_app)),
*(
Mount(reflection_app.path, cast(ASGIApp, reflection_app))
for reflection_app in reflection_apps(eliza_pb.desc())
),
]
)
```

Mount both `grpc.reflection.v1` and `grpc.reflection.v1alpha`. Both are
listed by `list_services`, and a client that calls v1alpha gets a 404 if it
isn't mounted.
`reflection_apps` returns applications for both `grpc.reflection.v1` and
`grpc.reflection.v1alpha`. Mount both: each lists the other, and a client
that calls v1alpha gets a 404 if it isn't mounted. Use
`reflection_apps_sync` for WSGI.
Comment on lines +35 to +38

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`reflection_apps` returns applications for both `grpc.reflection.v1` and
`grpc.reflection.v1alpha`. Mount both: each lists the other, and a client
that calls v1alpha gets a 404 if it isn't mounted. Use
`reflection_apps_sync` for WSGI.
`reflection_apps` / `reflection_apps_sync` return applications for both `grpc.reflection.v1` and
`grpc.reflection.v1alpha`. It is recommended to mount both - even if mounting one, both will be returned in `ListServices` and a client that calls the unmounted service will 404.

Mostly to clarify what lists the other means here which isn't clear. Let's apply the same to the docstrings


Reflection uses the gRPC protocol which requires a server that supports HTTP/2
trailers such as [pyvoy](https://pyvoy.dev).
3 changes: 3 additions & 0 deletions connectrpc-grpcreflect/connectrpc_grpcreflect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@
"ServerReflectionService",
"ServerReflectionServiceSync",
"ServerReflectionWSGIApplication",
"reflection_apps",
"reflection_apps_sync",
]

from ._apps import reflection_apps, reflection_apps_sync
from ._gen.grpc.reflection.v1.reflection_connect import (
ServerReflectionASGIApplication,
ServerReflectionWSGIApplication,
Expand Down
59 changes: 59 additions & 0 deletions connectrpc-grpcreflect/connectrpc_grpcreflect/_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from ._gen.grpc.reflection.v1.reflection_connect import (
ServerReflectionASGIApplication,
ServerReflectionWSGIApplication,
)
from ._gen.grpc.reflection.v1alpha.reflection_connect import (
ServerReflectionASGIApplication as ServerReflectionAlphaASGIApplication,
ServerReflectionWSGIApplication as ServerReflectionAlphaWSGIApplication,
)
from ._service import (
ServerReflectionAlphaService,
ServerReflectionAlphaServiceSync,
ServerReflectionService,
ServerReflectionServiceSync,
)

if TYPE_CHECKING:
from protobuf import DescFile, DescService


def reflection_apps(
*descs: DescFile | DescService,
) -> tuple[ServerReflectionASGIApplication, ServerReflectionAlphaASGIApplication]:
"""Create ASGI applications for the v1 and v1alpha gRPC reflection services.

Both services list each other, so serve both: mount each application at its
`path`.

Args:
*descs: The descriptors to make available for reflection, as accepted by
[ServerReflectionService][connectrpc_grpcreflect.ServerReflectionService].

"""
return (
ServerReflectionASGIApplication(ServerReflectionService(*descs)),
ServerReflectionAlphaASGIApplication(ServerReflectionAlphaService(*descs)),
)


def reflection_apps_sync(
*descs: DescFile | DescService,
) -> tuple[ServerReflectionWSGIApplication, ServerReflectionAlphaWSGIApplication]:
"""Create WSGI applications for the v1 and v1alpha gRPC reflection services.

Both services list each other, so serve both: mount each application at its
`path`.

Args:
*descs: The descriptors to make available for reflection, as accepted by
[ServerReflectionServiceSync][connectrpc_grpcreflect.ServerReflectionServiceSync].

"""
return (
ServerReflectionWSGIApplication(ServerReflectionServiceSync(*descs)),
ServerReflectionAlphaWSGIApplication(ServerReflectionAlphaServiceSync(*descs)),
)
26 changes: 8 additions & 18 deletions connectrpc-grpcreflect/test/test_grpcreflect.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,10 @@
from pyqwest.testing import ASGITransport, WSGITransport

from connectrpc_grpcreflect import (
ServerReflectionAlphaASGIApplication,
ServerReflectionAlphaService,
ServerReflectionAlphaServiceSync,
ServerReflectionAlphaWSGIApplication,
ServerReflectionASGIApplication,
ServerReflectionService,
ServerReflectionServiceSync,
ServerReflectionWSGIApplication,
reflection_apps,
reflection_apps_sync,
)
from connectrpc_grpcreflect._gen.grpc.reflection.v1.reflection_connect import (
ServerReflectionClient,
Expand Down Expand Up @@ -60,17 +56,13 @@ async def reflection_client(
) -> AsyncIterator[ReflectionClient]:
match request.param:
case "sync":
app = ServerReflectionWSGIApplication(
ServerReflectionServiceSync(haberdasher_pb.desc(), eliza_pb.desc())
)
app, _ = reflection_apps_sync(haberdasher_pb.desc(), eliza_pb.desc())
with ServerReflectionClientSync(
"http://localhost", http_client=SyncClient(WSGITransport(app=app))
) as client:
yield client
case "async":
app = ServerReflectionASGIApplication(
ServerReflectionService(haberdasher_pb.desc(), eliza_pb.desc())
)
app, _ = reflection_apps(haberdasher_pb.desc(), eliza_pb.desc())
async with ServerReflectionClient(
"http://localhost", http_client=Client(ASGITransport(app))
) as client:
Expand Down Expand Up @@ -457,9 +449,8 @@ def test_extensions() -> None:

@pytest.mark.asyncio
async def test_v1alpha() -> None:
app = ServerReflectionAlphaASGIApplication(
ServerReflectionAlphaService(eliza_pb.desc())
)
_, app = reflection_apps(eliza_pb.desc())
assert app.path == "/grpc.reflection.v1alpha.ServerReflection"
req = ServerReflectionAlphaRequest(
host="alpha.example.com", message_request=Oneof("list_services", "")
)
Expand All @@ -485,9 +476,8 @@ async def request_iter() -> AsyncIterator[ServerReflectionAlphaRequest]:


def test_v1alpha_sync() -> None:
app = ServerReflectionAlphaWSGIApplication(
ServerReflectionAlphaServiceSync(eliza_pb.desc())
)
_, app = reflection_apps_sync(eliza_pb.desc())
assert app.path == "/grpc.reflection.v1alpha.ServerReflection"
req = ServerReflectionAlphaRequest(
host="alpha.example.com", message_request=Oneof("list_services", "")
)
Expand Down
Loading