SDK reference
The Connector SDK contract - fields, lifecycle methods, pools, subscriptions and helper processes.
Zato ships with dozens of connection types out of the box, and the Connector SDK is how you add your own.
Do you have a client library that you would like to use as a first-class connection - an internal library your company maintains, a vendor's Python package, or a client for a custom protocol that only your systems speak? With the SDK, you wrap it once and, from then on, it works like any built-in connection type:
- Connections are created, edited, deleted and pinged centrally, by name
- Secrets are stored encrypted and never returned in listings
- Services access connections through
self.out, the same way they access everything else - The connector is a regular Python module that you hot-deploy - no server restarts and no changes to the platform itself
The contract
A connector is a subclass of Connector from zato.common.sdk. You give it a type name, declare its configuration fields and implement two methods - everything else is optional.
# -*- coding: utf-8 -*-
# Zato
from zato.common.sdk import Connector, Field
class MyConnector(Connector):
# Services will access connections of this type as self.out.my_type
type = 'my_type'
# Configuration schema
host = Field.Text()
port = Field.Int(default=9950)
api_key = Field.Secret()
def create_client(self):
# Build and return the underlying client object
...
def ping(self, client):
# Confirm the connection works, raise an exception if it does not
...
Configuration fields
Fields are class attributes declared with Field types. Their values are stored with each connection definition and delivered to the connector, already resolved, through self.config.
| Field type | Description |
|---|---|
Field.Text | A text field |
Field.Int | An integer field |
Field.Bool | A boolean field |
Field.Secret | A secret - stored encrypted and never returned when definitions are read |
Each field can declare a default, e.g. Field.Int(default=9950), which applies when a definition does not provide the value.
Methods the framework calls
| Method | Required | Description |
|---|---|---|
create_client | Yes | Builds and returns the underlying client object, called when the connection starts |
ping | Yes | Confirms the connection is usable, called when the connection is pinged |
on_stop | No | Closes the client, called when the definition is deleted or edited |
validate | No | Checks the client is still usable before each use - the default calls ping, and an exception makes the platform evict the client and reconnect |
refresh_credentials | No | Renews expired credentials - called when an invocation raises CredentialsExpired, after which the invocation is retried once |
start_process | - | Provided by the framework, never overridden - starts and supervises a helper process |
Invocation methods - get_customer, send, query, whatever your protocol calls for - are plain methods you add freely. The framework never calls them, your services do.
What every invocation gets
When a service calls an invocation method, the platform wraps the call with these behaviors:
- A watchdog timeout - the definition's
timeoutfield caps how long any call may take, and a single call can override it:conn.get_customer('C1', timeout=5) - Validation before use -
validateruns first and a client that fails it is evicted and rebuilt before the call proceeds. Pooled connections validate at checkout instead. - Credential refresh - an invocation that raises
CredentialsExpiredtriggersrefresh_credentialsand is retried once - Reconnect on loss - an invocation that raises
ConnectionLostmakes the platform rebuild the connection in the background, with backoff, while the caller gets the exception
Exceptions
zato.common.sdk defines two exceptions your client code raises to tell the platform what went wrong:
| Exception | Meaning and what the platform does |
|---|---|
ConnectionLost | The connection is gone - the client is evicted and rebuilt with backoff |
CredentialsExpired | The credentials expired - refresh_credentials runs and the call is retried once |
Pooled connections
When connections hold per-connection state - a logon handshake followed by a conversation, one call at a time - subclass PooledConnector instead of Connector. The platform owns a pool of connections, create_client builds one connection each time the pool grows, and invocation methods borrow one per call through get_connection:
# -*- coding: utf-8 -*-
# Zato
from zato.common.sdk import Field, PooledConnector
class MyConnector(PooledConnector):
type = 'my_type'
def create_client(self):
# Build and return one connection - the pool calls this as it grows
...
def send_command(self, command):
# Borrow a connection for the duration of the with block
with self.get_connection() as conn:
return conn.send(command)
PooledConnector adds these to the contract:
| Method | Required | Description |
|---|---|---|
get_connection | - | Provided by the framework, never overridden - borrows a connection for a with block |
on_get_from_pool | No | Resets conversational state, runs each time a connection is handed out |
on_return_to_pool | No | Cleans up, runs each time a with block using get_connection ends |
A definition's pool_size field caps how many connections its pool holds and connections are built lazily, as the load requires them. The handshake protocols page has a complete, worked example.
Subscribing connections
When the remote side pushes messages on its own - feeds, event streams - subclass SubscribingConnector. It adds one hook to the contract:
| Method | Required | Description |
|---|---|---|
on_started | No | Subscribes and replays state - runs when the connection first starts and again after every reconnect |
The platform watches subscribing connections in the background and, when one goes down, reconnects with backoff and re-runs on_started - so a subscription survives outages without any code beyond the hook itself. The server push page has a complete, worked example.
Helper processes
start_process runs a component next to the server - a Java jar, a .NET assembly, a native binary or a Python module in an interpreter of its own:
def create_client(self):
process = self.start_process(['java', '-jar', self.config.jar_path, '{port}'])
return MyClient('127.0.0.1', process.port)
Any {port} placeholder in the command is replaced with a local port allocated for the process. The returned Process object has these members:
| Member | Description |
|---|---|
pid | The process ID |
port | The local port allocated for the process |
is_running | Whether the process is still alive |
stop | Stops the process - first politely, then by force |
The process is supervised - if it dies unexpectedly, the platform rebuilds the whole connection with backoff, re-running create_client, and the process always stops together with the connection it belongs to. The foreign runtimes page has a complete, worked example.
Ambient attributes
Every connector instance has these attributes available:
| Attribute | Description |
|---|---|
self.name | The name of the connection definition this instance serves |
self.config | The resolved values of the fields the class declares |
self.client | The object create_client returned, once the connection has started |
self.logger | A logger writing to Zato server logs |
self.invoke | Hands a message over to a service, matching self.invoke in services |
self.publish | Publishes a message to a pub/sub topic, matching self.publish in services |
Many credential sets, one definition
When one definition serves many tenants, each with credentials of their own, services resolve them at runtime with with_config - the platform keeps one client per distinct set of overrides and stops the ones that sit idle for too long:
conn = self.out.crm['My CRM'].with_config(api_key=tenant_api_key)
response = conn.get_customer(customer_id)
Each distinct set of overrides gets a client of its own, built through create_client with the overridden values in self.config, and repeated calls with the same overrides reuse it.
What happens at runtime
When your module is deployed, the platform finds the Connector subclasses in it and registers each one as a new connection type. From that moment:
- Definitions of the type can be created, edited, deleted and pinged like any other connection
- Each definition is served by one connector instance with one client, shared by all the services on a server
- Editing a definition stops the old client through
on_stopand builds a new one with the new configuration - Redeploying the module updates the type in place and the definitions that are already running keep working
- After a server restart, definitions start automatically as soon as the module is deployed again at boot