Skip to content

Plugins

Nicklas Börjesson edited this page Jun 10, 2016 · 59 revisions

(For information on plugins to the admin user interface, see the relevant admin-ui docs )

#About

Why

Plugins aren't only to make the system extendable and customizeable. In OF, it has further purposes:

  • Stability: It enables the system to fail more gracefully
    If, for example, a plugin raises unexpected errors, it and its dependent plugins will not be loaded further.
    This enables the core system to start anyway and provide information and ways to solve the problem.
  • Structure: It contributes a measure of order and transparency
    The way plugins are implemented in OF, it is easy to know what is part of the plugin and part of the framework.
    The plugin is also left to do things more its own way, OF only provides services, it doesn't force implementations to be made a certain way.
  • Simplicity: Implementations become easier
    A plugin with a hook-based architecture is very easy to write, it lower the barrier of entry significantly

How

The OF backend manages its plugins centrally, in the Plugins class. The Plugin class is responsible for handling all aspects of the plugins, like loading, initialisation and control. When the broker starts, it walks through all plugins in the plugins folder an collects all necessary information from their definitions.json-files.

What

Plugins can add many kinds of things to the OF backend, like functionality, schemas, and even inject and change variables. Also, and because it is Python, the system can change significantly in run time.

First, however, we will learn how to create a plugin, but then delve into extending the API, which is likely to be a primary goal of any OF based development. Thankfully, that it pretty easy:

Creating the plugin

These are the steps:

  • Create a folder in the plugins directory with the name of the plugin.

Actually that was just one step. And that is actually true. A plugin in OF is a folder in the plugins folder. From now on, OF will now look for more information about the plugin in the folder.

definitions.json

More information is provided using a file called definitions.json. On initialisation, the definitions file of each plugin is cumulatively added to a the main definition structure. This means that a plugin can add to namespaces, or override settings made by other plugins.

Therefore, the structure will turn out like this:

{
  "plugins": {
    "example": {
      "description": "Our example plugin",
      "dependencies": {},
      "failOnError": true,
      "admin-ui": {
        "mountpoint": "example",
        "menus": [
          {
            "caption": "EXAMPLE",
            "weight": 0.4,
            "path": "control",
            "items": [],
            "right": "Administer"
          }
        ]
      },
      "hooks_module": "hooks_broker"
    }
  },
  "namespaces": {
    "example": {
      "description": "Example namespace",
      "title": "example"
    }
  }
}

There are two main parts, plugins and namespaces: Plugins Contains a list of the plugins that is to be configured, usually, this is only ones own.

Namespaces If a plugin want to add a namespaces to the system, this is where to do it. Namespaces are used extensively in the system to group definitions and functionality . For example, schemas in OF have a namespace property

Extending the API

The base of the Optimal Framework is the very intuitive CherryPy web framework, and with CherryPy, the class structure becomes the backend structure.

So, since:

  1. The CherryPyBroker class, called web_root, is mounted at "/"
  2. The CherryPyNode class has been set as the node property of the CherryPyBroker class
  3. save is a function in the CherryPyNode class

...like this:

--web_root (CherryPyBroker instance)
  --node(CherryPyNode instance)
    --save(Function)

...save ends up being published as /node/save.

Setting a mounted class's property value to a class instance effectively mounts that class there and publishes it. And to that class you can in turn add properties holding other classes, building a /property/property/property/function site structure.

And as this can be done dynamically, there are great possibilities, only limited by the architectural limits of Python, of which there are very few.

This is given that the class has functions with the @cherrypy.expose decorator, of course.

So to extend the backend API with ones own functionality, one simply declare one or more classes, add some decorators and then mount instances somewhere in the site structure. We look at mounting later, but first, how does such a class look?

Class declarations

As said before, OF is built on CherryPy. So adding stuff is just creating classes with functions that has the @cherrypy.expose decorator. So not only is the structure intuitive, creating such a class is easy.

Simple example

This example is a fully function working API extension class, it simply returns a hello-world string:

# First, import the cherrypy library, it is needed later
import cherrypy

class MyAPI(object):

    # This example doesn't take any data, it just returns the hello-string.
    @cherrypy.expose
    def hello_world(self, **kwargs):
        """
        Returns a hard coded hello-world-string
        :param kwargs: The query string as keyword arguments. When &my_param=1, kwargs["my_query_param"] holds "1"
        :return: A string with the hard coded message
        """
        return "HELLO OPTIMAL WORLD!"

More real-life example

No offence to the simple example, but usually, one want to use JSON and have authentication and rights checks, so here is a more realistic example. It takes a JSON structure, takes the value of the "string" attribute, makes it uppercase and returns it in a JSON structure:

