Important

Translation is a community effort you can join. This page is currently translated at 67.97%.

20. QGIS server et Python

20.1. Introduction

Pour en savoir plus sur QGIS Server, lisez le Guide/manuel de QGIS Server.

QGIS Server, c’est trois choses différentes :

  1. Bibliothèque QGIS Server : une bibliothèque qui fournit une API pour la création de services web OGC

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

  3. Developpement QGIS Server : une application binaire de serveur de développement qgis_mapserver qui implémente un ensemble de services OGC (WMS, WFS, WCS etc.) et des API OGC (WFS3/OAPIF)

Ce chapitre du livre de cuisine se concentre sur le premier sujet et, en expliquant l’utilisation de l’API QGIS Server, il montre comment il est possible d’utiliser Python pour étendre, améliorer ou personnaliser le comportement du serveur ou comment utiliser l’API QGIS Server pour intégrer QGIS Server dans une autre application.

Il existe plusieurs façons de modifier le comportement de QGIS Server ou d’étendre ses capacités pour offrir de nouveaux services ou API personnalisés, voici les principaux scénarios auxquels vous pouvez être confrontés :

  • EMBEDDING → Utiliser l’API QGIS Server depuis une autre application Python

  • STANDALONE → Exécuter QGIS Server comme un service WSGI/HTTP autonome

  • FILTRES → Améliorer/personnaliser QGIS Server avec des plugins de filtrage

  • SERVICES → Ajouter un nouveau SERVICE

  • OGC APIs → Ajouter une nouvelle API OGC

Les applications intégrées ou autonomes nécessitent l’utilisation de l’API Python de QGIS Server directement à partir d’un autre script ou application Python. Les autres options sont mieux adaptées lorsque vous souhaitez ajouter des fonctionnalités personnalisées à une application binaire standard de QGIS Server (FCGI ou serveur de développement) : dans ce cas, vous devrez écrire un plugin Python pour l’application serveur et enregistrer vos filtres, services ou API personnalisés.

20.2. Principes de base de l’API du serveur

Les classes fondamentales impliquées dans une application typique de QGIS Server sont les suivantes :

Le flux de travail QGIS Server FCGI ou serveur de développement peut être résumé comme suit :

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

Dans la méthode QgsServer.handleRequest(request, response) les callbacks des plugins de filtre sont appelés et QgsServerRequest et QgsServerResponse sont mis à la disposition des plugins par le biais de la QgsServerInterface.

Avertissement

Les classes QGIS Server ne sont pas sûres pour les threads, vous devez toujours utiliser un modèle de multitraitement ou des conteneurs lorsque vous construisez des applications évolutives basées sur l’API du serveur QGIS.

20.3. Autonome ou intégré

Pour les applications serveur autonomes ou integre, vous devrez utiliser directement les classes de serveur mentionnées ci-dessus, en les intégrant dans une implémentation de serveur web qui gère toutes les interactions du protocole HTTP avec le client.

