Pluggdapps

Component system. Web framework. And more ...

Source code for pluggdapps.platform

# -*- coding: utf-8 -*-

# This file is subject to the terms and conditions defined in
# file 'LICENSE', which is part of this source code package.
#       Copyright (c) 2011 R Pratap Chakravarthy

"""This module along with the :mod:`plugin` module implement the component
architechture. We expect that most of the logic written using pluggdapps will,
one way or the other, be organised as plugins implementing one or more
interface(s). This module,

* provide platform base class in whose context plugins are instantiated.
* responsible for parsing configuration parameters from various sources,
  aggregate them, make them avialable on plugin instances.
* supply methods for logging error and warning messages.

The platform is instantiated by calling the :meth:`Pluggdapps.boot` method.

**Pre-boot and boot**

Pluggdapps component system is always instantiated in the context of a
platform defined by :class:`Pluggdapps` class or by classes deriving from
`Pluggdapps`.

Platform boots in two phase, first there is a pre-boot which more or less
does every thing that is done during an actual boot and then the actual
booting. Pre-booting is designed such a way that other pluggdapps-packages can
take part in platform booting, such as loading dynamic plugins, pre-compiling
template scripts etc ...

So here is what happens duing pre-booting,

* all pluggdapps packages are loaded. But entry points are not called.
* component system is initialized by calling 
  :func:`pluggdapps.plugin.plugin_init`.
* a plain vanilla platform is instantiated using :class:`Pluggdapps`.
* configuration settings from .ini files and database backend, if available,
  is loaded.

Pre-booting comes to an end by a call to :func:`pluggdapps.callpackages`
function, which in turn is responsible for re-loading pluggdapps packages and 
calling the package entry-point. Note that package re-loading is handle in the
context of a plain vanilla platform instantiated during pre-boot phase.

During the actual boot phase, everything that was done in pre-booting phase
is repeated. But the entire blue-print of interfaces and plugins from all the
installed packages will be remembered in the context of, probably a more
sophisticated :class:`Webapps`, platform class. The choice of platform class
depends on how the user started pluggdapps.

Configuration:
--------------

Like mentioned before, platform classes are responsible for handling
configuration.

**default configuration,**

All plugins deriving from :class:`pluggdapps.plugin.Plugin` class, which is
how plugins are authored, will automatically implement 
ISettings interface. And configurable plugins must override `ISettings`
interface methods. Refer to :class:`pluggdapps.plugin.ISettings` interface to
learn more about their interface methods. When platform is booted,
default_settings from loaded plugins will be gathered and remembered.

**ini file,**

Platform is typically booted by supplying an ini-file regarded as master
configuration file. Settings in this master configuration file will override
package default settings for plugins and other `special sections`.
Configuration sections not prefixed with ``plugin:`` is considered as special
section which are explained further below.

Master configuration file can refer to other configuration file.
  
An example master configuration file,

.. code-block:: ini
    :linenos:

    [DEFAULT]
    <option> = <value>
    ... = ...

    [pluggdapps]
    <option> = <value>
    ... = ...

    [plugin:<pkgname>.<pluginname>]
    <option> = <value>
    ... = ...

    [...]

Special sections:
-----------------

**[DEFAULT]** special section. Settings from this section is applicable to all
configuration sections including plugins and referred configuration files.
Semantic meaning of [DEFAULT] section is same as described by ``configparser``
module from python stdlib.

**[pluggdapps]** special section. Settings from this section is applicable to
pluggdapps platform typically handled by :class:`Pluggdapps`.

**[mountloc]** special section. Specific to web-framework (explained below)
that is built on top of pluggdapps' component system. Provides 
configuration settings on how to mount web-applications on web-url. Handled
by :class:`Webapps` platform class.

To learn more about backend store for configuration settings refer to module,
:mod:`pluggdapps.config`.
  
Web-application platform:
-------------------------

Implemented by :class:`Webapps` class (which derives from base platform class
:class:`Pluggdapps`), it can host any number web-application, and/or instance
of same web-application in single python environment. Every web-application
is a plugin implementing :class:`pluggdapps.interfaces.IWebApp` interface.
Like mentioned above every plugin gets instantiated in the context of a
platform, and in this case, when plugins are instantiated by `IWebApp` plugin,
either directly or indirectly, the instantiated plugins are automatically
supplied with **.webapp** attribute which is now part of its context.

**[mountloc] section and application wise ini file,**

Web-applications can be mounted, hosted, on a netlocation and script-path
(collectively called as ``netpath``). This is configured under **[mountloc]**
special section. While mounting web-applications under [mountloc] additional 
configuration files can be referred. Example [mountloc] section,

.. code-block:: ini
    :linenos:

    [mountloc]
    pluggdapps.com/issues = <appname>, <ini-file>
    tayra.pluggdapps.com/issues = <appname>, <ini-file>
    tayra.pluggdapps.com/source = <appname>, <ini-file>

The `lhs` side is called netpath which typically contains subdomain, hostname
and scripth-path. The `rhs` side is a tuple of two elements. First is the
name of a `IWebApp` plugin and second is path to application configuration
file.

Referred configuration files are exclusive to the scope of the mounted
application, and shall not contain any special sections, except `[DEFAULT]`,
unless otherwise explicitly mentioned. When a plugin is instantiated in the
context of a web-application, configuration settings from application-ini-file
will override settings from the master-ini-file.
"""

