19. QGIS Server and Python

19.1. はじめに

QGIS Server is three different things:

  1. QGIS Server library: a library that provides an API for creating OGC web services

  2. QGIS Server FCGI: a FCGI binary application qgis_maserv.fcgi that together with a web server implements a set of OCG services (WMS, WFS, WCS etc.) and OGC APIs (WFS3/OAPIF)

  3. QGIS Development Server: a development server binary application qgis_mapserver that implements a set of OCG services (WMS, WFS, WCS etc.) and OGC APIs (WFS3/OAPIF)

This chapter of the cookbook focuses on the first topic and by explaining the usage of QGIS Server API it shows how it is possible to use Python to extend, enhance or customize the server behavior or how to use the QGIS Server API to embed QGIS server into another application.

There are a few different ways you can alter the behavior of QGIS Server or extend its capabilities to offer new custom services or APIs, these are the main scenarios you may face:

  • EMBEDDING → Use QGIS Server API from another Python application

  • STANDALONE → Run QGIS Server as a standlone WSGI/HTTP service

  • FILTERS → Enhance/Customize QGIS Server with filter plugins

  • SERVICES → Add a new SERVICE

  • OGC APIs → Add a new OGC API

Embeding and standalone applications require using the QGIS Server Python API directly from another Python script or application while the remaining options are better suited for when you want to add custom features to a standard QGIS Server binary application (FCGI or development server): in this case you'll need to write a Python plugin for the server application and register your custom filters, services or APIs.

19.2. Server API basics

The fundamental classes involved in a typical QGIS Server application are:

The QGIS Server FCGI or development server workflow can be summarized as follows:

1
2
3
4
5
6
7
8
9
initialize the QgsApplication
create the QgsServer
the main server loop waits forever for client requests:
    for each incoming request:
        create a QgsServerRequest request
        create a QgsServerResponse response
        call QgsServer.handleRequest(request, response)
            filter plugins may be executed
        send the output to the client

Inside the QgsServer.handleRequest(request, response) method the filter plugins callbacks are called and QgsServerRequest and QgsServerResponse are made available to the plugins through the QgsServerInterface.

警告

QGIS server classes are not thread safe, you should always use a multiprocessing model or containers when building scalable applications based on QGIS Server API.

19.3. Standalone or embedding

For standalone server applications or embedding, you will need to use the above mentioned server classes directly, wrapping them up into a web server implementation that manages all the HTTP protocol interactions with the client.

A minimal example of the QGIS Server API usage (without the HTTP part) follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from qgis.core import QgsApplication
from qgis.server import *
app = QgsApplication([], False)

# Create the server instance, it may be a single one that
# is reused on multiple requests
server = QgsServer()

# Create the request by specifying the full URL and an optional body
# (for example for POST requests)
request = QgsBufferServerRequest(
    'http://localhost:8081/?MAP=/qgis-server/projects/helloworld.qgs' +
    '&SERVICE=WMS&REQUEST=GetCapabilities')

# Create a response objects
response = QgsBufferServerResponse()

# Handle the request
server.handleRequest(request, response)

print(response.headers())
print(response.body().data().decode('utf8'))

app.exitQgis()

Here is a complete standalone application example developed for the continuous integrations testing on QGIS source code repository, it showcases a wide set of different plugin filters and authentication schemes (not mean for production because they were developed for testing purposes only but still interesting for learning):

https://github.com/qgis/QGIS/blob/master/tests/src/python/qgis_wrapped_server.py

19.4. Server plugins

Server python plugins are loaded once when the QGIS Server application starts and can be used to register filters, services or APIs.

The structure of a server plugin is very similar to their desktop counterpart, a QgsServerInterface object is made available to the plugins and the plugins can register one or more custom filters, services or APIs to the corresponding registry by using one of the methods exposed by the server interface.

19.4.1. Server filter plugins

Filters come in three different flavors and they can be instanciated by subclassing one of the classes below and by calling the corresponding method of QgsServerInterface:

Filter Type

Base Class

QgsServerInterface registration

I/O

QgsServerFilter

registerFilter

Access Control

