Acting as containers for enterprise APIs, Zato services are able to invoke each other to form higher-level processes and message flows. What if a service needs to invoke a hot-deployable Python function or method, though? Read on to learn details of how to accomplish it.

Background

Invoking another service is a simple matter - consider the two ones below. For simplicity, they are defined in the same Python module but they could be very well in different ones.


from zato.server.service import service

class MyAPI(Service):
    def handle(self):
        self.invoke('create.account', data={'username': 'my.user'})

class CreateAccount(Service):
    def handle(self):
        self.logger.info('I have been invoked with %s', self.request.raw_request)

MyAPI invokes CreateAccount by its name - the nice thing about it is that it is possible to apply additional conditions to such invocations, e.g. CreateAccount can be rate-limited.

On the other hand, invoking another service means simply executing a Python function call, an instance of the other service is created (a Python class instance) and then its handle method is invoked with all the request data and metadata available through various self attributes, e.g. self.request.http, self.request.input and similar.

This also means that at times it is convenient not to have to write a whole Python class, however simple, only to invoke it with some parameters. This is what the next section us be about.

Invoking Pythom methods

Let us change the Python code slightly.


from zato.server.service import service

class MyAPI(Service):
    def handle(self):
        instance = self.new_instance('customer.account')

        response1 = instance.create_account()
        response2 = instance.delete_account()
        response3 = instance.update_account()

class CustomerAccount(Service):

    def create_account(self):
        pass

    def delete_account(self):
        pass

    def update_account(self):
        pass

There are still two services but the second one can be effectively treated as a container for regular Python methods and functions. It no longer has its handle method defined, though there is nothing preventing it from doing so if required, and all it really has is three Python functions (methods).

Note, however, a fundamental difference with regards to how many services are needed to implement a fuller API.

Previously, a service was needed for each action to do with customer accounts, e.g. CreateAccount, DeleteAccount, UpdateAccount etc. Now, there is only one service, called CustomerAccount, and other services invoke its individual methods.

Such a multi-method service can be hot-deployed like any other which makes it a great way to group utility-like functionality in one place - such functions tend to be reused in many places, usually all over the code, so it is good to be able to update them at ease.

Code completion

There is still one aspect to remember about - when a new instance is created through new_instance, we would like to be able to have code auto-completion available.

If the other service is in the same Python module, it suffices to use this:

instance = self.new_instance('customer.account') # type: CustomerAccount

However, if the service whose methods are to be invoked is in a different Python module, we need to import for its name to be know to one's IDE. Yet, we do not really want to import it, we just need its name.

Hence, we guard the import with an if statement that never runs:

if 0:
    from my.api import CustomerAccount

class MyAPI(Service):
    def handle(self):
        instance = self.new_instance('customer.account') # type: CustomerAccount

Now, everything is ready - you can hot-deploy services with arbitrary functions and invoke them like any other Python function or method, including having access to code-completion in your IDE.

A recurring need in larger integration projects is generation of API documentation for users belonging to different, yet related, target groups. Read on to learn how to generate Zato-based API specifications for more than one group from a single source of information.

A typical scenario is granting access to the same APIs to external and internal users - what they have in common is that all of them may want to access the same APIs yet not all of them should have access to documentation on the same level of details.

For instance - external developers should only know what a given endpoint is for and how to use it but internal ones may also be given information about its inner workings, the kind of details that external users should never learn about.

If documentation for two such groups is kept separately, it may require more maintenance effort than necessary - after all, if most of it is the same for everyone then it would make sense to keep it in one place and only add or remove details, depending on which group particular API documentation needs to be generated for.

The previous article went through the process of generating API specifications step by step - this one goes even further by adding introducing the notion of tags that drive which parts of documentation to generate or not.

Python docstrings

Let us use one of the same services as previously - here is its basic form:

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

# Zato
from zato.server.service import Int, Service

class RechargeCard(Service):
    """ Recharges a pre-paid card.
    Amount must not be less than 1 and it cannot be greater than 10000.
    """

    class SimpleIO:
        input_required = 'number', Int('amount')
        output_required = Int('status')

The result, when generated as Sphinx, will be like below:

