gRPC in Python

Call any gRPC endpoint from Python services with typed Protocol Buffer messages.

Python services call systems that expose gRPC APIs through outgoing connections, with strongly-typed Protocol Buffer messages - the platform handles TLS, security and error mapping.

All four gRPC RPC types are supported:

  • Unary - one request message in, one response message out
  • Server streaming - one request in, a stream of responses out, yielded as they arrive
  • Client streaming - a stream of requests in, one response out, any iterable or generator works
  • Bidirectional streaming - streams in both directions

Create a connection

To create a connection, go to Connections > Outgoing > gRPC in the Dashboard, click Create a new connection and fill in the form:

  1. Name: Billing
  2. Address: the gRPC server's address, e.g. billing.example.com:50051
  3. Proto file path: the filesystem path of the service's .proto file
  4. Click OK

Every service can use the connection through self.grpc, passing the connection's name. The object it returns exposes the methods of the underlying gRPC service - you call them directly, passing Protocol Buffer messages.

Connection options

All the fields of the form, in the order they appear:

FieldWhat it doesDefault
NameA unique name of the connection, used in self.grpc['Name']---
ActiveWhether services can use the connectionOn
TLSWhether to connect over TLS, switch it off only for plaintext serversOn
AddressHost and port of the gRPC server, e.g. billing.example.com:50051localhost:50051
SecurityA Basic Auth, API key or OAuth definition whose credentials are sent as call metadataNo security
CA certs fileA PEM file with CA certificates to verify the server's certificate againstWell-known CAs
Proto file pathA .proto file the server generates the client code out of---
Stub moduleYour own pre-generated *_pb2_grpc module, used instead of a .proto file, e.g. acme.billing.v1.billing_pb2_grpc---
Stub classThe stub class to use, e.g. BillingStub, needed only when the code has more than one serviceFound automatically
Ping timeoutHow many seconds to wait for the server when pinging10
Max send sizeThe biggest message that can be sent, in bytes104857600 (100 MB)
Max receive sizeThe biggest message that can be received, in bytes104857600 (100 MB)

Each connection needs either "Proto file path" or "Stub module" - see below for how to choose.

Provide the Protocol Buffer code

Every gRPC service is described by a .proto file and the connection needs the Python code generated out of it. There are two ways to provide that code and each connection uses one of them:

  • Let the server generate it - set "Proto file path" on the connection to the filesystem path of your .proto file. The server runs the Protocol Buffer compiler itself, at connection create and edit time, and keeps the generated modules in its own work directory. If the .proto file imports other .proto files, keep them in the same directory and they will be compiled together.

  • Deploy your own modules - generate the code yourself with python -m grpc_tools.protoc and hot-deploy the resulting *_pb2.py and *_pb2_grpc.py modules like any other Python code. On the connection, set "Stub module" to the name of the *_pb2_grpc module and, if the module has more than one service, "Stub class" to the stub class to use.

When the server generates the code, you can download it from Dashboard through the connection's "Stubs" link - the modules arrive as a zip file you can add to your IDE for code completion. The downloaded copy is for your editor only, at runtime the server always uses what it generated itself.

Unary calls

The example below registers a debit mandate - a single request message is sent and a single response is returned. The connection is called "Mandates" in Dashboard.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

# Generated Protocol Buffer messages
import debit_mandate_pb2

class RegisterMandate(Service):

    def handle(self):

        # Get the connection by the name configured in Dashboard
        client = self.grpc['Mandates']

        # Build the request message ..
        request = debit_mandate_pb2.MandateRegistrationRequest(
            debtor_name='John Doe',
            debtor_identity_number='123456789',
            bank_account_number='GB29NWBK60161331926819',
            collection_frequency=3,
        )

        # .. invoke the gRPC endpoint ..
        response = client.RegisterMandate(request)

        # .. and use the typed response.
        self.logger.info('Response: %s (%s)', response.response_code, response.response_description)

Server streaming

When the remote method streams its responses, the call returns a generator - iterate over it and each message is yielded as it arrives from the server, without ever being buffered into a list first.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

# Generated Protocol Buffer messages
import billing_pb2

class ListInvoices(Service):

    def handle(self):

        client = self.grpc['Billing']
        request = billing_pb2.InvoiceListRequest(max_items=100)

        # Each invoice is processed as soon as the server sends it
        for invoice in client.ListInvoices(request):
            self.logger.info('Invoice: %s -> %s', invoice.invoice_id, invoice.amount_cents)

