One of the exciting additions of the upcoming Zato 3.2 release is the ability to invoke services through OpenAPI endpoints without a need for creation of REST channels explicitly - read more for details.

Python code

Supposing we have a service such as below - note that it is uses SimpleIO and that each of its input/output attributes is documented in the docstring - we would like to invoke it from an external application while delegating to Zato as much effort involved in it as possible.

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

# Zato
from zato.server.service import Service

class GetUserAccount(Service):
    """ Returns user account details based on user ID and type.
    """
    name = 'api.get.user.account'

    class SimpleIO:
        """
        * account_id - ID of the user account to return
        * user_type - Type of the user the account belongs to

        * user_name - Name of the user to whom account_id belongs
        * needs_review - A flag indicating whether the account needs to be reviewed
        """
        input_required = 'account_id', 'user_type'
        output_required = 'account_name', 'needs_review'

    def handle(self):

        # Local aliases
        account_id = self.request.input.account_id
        user_type  = self.request.input.user_type

        # In reality, additional work would be carried out here first ..
        self.logger.info('Returning data for `%s` (%s)', account_id, user_type )

        # .. produce the output now.
        self.response.payload.user_name = 'my.user'
        self.response.payload.needs_review = True

REST channels

We will not pursue this path in this article but, in other circumstances, we could just create a REST channel for the service. This is great and it works perfectly fine, always letting one adjust many options, such as caching or rate-limiting.

Creating a REST channel

However, today, we will auto-create API definitions using OpenAPI.

Creating OpenAPI specifications

Documentation, including OpenAPI specifications, can be created from the command line, as below:

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

What we will find in the output directory is a Sphinx project with human-readable documentation, which is nice in itself, along with formal specifications, including OpenAPI.

Sphinx documentation

Sphinx documentation

OpenAPI download

Inspecting the OpenAPI specification

Upon downloading the openapi.yaml file we can confirm that it is a formal specification of the APIs exposed - including request and response schemas as well as endpoint addresses.

Note that, because we are using SimpleIO, the specification includes description of parameters and the parameters have expected data types, e.g. account_id is an integer while needs_review is a boolean.

components:
  schemas:
    request_api_get_user_account:
      properties:
        account_id:
          description: ID of the user account to return
          format: int32
          type: integer
        user_type:
          description: Type of the user the account belongs to
          format: string
          type: string
      required:
      - account_id
      - user_type
      title: Request object for api.get.user.account
      type: object
    response_api_get_user_account:
      properties:
        needs_review:
          description: A flag indicating whether the account needs to be reviewed
          format: boolean
          type: boolean
        user_name:
          description: Name of the user to whom account_id belongs
          format: string
          type: string
      required:
      - user_name
      - needs_review
      title: Response object for api.get.user.account
      type: object
paths:
  /zato/api/invoke/api.get.user.account:
    post:
      consumes:
      - application/json
      operationId: post__zato_api_invoke_api_get_user_account
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/request_api_get_user_account'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/response_api_get_user_account'
info:
  title: API spec
  version: '1.0'
openapi: 3.0.2
servers:
- url: http://localhost:11223

Using OpenAPI

With the specification downloaded, we can start to invoke our service immediately - in this case, we will use Postman. Essentially, import a definition, choose the desired service, provide input data and click the "Send" button.

Afterwards, we can also confirm in Zato server logs that the request was processed correctly.

Naturally, Postman is just a sample client utilising the OpenAPI specification - clients in other languages can be easily used as well, including Python, Java, .NET, JavaScript and more.

Postman - import OpenAPI

Postman - select an endpoint

Postman - invoke an endpoint

Zato logs

Even more protocols

The above suffices for external clients to invoke your services purely based on their Python definitions with endpoints, documentation and specifications auto-generated by Zato.

But there is more. Have you noticed that nothing in the service used is specific to REST, OpenAPI or HTTP for that matter?

That is one of the benefits of using Zato - such a service is completely reusable across a range of other protocols, which means that it can exposed as-is through other channels, e.g. IBM MQ, AMQP, SOAP, WebSockets, Publish/Subscribe topics, Scheduler, File transfer and many more.

Be sure to check the Zato documentation for more details!

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.