20. Servidor QGIS y Python

20.1. Introducción

Para aprender mas sobre QGIS Server, lee Guía/Manual de Servidor QGIS.

QGIS Server es tres cosas diferentes:

  1. Biblioteca de QGIS Server: una biblioteca que proporciona una API para crear servicios web OGC

  2. QGIS Server FCGI: una aplicación binaria FCGI qgis_maserv.fcgi que junto con el servidor web implementa un conjunto de servicios OGC (WMS, WFS, WCS etc.) y OGC APIs (WFS3/OAPIF)

  3. QGIS Development Server: una aplicación binaria de servidor de desarrollo qgis_mapserver que implementa un conjunto de servicios OGC (WMS, WFS, WCS, etc.) y API de OGC (WFS3 / OAPIF)

Este capítulo del libro de cocina se centra en el primer tema y, al explicar el uso de la API de QGIS Server, muestra cómo es posible utilizar Python para ampliar, mejorar o personalizar el comportamiento del servidor o cómo utilizar la API de QGIS Server para incrustar el servidor QGIS en otra aplicación.

Hay algunas formas diferentes en las que puede alterar el comportamiento de QGIS Server o ampliar sus capacidades para ofrecer nuevos servicios personalizados o API, estos son los principales escenarios a los que puede enfrentarse:

  • EMBEDDING → Utilice la API de QGIS Server desde otra aplicación de Python

  • STANDALONE → Ejecutar QGIS Server como servicio autónomo WSGI/HTTP

  • FILTERS → Mejorar/personalizar QGIS Server con complementos de filtro

  • SERVICES → Añadir un nuevo SERVICE

  • OGC APIs → Añadir una nueva OGC API

Las aplicaciones integradas e independientes requieren el uso de la API de Python de QGIS Server directamente desde otro script o aplicación de Python. Las opciones restantes son más adecuadas para cuando desee agregar funciones personalizadas a una aplicación binaria estándar de QGIS Server (FCGI o servidor de desarrollo): en este caso, deberá escribir un complemento de Python para la aplicación del servidor y registrar sus filtros personalizados, servicios o API.

20.2. Conceptos básicos de la API del servidor

Las clases fundamentales involucradas en una aplicación típica de QGIS Server son:

El flujo de trabajo del servidor de desarrollo o FCGI de QGIS Server se puede resumir de la siguiente manera:

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

Dentro del métodod QgsServer.handleRequest(request, response) las devoluciones de llamada de los complementos de filtro se llaman y QgsServerRequest y QgsServerResponse están disponibles para los complementos a través de la clase QgsServerInterface

Advertencia

Las clases de servidor de QGIS no son seguras para subprocesos, siempre debe usar un modelo o contenedores de multiprocesamiento cuando cree aplicaciones escalables basadas en la API de QGIS Server.

20.3. Independiente o incrustado

Para aplicaciones de servidor independientes o incrustaciones, deberá usar las clases de servidor mencionadas anteriormente directamente, envolviéndolas en una implementación de servidor web que gestiona todas las interacciones del protocolo HTTP con el cliente.

A continuación, se muestra un ejemplo mínimo del uso de la API de QGIS Server (sin la parte HTTP):

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

Aquí hay un ejemplo completo de aplicación independiente desarrollado para las pruebas de integraciones continuas en el repositorio de código fuente de QGIS, muestra un amplio conjunto de diferentes filtros de complementos y esquemas de autenticación (no es para producción porque se desarrollaron solo con fines de prueba, pero sigue siendo interesante para el aprendizaje): https://github.com/qgis/QGIS/blob/release-3_22/tests/src/python/qgis_wrapped_server.py

20.4. Complementos del servidor

Los complementos de Python del servidor se cargan una vez cuando se inicia la aplicación QGIS Server y se pueden usar para registrar filtros, servicios o API.

La estructura de un complemento de servidor es muy similar a su contraparte de escritorio, un objeto QgsServerInterface está disponible para los complementos y los complementos pueden registrar uno o más filtros personalizados, servicios o API en el registro correspondiente mediante uno de los métodos expuestos por la interfaz del servidor.

20.4.1. Complementos de filtro de servidor

Los filtros vienen en tres sabores diferentes y se pueden instanciar subclasificando una de las clases a continuación y llamando al método correspondiente de QgsServerInterface:

Tipo de filtro

Clase Base

Registro de QgsServerInterface

E/S

QgsServerFilter

registerFilter()

Control de acceso

QgsAccessControlFilter

registerAccessControl()

Cache

QgsServerCacheFilter

registerServerCache()

20.4.1.1. Filtros de E/S

