# 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 ```python 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 {doc}`/edri_class`. Host/port default to `EDRI_GRPC_HOST` (`localhost`) / `EDRI_GRPC_PORT` (`50051`) — see {doc}`/env` — 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}`: ```python 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 | |---|---| | `str`, `int`, `float`, `bool` | matching proto3 scalar (`string` / `int64` / `double` / `bool`) | | `datetime` | `google.protobuf.Timestamp` | | `UUID` | `string` | | `Enum` subclass | `string` (the member name) | | nested `dataclass` | nested `message` | | `Optional[X]` | proto3 `optional X` | | `list[X]` | `repeated X` | | `dict[str, Any]` | `google.protobuf.Struct` | 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 {doc}`/health`) 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 {doc}`/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: ```python @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`. ```proto 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 {doc}`/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](https://grpc.io/docs/guides/reflection/) is always enabled, wired against the exact schema being served — point `grpcurl`, `evans`, or your language's codegen directly at the running server: ```bash 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: ```bash 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 | `grpc.StatusCode` | |---|---| | `InternalServerErrorResponseDirective` | `INTERNAL` | | `UnauthorizedResponseDirective` | `UNAUTHENTICATED` | | `NotFoundResponseDirective` | `NOT_FOUND` | | `ConflictResponseDirective` | `ALREADY_EXISTS` | | `BadRequestResponseDirective` | `INVALID_ARGUMENT` | | `UnprocessableContentResponseDirective` | `FAILED_PRECONDITION` | | `ServiceUnavailableResponseDirective` | `UNAVAILABLE` | | `TooManyRequestsResponseDirective` | `RESOURCE_EXHAUSTED` | | `NotImplementedResponseDirective` | `UNIMPLEMENTED` | | `GoneResponseDirective` | `NOT_FOUND` | `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 {doc}`/env`) 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. - `GRPCListener` runs as a single process — no built-in multi-worker scaling, matching the framework's general single-process-per-listener posture (see {doc}`/deployment`).