QgsAccessControlFilter

registerAccessControl

Cache

QgsServerCacheFilter

registerServerCache

19.4.1.1. I/O filters

I/O filters can modify the server input and output (the request and the response) of the core services (WMS, WFS etc.) allowing to do any kind of manipulation of the services workflow, it is possible for example to restrict the access to selected layers, to inject an XSL stylesheet to the XML response, to add a watermark to a generated WMS image and so on.

From this point, you might find useful a quick look to the server plugins API docs.

Each filter should implement at least one of three callbacks:

All filters have access to the request/response object (QgsRequestHandler) and can manipulate all its properties (input/output) and raise exceptions (while in a quite particular way as we’ll see below).

Here is the pseudo code showing how the server handles a typical request and when the filter’s callbacks are called:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
for each incoming request:
    create GET/POST request handler
    pass request to an instance of QgsServerInterface
    call requestReady filters
    if there is not a response:
        if SERVICE is WMS/WFS/WCS:
            create WMS/WFS/WCS service
            call service’s executeRequest
                possibly call sendResponse for each chunk of bytes
                sent to the client by a streaming services (WFS)
        call responseComplete
        call sendResponse
    request handler sends the response to the client

次の段落では、利用可能なコールバックを詳細に説明します。

19.4.1.1.1. requestReady

要求の準備ができたときに呼び出されます。受信URLとデータが解析され、コアサービス(WMS、WFSなど)スイッチに入る前に、これは入力を操作するなどのアクションを実行できるポイントです。

  • 認証/認可

  • リダイレクト

  • 特定のパラメーター(例えば、型名)を追加/除去

  • 例外を発生させる

SERVICE パラメーターを変更することでコアサービスを完全に置き換え、それによりコアサービスを完全にバイパスすることさえできるかもしれません(とはいえ、これはあまり意味がないということ)。

19.4.1.1.2. sendResponse

This is called whenever any output is sent to FCGI stdout (and from there, to the client), this is normally done after core services have finished their process and after responseComplete hook was called, but in a few cases XML can become so huge that a streaming XML implementation was needed (WFS GetFeature is one of them), in this case, sendResponse is called multiple times before the response is complete (and before responseComplete is called). The obvious consequence is that sendResponse is normally called once but might be exceptionally called multiple times and in that case (and only in that case) it is also called before responseComplete.

sendResponse is the best place for direct manipulation of core service’s output and while responseComplete is typically also an option, sendResponse is the only viable option in case of streaming services.

19.4.1.1.3. responseComplete

This is called once when core services (if hit) finish their process and the request is ready to be sent to the client. As discussed above, this is normally called before sendResponse except for streaming services (or other plugin filters) that might have called sendResponse earlier.

responseComplete is the ideal place to provide new services implementation (WPS or custom services) and to perform direct manipulation of the output coming from core services (for example to add a watermark upon a WMS image).

19.4.1.2. Raising exceptions from a plugin

Some work has still to be done on this topic: the current implementation can distinguish between handled and unhandled exceptions by setting a QgsRequestHandler property to an instance of QgsMapServiceException, this way the main C++ code can catch handled python exceptions and ignore unhandled exceptions (or better: log them).

このアプローチは、基本的に動作しますが、それは非常に「パイソン的」ではありません:より良いアプローチは、Pythonコードから例外を発生し、それらがそこで処理されるためにC ++ループに湧き上がるのを見ることでしょう。

19.4.1.3. サーバー・プラグインを書く

A server plugin is a standard QGIS Python plugin as described in Pythonプラグインを開発する, that just provides an additional (or alternative) interface: a typical QGIS desktop plugin has access to QGIS application through the QgisInterface instance, a server plugin has only access to a QgsServerInterface when it is executed within the QGIS Server application context.

To make QGIS Server aware that a plugin has a server interface, a special metadata entry is needed (in metadata.txt)

server=True

重要

Only plugins that have the server=True metadata set will be loaded and executed by QGIS Server.

The example plugin discussed here (with many more) is available on github at https://github.com/elpaso/qgis3-server-vagrant/tree/master/resources/web/plugins, a few server plugins are also published in the official QGIS plugins repository.

