benzene.grpc
The gRPC edge of the Benzene wire contract: the Benzene ↔ gRPC status mapping (wire-contracts.md
§4.2), the benzene-status trailer rule, and the server/client transport over grpcio.
Distribution: benzene-grpc (depends on benzene-core; the transport adds the [transport]
extra).
pip install benzene-grpc # the status mapping (no grpcio)
pip install 'benzene-grpc[transport]' # + the gRPC server/client transport
Status mapping
from benzene.grpc import to_grpc, from_grpc, BENZENE_STATUS_TRAILER
to_grpc("not-found") # -> "NotFound"
to_grpc("created") # -> "OK" (every success status collapses to OK)
to_grpc("some-extension") # -> "Internal" (unknown/missing failure)
from_grpc("InvalidArgument") # -> "bad-request"
from_grpc("SomeFutureCode") # -> "unexpected-error"
- Forward (
to_grpc, server side): a Benzene status → a gRPCStatusCodename. All success statuses map toOK; each failure status maps per §4.2; an unknown/missing status maps toInternal. - Reverse (
from_grpc, client side): a gRPCStatusCodename → a Benzene status; an unrecognised code falls back tounexpected-error.
gRPC codes are their canonical names as strings ("OK", "InvalidArgument", …), so the mapping
needs no grpcio dependency — a transport binding translates them to grpc.StatusCode members at its
edge.
The benzene-status trailer
Because several Benzene statuses collapse to one gRPC code (all success → OK), a gRPC server MUST
attach a benzene-status trailer carrying the raw status verbatim. A client, seeing the trailer, uses
it in preference to re-deriving from the code:
from_grpc("OK", trailer="created") # -> "created" (the trailer wins verbatim)
BENZENE_STATUS_TRAILER is the trailer key ("benzene-status"). Pinned by
grpc-status-mapping.json.
The transport ([transport] extra)
A Benzene gRPC service is a generic gRPC handler: the method name is the topic (the grpc topic
source is "method"), so one handler serves every topic. Each unary call carries the message body as its
bytes and the Benzene headers as request metadata.
from concurrent.futures import ThreadPoolExecutor
import grpc
from benzene.core import BenzeneMessageApplication
from benzene.grpc import add_benzene_handler, GrpcMessageSender
# Server: serve every topic as a unary method
server = grpc.server(ThreadPoolExecutor(max_workers=8))
add_benzene_handler(server, BenzeneMessageApplication(registry))
server.add_insecure_port("[::]:50051"); server.start()
# Client: a MessageSender over a channel
sender = GrpcMessageSender(grpc.insecure_channel("localhost:50051"))
result = await sender.send_message("orders:place", {"sku": "A"}, headers={"x-correlation-id": "c1"})
add_benzene_handler(server, application)registers aBenzeneGrpcHandleron agrpc.Server. The response carries the mappedStatusCodeand — on success and failure alike — abenzene-statustrailer with the raw status, so a status likecreated(which maps to gRPCOK) survives the round trip exactly.GrpcMessageSender(channel)is aMessageSender: it calls/benzene.Benzene/<topic>, forwards the headers as metadata, and maps the outcome back (the trailer wins verbatim, else the code is mapped). The blocking gRPC call runs on a worker thread, so it never blocks the event loop.method_for(topic)/topic_for(method)are the method-path convention if you need them directly.
Testing
create_test_host(StartUp).with_services(...).build_grpc() drives the real BenzeneGrpcHandler in
memory — the same one-specialization-step harness as the cloud hosts, no socket:
host = create_test_host(OrdersStartUp).with_services(overrides).build_grpc()
reply = host.send_grpc("orders:place", body={"sku": "A"}, headers={"x-correlation-id": "c1"})
assert reply.status == "created" # the benzene-status trailer, verbatim; reply.code has the StatusCode
send_grpc builds the native ingress (method = topic, headers = metadata, bytes body) and returns a
GrpcResponse(status, payload, code, details). See benzene.testing.
Spec note (documented bend). The method-path scheme
/benzene.Benzene/<topic>is this port's convention, not a wire contract. The gRPC binding catalog in transport-bindings §2 is informative and describes .NET's explicit (route → topic) registrations; the Python port instead serves every topic through one generic handler, which is the idiomatic gRPC-Python shape. Two Benzene Python services interoperate over gRPC out of the box; talking gRPC to a binding that uses a different method path (e.g. a .NET service's/package.Service/Method) means agreeing the path on both sides. This is a binding (tier-D) detail — the envelope, headers, status vocabulary, andbenzene-statustrailer are unaffected.
Exports
to_grpc, from_grpc, BENZENE_STATUS_TRAILER, add_benzene_handler, BenzeneGrpcHandler,
GrpcMessageSender, method_for, topic_for.
See also
benzene.http— the analogous Benzene ↔ HTTP status mapping and the ASGI binding.benzene.results— the status vocabulary.