This article explains how to make your projects compatible with pluggdapps, define interfaces and author plugins.
Pluggdapps uses python3, its stdlib, setuptools and virtual-environment to create a notion of platform. A platform is an environment, a run-time, using which developers can create programs and distribute them to other machines as long as they have the same virtual environment.
Pluggdapps project is packaged using setuptools and while doing so, it is expected that [pluggdapps] package entry point is defined, for eg.,
1 2 3 4 5 6 7 8 9 10 11 12 | # setup.py
...
setup(
...
...
entry_points={ # setuptools
'pluggdapps' : [
'package=<module.path>:<function-name>',
]
},
...
)
|
Typically, a pluggdapps project will define the package entry-point function as package. Presence of this entry point will indicate that the package is part of pluggdapps environment. This entry point will be called during pluggdapps boot-time, and expected to return a dictionay of package information.
Example package
Tayra is a templating language that is distributed as pluggdapps package, the following code is taken from tayra/__init__.py package file,
1 2 3 4 5 | def package( pa ) :
loadttls( pa, template_plugins )
return {
'ttlplugins' : template_plugins,
}
|
Note that tayra package is using this entry point to load template plugins and return a dictionary of information about them.
Below is the list of keys that can be returned as package information. Unless otherwise explicitly mentioned these keys are optional.
Every pluggdapps package can have any number of interface-specification defined in them. Just make sure that modules containing these interface specifications are imported by the package-init file - <package-path>/__init__.py. An example interface specification that define callback methods to persist configuration settings on a backed data-store,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | class IConfigDB( Interface ):
"""Interface specification to persist / access platform configuration to
backend data-store or database. When platform gets booted and if
config-backend is available, then configuration settings from datastore
will override settings from `.ini` files."""
def connect():
"""Do necessary initialization to connect with data-store."""
def dbinit( *args, **kwargs ): #*
"""If datastore does not exist, create one. Also intialize
configuration tables for each mounted application under
[mountloc] section. Expect this method to be called when ever
platform
starts-up. For more information refer to corresponding plugin's
documentation.
"""
def config( **kwargs ): #**
"""Get or set configuration parameter for platform. For more
information refer to corresponding plugin's documentation."""
def close():
"""Reverse of :meth:`connect`."""
|
other than definig the methods, explaining its purpose as doc-strings, interface specification doesn’t do much. The above interface will be used by platform class during pluggdapps boot-time.
Plugins derive from Plugin base class and implement one or more interfaces. Following is a plugin class implementing IConfigDB interface
class ConfigSqlite3DB( Plugin ):
"""Backend interface to persist configuration information in sqlite3
database."""
implements( IConfigDB )
def __init__( self ):
self.conn = sqlite3.connect( self['url'] ) if self['url'] else None
def connect( self, *args, **kwargs ): #*
""":meth:`pluggdapps.interfaces.IConfigDB.connect` interface
method."""
if self.conn == None and self['url'] :
self.conn = sqlite3.connect( self['url'] )
def dbinit( self, netpaths=[] ):
""":meth:`pluggdapps.interfaces.IConfigDB.dbinit` interface method.
Optional key-word argument,
``netpaths``,
list of web-application mount points. A database table will be
created for each netpath.
"""
if self.conn == None : return None
c = self.conn.cursor()
# Create the `platform` table if it does not exist.
c.execute(
"CREATE TABLE IF NOT EXISTS platform "
"(section TEXT PRIMARY KEY ASC, settings TEXT);" )
self.conn.commit()
for netpath in netpaths :
sql = ( "CREATE TABLE IF NOT EXISTS '%s' "
"(section TEXT PRIMARY KEY ASC, settings TEXT);" ) %\
netpath
c.execute( sql )
self.conn.commit()
def config( self, **kwargs ): #**
""":meth:`pluggdapps.interfaces.IConfigDB.config` interface method.
Keyword arguments,
``netpath``,
Netpath, including subdomain-hostname and script-path, on which
web-application is mounted. Optional.
``section``,
Section name to get or set config parameter. Optional.
``name``,
Configuration name to get or set for ``section``. Optional.
``value``,
If present, this method was invoked for setting configuration
``name`` under ``section``. Optional.
- if netpath, section, name and value kwargs are supplied, will
update config-parameter `name` under webapp's `section` with
`value`. Return the updated value.
- if netpath, section, name kwargs are supplied, will return
configuration `value` for `name` under webapp's `section`.
- if netpath, section kwargs are supplied, will return dictionary
of all configuration parameters under webapp's section.
- if netpath is supplied, will return the entire table as dictionary
of sections and settings.
- if netpath is not supplied, will use `section`, `name` and `value`
arguments in the context of ``platform`` table.
"""
if self.conn == None : return None
netpath = kwargs.get( 'netpath', 'platform' )
section = kwargs.get( 'section', None )
name = kwargs.get( 'name', None )
value = kwargs.get( 'value', None )
c = self.conn.cursor()
if section :
c.execute(
"SELECT * FROM '%s' WHERE section='%s'" % (netpath,section))
result = list(c)
secsetts = h.json_decode( result[0][1] ) if result else {}
if name and value :
secsetts[name] = value
secsetts = h.json_encode(secsetts)
c.execute( "DELETE FROM '%s' WHERE section='%s'" %
(netpath, section) )
c.execute( "INSERT INTO '%s' VALUES ('%s', '%s')" %
(netpath, section, secsetts) )
self.conn.commit()
rc = value
elif name :
rc = secsetts[name]
else :
rc = secsetts
else :
c.execute( "SELECT * FROM '%s'" % (netpath,) )
rc = { section : h.json_decode( setts )
for section, setts in list(c) }
return rc
def close( self ):
""":meth:`pluggdapps.interfaces.IConfigDB.close` method."""
if self.conn :
self.conn.close()
#---- ISettings interface methods
@classmethod
def default_settings( cls ):
""":meth:`pluggdapps.plugin.ISettings.default_settings` interface
method.
"""
return _default_settings
@classmethod
def normalize_settings( cls, sett ):
""":meth:`pluggdapps.plugin.ISettings.normalize_settings` interface
method.
"""
return sett
some of the points that you should make note of,
Overriding plugin methods
When a plugin class derives from a base plugin class, overriding its interface methods and non-interface methods are similar to python inheritance concepts, except for __init__ method. For example, let us say that a plugin class MyPlugin inherits from another plugin class YourPlugin, it must look something similar to following snippet.
1 2 3 4 5 6 7 | class YourPlugin( Plugin ):
def __init__( self, arg2, arg3 ):
pass
class MyPlugin( YourPlugin ):
def __init__( self, arg1, arg2, arg3 ):
self._super_init( __class__, arg2, arg3 )
|
instead of using super() built-in function to access base-class’ __init__() method, you must always use self._super_init() method to call plugin’s base-class’ __init__() method.