19.4.1.4. プラグインファイル

私たちの例のサーバー・プラグインのディレクトリ構造はこちらです

1
2
3
4
5
PYTHON_PLUGINS_PATH/
  HelloServer/
    __init__.py    --> *required*
    HelloServer.py  --> *required*
    metadata.txt   --> *required*
19.4.1.4.1. __init__.py

This file is required by Python's import system. Also, QGIS Server requires that this file contains a serverClassFactory() function, which is called when the plugin gets loaded into QGIS Server when the server starts. It receives reference to instance of QgsServerInterface and must return instance of your plugin's class. This is how the example plugin __init__.py looks like

def serverClassFactory(serverIface):
    from .HelloServer import HelloServerServer
    return HelloServerServer(serverIface)
19.4.1.4.2. HelloServer.py

魔法が起こると、これは魔法がどのように見えるかであるところである:(例 HelloServer.py

A server plugin typically consists in one or more callbacks packed into instances of a QgsServerFilter.

Each QgsServerFilter implements one or more of the following callbacks:

The following example implements a minimal filter which prints HelloServer! in case the SERVICE parameter equals to “HELLO”

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class HelloFilter(QgsServerFilter):

    def __init__(self, serverIface):
        super().__init__(serverIface)

    def requestReady(self):
        QgsMessageLog.logMessage("HelloFilter.requestReady")

    def sendResponse(self):
        QgsMessageLog.logMessage("HelloFilter.sendResponse")

    def responseComplete(self):
        QgsMessageLog.logMessage("HelloFilter.responseComplete")
        request = self.serverInterface().requestHandler()
        params = request.parameterMap()
        if params.get('SERVICE', '').upper() == 'HELLO':
            request.clear()
            request.setResponseHeader('Content-type', 'text/plain')
            # Note that the content is of type "bytes"
            request.appendBody(b'HelloServer!')

The filters must be registered into the serverIface as in the following example:

class HelloServerServer:
    def __init__(self, serverIface):
        serverIface.registerFilter(HelloFilter(), 100)

The second parameter of registerFilter sets a priority which defines the order for the callbacks with the same name (the lower priority is invoked first).

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 class 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()).

次の例は、いくつかの一般的なユースケースをカバーします:

19.4.1.4.3. 入力を変更する

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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")
        else:
            QgsMessageLog.logMessage("FAIL    - ParamsFilter.responseComplete")

これは、ログファイルに見るものの抽出物である:

1
2
3
4
5
6
 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/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [1ms] inserting pair SERVICE // HELLO 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] SUCCESS - ParamsFilter.responseComplete

強調表示された行の「SUCCESS」の文字列は、プラグインがテストに合格したことを示しています。

同じ手法が、コアのサービスでなくカスタムサービスを利用するために利用できます:たとえば WFS SERVICE 要求または任意の他のコア要求を SERVICE パラメーターを別の何かに変更するだけでスキップできます、そしてコアサービスはスキップされ、それからカスタム結果を出力に注入してそれらをクライアントに送信できます(これはここで以下に説明される)。

ちなみに

If you really want to implement a custom service it is recommended to subclass QgsService and register your service on registerFilter by calling its registerService(service)

19.4.1.4.4. 出力を変更または置き換えする

透かしフィルタの例は、WMSコアサービスによって作成されたWMS画像の上に透かし画像を加算した新たな画像でWMS出力を置き換える方法を示しています:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from qgis.server import *
from qgis.PyQt.QtCore import *
from qgis.PyQt.QtGui import *

class WatermarkFilter(QgsServerFilter):

    def __init__(self, serverIface):
        super().__init__(serverIface)

    def responseComplete(self):
        request = self.serverInterface().requestHandler()
        params = request.parameterMap( )
        # Do some checks
        if (params.get('SERVICE').upper() == 'WMS' \
                and params.get('REQUEST').upper() == 'GETMAP' \
                and not request.exceptionRaised() ):
            QgsMessageLog.logMessage("WatermarkFilter.responseComplete: image ready %s" % request.parameter("FORMAT"))
            # 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" if "png" in request.parameter("FORMAT") else "JPG")
            # 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 supporting PNG or JPG only.

