Modifying The Code

Repository Organization

The Repository has four sub-directories of note:

  • docs - Contains the project documentation (you know, this)

  • pyzohoapi - Contains the module code

  • tests - Contains the unit and functional tests

  • tools - Contains tools to help in development

    • interactive-test-server.py is a simple web interface for interrogating the Zoho APIs

    • test-shell.py starts up a python REPL shell with the API Objects preloaded

Code Style

Generally, we adhere to the PEP8 Code Style. However, as noted elsewhere in these docs, our public interface (properties and methods) for “ZohoObjects” use CamelCase rather than underscore_separated_words, to avoid possible conflict with Zoho-surfaced object fields. This should only apply to ZohoObjects; all others should follow PEP8.

In short:

  • Indention is 4 spaces, not tabs

  • method_names_look_like_this except for in ZohoObject classes

  • ClassNamesLookLikeThis

Classes

ZohoObject classes represent particular objects exposed by the API.

ZohoAPI classes connect to particular API endpoints.

Adding new ZohoObject Classes

We’ll use Zoho Inventory API as an example in this section.

Note

Unfortunately, the official Zoho API Docs aren’t always accurate. When in doubt, use any of the various API client tools available to test the API directly. See our interactive tools.

Examine the API Request and Response

Let’s consider Zoho Composite Items:

We see that to retrieve a list of Composite Items, we call:

get /compositeitems

The JSON we get back looks like:

{
    "code": 0,
    "message": "success",
    "composite_items": [
        {
            "composite_item_id": "9999999000001049029",

If we retrive a particular Composite Item with:

get /compositeitems/9999999000001049029

we’ll get:

{
    "code": 0,
    "message": "success",
    "composite_item": {
        "composite_item_id": "9999999000001049029",

So we see that:

  • The object type (as defined by Zoho) is “Composite Items”.

  • The Python class name should be CompositeItem (singular).

  • The URL fragment is compositeitems (plural). This is the pluralized, lowercase version of the class name.

  • The “list of Composite Items” key is composite_items (plural). This is different from the class name.

  • The “single Composite Item” key is composite_item (singular). This is different from the class name.

  • The key for the unique ID of each Composite Item is composite_item_id. This is different from the class name.

Further inspection of the API docs indicate that, in addition the usual Create, Retrieve, Update, Delete and List All operations, we can also perform Mark as Active and Mark as Inactive operations.

Add the class

ZohoObject classes are created by the ZohoObjectFactory() function in pyzohoapi/objecttypes/__init__.py. In this case, we use:

CompositeItem = ZohoObjectFactory("CompositeItem",
    responseKey="composite_item", idKey="composite_item_id",
    mixins=[HasActivate, HasBundle, HasCustomFields])
  • “CompositeItem” is the Python class name exposed by pyzohoapi.objecttypes.

  • “CompositeItem” will be pluralized and lowercased to create the URL fragment. If we needed to use a different URL, we’d use the urlPath parameter to override the default.

  • responseKey=”composite_item”… defines the keys (singular and plural) of the objects in the JSON response data. This is needed because the corresponding JSON key is not the same as the class name.

  • idKey=”composite_item_id” defines the root of the key used to determine the ID of the object. This is needed because the corresponding JSON key is not the same as the class name.

  • mixins=[HasActivate] adds the Activate() and Deactivate() operations by way of a mixin in pyzohoapi.objecttypes.mixins; keep reading for how that works.

Extending ZohoObject Classes

The pyzohoapi.objecttypes.mixins module contains classes which expose one or more additional methods to add to particular object types. In the example above, we’ve mixed in HasActivate class, which adds the Activate() and Deactive() methods to the CompositeItem class. See Type-Specific Methods for the breakdown of the existing methods.

Examine the API Docs

We’ll look at the existing mixin HasActivate as an example.

Looking at the API Docs, we see that there are several different Zoho objects which support the “Mark as Active” and “Mark as Inactive” operations. Every object either supports both or neither of these operations.

We also see that the method for performing these operations is of the form:

post /{object-url-fragment}/{object-id}/{active|inactive}

These factors suggest we create a mixin class which implements a method handling the post to Zoho, and methods to expose both operations. This class, then, can be applied to the appropriate object types.

Create a Mixin Class

class HasActivate:
    ...

The pattern for the class name is Has{Feature}.

Create Internal Method(s)

class HasActivate:
    def _do_operation(self, status, funcname):
        ...

This is optional, but since in this case there are two, basically identical, operations we want to expose, we’ll build an internal method to actually perform the operation.

Internal method names should begin with _, both to indicate they are “private” and to avoid collision with Zoho object field names.

In this case, we need the new status (‘active’ or ‘inactive’, per the API docs), and the name of the function being called (for exceptions).

Ensure the Operation is Valid

class HasActivate:
    def _do_operation(self, status, funcname):
        if self._id and self._data:
            ...
        raise ZohoInvalidOpError(funcname, self)

This operation is only valid on single objects (they have an _id) and which already exist (they have _data). If those conditions aren’t met, we’ll raise a ZohoInvalidOpError.

Perform the API Call

class HasActivate:
    def _do_operation(self, status, funcname):
      if self._id and self._data:
          try:
              self._api.post(self._url_fragment(extraPath=[status]))
              self.status = status
              return True
          except ZohoException as e:
              return False
      raise ZohoInvalidOpError(funcname, self)

In order to post to the API, we use the API object’s post() method. We have to tell post() where to post to, which is what our _url_fragment() functions does. It constructs the object-specific portion of the eventual URL with our object type (i.e. /compositeitems), our ID (if appropriate), and adds anything in the extraPath parameter. The API object takes care of the https://{whatever}.zoho.{whatever}.

In this case, if post() raises an exception, we suppress it and return False.

Create Public Method(s)

class HasActivate:
    ...
    def Activate(self):
        return self._do_operation('active', "Activate")

    def Deactivate(self):
        return self._do_operation('inactive', "Deactivate")

Public Method names should be CamelCase, for reasons noted elsewhere. Parameters are operation-specific.

Adding a New API Object

Create a Module for the API

See pyzohoapi/inventory.py as an example.

Define the Class

Inherit from ZohoAPIBase.

from .core import ZohoAPIBase
...
class ZohoInventory(ZohoAPIBase):

Set the OAuth Scope

from .core import ZohoAPIBase
...
class ZohoInventory(ZohoAPIBase):
    _scope = "ZohoInventory.FullAccess.all"

(Optional) Determine Available Regions

Override the _regionmap member if the API isn’t available in every Zoho data center. See the code for ZohoAPIBase for a guidance.

Write get_endpoint()

The get_endpoint() method returns the endpoint of the api.

from .core import ZohoAPIBase
...
class ZohoInventory(ZohoAPIBase):
    _scope = "ZohoInventory.FullAccess.all"

    def get_endpoint(self, region):
        return f"https://inventory.zoho.{self._regionmap[region]}/api/v1"

Expose Available ZohoObjects

from .core import ZohoAPIBase
from . import objecttypes
...
class ZohoInventory(ZohoAPIBase):
    _scope = "ZohoInventory.FullAccess.all"

    def get_endpoint(self, region):
        return f"https://inventory.zoho.{self._regionmap[region]}/api/v1"
    ...
    def Account(self, *args, **kwargs): return objecttypes.Account(self, *args, **kwargs)
    def Bundle(self, *args, **kwargs): return objecttypes.Bundle(self, *args, **kwargs)
    ...

Expose the module

In pyzohoapi.__init__.py, import the module and add it to the __all__ list.

from .inventory import ZohoInventory
__all__ = ["ZohoInventory", ...]