Now, we would like to add some details that only internal users should have access to - this is how the docstring should be modified, assuming for a moment that there are two CRM systems in the company the usage of which depends on a particular end user's account ..

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

# Zato
from zato.server.service import Int, Service

class RechargeCard(Service):
    """ Recharges a pre-paid card.
    Amount must not be less than 1 and it cannot be greater than 10000.

    #internal

    For user accounts starting with QM - MyCRM is queried.
    For any other account - MyCRM-2 is queried.
    """

    class SimpleIO:
        input_required = 'number', Int('amount')
        output_required = Int('status')

.. with the corresponding change in the output:

What happened above was that we applied a tag called #internal - anything that follows it is included in the output only if that very tag is requested when generating the documentation.

Note that the tag name is arbitrary, it could be any other name. Note also that there may be more than tag in a docstring, e.g. #confidential, #private or anything else.

Command-line usage

To actually include the #internal tag, the zato apispec command needs to be told about it explicitly, as below:

$ zato apispec /path/to/server \
  --dir /path/to/output/directory \
  --include api.* \
  --tags public,internal

If you do not give the command any tags, only the public part of the docstring, one without any tags, will be included in the resulting documentation.

To make your clear to other developers, you can also directly use the tag public in the command - this will make it easier to understand that you want to generate publicly available information and nothing else.

$ zato apispec /path/to/server \
  --dir /path/to/output/directory \
  --include api.* \
  --tags public

Wrapping up

This is it, you are done now - you can keep your API documentation in one place now and include only the relevant parts of it depending on context - this ensures that no matter who the recipient of your documentation is, it will be always generated from a single source, thus ensuring that all the output will stay in sync.

In addition to a GUI, Python and REST APIs, it is now possible to access your Zato caches from command line. Learn from this article how to quickly check, set and delete keys in this way - particularly useful for remote SSH connections to Zato environments.

Prerequisites

This functionality will be released in Zato 3.2 (June 2020) - right now, if you would like to use it, Zato needs to be installed from source.

In web-admin

First, let us create a couple of new keys in the GUI - my.key and my.key2 - to work with them later on from command line.

Command line

Now, we can get, set and delete the keys using the CLI. Observe the below and notice that set and delete commands not only carry out what they ought to but they also return the previous value of a given key.

$ zato cache get my.key --path /path/to/server1 ; echo
{"value": "my.value"}
$
$ zato cache get my.key2 --path /path/to/server1 ; echo
{"value": "my.value2"}
$
$ zato cache set my.key my.new.value --path /path/to/server1 ; echo
{"prev_value": "my.value"}
$
$ zato cache delete my.key2 --path /path/to/server1 ; echo
{"prev_value": "my.value2"}
$ zato cache set my.key3 my.value3 --path /path/to/server1 ; echo
{}
$

Back to web-admin

The last command created a new key - we can confirm its existence in web-admin:

Summary

That it is all - as simple as possible, just log in to an SSH server, point your command line to Zato and you can access your caches right away.

This article presents a workflow for auto-generation of API specifications for your Zato services - if you need to share your APIs with partners, external or internal, this is how it can be done.

Sample services

Let's consider the services below - they represent a subset of a hypothetical API of a telecommunication company. In this case, they are to do with pre-paid cards. Deploy them on your servers in a module called api.py.

Note that their implementation is omitted, we only deal with their I/O, as it is expressed using SimpleIO.

What we would like to have, and what we will achieve here, is a website with static HTML describing the services in terms of a formal API specification.

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

# Zato
from zato.server.service import Int, Service

# #####################################################################

class RechargeCard(Service):
    """ Recharges a pre-paid card.
    Amount must not be less than 1 and it cannot be greater than 10000.
    """
    class SimpleIO:
        input_required = 'number', Int('amount')
        output_required = Int('status')

    def handle(self):
        pass

# #####################################################################

class GetCurrentBalance(Service):
    """ Returns current balance of a pre-paid card.
    """
    class SimpleIO:
        input_required = Int('number')
        output_required = Int('status')
        output_optional = 'balance'

    def handle(self):
        pass

# #####################################################################