19.4.1.5. Access control filters

Access control filters gives the developer a fine-grained control over which layers, features and attributes can be accessed, the following callbacks can be implemented in an access control filter:

19.4.1.5.1. プラグインファイル

Here's the directory structure of our example plugin:

1
2
3
4
5
PYTHON_PLUGINS_PATH/
  MyAccessControl/
    __init__.py    --> *required*
    AccessControl.py  --> *required*
    metadata.txt   --> *required*
19.4.1.5.2. __init__.py

This file is required by Python's import system. As for all QGIS server plugins, this file contains a serverClassFactory() function, which is called when the plugin gets loaded into QGIS Server at startup. It receives a reference to an instance of QgsServerInterface and must return an instance of your plugin's class. This is how the example plugin __init__.py looks like:

def serverClassFactory(serverIface):
    from MyAccessControl.AccessControl import AccessControlServer
    return AccessControlServer(serverIface)
19.4.1.5.3. AccessControl.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class AccessControlFilter(QgsAccessControlFilter):

    def __init__(self, server_iface):
        super().__init__(server_iface)

    def layerFilterExpression(self, layer):
        """ Return an additional expression filter """
        return super().layerFilterExpression(layer)

    def layerFilterSubsetString(self, layer):
        """ Return an additional subset string (typically SQL) filter """
        return super().layerFilterSubsetString(layer)

    def layerPermissions(self, layer):
        """ Return the layer rights """
        return super().layerPermissions(layer)

    def authorizedLayerAttributes(self, layer, attributes):
        """ Return the authorised layer attributes """
        return super().authorizedLayerAttributes(layer, attributes)

    def allowToEdit(self, layer, feature):
        """ Are we authorise to modify the following geometry """
        return super().allowToEdit(layer, feature)

    def cacheKey(self):
        return super().cacheKey()

class AccessControlServer:

   def __init__(self, serverIface):
      """ Register AccessControlFilter """
      serverIface.registerAccessControl(AccessControlFilter(self.serverIface), 100)

この例では全員に完全なアクセス権を与えています。

誰がログオンしているかを知るのはこのプラグインの役割です。

これらすべての方法で私達は、レイヤーごとの制限をカスタマイズできるようにするには、引数のレイヤーを持っています。

19.4.1.5.4. layerFilterExpression

結果を制限するために式を追加するために使用し、例えば:

def layerFilterExpression(self, layer):
    return "$role = 'user'"

属性の役割が「ユーザー」に等しい地物に制限するため。

19.4.1.5.5. layerFilterSubsetString

以前よりも同じですが、(データベース内で実行) SubsetString を使用

def layerFilterSubsetString(self, layer):
    return "role = 'user'"

属性の役割が「ユーザー」に等しい地物に制限するため。

19.4.1.5.6. layerPermissions

レイヤーへのアクセスを制限します。

Return an object of type LayerPermissions, which has the properties:

  • canRead to see it in the GetCapabilities and have read access.

  • canInsert to be able to insert a new feature.

  • canUpdate to be able to update a feature.

  • canDelete to be able to delete a feature.

例:

1
2
3
4
5
def layerPermissions(self, layer):
    rights = QgsAccessControlFilter.LayerPermissions()
    rights.canRead = True
    rights.canInsert = rights.canUpdate = rights.canDelete = False
    return rights

読み取り専用のアクセスのすべてを制限します。

19.4.1.5.7. authorizedLayerAttributes

属性の特定のサブセットの可視性を制限するために使用します。

引数の属性が表示属性の現在のセットを返します。

例:

def authorizedLayerAttributes(self, layer, attributes):
    return [a for a in attributes if a != "role"]

「role」属性を非表示にします。

19.4.1.5.8. allowToEdit

これは、地物のサブセットに編集を制限するために使用されます。

これは、 WFS-Transaction プロトコルで使用されています。

例:

def allowToEdit(self, layer, feature):
    return feature.attribute('role') == 'user'