from   configparser          import SafeConfigParser
from   os.path               import dirname, isfile, abspath
from   copy                  import deepcopy
import re

from   pluggdapps.const      import SPECIAL_SECS, URLSEP
from   pluggdapps.interfaces import IWebApp, IConfigDB
import pluggdapps.utils      as h

SPECIAL_SECTIONS = ['DEFAULT', 'pluggdapps']

def DEFAULT():
    """Global default settings that are applicable to all plugins and
    sections, and can be overriden by base configuration file's 'DEFAULT'
    section. Semantic meaning of [DEFAULT] section is same as described by
    ``configparser`` module from stdlib."""
    sett = h.ConfigDict()
    sett.__doc__ = (
        "Configuration settings under this section are global and applicable "
        "every other sections."
    )
    sett['debug']  = {
        'default'  : False,
        'types'    : (bool,),
        'help'     : "Set this to True while developing with pluggdapps. This "
                     "will enable useful logging and other features like "
                     "module reloading. Can be modified only in the .ini "
                     "file.",
        'webconfig': False,
    }
    return sett


def normalize_defaults( sett ):
    """Normalize settings for [DEFAULT] special section."""
    sett['debug'] = h.asbool( sett.get('debug', False) )
    return sett


def pluggdapps_defaultsett():
    """Default settings for [pluggdapps] section in ini file."""
    sett = h.ConfigDict()
    sett.__doc__ = "Platform settings."
    sett['configdb'] = {
        'default'   : 'pluggdapps.ConfigSqlite3DB',
        'types'     : (str,),
        'help'      : "Backend plugin to persist configurations done via "
                      "webadmin application. Can be modified only in the .ini "
                      "file.",
        'webconfig' : False,
    }
    sett['host'] = {
        'default'   : 'localhost',
        'types'     : (str,),
        'help'      : "Top level domain name of host, web server, which "
                      "receives the request. Can be modified only in the .ini "
                      "file.",
        'webconfig' : False,
    }
    sett['logging.file'] = {
        'default'   : '',
        'types'     : (str,),
        'help'      : "File name to log messages. Make sure to add `file` in "
                      "`logging.output` parameter."
    }
    sett['logging.output'] = {
        'default' : 'console',
        'types'   : (str,),
        'help'    : "Comma separated value of names to log messages. "
                    "Supported names are `console`, `file`.",
        'options' : [ 'console', 'file' ],
    }
    sett['port'] = {
        'default'   : 8080,
        'types'     : (int,),
        'help'      : "Port addres to bind the http server. This "
                      "configuration will be used when using pluggdapps' "
                      "native HTTP server. Can be modified only in the .ini "
                      "file.",
        'webconfig' : False,
    }
    sett['scheme'] = {
        'default'   : 'http',
        'types'     : (str,),
        'help'      : "HTTP Scheme to use, either `http` or `https`. This "
                      "configuration will be used when using pluggdapps' "
                      "native HTTP server. Can be modified only in the .ini "
                      "file.",
        'webconfig' : False,
    }
    return sett


def normalize_pluggdapps( sett ):
    """Normalize settings for [pluggdapps] special section."""
    sett['port'] = h.asint( sett['port'] )
    if isinstance( sett['logging.output'], str ):
        sett['logging.output'] = h.parsecsv( sett['logging.output'] )
    return sett

def mountloc_defaultsett():
    sett = h.ConfigDict()
    sett.__doc__ = "Mount application settings"
    return sett

