Plugin Architecture

A plugin is a mixin class. To create a complete HTSQL application, you create a new class, base classes of which are plugins you want to include.

Example:

class MyApp(dbgui.DBGUI, htsql.PostgreSQL, htsql.Base):
    pass

app = MyApp(dbgui_debug=False, database='htsql_test', config='test/config.yaml')

htsqld = wsgiref.simple_server.make_server('', 8080, app)
htsqld.serve_forever()

Using mixin classes gives us a flexibility. For instance, to change the way the htsql catalog is constructed, one would need to create a mixin class that override the make_catalog method, overriding make_connection will allow you to employ different pooling strategies for database connections, and so on.

A plugin would be able to add additional commands, formatters and functions. For commands, it could look like this:

class PluginCommand(commands.Command):
    root = True
    plugin = SomePlugin
    namespace = 'some_plugin'
    # This is the base abtract class for all plugin commands.

class ASpecificPluginCommand(PluginCommand):
    name = 'some_command'
    # Note that the "namespace" parameter is inherited from the base class.
    [...]
    def do_stuff(self):
        # self.app is an application instance, which is a subclass of "MyPlugin".

[...]

class MyPlugin(plugins.Plugin):

    commands = PluginCommand.container
    # This container will be checked when a command is looked up.

    def __init__(self, plugin_param, **kwds):
        super(MyPlugin, self).__init__(**kwds)
        self.plugin_param = plugin_param
        [...]

Disadvantages:

  • shared namespace for all plugins (but it could be quite convenient)
  • no more than one instance of a plugin per application could be used
  • the order in which the plugins are listed is important

Plugin Architecture

In the current stable branch, any command or formatter with a name is automatically registered in the respective global container. In order to include extra commands and formatters, one just needs to import the module in which the classes are defined.

Example:

# some_plugin.py
# The plugin code.

from htsql import commands, formatters

class SomeExtraCommand(commands.Command):
    namespace = 'some_plugin'
    name = 'some_command'
    # The command is automatically added to the global command container.
    # It will be available as "/request/some_plugin:some_command()".
    [...the command code...]

class SomeOtherCommand(SomeExtraCommand):
    name = 'some_other_command'
    # Note that you don't need to specify "namespace" again since it is
    # defined in the base class.
    [...the command code...]

class SomeExtraFormatter(formatters.Formatter):
    namespace = 'some_plugin'
    name = 'some_format'
    # The formatter is automatically added to the global formatter container.
    # It will we available as "/request.some_plugin:some_formatter".
    [...the formatter code...]

[...the rest of the module...]
# main.py
# The main program code.

import wsgiref
from htsql import server

import some_plugin # Effectively makes the plugin commands and formatters available.

[...]

htsqld = wsgiref.simple_server.make_server('', 8080, server.Server([...]))
htsqld.serve_forever()

This approach works reasonably well, but it fails in two cases:

  • it doesn't allow to run several instances of the HTSQL server in the same process with different sets of plugins. Since the HTSQL server is a WSGI server, it's normal to have more than one instance of it running within the same process.
  • there are no convenient ways to configure plugins.

To solve the former issue, we need to eliminate all global variables, that is, we need to give up auto-registration. Instead we have to list the plugin classes explicitly.

Consider the example:

# some_plugin.py

from htsql import plugins

[...]

class SomeCommand([...]):
    [...]
class AnotherCommand([...]):
    [...]
class SomeFormatter([...]):
    [...]
class AnotherFormatter([...]):
    [...]

[...]

plugin = plugins.Plugin(commands=[SomeCommand, AnotherCommand, ...],
                        formatters=[SomeFormatter, AnotherFormatter, ...])
# main.py

import wsgiref
from htsql import server

import some_plugin

[...]

htsql_server = server.Server(plugins=[some_plugin.plugin,...], ...)
htsqld = wsgiref.simple_server.make_server('', 8080, htsql_server)
htsqld.serve_forever()

This code still has some disadvantages. First, it forces a programmer to duplicate the names of the commands and formatters in the plugin initialization code. Forgetting to add a new command or formatter could become a common and annoying mistake, and, in general, it feels like a bad design. So it would be nice to retain the auto-registration feature, but not to use the same global container for all commands. Second, there still no way to specify the plugin parameters.

Now, consider:

# some_plugin.py

from htsql import commands, formatters, plugins

class SomePlugin(plugins.Plugin):
    commands = commands.Container
    formatters = formatters.Container
    # This code creates two new containers to be used to hold the plugin commands and formatters.
    [...]

class PluginCommand(commands.Command):
    plugin = SomePlugin
    namespace = 'some_plugin'
    # This is the base abtract class for all plugin commands.

class PluginFormatter(formatters.Formatter):
    plugin = SomePlugin
    namespace = 'some_plugin'
    # This is the base abstract class for all plugin formatters.

class ASpecificCommand(PluginCommand):
    name = 'some_command'
    # Note that the "plugin" and "namespace" parameters are inherited from the base class.
    [...]

class ASpecificFormatter(PluginFormatter):
    name = 'some_formatter'
    [...]

[...]
# main.py

import wsgiref
from htsql import server

import some_plugin

[...]

plugin = some_plugin.SomePlugin([...])
htsql_server = server.Server(plugins=[plugin], ...)
htsqld = wsgiref.simple_server.make_server('', 8080, htsql_server)
htsqld.serve_forever()

It will be also possible to employ a .conf file to initialize the plugin and the main server.

# htsql.conf

server:
  plugins:
  - class: some_plugin.SomePlugin
    arguments: {...}
  ...
daemon:
  host: ''
  port: 8080

Notes

  • An open question: how commands and formatters access the configuration of the plugin they are defined it? Perhaps the plugin instance should be passed to the command's or formatter's __init__.
  • An open question: how commands and formatters access the configuration of other plugins and the main server?
  • Functions behave like commands and formatters and should be changed to accommodate the new plugin API.
  • A question: the basic commands and formatters (and functions as well) form some plugin too, don't they?