Docstrings and SimpleIO

In the sample services, observe that:

  • Documentation is added as docstrings - this is something that services, being simply Python classes, will have anyway

  • One of the services has a multi-line docstring whereas the other one's is single-line, this will be of significance later on

  • SimpleIO definitions use both string types and integers

Command line usage

To generate API specifications, command zato apispec is used. This is part of the CLI that Zato ships with.

Typically, only well-chosen services should be documented publicly, and the main two options the command has are --include and --exclude.

Both accept a comma-separated list of shell-like glob patterns that indicate which services should or should not be documented.

For instance, if the code above is saved in api.py, the command to output their API specification is:

zato apispec /path/to/server        \
    --dir /path/to/output/directory \
    --include api.*

Next, we can navigate to the directory just created and type the command below to build HTML.

cd /path/to/output/directory
make html

OpenAPI, WSDL and Sphinx

The result of the commands is as below - OpenAPI and WSDL files are in the menu column to the left.

Also, note that in the main index only the very first line of a docstring is used but upon opening a sub-page for each service its full docstring is used.

Branding and customisation

While the result is self-contained and it can be already used as-is, there is still room for more.

Given that the output is generated using Sphinx, it is possible to customise it as needed, for instance, by applying custom CSS or other branding information, such as the logo of a company exposing a given API.

All of the files used for generation of HTML are stored in config directories of each server - if the path to a server is /path/to/server then the full path to Sphinx templates is in /path/to/server/config/repo/static/sphinxdoc/apispec.

Summary

That is everything - generating static documentation is a matter of just a single command. The output can be fully customised while the resulting OpenAPI and WSDL artifacts can be given to partners to let third-parties automatically generate API clients for your Zato services.

If you need to configure Zato for Oracle DB connections and you want to ensure the highest performance possible, this is the post which goes through the process step-by-step. Read on for details.

Overview

Note that Zato 3.1+ is required. The tasks involved are:

  • Installing Zato
  • Installing an Oracle client
  • Installing cx_Oracle
  • Greenifying the Oracle client
  • Starting servers
  • Creating database connection definitions
  • Confirming the installation

Installing Zato

  • Choose your preferred operating system and follow the general installation instructions - Oracle DB connections will work the same no matter the system Zato runs on

  • Create a Zato environment with as many servers as required. Again, there are no Oracle-specific steps at this point yet.

Installing an Oracle client

  • Download an Oracle client and install it on all the systems with Zato servers

  • Add the client's installation path to LD_LIBRARY_PATH. For instance, if the client is installed to /opt/zato/instantclient_19_3, add the following to ~/.bashrc:

    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/zato/instantclient_19_3

Installing cx_Oracle

  • cx_Oracle is a Python driver for Oracle DB, it can be installed using pip, as user zato, on each Linux instance that Zato servers use:

    $ cd /opt/zato/current
    $ ./code/bin/pip install cx_Oracle
    

Greenifying the Oracle client

  • This step is crucial for achieving the highest performance of SQL queries

  • For each Zato server installed, open its server.conf file and find stanza [greenify]

  • If there is no such stanza in server.conf, create it yourself

  • Modify the stanza to greenify the libclntsh.so library - this is a library that the Oracle client ships with. For instance, if the client's installation path is /opt/zato/instantclient_19_3, the full path to the library is /opt/zato/instantclient_19_3/libclntsh.so

  • The stanza should read as below, assuming the installation path as above:

    [greenify]
    /opt/zato/instantclient_19_3/libclntsh.so=True
    
  • Note that entries in this stanza are followed by =True, which is a signal to Zato that a particular library should be processed

Starting servers

  • Start Zato servers
  • For each, make sure that an entry such as below is saved in server logs

    INFO - Greenified library `/opt/zato/instantclient_19_3/libclntsh.so.19.1`
    

Creating database connection definitions

  • Go to web-admin and create a new Oracle DB connection via Connections -> Outgoing -> SQL, as below:


Create a new SQL connection

  • Update the newly created connection's password by clicking Change password

  • Click Ping to confirm the connectivity to the remote server

  • This concludes the process, the connection is ready for use in Zato services now