Los filtros de E/S pueden modificar la entrada y salida del servidor (la solicitud y la respuesta) de los servicios centrales (WMS, WFS, etc.) permitiendo realizar cualquier tipo de manipulación del flujo de trabajo de los servicios. Es posible, por ejemplo, restringir el acceso a las capas seleccionadas, inyectar una hoja de estilo XSL a la respuesta XML, agregar una marca de agua a una imagen WMS generada, etc.

A partir de este punto, encontrará útil un vistazo rápido a server plugins API docs.

Cada filtro debe implementar al menos una de las tres devoluciones de llamada:

Todos los filtros tienen acceso al objeto de solicitud/respuesta (QgsRequestHandler) y puede manipular todas sus propiedades (entrada/salida) y generar excepciones (aunque de una manera bastante particular como veremos a continuación).

Aquí está el pseudocódigo que muestra cómo el servidor maneja una solicitud típica y cuándo se llaman las devoluciones de llamada del filtro:

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

Los siguientes párrafos describen las devoluciones de llamada disponibles en detalle.

20.4.1.1.1. requestReady

Esto se llama cuando la solicitud está lista: la URL entrante y los datos se han analizado y antes de ingresar al conmutador de servicios centrales (WMS, WFS, etc.), este es el punto donde puede manipular la entrada y realizar acciones como:

  • autenticación/autorización

  • redirije

  • añadir/borrar ciertos parámetros (nombres de tipos, por ejemplo)

  • plantear excepciones

Incluso podría sustituir un servicio central por completo cambiando el parámetro SERVICE y, por lo tanto, omitiendo el servicio central por completo (aunque esto no tiene mucho sentido).

20.4.1.1.2. sendResponse

Esto se llama cada vez que se envía cualquier salida a FCGI stdout (y desde allí, al cliente). Esto normalmente se hace después de que los servicios centrales hayan terminado su proceso y después de que se haya llamado al enlace responseComplete, pero en algunos casos XML puede volverse tan grande que se necesita una implementación de XML de transmisión (WFS GetFeature es uno de ellos). En ese caso, en lugar de una sola llamada a sendResponse(), el método se puede llamar excepcionalmente varias veces antes de que la respuesta esté completa, y en ese caso (y solo en ese caso) también se llama antes responseComplete().

sendResponse() es el mejor lugar para la manipulación directa de la salida del servicio principal y mientras responseComplete() normalmente también es una opción, sendResponse() es la única opción viable en el caso de los servicios de transmisión.

20.4.1.1.3. responseComplete

Esto se llama una vez cuando los servicios centrales (si se activan) terminan su proceso y la solicitud está lista para enviarse al cliente. Como se discutió anteriormente, esto normalmente llama antes a sendResponse() excepto para servicios streaming (u otro filtro de complemento) que podría haber llamado antes a sendResponse().

responseComplete() es el lugar ideal para proporcionar la implementación de nuevos servicios (WPS o servicios personalizados) y para realizar la manipulación directa de la salida proveniente de los servicios centrales (por ejemplo, para agregar una marca de agua en una imagen WMS).

20.4.1.2. Generación de excepciones de un complemento

Aún queda trabajo por hacer en este tema: la implementación actual puede distinguir entre excepciones manejadas y no manejadas estableciendo una propiedad QgsRequestHandler a una instancia de QgsMapServiceException, de esta manera el código principal de C ++ puede detectar excepciones de Python controladas e ignorar las excepciones no controladas (o mejor: registrarlas).

Este enfoque básicamente funciona, pero no es muy «pitónico»: un mejor enfoque sería generar excepciones del código de Python y verlas burbujear en el bucle C ++ para que se manejen allí.

20.4.1.3. Escribiendo un complemento del servidor

Un complemento de servidor es un complemento estándar de QGIS Python como se describe en Desarrollando Plugins Python, que solo proporciona una interfaz adicional (o alternativa): un complemento de escritorio QGIS típico tiene acceso a la aplicación QGIS a través de: class:` QgisInterface <qgis. gui.QgisInterface> , un complemento de servidor solo tiene acceso a :class:`QgsServerInterface <qgis.server.QgsServerInterface> cuando se ejecuta dentro del contexto de la aplicación QGIS Server.

Para que QGIS Server sepa que un complemento tiene una interfaz de servidor, se necesita una entrada de metadatos especial (en metadata.txt):

server=True

Importante

QGIS Server solo cargará y ejecutará los complementos que tengan el conjunto de metadatos server=True.

El complemento de ejemplo discutido aquí (con muchos más) está disponible en github en https://github.com/elpaso/qgis3-server-vagrant/tree/master/resources/web/plugins, algunos complementos de servidor también se publican en el repositorio oficial de complementos de QGIS <https://plugins.qgis.org/plugins/server>`_.

20.4.1.4. Archivos de complementos

