20. Servidor QGIS e Python
20.1. Introdução
Para saber mais sobre o Servidor QGIS, leia o:ref:QGIS-Server-manual.
O Servidor QGIS é três coisas diferentes:
Biblioteca do Servidor QGIS: uma biblioteca que fornece uma API para criar serviços da web OGC
QGIS Server FCGI: a FCGI binary application
qgis_mapserv.fcgi
that together with a web server implements a set of OGC services (WMS, WFS, WCS etc.) and OGC APIs (WFS3/OAPIF)QGIS Development Server: a development server binary application
qgis_mapserver
that implements a set of OGC services (WMS, WFS, WCS etc.) and OGC APIs (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 → Run QGIS Server as a standalone WSGI/HTTP service
FILTERS → Aprimora/personaliza o Servidor QGIS com complementos de filtro
SERVICES → Adiciona um novo SERVICE
OGC APIs → Adiciona um novo OGC API
Embedding and standalone applications require using the QGIS Server Python API directly from another Python script or application. 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.
20.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)
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:
1initialize the QgsApplication
2create the QgsServer
3the main server loop waits forever for client requests:
4 for each incoming request:
5 create a QgsServerRequest request
6 create a QgsServerResponse response
7 call QgsServer.handleRequest(request, response)
8 filter plugins may be executed
9 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
class.
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.
20.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:
1from qgis.core import QgsApplication
2from qgis.server import *
3app = QgsApplication([], False)
4
5# Create the server instance, it may be a single one that
6# is reused on multiple requests
7server = QgsServer()
8
9# Create the request by specifying the full URL and an optional body
10# (for example for POST requests)
11request = QgsBufferServerRequest(
12 'http://localhost:8081/?MAP=/qgis-server/projects/helloworld.qgs' +
13 '&SERVICE=WMS&REQUEST=GetCapabilities')
14
15# Create a response objects
16response = QgsBufferServerResponse()
17
18# Handle the request
19server.handleRequest(request, response)
20
21print(response.headers())
22print(response.body().data().decode('utf8'))
23
24app.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): qgis_wrapped_server.py
20.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.
20.4.1. Complementos de filtro de servidor
Os filtros vêm em três sabores diferentes e podem ser instanciados subclassificando uma das classes abaixo e chamando o método correspondente de QgsServerInterface
:
Tipo de Filtro |
Classe Básica |
Registro da QgsServerInterface |
I/O |
||
Controle de Acesso |
||
Cache |
20.4.1.1. Filtros I/O
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.
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).
All these methods return a boolean value indicating if the call should be propagated to the subsequent
filters. If one of these method returns False
then the chain stop, otherwise the call will propagate
to the next filter.
Here is the pseudo code showing how the server handles a typical request and when the filter’s callbacks are called:
1for each incoming request:
2 create GET/POST request handler
3 pass request to an instance of QgsServerInterface
4 call onRequestReady filters
5
6 if there is not a response:
7 if SERVICE is WMS/WFS/WCS:
8 create WMS/WFS/WCS service
9 call service’s executeRequest
10 possibly call onSendResponse for each chunk of bytes
11 sent to the client by a streaming services (WFS)
12 call onResponseComplete
13 request handler sends the response to the client
Os parágrafos seguintes descrevem em detalhes as funções de retorno disponíveis.
20.4.1.1.1. onRequestReady
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
redireciona
adicionar/remover certos parâmetros (nomes de tipo, por exemplo)
levantar exceções
Você pode até substituir completamente um serviço principal alterando o parâmetro SERVIÇO e, portanto, ignorar completamente o serviço principal (não que isso faça muito sentido).
20.4.1.1.2. onSendResponse
This is called whenever any partial output is flushed from response buffer (i.e to FCGI stdout
if the fcgi server is used) and from there, to the client.
This occurs when huge content is streamed (like WFS GetFeature). In this case
onSendResponse()
may be called multiple times.
Note that if the response is not streamed, then onSendResponse()
will not be called at all.
In all case, the last (or unique) chunk will be sent to client after a call to
onResponseComplete()
.
Returning False
will prevent flushing of data to the client. This is desirable when a plugin
wants to collect all chunks from a response and examine or change the response in
onResponseComplete()
.
20.4.1.1.3. onResponseComplete
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 method will be called before the last (or unique) chunk of
data is sent to the client.
For streaming services, multiple calls to onSendResponse()
might have been called.
onResponseComplete()
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).
Note that returning False
will prevent the next plugins to execute
onResponseComplete()
but, in any case, prevent response to be sent to the client.
20.4.1.1.4. 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.
20.4.1.1.5. 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 qgis3-server-vagrant example plugin discussed here (with many more) is available on github, a few server plugins are also published in the official QGIS plugins repository.
20.4.1.1.5.1. Arquivos de complementos
Aqui está a estrutura de diretórios do nosso exemplo de plugin de servidor.
1PYTHON_PLUGINS_PATH/
2 HelloServer/
3 __init__.py --> *required*
4 HelloServer.py --> *required*
5 metadata.txt --> *required*
20.4.1.1.5.1.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)
20.4.1.1.5.1.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”:
1class HelloFilter(QgsServerFilter):
2
3 def __init__(self, serverIface):
4 super().__init__(serverIface)
5
6 def onRequestReady(self) -> bool:
7 QgsMessageLog.logMessage("HelloFilter.onRequestReady")
8 return True
9
10 def onSendResponse(self) -> bool:
11 QgsMessageLog.logMessage("HelloFilter.onSendResponse")
12 return True
13
14 def onResponseComplete(self) -> bool:
15 QgsMessageLog.logMessage("HelloFilter.onResponseComplete")
16 request = self.serverInterface().requestHandler()
17 params = request.parameterMap()
18 if params.get('SERVICE', '').upper() == 'HELLO':
19 request.clear()
20 request.setResponseHeader('Content-type', 'text/plain')
21 # Note that the content is of type "bytes"
22 request.appendBody(b'HelloServer!')
23 return True
The filters must be registered into the serverIface as in the following example:
class HelloServerServer:
def __init__(self, serverIface):
serverIface.registerFilter(HelloFilter(serverIface), 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()
).
Os exemplos a seguir cobrem alguns casos de uso comuns:
20.4.1.1.5.2. Modificando a entrada
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:
1class ParamsFilter(QgsServerFilter):
2
3 def __init__(self, serverIface):
4 super(ParamsFilter, self).__init__(serverIface)
5
6 def onRequestReady(self) -> bool:
7 request = self.serverInterface().requestHandler()
8 params = request.parameterMap( )
9 request.setParameter('TEST_NEW_PARAM', 'ParamsFilter')
10 return True
11
12 def onResponseComplete(self) -> bool:
13 request = self.serverInterface().requestHandler()
14 params = request.parameterMap( )
15 if params.get('TEST_NEW_PARAM') == 'ParamsFilter':
16 QgsMessageLog.logMessage("SUCCESS - ParamsFilter.onResponseComplete")
17 else:
18 QgsMessageLog.logMessage("FAIL - ParamsFilter.onResponseComplete")
19 return True
Este é um extrato do que você vê no arquivo de registro:
1 src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] HelloServerServer - loading filter ParamsFilter
2 src/core/qgsmessagelog.cpp: 45: (logMessage) [1ms] 2014-12-12T12:39:29 Server[0] Server plugin HelloServer loaded!
3 src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 Server[0] Server python plugins loaded
4 src/mapserver/qgshttprequesthandler.cpp: 547: (requestStringToParameterMap) [1ms] inserting pair SERVICE // HELLO into the parameter map
5 src/mapserver/qgsserverfilter.cpp: 42: (onRequestReady) [0ms] QgsServerFilter plugin default onRequestReady called
6 src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] SUCCESS - ParamsFilter.onResponseComplete
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 below).
Dica
Se você realmente deseja implementar um serviço personalizado, é recomendado subclasse QgsService
e registrar seu serviço em registerFilter()
chamando seu registerService(service)
20.4.1.1.5.3. Modificando ou substituindo a saída
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:
1from qgis.server import *
2from qgis.PyQt.QtCore import *
3from qgis.PyQt.QtGui import *
4
5class WatermarkFilter(QgsServerFilter):
6
7 def __init__(self, serverIface):
8 super().__init__(serverIface)
9
10 def onResponseComplete(self) -> bool:
11 request = self.serverInterface().requestHandler()
12 params = request.parameterMap( )
13 # Do some checks
14 if (params.get('SERVICE').upper() == 'WMS' \
15 and params.get('REQUEST').upper() == 'GETMAP' \
16 and not request.exceptionRaised() ):
17 QgsMessageLog.logMessage("WatermarkFilter.onResponseComplete: image ready %s" % request.parameter("FORMAT"))
18 # Get the image
19 img = QImage()
20 img.loadFromData(request.body())
21 # Adds the watermark
22 watermark = QImage(os.path.join(os.path.dirname(__file__), 'media/watermark.png'))
23 p = QPainter(img)
24 p.drawImage(QRect( 20, 20, 40, 40), watermark)
25 p.end()
26 ba = QByteArray()
27 buffer = QBuffer(ba)
28 buffer.open(QIODevice.WriteOnly)
29 img.save(buffer, "PNG" if "png" in request.parameter("FORMAT") else "JPG")
30 # Set the body
31 request.clearBody()
32 request.appendBody(ba)
33 return True
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.
20.4.1.2. Filtros de controle de acesso
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:
20.4.1.2.1. Arquivos de complementos
Aqui está a estrutura do diretório do nosso exemplo de plugin:
1PYTHON_PLUGINS_PATH/
2 MyAccessControl/
3 __init__.py --> *required*
4 AccessControl.py --> *required*
5 metadata.txt --> *required*
20.4.1.2.1.1. __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)
20.4.1.2.1.2. AcessoControle.py
1class AccessControlFilter(QgsAccessControlFilter):
2
3 def __init__(self, server_iface):
4 super().__init__(server_iface)
5
6 def layerFilterExpression(self, layer):
7 """ Return an additional expression filter """
8 return super().layerFilterExpression(layer)
9
10 def layerFilterSubsetString(self, layer):
11 """ Return an additional subset string (typically SQL) filter """
12 return super().layerFilterSubsetString(layer)
13
14 def layerPermissions(self, layer):
15 """ Return the layer rights """
16 return super().layerPermissions(layer)
17
18 def authorizedLayerAttributes(self, layer, attributes):
19 """ Return the authorised layer attributes """
20 return super().authorizedLayerAttributes(layer, attributes)
21
22 def allowToEdit(self, layer, feature):
23 """ Are we authorised to modify the following geometry """
24 return super().allowToEdit(layer, feature)
25
26 def cacheKey(self):
27 return super().cacheKey()
28
29class AccessControlServer:
30
31 def __init__(self, serverIface):
32 """ Register AccessControlFilter """
33 serverIface.registerAccessControl(AccessControlFilter(serverIface), 100)
Este exemplo dá acesso total para todos.
É papel do complemento saber quem está conectado.
Em todos esses métodos, temos a camada no argumento para poder personalizar a restrição por camada.
20.4.1.2.2. layerFilterExpression
Used to add an Expression to limit the results.
For example, to limit to features where the attribute role
is equal to user
.
def layerFilterExpression(self, layer):
return "$role = 'user'"
20.4.1.2.3. layerFilterSubsetString
Same than the previous but use the SubsetString
(executed in the database)
For example, to limit to features where the attribute role
is equal to user
.
def layerFilterSubsetString(self, layer):
return "role = 'user'"
20.4.1.2.4. layerPermissions
Limitar o acesso à camada.
Return an object of type LayerPermissions()
, which has the properties:
canRead
to see it in theGetCapabilities
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.
For example, to limit everything on read only access:
1def layerPermissions(self, layer):
2 rights = QgsAccessControlFilter.LayerPermissions()
3 rights.canRead = True
4 rights.canInsert = rights.canUpdate = rights.canDelete = False
5 return rights
20.4.1.2.6. allowToEdit
This is used to limit the editing on a subset of features.
It is used in the WFS-Transaction
protocol.
For example, to be able to edit only feature that has the attribute role
with the value user
:
def allowToEdit(self, layer, feature):
return feature.attribute('role') == 'user'
20.4.1.2.7. cacheKey
QGIS Server maintains 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.
20.4.2. Serviços personalizados
In QGIS Server, core services such as WMS, WFS and WCS are implemented as subclasses of
QgsService
.
To implement a new service that will be executed when the query string parameter SERVICE
matches the service name,
you can implement 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
:
1from qgis.server import QgsService
2from qgis.core import QgsMessageLog
3
4class CustomServiceService(QgsService):
5
6 def __init__(self):
7 QgsService.__init__(self)
8
9 def name(self):
10 return "CUSTOM"
11
12 def version(self):
13 return "1.0.0"
14
15 def executeRequest(self, request, response, project):
16 response.setStatusCode(200)
17 QgsMessageLog.logMessage('Custom service executeRequest')
18 response.write("Custom service executeRequest")
19
20
21class CustomService():
22
23 def __init__(self, serverIface):
24 serverIface.serviceRegistry().registerService(CustomServiceService())
20.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 implement a new API that will be executed when the url path matches a certain URL,
you can implement 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
:
1import json
2import os
3
4from qgis.PyQt.QtCore import QBuffer, QIODevice, QTextStream, QRegularExpression
5from qgis.server import (
6 QgsServiceRegistry,
7 QgsService,
8 QgsServerFilter,
9 QgsServerOgcApi,
10 QgsServerQueryStringParameter,
11 QgsServerOgcApiHandler,
12)
13
14from qgis.core import (
15 QgsMessageLog,
16 QgsJsonExporter,
17 QgsCircle,
18 QgsFeature,
19 QgsPoint,
20 QgsGeometry,
21)
22
23
24class CustomApiHandler(QgsServerOgcApiHandler):
25
26 def __init__(self):
27 super(CustomApiHandler, self).__init__()
28 self.setContentTypes([QgsServerOgcApi.HTML, QgsServerOgcApi.JSON])
29
30 def path(self):
31 return QRegularExpression("/customapi")
32
33 def operationId(self):
34 return "CustomApiXYCircle"
35
36 def summary(self):
37 return "Creates a circle around a point"
38
39 def description(self):
40 return "Creates a circle around a point"
41
42 def linkTitle(self):
43 return "Custom Api XY Circle"
44
45 def linkType(self):
46 return QgsServerOgcApi.data
47
48 def handleRequest(self, context):
49 """Simple Circle"""
50
51 values = self.values(context)
52 x = values['x']
53 y = values['y']
54 r = values['r']
55 f = QgsFeature()
56 f.setAttributes([x, y, r])
57 f.setGeometry(QgsCircle(QgsPoint(x, y), r).toCircularString())
58 exporter = QgsJsonExporter()
59 self.write(json.loads(exporter.exportFeature(f)), context)
60
61 def templatePath(self, context):
62 # The template path is used to serve HTML content
63 return os.path.join(os.path.dirname(__file__), 'circle.html')
64
65 def parameters(self, context):
66 return [QgsServerQueryStringParameter('x', True, QgsServerQueryStringParameter.Type.Double, 'X coordinate'),
67 QgsServerQueryStringParameter(
68 'y', True, QgsServerQueryStringParameter.Type.Double, 'Y coordinate'),
69 QgsServerQueryStringParameter('r', True, QgsServerQueryStringParameter.Type.Double, 'radius')]
70
71
72class CustomApi():
73
74 def __init__(self, serverIface):
75 api = QgsServerOgcApi(serverIface, '/customapi',
76 'custom api', 'a custom api', '1.1')
77 handler = CustomApiHandler()
78 api.registerHandler(handler)
79 serverIface.serviceRegistry().registerApi(api)