gRPC¶
EDRI can expose the same @api-decorated Event dataclasses that already drive REST/WebSocket/HTML over gRPC — one unary rpc per event, plus one shared bidi-streaming rpc. The .proto schema is generated at runtime from those dataclasses; there is no hand-written .proto file to keep in sync and no committed generated _pb2.py stub files.
Enabling it¶
from edri import EDRI
edri = EDRI()
edri.add_component(...)
edri.start_grpc()
edri.run()
start_grpc() runs its own process (GRPCListener), analogous to the ASGI Listener start_api() starts — alongside it, or on its own. Both share the same Broker, started automatically if it isn't running yet. See EDRI.
Host/port default to EDRI_GRPC_HOST (localhost) / EDRI_GRPC_PORT (50051) — see Environment Variables — or pass host/port to start_grpc() explicitly.
Excluding events¶
Every event is GRPC-eligible by default, the same as REST/WS/HTML. Opt one out with exclude={ApiType.GRPC}:
from edri.config.constant import ApiType
@api(method=HTTPMethod.GET, exclude={ApiType.GRPC})
class InternalOnlyEvent(Event):
...
File-only events (upload/download) are excluded automatically — file transfer isn't supported over gRPC yet (see Limitations below).
Type mapping¶
proto3's scalar set doesn't cover every type an Event field commonly uses. edri.api.grpc.type_mapping maps:
Python / Event field type |
proto3 representation |
|---|---|
|
matching proto3 scalar ( |
|
|
|
|
|
|
nested |
nested |
|
proto3 |
|
|
|
|
A field type this table can't represent (a bare Any, a non-dataclass custom object, Optional[list[X]], a multi-type union, an Injection-validated field) makes the whole event unavailable over gRPC — logged as a warning at schema-generation time, not a startup crash, the same "best effort" posture HTML already applies to template-less events.
This affects two of the framework's own built-in events: EdriHealth and EdriReady (see Health checking) both respond with a list[Record], and Record carries a pipe: Connection and a definition: ManagerBase field — neither representable here — so both events are excluded from gRPC and absent from the schema and reflection. EdriLive and Topology (see Router) have no such field and remain gRPC-eligible; reach EdriHealth over REST/WS and EdriReady over REST instead.
Unary calls¶
One unary rpc per GRPC-eligible event, named after the event's class:
@response
class GreetResponse(Response):
message: str
@api(method=HTTPMethod.GET, exclude={ApiType.HTML})
class GreetEvent(Event):
name: str
response: GreetResponse
exposes rpc GreetEvent (GreetEventRequest) returns (GreetEventResponse) on the EdriService service. GreetEventRequest carries every field the caller supplies (name); GreetEventResponse always carries status first (a string, mirroring Response.as_dict()'s own field order), followed by the event's own response fields (message).
Bidi streaming¶
One shared bidi-streaming rpc, Connect, mirrors the WebSocket connection: register once, exchange as many events as you like over the lifetime of the stream, correlated by Event._id.
rpc Connect (stream ClientMessage) returns (stream ServerMessage);
ClientMessage/ServerMessage are oneof envelopes over every GRPC-eligible event's own request/response message, keyed by the event's snake_case name (greet_event, ...). Send a message by setting the matching oneof field; read a reply the same way, via WhichOneof("event").
A client may half-close its write side (finish sending) while replies to already-sent requests are still in flight — those replies are still delivered, up to EDRI_API_RESPONSE_TIMEOUT after the last request, before the stream ends.
A Connect stream is a persistent connection like a WebSocket, so it also receives events with handling=EventHandlingType.SUBSCRIBED/ALL (see Event): send EdriRegister/EdriUnregister over the stream (param_set maps to google.protobuf.Struct, per the type mapping above) the same way a WS client would, to subscribe to a shareable event's pushes. A gRPC unary call cannot receive these — its pipe is only read for the duration of that one call.
Reflection and codegen¶
The standard gRPC reflection service is always enabled, wired against the exact schema being served — point grpcurl, evans, or your language's codegen directly at the running server:
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext localhost:50051 describe edri.grpc.v1.EdriService
For a static .proto file instead (for example, to commit generated stubs for a non-Python client), render the same schema to text:
python -m edri.api.grpc.export edri.proto # write to a file
python -m edri.api.grpc.export # or to stdout
Each event's wire field number is derived from its own class name (not its position among whatever other events happen to be registered), so a stub compiled from one export stays wire-compatible as events are added or removed elsewhere in the codebase.
Errors¶
A ResponseDirective your business logic returns maps to a grpc.StatusCode, the same way REST maps directives to HTTP status codes and WS maps them to failure responses:
Directive |
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
HeaderResponseDirective maps into trailing metadata instead of a status code. Directives with no gRPC equivalent (CookieResponseDirective, the redirect family, RangeNotSatisfiableResponseDirective, UnsupportedMediaTypeResponseDirective) fall through to the same "unhandled directive" logging REST/WS already have.
A unary call that gets no reply in time fails DEADLINE_EXCEEDED — bounded by the client's own gRPC deadline if it set one, capped by EDRI_API_RESPONSE_TIMEOUT (see Environment Variables) otherwise.
Limitations¶
No file upload/download over gRPC yet — file-only events are excluded automatically.
No TLS/mTLS or auth interceptors built in — terminate TLS and authenticate the same way you already do in front of the process; a future iteration can add gRPC interceptors the way REST/WS have middleware today.
GRPCListenerruns as a single process — no built-in multi-worker scaling, matching the framework's general single-process-per-listener posture (see Deployment).