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 asself.out.crm- the SDK derives everything else from ithost,portandapi_keydeclare the configuration schema - each definition of this type has its own values andapi_key, being aField.Secret, is stored encryptedcreate_clientandpingare the two methods the platform requireson_stopis optional - it runs when a definition is deleted or edited, which is the moment to flush and close whatever the client keeps openget_customeris 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:
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:
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 throughon_stopand 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