def normalize_mountloc( sett ):
    return sett

[docs]class Pluggdapps( object ): """Platform class tying together pluggdapps platform, component architecture and configuration system. Do not instantiate this class directly, instead use the boot() method call (which is a classmethod) to start the platform.""" inifile = None """Master Configuration file, absolute location.""" settings = {} """Default settings for plugins, gathered from plugin-default settings, master configuration file and other backend stores, if any.""" configdb = None """:class:`pluggdapps.interfaces.IConfigDB` plugin instance.""" def __init__( self, erlport=None ): self.erlport = erlport # TODO: Document this once bolted with netscale def _preboot( cls, baseini, *args, **kwargs ): """Prebooting. We need pre-booting because package() entry point can have option of creating dynamic plugins when it is called. The package() entry point for each and every package will be called only when callpackages() is called. Since callpackages() need a platform context, we first pre-boot the system and then actually boot the system.""" from pluggdapps import callpackages pa = Pluggdapps( *args, **kwargs ) pa.settings = pa._loadsettings( baseini ) # Configuration from backend store. storetype = pa.settings['pluggdapps']['configdb'] configdb = pa.qp( pa, IConfigDB, storetype ) configdb.dbinit() dbsett = configdb.config() if dbsett : [ pa.settings[section].update(d) for section, d in dbsett.items() ] pa.logsett = h.settingsfor( 'logging.', pa.settings['pluggdapps'] ) callpackages( pa ) return configdb #---- Overridable methods. @classmethod
[docs] def boot( cls, baseini, *args, **kwargs ): """Boot the platform using master configuration file ``baseini``. Return a new instance of this class object. This is the only way to create a platform instance. """ configdb = Pluggdapps._preboot( cls, baseini, *args, **kwargs ) pa = cls( *args, **kwargs ) pa.inifile = baseini pa.settings = pa._loadsettings( baseini ) pa.configdb = configdb # Configuration from backend store dbsett = pa.configdb.config() if dbsett : [ pa.settings[section].update(d) for section, d in dbsett.items() ] # Logging related settings go under `[pluggdapps]` section pa.logsett = h.settingsfor( 'logging.', pa.settings['pluggdapps'] ) return pa
[docs] def start( self ): """Expected to be called after boot(). Start pluggdapps.""" pass
[docs] def shutdown( self ): """Reverse of start() method.""" pass
[docs] def masterinit( self, plugin, *args, **kwargs ): """Call back function during plugin instantiation, from :class:`pluggdapps.plugin.PluginMeta` class. ``plugin``, Instantiated plugin. ``plugin`` is automatically populated with configuration settings. ``args`` and ``kwargs``, are received from query_plugin's ``args`` and ``kwargs``. """ plugin._settngx.update( self.settings[ h.plugin2sec(plugin.caname) ]) plugin.pa = self plugin.query_plugins = h.hitch_method( plugin, plugin.__class__, Pluggdapps.query_plugins, self ) plugin.query_pluginr = h.hitch_method( plugin, plugin.__class__, Pluggdapps.query_pluginr, self ) plugin.query_plugin = h.hitch_method( plugin, plugin.__class__, Pluggdapps.query_plugin, self ) plugin.qps = h.hitch_method( plugin, plugin.__class__, Pluggdapps.query_plugins, self ) plugin.qpr = h.hitch_method( plugin, plugin.__class__, Pluggdapps.query_pluginr, self ) plugin.qp = h.hitch_method( plugin, plugin.__class__, Pluggdapps.query_plugin, self ) # Plugin settings plugin._settngx.update( kwargs.pop( 'settings', {} )) return args, kwargs #---- Configuration APIs
[docs] def config( self, **kwargs ): """Get or set configuration parameter for the platform. If no keyword arguments are supplied, will return platform-settings. This API is meant applications who wish to admister the platform configuration. Keyword arguments, ``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. """ section = kwargs.get( 'section', None ) name = kwargs.get( 'name', None ) value = kwargs.get( 'value', None ) if section and name and value : self.settings[section][name] = value return self.configdb.config( **kwargs ) #---- Internal methods.
def _loadsettings( self, inifile ): """Load ``inifile`` and override the default settings with inifile's configuration. Return them as dictionary of global settings.""" SPECIAL_SECS = [ 'pluggdapps' ] defaults = self._defaultsettings() # Override plugin defaults with configuration from ini-file(s) return self._loadini( inifile, defaults ) def _loadini( self, baseini, defaultsett ): """Parse master ini configuration file ``baseini`` and ini files refered by `baseini`. Construct a dictionary of settings for special sections and plugin sections.""" from pluggdapps.plugin import pluginnames, plugin_info if not baseini or (not isfile(baseini)) : return deepcopy( defaultsett ) # Initialize return dictionary. settings = {} # context for parsing ini files. _vars = { 'here' : abspath( dirname( baseini )) } # Read master ini file. cp = SafeConfigParser() cp.read( baseini ) # [DEFAULT] overriding global def. s = deepcopy( defaultsett['DEFAULT'] ) s.update( dict( cp.defaults() )) settings['DEFAULT'] = normalize_defaults( s ) # [pluggdapps] s = deepcopy( defaultsett['pluggdapps'] ) if cp.has_section('pluggdapps') : s.update( dict( cp.items( 'pluggdapps', vars=_vars ))) s.pop( 'here', None ) # TODO : how `here` gets populated ?? settings['pluggdapps'] = normalize_pluggdapps( s ) # Override plugin's package default settings with [DEFAULT] settings. for pluginsec, sett in defaultsett.items() : if not pluginsec.startswith( 'plugin:' ) : continue sett = h.mergedict( sett, settings['DEFAULT'] ) if cp.has_section( pluginsec ) : sett.update( dict( cp.items( pluginsec, vars=_vars ))) sett.pop( 'here', None ) # TODO : how `here` ?? cls = plugin_info( h.sec2plugin( pluginsec ) )['cls'] for b in reversed( cls.mro() ) : if hasattr( b, 'normalize_settings' ) : sett = b.normalize_settings( sett ) settings[ pluginsec ] = sett return settings def _defaultsettings( self ): """By now it is expected that all interface specs and plugin definitions would have been loaded by loading packages implementing them and pluggdapps' plugin meta-classing. This function will collect their default settings and return them as settings dictionary,:: { "plugin:<pkgname>.<pluginname>" : default_settings, ... } """ from pluggdapps.plugin import PluginMeta # Default settings for plugins. default = dict( DEFAULT().items() ) defaultsett = { 'DEFAULT' : deepcopy(default) } defaultsett['pluggdapps'] = deepcopy(default) defaultsett['pluggdapps'].update( dict( pluggdapps_defaultsett().items() )) # Fetch all the default-settings for loaded plugins using `ISettings` # interface. Plugin inheriting from other plugins will override its # base's default_settings() in cls.mro() order. for name, info in PluginMeta._pluginmap.items() : bases = reversed( info['cls'].mro() ) sett = deepcopy( default ) for b in bases : if hasattr( b, 'default_settings' ) : sett.update( dict( b.default_settings().items() )) sett = b.normalize_settings( sett ) defaultsett[ h.plugin2sec(name) ] = sett return defaultsett #---- Query APIs @staticmethod
[docs] def query_plugins( pa, interface, *args, **kwargs ): """Use this API to query for plugins using the ``interface`` class it implements. Positional and keyword arguments will be used to instantiate the plugin object. ``pa``, Platform object, whose base class is :class:`Pluggdapps`. ``interface``, :class:`pluggdapps.plugin.Interface` class or canonical form of interface-name. ``args`` and ``kwargs``, Positional and key-word arguments used to instantiate the plugin. If `settings` key-word argument is present, it will be used to override default plugin settings. Returns a list of plugin instance implementing `interface` """ from pluggdapps.plugin import PluginMeta if isinstance(interface, str) : intrf = interface.lower() interface = PluginMeta._interfmap.get(intrf, {}).get('cls', None) pmap = PluginMeta._implementers.get(interface, {}) return [ pcls( pa, *args, **kwargs ) for pcls in pmap.values() ]
qps = query_plugins # Alias @staticmethod
[docs] def query_pluginr( pa, interface, pattern, *args, **kwargs ): """Use this API to query for plugins using the ``interface`` class it implements. Positional and keyword arguments will be used to instantiate the plugin object. ``pa``, Platform object, whose base class is :class:`Pluggdapps`. ``interface``, :class:`pluggdapps.plugin.Interface` class or canonical form of interface-name. ``pattern``, Instantiate plugins who's name match this pattern. Pattern can be any regular-expression. ``args`` and ``kwargs``, Positional and key-word arguments used to instantiate the plugin. If `settings` key-word argument is present, it will be used to override default plugin settings. Returns a list of plugin instance implementing `interface` """ from pluggdapps.plugin import PluginMeta if isinstance(interface, str) : intrf = interface.lower() interface = PluginMeta._interfmap.get(intrf, {}).get('cls', None) pattc = re.compile(pattern) pmap = PluginMeta._implementers.get(interface, {}) return [ pcls( pa, *args, **kwargs ) for pcls in pmap.values() if re.match(pattc, pcls.caname) ]
qpr = query_pluginr # Alias @staticmethod
[docs] def query_plugin( pa, interface, name, *args, **kwargs ): """Same as queryPlugins, but returns a single plugin instance as opposed an entire list. `name` will be used to identify that plugin. Positional and keyword arguments will be used to instantiate the plugin object. ``pa``, Platform object, whose base class is :class:`Pluggdapps`. ``interface``, :class:`pluggdapps.plugin.Interface` class or canonical form of interface-name. ``name``, Plugin name in canonical format. For example, canonical name for plugin class `ConfigSqlite3DB` defined under `pluggdapps` package will be, `pluggdapps.ConfigSqlite3DB`. ``args`` and ``kwargs``, Positional and key-word arguments used to instantiate the plugin. If ``settings`` key-word argument is present, it will be used to override default plugin settings. Return a single Plugin instance. """ from pluggdapps.plugin import PluginMeta, ISettings if isinstance(interface, str) : intrf = interface.lower() interface = PluginMeta._interfmap.get(intrf, {}).get('cls', None) cls = PluginMeta._implementers.get(interface, {}).get(name.lower(), None) return cls( pa, *args, **kwargs ) if cls else None
qp = query_plugin # Alias #---- platform logging
[docs] def loginfo( self, formatstr, values=[] ): """Use this method to log informational messages. The log messages will be formated and handled based on the configuration settings from ``[pluggdapps]`` section. """ output, erlport = self.logsett['output'], self.erlport if 'cloud' in output and erlport : self.erlport.loginfo( formatstr, values ) if 'file' in output and self.logsett['file'] : open( self.logsett['file'], 'a' ).write( formatstr ) if 'console' in output : print( formatstr )
[docs] def logdebug( self, formatstr, values=[] ): """Use this method to log debug messages. The log messages will be formated and handled based on the configuration settings from ``[pluggdapps]`` section. """ if self.settings['DEFAULT']['debug'] != True : return output, erlport = self.logsett['output'], self.erlport formatstr = 'DEBUG: ' + formatstr if 'cloud' in output and erlport : self.erlport.logdebug( formatstr, values ) if 'file' in output and self.logsett['file'] : open( self.logsett['file'], 'a' ).write( formatstr ) if 'console' in output : print( h.colorize( formatstr, color='33' ))
[docs] def logwarn( self, formatstr, values=[] ): """Use this method to log warning messages. The log messages will be formated and handled based on the configuration settings from ``[pluggdapps]`` section. """ output, erlport = self.logsett['output'], self.erlport formatstr = 'WARN: ' + formatstr if 'cloud' in output and erlport : self.erlport.logwarn( formatstr, values ) if 'file' in output and self.logsett['file'] : open( self.logsett['file'], 'a' ).write( formatstr ) if 'console' in output : print( h.colorize( formatstr, color='32' ))
[docs] def logerror( self, formatstr, values=[] ): """Use this method to log error messages. The log messages will be formated and handled based on the configuration settings from ``[pluggdapps]`` section. """ output, erlport = self.logsett['output'], self.erlport formatstr = 'ERROR: ' + formatstr if 'cloud' in output and erlport : self.erlport.logerror( formatstr, values ) if 'file' in output and self.logsett['file'] : open( self.logsett['file'], 'a' ).write( formatstr ) if 'console' in output : print( h.colorize( formatstr, color='31' ))
[docs]class Webapps( Pluggdapps ): """Provides a web-framework based on a pluggable MVC design pattern. Can mount any number of web-application in the same python environment.""" webapps = {} """Dictionay mapping of mount-key and :class:`pluggdapps.web.webapp.WebApp` instance, where mount-key is a tuple of appsec, netpath, application-ini""" netpaths= {} """Dictionay mapping of netpath, and :class:`pluggdapps.web.webapp.WebApp` instance.""" appurls = {} """A dictionary map of webapp's instkey and its base-url. The base url consists of scheme, netloc, scriptname.""" _app_resolve_cache = {} """A dictionary map of (netloc, script-path) to Web-application object.""" _monitoredfiles = [] """Attribute used in debug mode to collect and monitor files that will be modified during developement.""" def __init__( self, *args, **kwargs ): super().__init__( *args, **kwargs ) def findapp( self, appname=None, appsec=None ): appsec = appsec or (h.plugin2sec( appname ) if appname else None) if appsec : for asec, netpath, config in self.webapps.keys() : if appsec==asec : return self.webapps[(asec, netpath, config)] else : return None return None #---- Overridable methods @classmethod
[docs] def boot( cls, baseini, *args, **kwargs ): """Parse ``[mountloc]`` section for application-wise configuration settings and return a fresh instance of :class:`Webapps` class. """ pa = super().boot( baseini, *args, **kwargs ) pa.webapps = {} pa.appurls = {} appsettings = pa._mountapps() netpaths = [ instkey[1] for instkey, appsett in appsettings.items() ] pa.configdb.dbinit( netpaths=netpaths ) # Mount webapp instances for subdomains and scripts. for instkey, appsett in appsettings.items() : appsec, netpath, config = instkey # Instantiate the IWebAPP plugin here for each `instkey` appname = h.sec2plugin( appsec ) webapp = pa.qp( pa, instkey, IWebApp, appname, appsett ) webapp.appsettings = appsett # Update with backend configuration. [ webapp.appsettings[section].update( d ) for section, d in pa.configdb.config( netpath=netpath ).items() ] webapp.instkey, webapp.netpath = instkey, None pa.webapps[ instkey ] = webapp pa.netpaths[ netpath ] = webapp pa.appurls[ instkey ] = webapp.baseurl = pa._make_appurl( instkey ) pa._app_resolve_cache[ h.parse_netpath(netpath) ] = webapp # Resolution mapping for web-applications webapp.netpath = netpath return pa
[docs] def start( self ): """Boot all loaded application. Web applicationss are loaded when the system is booted via boot() call. Apps are booted only when an explicit call is made to this method.""" super().start() appnames = [] for instkey, webapp in self.webapps.items() : appsec, netpath, configini = instkey self.loginfo( "Booting application %r ..." % netpath ) webapp.startapp() appnames.append( h.sec2plugin( appsec )) return appnames
[docs] def shutdown( self ): """Reverse of start() method.""" for instkey, webapp in self.webapps.items() : self.loginfo( "Shutting down application %r ..." % appname ) webapp.shutdown() super().shutdown()
[docs] def masterinit( self, plugin, webapp, *args, **kwargs ): """Call back function during plugin instantiation, from :class:`pluggdapps.plugin.PluginMeta` class. ``plugin``, Instantiated plugin. ``plugin`` is automatically populated with configuration settings and ``webapp`` attribute. ``args`` and ``kwargs``, are received from query_plugin's ``args`` and ``kwargs``. """ from pluggdapps.web.webapp import WebApp caname = plugin.caname if isinstance( plugin, WebApp ) : # Ought to be IWebApp plugin appsec, netpath, config = webapp plugin._settngx.update( args[0][ appsec ] ) plugin.webapp = plugin args = args[1:] elif webapp : # Not a IWebApp plugin plugin._settngx.update( webapp.appsettings[ h.plugin2sec(caname) ]) plugin.webapp = webapp else : # plugin not under a webapp plugin._settngx.update( self.settings[ h.plugin2sec(caname) ]) plugin.webapp = None plugin.pa = self plugin.query_plugins = h.hitch_method( plugin, plugin.__class__, Webapps.query_plugins, self, plugin.webapp ) plugin.query_pluginr = h.hitch_method( plugin, plugin.__class__, Webapps.query_pluginr, self, plugin.webapp ) plugin.query_plugin = h.hitch_method( plugin, plugin.__class__, Webapps.query_plugin, self, plugin.webapp ) plugin.qps = h.hitch_method( plugin, plugin.__class__, Webapps.query_plugins, self, plugin.webapp ) plugin.qpr = h.hitch_method( plugin, plugin.__class__, Webapps.query_pluginr, self, plugin.webapp ) plugin.qp = h.hitch_method( plugin, plugin.__class__, Webapps.query_plugin, self, plugin.webapp ) # Plugin settings plugin._settngx.update( kwargs.pop( 'settings', {} )) return args, kwargs #---- Configuration APIs
[docs] def config( self, **kwargs ): """Get or set configuration parameter for the platform. If no keyword arguments are present, will return a dictionary of webapp-settings, where each webapp is identified by netpath as key. Keyword arguments, ``netpath``, Netpath, including 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. """ netpath = kwargs.get( 'netpath', None ) if netpath == None : super().config( **kwargs ) else : section = kwargs.get( 'section', None ) name = kwargs.get( 'name', None ) value = kwargs.get( 'value', None ) if netpath == 'platform' : settings = self.settings else : settings = self.netpaths[ netpath ].appsettings if section and name and value : settings[section][name] = value return self.configdb.config( **kwargs ) #---- Internal methods
def _mountapps( self ): """Create application wise settings, using special section [mountloc], if any. Also parse referred configuration files.""" from pluggdapps.plugin import pluginnames, webapps settings = self.settings # context for parsing ini files. _vars = { 'here' : abspath( dirname( self.inifile )) } # Fetch special section [mountloc]. And override them with [DEFAULT] # settings. cp = SafeConfigParser() cp.read( self.inifile ) if cp.has_section('mountloc') : mountloc = cp.items( 'mountloc', vars=_vars ) else : mountloc = [] settings['mountloc'] = dict( mountloc ) settings.pop( 'here', None ) # TODO : how `here` gets populated. # Parse mount configuration. appsecs = list( map( h.plugin2sec, webapps() )) mountls = [] _skipopts = list(_vars.keys()) + list(settings['DEFAULT'].keys()) for netpath, mounton in mountloc : if netpath in _skipopts : continue parts = [ x.strip() for x in mounton.split(',', 1) ] appname = parts.pop(0) configini = parts.pop(0) if parts else None appsec = h.plugin2sec( appname ) if appsec not in appsecs : raise Exception("%r application not found." % appname ) if not configini : raise Exception("configuration file %r not supplied"%configini) if not isfile( configini ) : raise Exception("configuration file %r not valid"%configini) mountls.append( (appsec,netpath,configini) ) # Load application configuration from instance configuration file. appsettings = {} for appsec, netpath, instconfig in mountls : appsett = deepcopy( settings ) [ appsett.pop(k) for k in SPECIAL_SECS ] if instconfig : self._loadinstance( appsett, instconfig ) appsettings[ (appsec,netpath,instconfig) ] = appsett return appsettings def _loadinstance( self, appsett, instanceini ): """Load configuration settings for a web application's instance.""" from pluggdapps.plugin import plugin_info _vars = { 'here' : abspath( dirname( instanceini )) } cp = SafeConfigParser() cp.read( instanceini ) # Update appsett with [DEFAULT] section of instanceini defaultsett = normalize_defaults( dict( cp.defaults() )) appsett['DEFAULT'].update( defaultsett ) [ sett.update( appsett['DEFAULT'] ) for key, sett in appsett.items() ] # Update plugin sections in appsett from instanceini for sec in cp.sections() : if not sec.startswith( 'plugin:' ) : continue sett = dict( cp.items( sec, vars=_vars )) sett.pop( 'here', None ) # TODO : how `here` gets populated ?? appsett[sec].update( sett ) cls = plugin_info( h.sec2plugin( sec ) )['cls'] for b in reversed( cls.mro() ) : if hasattr( b, 'normalize_settings' ) : appsett[sec] = b.normalize_settings( appsett[sec] ) def _make_appurl( self, instkey ): """Compute the base url for application specified by `instkey` which is a tuple of, ( appsec, mount-type, mount-name, configfile) Return the base-url as byte-string. """ port = self.settings['pluggdapps']['port'] scheme = self.settings['pluggdapps']['scheme'] appsec, netpath, config = instkey netloc, script = h.parse_netpath( netpath ) # Prefix scheme appurl = scheme + '://' + netloc # prefix port if port : if (scheme, port) in [ ('http', 80), ('https', 443) ] : port = '' else : port = str(port) appurl += ':' + port # Prefix SCRIPT-NAME, mountname is already prefixed with URLSEP appurl += script return appurl #---- Query APIs @staticmethod
[docs] def query_plugins( pa, webapp, interface, *args, **kwargs ): """Use this API to query for plugins using the ``interface`` class it implements. Positional and keyword arguments will be used to instantiate the plugin object. ``pa``, Platform object, whose base class is :class:`Pluggdapps`. ``webapp``, Web application object, plugin implementing :class:`pluggdapps.interfaces.IWebApp` interface. It is an optional argument, which must be passed ``None`` otherwise. ``interface``, :class:`pluggdapps.plugin.Interface` class or canonical form of interface-name. ``args`` and ``kwargs``, Positional and key-word arguments used to instantiate the plugin. If `settings` key-word argument is present, it will be used to override default plugin settings. Returns a list of plugin instance implementing `interface` """ from pluggdapps.plugin import PluginMeta if isinstance(interface, str) : intrf = interface.lower() interface = PluginMeta._interfmap.get(intrf, {}).get('cls', None) pmap = PluginMeta._implementers.get(interface, {}) return [ pcls( pa, webapp, *args, **kwargs ) for pcls in pmap.values() ]
qps = query_plugins # Alias @staticmethod
[docs] def query_pluginr( pa, webapp, interface, pattern, *args, **kwargs ): """Use this API to query for plugins using the ``interface`` class it implements. Positional and keyword arguments will be used to instantiate the plugin object. ``pa``, Platform object, whose base class is :class:`Pluggdapps`. ``webapp``, Web application object, plugin implementing :class:`pluggdapps.interfaces.IWebApp` interface. It is an optional argument, which must be passed ``None`` otherwise. ``interface``, :class:`pluggdapps.plugin.Interface` class or canonical form of interface-name. ``pattern``, Instantiate plugins who's name match this pattern. Pattern can be any regular-expression. ``args`` and ``kwargs``, Positional and key-word arguments used to instantiate the plugin. If `settings` key-word argument is present, it will be used to override default plugin settings. Returns a list of plugin instance implementing `interface` """ from pluggdapps.plugin import PluginMeta if isinstance(interface, str) : intrf = interface.lower() interface = PluginMeta._interfmap.get(intrf, {}).get('cls', None) pattc = re.compile(pattern) pmap = PluginMeta._implementers.get(interface, {}) return [ pcls( pa, webapp, *args, **kwargs ) for pcls in pmap.values() if re.match(pattc, pcls.caname) ]
qpr = query_pluginr # Alias @staticmethod
[docs] def query_plugin( pa, webapp, interface, name, *args, **kwargs ): """Same as queryPlugins, but returns a single plugin instance as opposed an entire list. `name` will be used to identify that plugin. Positional and keyword arguments will be used to instantiate the plugin object. ``pa``, Platform object, whose base class is :class:`Pluggdapps`. ``webapp``, Web application object, plugin implementing :class:`pluggdapps.interfaces.IWebApp` interface. It is an optional argument, which must be passed ``None`` otherwise. ``interface``, :class:`pluggdapps.plugin.Interface` class or canonical form of interface-name. ``name``, plugin name in canonical format. For example, canonical name for plugin class `ConfigSqlite3DB` defined under `pluggdapps` package will be, `pluggdapps.ConfigSqlite3DB`. ``args`` and ``kwargs``, Positional and key-word arguments used to instantiate the plugin. If ``settings`` key-word argument is present, it will be used to override default plugin settings. Return a single Plugin instance. """ from pluggdapps.plugin import PluginMeta if isinstance(interface, str) : intrf = interface.lower() interface = PluginMeta._interfmap.get(intrf, {}).get('cls', None) cls = PluginMeta._implementers.get(interface, {}).get(name.lower(), None) return cls( pa, webapp, *args, **kwargs ) if cls else None
qp = query_plugin # Alias #---- APIs related to hosting multiple-applications.
[docs] def resolveapp( self, uri, hdrs ): """Resolve application for request. ``uri``, byte-string of request URI. ``hdrs``, Dictionary of request headers. Return a tuple of, (uriparts, mountedat). ``uriparts``, dictionary of URL parts ``webapp``, :class:`IWebApp` plugin instance. """ uriparts = h.parse_url( uri, host=hdrs.get('host', None) ) for key, webapp in self._app_resolve_cache.items() : netloc, script = key uri_netloc = ( uriparts['host'][3:] if uriparts['host'].startswith('www') else uriparts['host'] ) if netloc == uri_netloc : if script in ['/', ''] : break elif uriparts['path'].startswith( script ) : uriparts['script'] = script uriparts['path'] = uriparts['path'][len(script):] break else : webapp = None return uriparts, webapp

Related Topics