Voici un exemple minimal d’utilisation de l’API QGIS Server (sans la partie 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()

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. Plugins de serveur

Les plugins python du serveur sont chargés une fois lorsque l’application QGIS Server démarre et peuvent être utilisés pour enregistrer des filtres, des services ou des API.

La structure d’un plugin serveur est très similaire à son homologue de bureau, un objet QgsServerInterface est mis à la disposition des plugins et ceux-ci peuvent enregistrer un ou plusieurs filtres, services ou API personnalisés dans le registre correspondant en utilisant une des méthodes exposées par l’interface serveur.

20.4.1. Plugins pour filtres de serveur

Les filtres existent en trois possibilite différentes et peuvent être instanciés en sous-classant l’une des classes ci-dessous et en appelant la méthode correspondante de QgsServerInterface :

Type de filtre

Classe de base

Enregistrement de QgsServerInterface

I/O

QgsServerFilter

registerFilter()

Contrôle d’accès

QgsAccessControlFilter

registerAccessControl()

Cache

QgsServerCacheFilter

registerServerCache()

20.4.1.1. Filtres I/O

Les filtres I/O peuvent modifier l’entrée et la sortie du serveur (la demande et la réponse) des services de base (WMS, WFS, etc.), ce qui permet d’effectuer tout type de manipulation du flux de travail des services. Il est possible, par exemple, de restreindre l’accès à des couches sélectionnées, d’injecter une feuille de style XSL dans la réponse XML, d’ajouter un filigrane à une image WMS générée, etc.

A partir de là, il vous sera peut-être utile de jeter un coup d’oeil rapide à l’API des plugins server.

Chaque filtre doit mettre en œuvre au moins un des trois rappels :

Tous les filtres ont accès à l’objet requête/réponse (QgsRequestHandler) et peuvent manipuler toutes ses propriétés (entrée/sortie) et lever des exceptions (mais d’une manière assez particulière comme nous le verrons plus loin).

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.

Voici le pseudo-code montrant comment le serveur traite une requête typique et quand les rappels du filtre sont appelés :

 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

Les paragraphes qui suivent décrivent les fonctions de rappel disponibles en détails.

20.4.1.1.1. onRequestReady

Cette fonction est appelée lorsque la requête est prêt: l’URL entrante et ses données ont été analysées et juste avant de passer la main aux services principaux (WMS, WFS, etc.), c’est le point où vous pouvez manipuler l’entrée et dérouler des actions telles que:

  • l’authentification/l’autorisation

  • les redirections

  • l’ajout/suppression de certains paramètres (les noms de type par exemple)

  • le déclenchement d’exceptions

Vous pouvez également substituer l’intégralité d’un service principal en modifiant le paramètre SERVICE et complètement outrepasser le service (ce qui n’a pas beaucoup d’intérêt).

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. Lever les exceptions d’un plugin

Un certain travail reste à faire sur ce sujet : l’implémentation actuelle peut distinguer les exceptions gérées et non gérées en définissant une propriété QgsRequestHandler à une instance de QgsMapServiceException, de cette façon le code C++ principal peut attraper les exceptions python gérées et ignorer les exceptions non gérées (ou mieux : les enregistrer).

Cette approche fonctionne globalement mais elle n’est pas très « pythonesque »: une meilleure approche consisterait à déclencher des exceptions depuis le code Python et les faire remonter dans la boucle principale C++ pour y être traitées.

20.4.1.1.5. Écriture d’une extension serveur

Un plugin serveur est un plugin Python QGIS standard tel que décrit dans Développer des extensions Python, qui fournit juste une interface supplémentaire (ou alternative) : un plugin de bureau QGIS typique a accès à l’application QGIS par le biais de la QgisInterface, un plugin serveur a seulement accès à une QgsServerInterface lorsqu’il est exécuté dans le contexte de l’application QGIS Server.

Pour que QGIS serveur sache qu’un plugin a une interface serveur, une entrée spéciale de métadonnées est nécessaire (dans metadata.txt) :

server=True

Important

Seuls les plugins qui ont le jeu de métadonnées server=True seront chargés et exécutés par 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. Fichiers de l’extension

Vous pouvez voir ici la structure du répertoire de notre exemple d’extension pour serveur.

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

Ce fichier est requis par le système d’importation de Python. De plus, QGIS Server exige que ce fichier contienne une fonction serverClassFactory(), qui est appelée lorsque le plugin est chargé dans QGIS Server au démarrage du serveur. Elle reçoit une référence à l’instance de QgsServerInterface et doit retourner une instance de la classe de votre plugin. Voici à quoi ressemble le plugin d’exemple __init__.py :

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

C’est l’endroit où tout se passe et voici à quoi il devrait ressembler : (ex. HelloServer.py)

Un plugin serveur consiste généralement en un ou plusieurs callbacks regroupés dans les instances d’un QgsServerFilter.

Chaque QgsServerFilter implémente un ou plusieurs des callbacks suivants :

L’exemple qui suit implémente un filtre minimaliste qui affiche HelloServer! pour le cas où le paramètre SERVICE vaut “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

Les filtres doivent être enregistrés dans la serverIface comme dans l’exemple suivant :

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

Le second paramètre de registerFilter() fixe une priorité qui définit l’ordre des rappels ayant le même nom (la priorité la plus basse est invoquée en premier).

En utilisant les trois rappels, les plugins peuvent manipuler l’entrée et/ou la sortie du serveur de nombreuses manières différentes. À chaque instant, l’instance du plugin a accès à la QgsRequestHandler par le biais de la QgsServerInterface. La classe QgsRequestHandler a de nombreuses méthodes qui peuvent être utilisées pour modifier les paramètres d’entrée avant d’entrer dans le traitement de base du serveur (en utilisant requestReady()) ou après que la requête ait été traitée par les services de base (en utilisant sendResponse()).

Les exemples suivants montrent quelques cas d’utilisation courants :

20.4.1.1.5.2. Modifier la couche en entrée

L’exemple de plugin contient un exemple de test qui modifie les paramètres d’entrée provenant de la chaîne de requête, dans cet exemple un nouveau paramètre est injecté dans le « parameterMap » (déjà analysé), ce paramètre est ensuite visible par les services centraux (WMS etc.), à la fin du traitement des services centraux nous vérifions que le paramètre est toujours là :

 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

Ceci est un extrait de ce que vous pouvez voir dans le fichier 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: (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

Sur la ligne en surbrillance, la chaîne « SUCCESS » indique que le plugin a réussi le test.

La même technique peut être employée pour utiliser un service personnalisé à la place d’un service principal : vous pouvez par exemple sauter une requête WFS SERVICE ou n’importe quelle requête principale en modifiant le paramètre SERVICE par quelque-chose de différent et le service principal ne serait alors pas lancé. Vous pourrez ensuite injecter vos résultats personnalisés dans la sortie et les renvoyer au client (ceci est expliqué ci-dessous).

Astuce

Si vous voulez vraiment implémenter un service personnalisé, il est recommandé de sous-classer QgsService et d’enregistrer votre service sur registerFilter() en appelant son registerService(service)

20.4.1.1.5.3. Modifier ou remplacer la couche en sortie

L’exemple du filtre de filigrane montre comment remplacer la sortie WMS avec une nouvelle image obtenue par l’ajout d’un filigrane plaqué sur l’image WMS générée par le service principal 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 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

Dans cet exemple, la valeur du paramètre SERVICE est vérifiée et si la demande entrante est un WMS GETMAP et qu’aucune exception n’a été définie par un plugin exécuté précédemment ou par le service central (WMS dans ce cas), l’image générée par le WMS est récupérée dans le tampon de sortie et l’image en filigrane est ajoutée. L’étape finale consiste à effacer le tampon de sortie et à le remplacer par l’image nouvellement générée. Veuillez noter que dans une situation réelle, nous devons également vérifier le type d’image demandé au lieu de supporter uniquement les PNG ou JPG.

20.4.1.2. Filtres de contrôle d’accès

Les filtres de contrôle d’accès donnent au développeur un contrôle fin sur les couches, les entités et les attributs auxquels il peut accéder, les rappels suivants peuvent être mis en œuvre dans un filtre de contrôle d’accès :

20.4.1.2.1. Fichiers de l’extension

Voici la structure des répertoires de notre exemple 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

Ce fichier est requis par le système d’importation de Python. Comme pour tous les plugins QGIS Server , ce fichier contient une fonction serverClassFactory(), qui est appelée lorsque le plugin est chargé dans QGIS Server au démarrage. Elle reçoit une référence à une instance de QgsServerInterface et doit retourner une instance de la classe de votre plugin. Voici à quoi ressemble le plugin d’exemple __init__.py :

def serverClassFactory(serverIface):
    from MyAccessControl.AccessControl import AccessControlServer
    return AccessControlServer(serverIface)
20.4.1.2.1.2. 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)

Cet exemple donne un accès total à tout le monde.

C’est le rôle de l’extension de connaître qui est connecté dessus.

Pour toutes ces méthodes nous avons la couche passée en argument afin de personnaliser la restriction par couche.

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

Comme le point précédent mais utilise SubsetString (exécuté au niveau de la base de données).

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

Limiter l’accès à la couche.

Retourne un objet de type LayerPermissions(), qui a les propriétés :

  • canRead voir dans le GetCapabilities et acces lecture seule.

  • canInsert pour pouvoir insérer une nouvelle entité .

  • canUpdate pour pouvoir mettre à jour une entité.

  • canDelete pour pouvoir supprimer une entité.

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.5. authorizedLayerAttributes

Utilisé pour limiter la visibilité d’un sous-groupe d’attribut spécifique.

L’argument attributes renvoie la liste des attributs réellement visibles.

For example, to hide the role attribute:

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

Il permet de limiter l’édition à un sous-ensemble d’entités.

Il est utilisé dans le protocole WFS-Transaction.

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. Services personnalisés

Dans QGIS Server, les services de base tels que WMS, WFS et WCS sont implémentés en tant que sous-classes de 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. API personnalisées

Dans QGIS Server, les API OGC de base telles que OAPIF (alias WFS3) sont implémentées sous forme de collections de QgsServerOgcApiHandler sous-classes qui sont enregistrées dans une instance de QgsServerOgcApi (ou sa classe parente 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).

Voici un exemple d’API personnalisée qui sera exécutée lorsque l’URL contient « /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)