Connector SDK tutorial

One complete connector, step by step - an internal CRM client wrapped as a first-class connection type.

This tutorial builds one complete connector with the Connector SDK. It wraps an internal CRM gateway that speaks its own protocol - you send it one line of text and it answers with one line.

By the end, the client is a first-class connection type - its definitions are managed centrally, its API key is stored encrypted, and services use it through self.out.crm, by name.

The connector module

The whole connector is one Python module. It contains the client itself and the connector class that wraps it - if the client comes from a library that is already installed, the module contains just 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 speaks 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}')

In the code above:

  • type = 'crm' is what makes services access these connections as self.out.crm - the SDK derives everything else from it
  • host, port and api_key declare the configuration schema - each definition of this type has its own values and api_key, being a 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 the moment to flush and close whatever the client keeps open
  • get_customer is an invocation method - the platform never calls it, services do, and you add as many of these as your protocol needs

Deploying the connector

The module is deployed like any other Python code in Zato - drop it into your hot-deployment directory and the server picks it up:

Registered connector type `outconn-crm` (CRMConnector)

From this moment the server knows the outconn-crm type. Redeploying the module later updates the type in place - definitions that are already running keep working.

Creating a definition

Definitions of the new type are managed by the same API that manages all connections. Create one by invoking zato.generic.connection.create:

$ curl http://admin.invoke:<password>@localhost:17010/zato/api/invoke/zato.generic.connection.create -d '{
    "name": "My CRM",
    "type_": "outconn-crm",
    "is_active": true,
    "is_internal": false,
    "is_channel": false,
    "is_outconn": true,
    "host": "10.152.81.19",
    "port": 9950,
    "api_key": "my-api-key"
  }'

The response contains the definition's ID:

{"id": 123, "name": "My CRM"}

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

To confirm the connection works, ping it by its ID:

$ curl http://admin.invoke:<password>@localhost:17010/zato/api/invoke/zato.generic.connection.ping -d '{"id": 123}'

This runs the connector's ping method against the live client.

Using it from services

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

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

The lifecycle from here on

  • Editing the definition - through zato.generic.connection.edit - stops the current client through on_stop and builds a new one with the new configuration
  • Deleting it - through zato.generic.connection.delete - stops the client and removes the definition
  • After a server restart, the definition starts automatically once the connector module is deployed at boot

Learn more