# First, import the cherrypy library, it is needed later
import cherrypy
# Then import the web session cookie checking decorator
from of.broker.cherrypy_api.authentication import aop_check_session

# We need to be able to check if a user belongs to a group with the right to even call this function
from of.common.security.groups import aop_has_right

# We need to know the id of the "admin everything"-right.
from of.schemas.constants import id_right_admin_everything


# This function is for supplying runtime data to @aop_has_right
def get_uppercase_rights():
    """
    This function is necessary to hand *runtime* data to the decorator.
    Because as the lexical input to python decorators is set in runtime, even if you change the value of a variable,
    it will not be reflected in runtime. A function, however, will be called and its return value used anyway.
    :return: a list of node rights that have the right to call

    """
    return [id_right_admin_everything]



"""
The internal classes in Optimal Framework are called CherryPyClassName to separate them from the actual API
implementations. Unless you have the same structure, you can call your classes what you want.

"""""

class MyRealisticAPI(object):

    """
    * It requires the input to be json
    * It requires the output from the function to be a json-style dict (don't put any unserializable stuff in there)
    * It requires the user to be logged on
    * It requires the user to have the "admin everything"-right. If there's a separate API, this check may be there instead.
    """
    @cherrypy.expose
    @cherrypy.tools.json_in()
    @cherrypy.tools.json_out(content_type='application/json')
    @aop_check_session
    @aop_has_right(get_uppercase_rights)
    def uppercase(self, **kwargs):
        """
        Returns the uppercase version of the "string" attribute in the indata
        :param kwargs: The query string as keyword arguments + user information added by kwargs["_user"]
        :return:
        """
        # The json data is in cherrypy.request.json[
        _string = cherrypy.request.json["string"]
        _uppercased = _string.upper()
        return {"uppercased": _uppercased}

Mounting

Obviously, just declaring a class doesn't tell the broker where to publish the class, and continuing by using the realistic example, we want the function to be available as /realistic/uppercase. So we simply set a realistic property on the web root class in the hook(more on hooks in the next section):

from .api_extentions.realistic import MyRealisticClass

def init_web(_broker_scope):
    # Initialize the MyRealisticClass at /realistic
    _broker_scope["web_root"].realistic = MyRealisticClass()

Hooks

An important feature of the Optimal Framework plugins is its hook system.

Hooks are basically centrally organised callbacks, that are called if they are defined.

It has a callHook()-function: plugins.call_hook(self, _hook_name, **kwargs)

The only thing the plugin has to do, is to implement the function it in its hooks.py. Or whatever it chooses to call the hook file(defined in the hooks_module-setting).

##Defining hooks

Actually, you do not have to define hooks in the backend, you just call them and the plugin management handles if it is implemented anywhere. This is a distinct difference from for example the admin UI plugin frontend, where the plugins are defined in the definitions. This because the introspection features are a bit more difficult to handle there, and because it is typescript.

This means that calling a hook is just:

plugins.call_hook("my_hook_name", argument1, .. , argumentN)

And if any plugin has implemented a function called "my_hook_name" in its hooks-file, that function will be called.

So this call:

plugins.call_hook("my_hook_name", "Text1", "Text2")

..will, if defined in the hooks file, cause this function:

def my_hook_name(firstpart, lastpart):
   print("My hook wrote " + firstpart + " and " + lastpart + "!")

..to be called, producing the following output:

My hook wrote Text1 and Text2!

Here is an example of how an actual hook may look, this is the hook that adds the administrative backend to the broker, and it even also calls its own hooks, hooks implemented by plugins that want to add to the OF backend after the OF admin plugin has been initiated:

from .lib.admin import CherryPyAdmin


def init_web(_broker_scope):
    # Initialize admin user interface /admin
    _broker_scope["web_root"].admin = CherryPyAdmin(_process_id=_broker_scope["web_root"].process_id,
                                                    _address=_broker_scope["address"],
                                                    _stop_broker=_broker_scope["stop_broker"],
                                                    _root_object=_broker_scope["web_root"],
                                                    _web_config=_broker_scope["web_config"],
                                                    _plugins=_broker_scope["plugins"].plugins)

    # Call our own backend hooks
    _broker_scope["plugins"].call_hook("after_admin_ui", _broker_scope = _broker_scope,
                                       _admin_object = _broker_scope["web_root"].admin)

The purpose of the "after_admin_ui" hook is to make it possible for other server side plugins, like the Optimal BPM one, to do stuff after the admin plugin has initialized. They would just implement the "after_admin_ui" function in their hooks.py, or whatever they may call their main hook file.

Clone this wiki locally