QGIS server python plugins tutorial
This is the second article about python plugins for QGIS server, see also the introductory article posted a few days ago.
In this post I will introduce the helloServer example plugin that shows some common implementation patterns exploiting the new QGIS Server Python Bindings API.
Server plugins and desktop interfaces
Server plugins can optionally have a desktop interface exactly like all standard QGIS plugins.
A typical use case for a server plugin that also has a desktop interface is to allow the users to configure the server-side of the plugin from QGIS desktop, this is the same principle of configuring WMS/WFS services of QGIS server from the project properties.
The only important difference it that while the WMS/WFS services configuration is stored in the project file itself, the plugins can store and access project data but not to the user’s settings (because the server process normally runs with a different user). For this reason, if you want to share configuration settings between the server and the desktop, provided that you normally run the server with a different user, paths and permissions have to be carefully configured to grant both users access to the shared data.
Server configuration
This is an example configuration for Apache, it covers both FCGI and CGI:
ServerAdmin webmaster@localhost # Add an entry to your /etc/hosts file for xxx localhost e.g. # 127.0.0.1 xxx ServerName xxx # Longer timeout for WPS... default = 40 FcgidIOTimeout 120 FcgidInitialEnv LC_ALL "en_US.UTF-8" FcgidInitialEnv PYTHONIOENCODING UTF-8 FcgidInitialEnv LANG "en_US.UTF-8" FcgidInitialEnv QGIS_DEBUG 1 FcgidInitialEnv QGIS_CUSTOM_CONFIG_PATH "/home/xxx/.qgis2/" FcgidInitialEnv QGIS_SERVER_LOG_FILE /tmp/qgis.log FcgidInitialEnv QGIS_SERVER_LOG_LEVEL 0 FcgidInitialEnv QGIS_OPTIONS_PATH "/home/xxx/public_html/cgi-bin/" FcgidInitialEnv QGIS_PLUGINPATH "/home/xxx/.qgis2/python/plugins" FcgidInitialEnv LD_LIBRARY_PATH "/home/xxx/apps/lib" # For simple CGI: ignored by fcgid SetEnv QGIS_DEBUG 1 SetEnv QGIS_CUSTOM_CONFIG_PATH "/home/xxx/.qgis2/" SetEnv QGIS_SERVER_LOG_FILE /tmp/qgis.log SetEnv QGIS_SERVER_LOG_LEVEL 0 SetEnv QGIS_OPTIONS_PATH "/home/xxx/public_html/cgi-bin/" SetEnv QGIS_PLUGINPATH "/home/xxx/.qgis2/python/plugins" SetEnv LD_LIBRARY_PATH "/home/xxx/apps/lib" RewriteEngine On RewriteCond %{HTTP:Authorization} . RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] ScriptAlias /cgi-bin/ /home/xxx/apps/bin/ <Directory "/home/xxx/apps/bin/"> AllowOverride All Options +ExecCGI -MultiViews +FollowSymLinks Require all granted ErrorLog ${APACHE_LOG_DIR}/xxx-error.log CustomLog ${APACHE_LOG_DIR}/xxx-access.log combined
In this particular example, I’m using a QGIS server built from sources and installed in /home/xxx/apps/bin
the libraries are in /home/xxx/apps/lib
and LD_LIBRARY_PATH
poins to this location.
QGIS_CUSTOM_CONFIG_PATH
tells the server where to search for QGIS configuration (for example qgis.db
).
QGIS_PLUGINPATH
is searched for plugins as start, your server plugins must sit in this directory, while developing you can choose to use the same directory of your QGIS desktop installation.
QGIS_DEBUG
set to 1 to enable debug and logging.
Anatomy of a server plugin
For a plugin to be seen as a server plugin, it must provide correct metadata informations and a factory method:
Plugin metadata
A server enabled plugins must advertise itself as a server plugin by adding the line
server=True
in its metadata.txt
file.
The serverClassFactory method
A server enabled plugins is basically just a standard QGIS Python plugins that provides a serverClassFactory(serverIface)
function in its __init__.py
. This function is invoked once when the server starts to generate the plugin instance (it’s called on each request if running in CGI mode: not recommended) and returns a plugin instance:
def serverClassFactory(serverIface): from HelloServer import HelloServerServer return HelloServerServer(serverIface)
You’ll notice that this is the same pattern we have in “traditional” QGIS plugins.
Server Filters
A server plugin typically consists in one or more callbacks packed into objects called QgsServerFilter
.
Each QgsServerFilter
implements one or all of the following callbacks:
- requestReady ()
- responseComplete ()
- sendResponse ()
The following example implements a minimal filter which prints HelloServer! in case the SERVICE
parameter equals to “HELLO”.
from qgis.server import * from qgis.core import * class HelloFilter(QgsServerFilter): def __init__(self, serverIface): super(HelloFilter, self).__init__(serverIface) def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap() if params.get('SERVICE', '').upper() == 'HELLO': request.clearHeaders() request.setHeader('Content-type', 'text/plain') request.clearBody() request.appendBody('HelloServer!')
The filters must be registered into the serverIface
as in the following example:
class HelloServerServer: def __init__(self, serverIface): # Save reference to the QGIS server interface self.serverIface = serverIface serverIface.registerFilter( HelloFilter, 100 )
The second parameter of registerFilter
allows to set a priority which defines the order for the callbacks with the same name (the lower priority is invoked first).
Full control over the flow
By using the three callbacks, plugins can manipulate the input and/or the output of the server in many different ways. In every moment, the plugin instance has access to the QgsRequestHandler through the QgsServerInterface, the QgsRequestHandler
has plenty of methods that can be used to alter the input parameters before entering the core processing of the server (by using requestReady
) or after the request has been processed by the core services (by using sendResponse
).
The following examples cover some common use cases:
Modifying the input
The example plugin contains a test example that changes input parameters coming from the query string, in this example a new parameter is injected into the (already parsed) parameterMap
, this parameter is then visible by core services (WMS
etc.), at the end of core services processing we check that the parameter is still there.
from qgis.server import * from qgis.core import * class ParamsFilter(QgsServerFilter): def __init__(self, serverIface): super(ParamsFilter, self).__init__(serverIface) def requestReady(self): request = self.serverInterface().requestHandler() params = request.parameterMap( ) request.setParameter('TEST_NEW_PARAM', 'ParamsFilter') def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap( ) if params.get('TEST_NEW_PARAM') == 'ParamsFilter': QgsMessageLog.logMessage("SUCCESS - ParamsFilter.responseComplete", 'plugin', QgsMessageLog.INFO) else: QgsMessageLog.logMessage("FAIL - ParamsFilter.responseComplete", 'plugin', QgsMessageLog.CRITICAL)
This is an extract of what you see in the log file:
src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloServerServer - loading filter ParamsFilter src/core/qgsmessagelog.cpp: 45: (logMessage) [1ms] 2014-12-12T12:39:29 Server[0] Server plugin HelloServer loaded! src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 Server[0] Server python plugins loaded src/mapserver/qgsgetrequesthandler.cpp: 35: (parseInput) [0ms] query string is: SERVICE=HELLO&request=GetOutput src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [1ms] inserting pair SERVICE // HELLO into the parameter map src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [0ms] inserting pair REQUEST // GetOutput into the parameter map src/mapserver/qgsserverfilter.cpp: 42: (requestReady) [0ms] QgsServerFilter plugin default requestReady called src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.requestReady src/mapserver/qgis_map_serv.cpp: 235: (configPath) [0ms] Using default configuration file path: /home/xxx/apps/bin/admin.sld src/mapserver/qgshttprequesthandler.cpp: 49: (setHttpResponse) [0ms] Checking byte array is ok to set... src/mapserver/qgshttprequesthandler.cpp: 59: (setHttpResponse) [0ms] Byte array looks good, setting response... src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.responseComplete src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] SUCCESS - ParamsFilter.responseComplete src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] RemoteConsoleFilter.responseComplete src/mapserver/qgshttprequesthandler.cpp: 158: (sendResponse) [0ms] Sending HTTP response src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloFilter.sendResponse
On line 13 the “SUCCESS” string indicates that the plugin passed the test.
The same technique can be exploited to use a custom service instead of a core one: you could for example skip a WFS SERVICE
request or any other core request just by changing the SERVICE
parameter to something different and the core service will be skipped, then you can inject your custom results into the output and send them to the client (this is explained here below).
Changing or replacing the output
The watermark filter example shows how to replace the WMS output with a new image obtained by adding a watermark image on the top of the WMS image generated by the WMS core service:
import os from qgis.server import * from qgis.core import * from PyQt4.QtCore import * from PyQt4.QtGui import * class WatermarkFilter(QgsServerFilter): def __init__(self, serverIface): super(WatermarkFilter, self).__init__(serverIface) def responseComplete(self): request = self.serverInterface().requestHandler() params = request.parameterMap( ) # Do some checks if (request.parameter('SERVICE').upper() == 'WMS' \ and request.parameter('REQUEST').upper() == 'GETMAP' \ and not request.exceptionRaised() ): QgsMessageLog.logMessage("WatermarkFilter.responseComplete: image ready %s" % request.infoFormat(), 'plugin', QgsMessageLog.INFO) # Get the image img = QImage() img.loadFromData(request.body()) # Adds the watermark watermark = QImage(os.path.join(os.path.dirname(__file__), 'media/watermark.png')) p = QPainter(img) p.drawImage(QRect( 20, 20, 40, 40), watermark) p.end() ba = QByteArray() buffer = QBuffer(ba) buffer.open(QIODevice.WriteOnly) img.save(buffer, "PNG") # Set the body request.clearBody() request.appendBody(ba)
In this example the SERVICE
parameter value is checked and if the incoming request is a WMS
GETMAP
and no exceptions have been set by a previously executed plugin or by the core service (WMS
in this case), the WMS
generated image is retrieved from the output buffer and the watermark image is added. The final step is to clear the output buffer and replace it with the newly generated image. Please note that in a real-world situation we should also check for the requested image type instead of returning PNG in any case.
The power of python
The examples above are just meant to explain how to interact with QGIS server python bindings but server plugins have full access to all QGIS python bindings and to thousands of python libraries, what you can do with python server plugins is just limited by your imagination!
See all QGIS Server related posts