Client streaming

When the remote method consumes a stream of requests, pass any iterable - a generator keeps the requests from existing in memory all at once.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

# Generated Protocol Buffer messages
import billing_pb2

class SubmitPayments(Service):

    def handle(self):

        client = self.grpc['Billing']

        def payments():
            for item in self.request.input.payments:
                yield billing_pb2.Payment(
                    invoice_id=item['invoice_id'],
                    amount_cents=item['amount_cents'],
                )

        # The server replies with a single summary once the stream ends
        summary = client.SubmitPayments(payments())

        self.logger.info('Accepted %s payments, %s cents in total', summary.payment_count, summary.total_cents)

Bidirectional streaming

Both patterns combined - send a generator of requests and iterate over the responses.

# -*- coding: utf-8 -*-

# Zato
from zato.server.service import Service

# Generated Protocol Buffer messages
import billing_pb2

class ReconcilePayments(Service):

    def handle(self):

        client = self.grpc['Billing']

        def payments():
            for item in self.request.input.payments:
                yield billing_pb2.Payment(
                    invoice_id=item['invoice_id'],
                    amount_cents=item['amount_cents'],
                )

        for receipt in client.ReconcilePayments(payments()):
            self.logger.info('Receipt: %s settled=%s', receipt.invoice_id, receipt.is_settled)

TLS

Connections use TLS by default. If the remote end's certificate is not signed by a well-known authority, point CA certs file at a PEM file with the certificates to verify it against. To use a plaintext connection, switch off TLS.

Security

A gRPC connection is secured by attaching a security definition to it, as with any other outgoing connection. Every invocation includes the credentials as gRPC call metadata.

The following security types are supported:

  • Basic Auth - sends the username and password in the standard authorization metadata key
  • API key - sends the key in the metadata header the definition names
  • OAuth - obtains a Bearer token from an OAuth endpoint, refreshes it as needed, and attaches it to each call

The connection attaches the credentials to every call - your service code stays the same regardless of the security mechanism.

Error handling

When a gRPC call fails, the error's status code is mapped to the matching Zato exception, with the details the server sent as the message. If your service lets such an exception bubble up to a REST channel, the channel returns the corresponding HTTP status to its own caller - a NOT_FOUND from the gRPC backend becomes a 404 in your REST API, not a generic 500.

gRPC status codeZato exceptionHTTP status
INVALID_ARGUMENTBadRequest400
UNAUTHENTICATEDUnauthorized401
PERMISSION_DENIEDForbidden403
NOT_FOUNDNotFound404
ALREADY_EXISTS, ABORTEDConflict409
RESOURCE_EXHAUSTEDTooManyRequests429
UNAVAILABLE, DEADLINE_EXCEEDEDServiceUnavailable503
Anything elseInternalServerError500

You can also catch the exceptions yourself - they are all importable from zato.common.exception:

# -*- coding: utf-8 -*-

# Zato
from zato.common.exception import NotFound
from zato.server.service import Service

# Generated Protocol Buffer messages
import billing_pb2

class GetInvoice(Service):

    def handle(self):

        client = self.grpc['Billing']
        request = billing_pb2.InvoiceRequest(invoice_id='inv-123')

        try:
            invoice = client.GetInvoice(request)
        except NotFound:
            self.logger.info('No such invoice')
        else:
            self.logger.info('Invoice: %s', invoice.customer)

Pings and test invocations

Each connection has a ping button in the Dashboard - it waits until the underlying channel is ready, confirming that the remote end is reachable. The Dashboard can also invoke unary methods directly - pick the method, provide the request as JSON and the response is shown as JSON too, so you can check a connection before any service code exists.

Define connections in YAML

Instead of filling out the Dashboard form, you can declare connections in YAML and import them with enmasse, the tool that automated deployments and version-controlled configuration rely on. Every field is listed in the enmasse reference.

outgoing_grpc:

  - name: Billing
    address: billing.example.com:50051
    proto_path: /opt/zato/proto/billing.proto

  - name: Mandates
    address: mandates.example.com:50051
    stub_module: debit_mandate_pb2_grpc
    security: mandates.oauth

See also

FeatureWhat it does
GraphQLQuery GraphQL endpoints when the system exposes GraphQL instead
REST outgoing connectionsCall external REST APIs with pools, timeouts and retries
Hot deploymentDeploy generated Protocol Buffer modules with no restarts
EnmasseDefine connections and security in YAML for automated deployments

Learn more