Aquí está la estructura de directorios de nuestro complemento de servidor de ejemplo.

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

Este archivo es requerido por el sistema de importación de Python. Además, QGIS Server requiere que este archivo contenga una función serverClassFactory(), que se llama cuando el complemento se carga en QGIS Server cuando se inicia el servidor. Recibe una referencia a la instancia de QgsServerInterface y debe devolver la instancia de la clase de su complemento. Así es como se ve el complemento de ejemplo __init __. Py:

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

Aquí es donde ocurre la magia y así es como se ve la magia: (por ejemplo HelloServer.py)

Un complemento de servidor generalmente consiste en una o más devoluciones de llamada empaquetadas en instancias de QgsServerFilter.

Cada QgsServerFilter implementa una o más de las siguientes devoluciones de llamada:

El siguiente ejemplo implementa un filtro mínimo que imprime HelloServer! En caso de que el parámetro SERVICE sea igual a «HELLO»:

 1class HelloFilter(QgsServerFilter):
 2
 3    def __init__(self, serverIface):
 4        super().__init__(serverIface)
 5
 6    def requestReady(self):
 7        QgsMessageLog.logMessage("HelloFilter.requestReady")
 8
 9    def sendResponse(self):
10        QgsMessageLog.logMessage("HelloFilter.sendResponse")
11
12    def responseComplete(self):
13        QgsMessageLog.logMessage("HelloFilter.responseComplete")
14        request = self.serverInterface().requestHandler()
15        params = request.parameterMap()
16        if params.get('SERVICE', '').upper() == 'HELLO':
17            request.clear()
18            request.setResponseHeader('Content-type', 'text/plain')
19            # Note that the content is of type "bytes"
20            request.appendBody(b'HelloServer!')

Los filtros deben registrarse en serverIface como en el siguiente ejemplo:

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

El segundo parámetro del método registerFilter() establece una prioridad que define el orden de las devoluciones de llamada con el mismo nombre (la prioridad más baja se invoca primero).

Al utilizar las tres devoluciones de llamada, los complementos pueden manipular la entrada y / o la salida del servidor de muchas formas diferentes. En todo momento, la instancia del complemento tiene acceso a QgsRequestHandler a través de QgsServerInterface. La clase QgsRequestHandler tiene muchos métodos que se pueden usar para modificar los parámetros de entrada antes de ingresar al procesamiento central del servidor (usando requestReady()) o después de la solicitud ha sido procesado por los servicios centrales (usando sendResponse()).

Los siguientes ejemplos cubren algunos casos comunes de uso:

20.4.1.4.3. Modificando la entrada

El complemento de ejemplo contiene un ejemplo de prueba que cambia los parámetros de entrada provenientes de la cadena de consulta, en este ejemplo se inyecta un nuevo parámetro en el (ya analizado) parameterMap, este parámetro es visible luego por los servicios centrales (WMS, etc.) , al final del procesamiento de los servicios centrales, verificamos que el parámetro todavía esté allí:

 1class ParamsFilter(QgsServerFilter):
 2
 3    def __init__(self, serverIface):
 4        super(ParamsFilter, self).__init__(serverIface)
 5
 6    def requestReady(self):
 7        request = self.serverInterface().requestHandler()
 8        params = request.parameterMap( )
 9        request.setParameter('TEST_NEW_PARAM', 'ParamsFilter')
10
11    def responseComplete(self):
12        request = self.serverInterface().requestHandler()
13        params = request.parameterMap( )
14        if params.get('TEST_NEW_PARAM') == 'ParamsFilter':
15            QgsMessageLog.logMessage("SUCCESS - ParamsFilter.responseComplete")
16        else:
17            QgsMessageLog.logMessage("FAIL    - ParamsFilter.responseComplete")

Esto es un extracto de lo que puede ver en el archivo de log:

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: (requestReady) [0ms] QgsServerFilter plugin default requestReady called
6 src/core/qgsmessagelog.cpp: 45: (logMessage) [0ms] 2014-12-12T12:39:29 plugin[0] SUCCESS - ParamsFilter.responseComplete

En la línea resaltada, la cadena «SUCCESS» indica que el complemento pasó la prueba.

La misma técnica se puede aprovechar para usar un servicio personalizado en lugar de uno central: por ejemplo, podría omitir una solicitud SERVICE WFS o cualquier otra solicitud principal simplemente cambiando el parámetro SERVICE a algo diferente y se omitirá el servicio principal. Luego, puede inyectar sus resultados personalizados en la salida y enviarlos al cliente (esto se explica a continuación).

Truco

Si realmente desea implementar un servicio personalizado, se recomienda crear una subclase QgsService y registre su servicio en registerFilter() llamando a su método registerService(service)

