19. Servidor QGIS e Python

19.1. Introdução

O Servidor QGIS é três coisas diferentes:

  1. Biblioteca do Servidor QGIS: uma biblioteca que fornece uma API para criar serviços da web OGC

  2. Servidor QGIS FCGI: um aplicativo binário FCGI qgis_maserv.fcgi que, juntamente com um servidor web, implementa um conjunto de serviços OCG (WMS, WFS, WCS etc.) e APIs OGC (WFS3/OAPIF)

  3. Servidor de Desenvolvimento QGIS: um aplicativo binário do servidor de desenvolvimento qgis_mapserver que implementa um conjunto de serviços OCG (WMS, WFS, WCS etc.) e APIs OGC (WFS3/OAPIF)

Este capítulo do livro de receitas se concentra no primeiro tópico e, ao explicar o uso da API do Servidor QGIS, mostra como é possível usar o Python para estender, aprimorar ou personalizar o comportamento do servidor ou como usar a API do Servidor QGIS para incorporar o servidor QGIS ao outra aplicação.

Existem algumas maneiras diferentes de alterar o comportamento do Servidor QGIS ou estender seus recursos para oferecer novos serviços ou APIs personalizados; estes são os principais cenários que você pode encontrar:

  • EMBEDDING → Usa a API do Servidor QGIS de outro aplicativo Python

  • STANDALONE → Executa o Servidor QGIS como um serviço WSGI/HTTP independente

  • FILTERS → Aprimora/personaliza o Servidor QGIS com complementos de filtro

  • SERVICES → Adiciona um novo SERVICE

  • OGC APIs → Adiciona um novo OGC API

Aplicativos de incorporação e independentes exigem o uso da API Python do Servidor QGIS diretamente de outro script ou aplicativo Python, enquanto as opções restantes são mais adequadas para quando você deseja adicionar recursos personalizados a um aplicativo binário padrão do Servidor QGIS (FCGI ou servidor de desenvolvimento): neste caso você precisará escrever um complemento Python para o aplicativo do servidor e registrar seus filtros, serviços ou APIs personalizados.

19.2. Noções básicas da API do servidor

As classes fundamentais envolvidas em um aplicativo típico do Servidor QGIS são:

  • QgsServer a instância do servidor (geralmente uma instância única para toda a vida do aplicativo)

  • :class:`QgsServerRequest <qgis.server.QgsServerRequest>`o objeto de solicitação (normalmente recriado em cada solicitação)

  • QgsServerResponse o objeto de resposta (normalmente recriado em cada solicitação)

  • QgsServer.handleRequest(request, response) processa a solicitação e preenche a resposta

O Servidor QGIS FCGI ou o fluxo de trabalho do servidor de desenvolvimento pode ser resumido da seguinte maneira:

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

Dentro do método QgsServer.handleRequest (request, response), os callbacks dos complementos de filtro são chamados e QgsServerRequest e QgsServerResponse são disponibilizados para os complementos através da QgsServerInterface.

Aviso

As classes de servidor QGIS não são seguras para threads, você sempre deve usar um modelo ou contêineres de multiprocessamento ao criar aplicativos escaláveis ​​com base na API do Servidor QGIS.

19.3. Independente ou incorporado

Para servidor autônomo ou aplicativos integrados, você precisará usar as classes de servidor mencionadas acima diretamente, agrupando-as em uma implementação de servidor da Web que gerencia todas as interações do protocolo HTTP com o cliente.

Um exemplo mínimo do uso da API do Servidor QGIS (sem a parte HTTP) é a seguir:

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

Aqui está um exemplo de aplicativo autônomo completo desenvolvido para o teste de integração contínua no repositório de código-fonte QGIS, ele mostra um amplo conjunto de filtros de complementos diferentes e esquemas de autenticação (não significa para produção porque eles foram desenvolvidos apenas para fins de teste, mas ainda interessantes para aprendizagem) :

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

19.4. Complementos do servidor

Os complementos de servidor python são carregados uma vez quando o aplicativo Servidor QGIS é iniciado e podem ser usados ​​para registrar filtros, serviços ou APIs.

A estrutura de um complemento de servidor é muito semelhante à sua contraparte na área de trabalho, um objeto QgsServerInterface é disponibilizado para os complemento e os complemento podem registrar um ou mais filtros, serviços ou APIs personalizados para o registro correspondente usando um dos métodos expostos pela interface do servidor.

19.4.1. Complementos de filtro de servidor

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:

Tipo de Filtro

Classe Básica

Registro da QgsServerInterface

I/O

QgsServerFilter

registerFilter

Controle de Acesso

