Connector SDK tutorial

Wrap a Python client as a connection type that services call by name.

With the Connector SDK, you wrap a Python client library once and services use it as a named connection type, with its definitions managed centrally and its API key stored encrypted.

This page shows how to wrap a client for an internal CRM gateway - the protocol is one line of text per request and one line per response - and how to call it from a service through self.out.crm.

The connector module

The whole connector is one Python module - the client class and the connector class that wraps it. When the client comes from a library that is already installed, the module contains only the connector class.

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

# stdlib
import socket

# Zato
from zato.common.sdk import Connector, Field

class CRMClient:
    """ A client for a CRM gateway that uses a line protocol - each request is one line of text
    and the gateway answers with one line too. A new connection is opened per request, which makes
    the client safe to share between concurrent calls.
    """
    def __init__(self, host:'str', port:'int', api_key:'str') -> 'None':
        self.host = host
        self.port = port
        self.api_key = api_key

    def send(self, data:'str') -> 'str':

        # Connect for the duration of one request ..
        with socket.create_connection((self.host, self.port)) as conn:

            # .. send the request line, prefixed with the key the gateway expects ..
            request = f'{self.api_key} {data}\n'
            conn.sendall(request.encode('utf8'))

            # .. and read the response line back.
            with conn.makefile('r', encoding='utf8') as reader:
                response = reader.readline()

        return response.strip()

class CRMConnector(Connector):
    """ Wraps the CRM gateway's client as a connection type that services access through self.out.crm.
    """
    type = 'crm'

    # Configuration schema
    host = Field.Text()
    port = Field.Int(default=9950)
    api_key = Field.Secret()

    def create_client(self) -> 'CRMClient':
        return CRMClient(self.config.host, self.config.port, self.config.api_key)

    def ping(self, client:'CRMClient') -> 'None':
        client.send('ping')

    def on_stop(self, client:'CRMClient') -> 'None':
        self.logger.info('CRM client for `%s` stopped', self.name)

    def get_customer(self, customer_id:'str') -> 'str':
        return self.client.send(f'get-customer {customer_id}')

type = 'crm' is the one field every connector declares. The platform derives the rest from it - services access connections of this type through self.out.crm, the registered type name is outconn-crm and the enmasse key is custom_crm.

host, port and api_key declare the configuration schema. Each definition of the crm type carries its own values for them, and api_key, declared as Field.Secret, is stored encrypted.

create_client and ping are the two methods the platform requires. on_stop is optional - it runs when a definition is deleted or edited, which is where a client flushes its buffers and closes its sockets. This client opens a connection per request, so its on_stop only logs the stop.

Services call get_customer and any other invocation methods you add - one for each operation the remote system supports.

Deploy the connector

Copy the module to your hot-deployment directory, like any other Python code in Zato. The server registers the new type and logs the registration:

Registered connector type `outconn-crm` (CRMConnector)

Redeploying the module later updates the type in place - definitions that are already running keep working.

Create a definition

Create definitions of the new type in an enmasse file, under the custom_crm key - custom_ followed by the connector's type:

custom_crm:
  - name: My CRM
    host: 10.152.81.19
    port: 9950
    api_key: my-api-key

host, port and api_key are the fields the connector declared - every definition of the crm type includes them, and api_key is encrypted before it is stored.

Deploy the enmasse file like the rest of your configuration, as described in the DevOps blueprint. The connection starts as soon as the file is imported, without a server restart.

Call the connection from a service

Services reach the connection through self.out.crm, by the name the definition was created with:

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

# Zato
from zato.server.service import Service

class GetCustomer(Service):
    """ Returns one customer from the CRM gateway.
    """
    name = 'demo.crm.get-customer'

    input = 'customer_id'

    def handle(self) -> 'None':
        conn = self.out.crm['My CRM']
        response = conn.get_customer(self.request.input.customer_id)
        self.response.payload = response

self.out.crm['My CRM'] returns the connector instance, so conn.get_customer is the method the connector defined, with the client already built and configured underneath.

Lifecycle

Editing the definition - changing its fields in the YAML and deploying the file again - stops the current client through on_stop and builds a new one with the new configuration. Deleting the definition stops the client the same way and removes it from the server. After a server restart, the definition starts automatically once the connector module is deployed at boot.

The connector is complete - services call the CRM gateway through a named connection and the platform manages the client's lifecycle.

See also

PageWhat it covers
SDK referenceField types, lifecycle methods and the behavior of every invocation
Handshake protocols and pooled connectionsClients that log on first, pooled by the platform
Server push and subscriptionsDeliver pushed messages to services and topics