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 call shapes 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 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.

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 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 "Download 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 goes out and a single response comes back. 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.raw_request['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.raw_request['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 TLS CA certs file at a PEM file with the certificates to verify it against. To use a plaintext connection, uncheck the TLS option.

Security

gRPC connections are secured the same way as all other outgoing connections - by attaching a security definition. The credentials travel as gRPC call metadata with every invocation.

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 turned into 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, which is what automated deployments and version-controlled configuration use. 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