QgsAccessControlFilter

registerAccessControl

Cache

QgsServerCacheFilter

registerServerCache

19.4.1.1. Filtros I/O

Os filtros I/O podem modificar a entrada e saída do servidor (a solicitação e a resposta) dos serviços principais (WMS, WFS etc.), permitindo fazer qualquer tipo de manipulação do fluxo de trabalho dos serviços; é possível, por exemplo, restringir o acesso nas camadas selecionadas, injetar uma folha de estilo XSL na resposta XML, adicionar uma marca d’água a uma imagem WMS gerada e assim por diante.

A partir deste ponto, você pode achar útil uma rápida olhada no server plugins API docs.

Cada filtro deve implementar pelo menos um dos três retornos de chamada:

Todos os filtros têm acesso ao objeto de solicitação/resposta (QgsRequestHandler) e podem manipular todas as suas propriedades (entrada/saída) e gerar exceções (embora de uma maneira bastante específica, como iremos ver abaixo).

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

The following paragraphs describe the available callbacks in details.

19.4.1.1.1. requestReady

This is called when the request is ready: incoming URL and data have been parsed and before entering the core services (WMS, WFS etc.) switch, this is the point where you can manipulate the input and perform actions like:

  • autenticação/autorização

  • redirects

  • add/remove certain parameters (typenames for example)

  • raise exceptions

You could even substitute a core service completely by changing SERVICE parameter and hence bypassing the core service completely (not that this make much sense though).

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 é o melhor local para manipulação direta da saída do serviço principal e enquanto responseComplete tipicamente também é uma opção, sendResponse é a única opção viável no caso de serviços de streaming.

19.4.1.1.3. responseComplete

Isso é chamado uma vez quando os serviços principais (se atingidos) concluem o processo e a solicitação está pronta para ser enviada ao cliente. Como discutido acima, isso normalmente é chamado antes sendResponse exceto para serviços de streaming (ou outros filtros de complementos) que podem ter chamado :meth:`sendResponse <qgis.server.QgsServerFilter.sendResponse> `anteriormente.

responseComplete é o local ideal para fornecer a implementação de novos serviços (WPS ou serviços personalizados) e executar a manipulação direta da saída proveniente dos serviços principais (por exemplo, para adicionar uma marca d’água em uma imagem WMS).

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

This approach basically works but it is not very “pythonic”: a better approach would be to raise exceptions from python code and see them bubbling up into C++ loop for being handled there.

19.4.1.3. Escrevendo um complemento de servidor

A server plugin is a standard QGIS Python plugin as described in Desenvolvendo complementos 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

Importante

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. Arquivos de complementos

Aqui está a estrutura de diretórios do nosso complemento de servidor de exemplo

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

This is where the magic happens and this is how magic looks like: (e.g. 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()).

The following examples cover some common use cases:

19.4.1.4.3. 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:

 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")

This is an extract of what you see in the log file:

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

On the highlighted line 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).

Dica

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. Modifying 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:

 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. Arquivos de complementos

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

Este arquivo é requerido pelo sistema de importação do Python. Como para todos os complementos de servidor QGIS, este arquivo contém uma função serverClassFactory(), chamada quando o complemento é carregado no QGIS Server na inicialização. Ele recebe uma referência a uma instância de QgsServerInterface e deve retornar uma instância da classe do seu complemento. É assim que o exemplo do complemento __init__.py se parece com:

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)

This example gives a full access for everybody.

É papel do complemento saber quem está conectado.

On all those methods we have the layer on argument to be able to customise the restriction per layer.

19.4.1.5.4. layerFilterExpression

Usado para adicionar uma Expressão para limitar os resultados, por exemplo:

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

To limit on feature where the attribute role is equals to “user”.

19.4.1.5.5. layerFilterSubsetString

Same than the previous but use the SubsetString (executed in the database)

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

To limit on feature where the attribute role is equals to “user”.

19.4.1.5.6. layerPermissions

Limit the access to the layer.

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.

Exemplo:

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

To limit everything on read only access.

19.4.1.5.7. authorizedLayerAttributes

Used to limit the visibility of a specific subset of attribute.

The argument attribute return the current set of visible attributes.

Exemplo:

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

To hide the ‘role’ attribute.

19.4.1.5.8. allowToEdit

This is used to limit the editing on a subset of features.

It is used in the WFS-Transaction protocol.

Exemplo:

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

To be able to edit only feature that has the attribute role with the value user.

19.4.1.5.9. cacheKey

QGIS server maintain a cache of the capabilities then to have a cache per role you can return the role in this method. Or return None to completely disable the cache.

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)