20.4.1.4.4. Modificar o reemplazar la salida

El ejemplo del filtro de marca de agua muestra cómo reemplazar la salida de WMS con una nueva imagen obtenida agregando una imagen de marca de agua en la parte superior de la imagen de WMS generada por el servicio principal de WMS:

 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 responseComplete(self):
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.responseComplete: 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)

En este ejemplo, el valor del parámetro SERVICE se verifica y si la solicitud entrante es un WMS GETMAP y no se han establecido excepciones por un complemento ejecutado previamente o por el servicio central (WMS en este caso), la imagen generada por WMS se recupera del búfer de salida y se agrega la imagen de marca de agua. El último paso es borrar el búfer de salida y reemplazarlo con la imagen recién generada. Tenga en cuenta que, en una situación del mundo real, también deberíamos comprobar el tipo de imagen solicitada en lugar de admitir solo PNG o JPG.

20.4.1.5. Filtros de control de acceso

Los filtros de control de acceso brindan al desarrollador un control detallado sobre las capas, características y atributos a los que se puede acceder; las siguientes devoluciones de llamada se pueden implementar en un filtro de control de acceso:

20.4.1.5.1. Archivos de complementos

Aquí está la estructura de directorios de nuestro complemento de ejemplo:

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

Este archivo es requerido por el sistema de importación de Python. Como para todos los complementos del servidor QGIS, este archivo contiene una función serverClassFactory (), que se llama cuando el complemento se carga en el servidor QGIS al inicio. Recibe una referencia a una instancia de QgsServerInterface y debe devolver una instancia de la clase de su complemento. Así es como el complemento de ejemplo __init__.py aparece:

def serverClassFactory(serverIface):
    from MyAccessControl.AccessControl import AccessControlServer
    return AccessControlServer(serverIface)
20.4.1.5.3. AccessControl.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 ejemplo otorga acceso total para todos.

Es rol del complemento saber quién ha ingresado.

En todos esos métodos tenemos el argumento layer on para poder personalizar la restricción por capa.

20.4.1.5.4. layerFilterExpression

Usado para agregar una Expresión para limitar los resultados, ej.:

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

Para limitar la función donde el rol de atributo es igual a «usuario».

20.4.1.5.5. layerFilterSubsetString

Igual que el anterior pero usa el SubsetString (ejecutado en la base de datos)

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

Para limitar la función donde el rol de atributo es igual a «usuario».

20.4.1.5.6. layerPermissions

Limitar el acceso a la capa.

Devuelve un objeto de tipo LayerPermissions(), el cuál tiene las propiedades:

  • canRead para verlo en GetCapabilities y tiene acceso de lectura.

  • canInsert para poder insertar una nueva característica.

  • canUpdate para ser capaz de actualizar una característica.

  • canDelete para ser capaz de borrar una característica.

Ejemplo:

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

Para limitar todo en acceso de solo lectura.

20.4.1.5.7. authorizedLayerAttributes

Usado para limitar la visibilidad de un subconjunto específico de atributo.

El atributo del argumento devuelve el conjunto actual de atributos visibles.

Ejemplo:

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

Para ocultar el atributo “rol”.

20.4.1.5.8. allowToEdit

Esto es usado para limitar la edición de un subconjunto de objetos espaciales.

Se utiliza en el protocolo WFS-Transaction.

Ejemplo:

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

Para poder editar solo la característica que tiene el rol de atributo con el valor del usuario.

20.4.1.5.9. cacheKey

El servidor QGIS mantiene un caché de las capacidades y luego, para tener un caché por rol, puede devolver el rol en este método. O devuelva Ninguno para deshabilitar completamente la caché.

20.4.2. Servicios personalizados

En QGIS Server, los servicios centrales como WMS, WFS y WCS se implementan como subclases de QgsService.

Para implementar un nuevo servicio que se ejecutará cuando el parámetro de la cadena de consulta SERVICE coincida con el nombre del servicio, puede implementar su propio QgsService y registre sus servicio en el serviceRegistry() llamando a su registerService(service).

A continuación, se muestra un ejemplo de un servicio personalizado denominado 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. APIs personalizadas

En QGIS Server, las API principales de OGC como OAPIF (también conocido como WFS3) se implementan como colecciones de QgsServerOgcApiHandler subclases que están registradas en una instancia de QgsServerOgcApi).

Para implementar una nueva API que se ejecutará cuando la ruta de la URL coincida con una URL determinada, puede implementar sus propias instancias QgsServerOgcApiHandler, agréguelas a una QgsServerOgcApi y registre la API en el serviceRegistry() llamando a su registerApi(api).

A continuación, se muestra un ejemplo de una API personalizada que se ejecutará cuando la URL contenga /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)