値「user」の属性「role」を持つ地物だけを編集できます。

19.4.1.5.9. cacheKey

QGISサーバーは、このメソッド中に役割を返すことができる役割ごとにキャッシュを持っている能力のキャッシュを維持します。または None を返し、完全にキャッシュを無効にします。

19.4.2. Custom services

In QGIS Server, core services such as WMS, WFS and WCS are implemented as subclasses of QgsService.

To implemented a new service that will be executed when the query string parameter SERVICE matches the service name, you can implemented your own QgsService and register your service on the serviceRegistry by calling its registerService(service).

Here is an example of a custom service named CUSTOM:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from qgis.server import QgsService
from qgis.core import QgsMessageLog

class CustomServiceService(QgsService):

    def __init__(self):
        QgsService.__init__(self)

    def name(self):
        return "CUSTOM"

    def version(self):
        return "1.0.0"

    def allowMethod(method):
        return True

    def executeRequest(self, request, response, project):
        response.setStatusCode(200)
        QgsMessageLog.logMessage('Custom service executeRequest')
        response.write("Custom service executeRequest")


class CustomService():

    def __init__(self, serverIface):
        serverIface.serviceRegistry().registerService(CustomServiceService())

19.4.3. Custom APIs

In QGIS Server, core OGC APIs such OAPIF (aka WFS3) are implemented as collections of QgsServerOgcApiHandler subclasses that are registered to an instance of QgsServerOgcApi (or it's parent class QgsServerApi).

To implemented a new API that will be executed when the url path matches a certain URL, you can implemented your own QgsServerOgcApiHandler instances, add them to an QgsServerOgcApi and register the API on the serviceRegistry by calling its registerApi(api).

Here is an example of a custom API that will be executed when the URL contains /customapi:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import json
import os

from qgis.PyQt.QtCore import QBuffer, QIODevice, QTextStream, QRegularExpression
from qgis.server import (
    QgsServiceRegistry,
    QgsService,
    QgsServerFilter,
    QgsServerOgcApi,
    QgsServerQueryStringParameter,
    QgsServerOgcApiHandler,
)

from qgis.core import (
    QgsMessageLog,
    QgsJsonExporter,
    QgsCircle,
    QgsFeature,
    QgsPoint,
    QgsGeometry,
)


class CustomApiHandler(QgsServerOgcApiHandler):

    def __init__(self):
        super(CustomApiHandler, self).__init__()
        self.setContentTypes([QgsServerOgcApi.HTML, QgsServerOgcApi.JSON])

    def path(self):
        return QRegularExpression("/customapi")

    def operationId(self):
        return "CustomApiXYCircle"

    def summary(self):
        return "Creates a circle around a point"

    def description(self):
        return "Creates a circle around a point"

    def linkTitle(self):
        return "Custom Api XY Circle"

    def linkType(self):
        return QgsServerOgcApi.data

    def handleRequest(self, context):
        """Simple Circle"""

        values = self.values(context)
        x = values['x']
        y = values['y']
        r = values['r']
        f = QgsFeature()
        f.setAttributes([x, y, r])
        f.setGeometry(QgsCircle(QgsPoint(x, y), r).toCircularString())
        exporter = QgsJsonExporter()
        self.write(json.loads(exporter.exportFeature(f)), context)

    def templatePath(self, context):
        # The template path is used to serve HTML content
        return os.path.join(os.path.dirname(__file__), 'circle.html')

    def parameters(self, context):
        return [QgsServerQueryStringParameter('x', True, QgsServerQueryStringParameter.Type.Double, 'X coordinate'),
                QgsServerQueryStringParameter(
                    'y', True, QgsServerQueryStringParameter.Type.Double, 'Y coordinate'),
                QgsServerQueryStringParameter('r', True, QgsServerQueryStringParameter.Type.Double, 'radius')]


class CustomApi():

    def __init__(self, serverIface):
        api = QgsServerOgcApi(serverIface, '/customapi',
                            'custom api', 'a custom api', '1.1')
        handler = CustomApiHandler()
        api.registerHandler(handler)
        serverIface.serviceRegistry().registerApi(api)