Zato services as containers for Python functions and methods

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 Python 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.