{{toc}} h1. Développement d’un connecteur avec Passerelle Passerelle est le module en charge de l'interconnexion de Publik vers les API d'autres applications; cette documentation concerne le développement d'un connecteur en utilisant Passerelle et s'appuie pour cela sur "l'installation d'un environnement de développement local":https://doc-publik.entrouvert.com/dev/installation-developpeur/. ⚠ Passerelle n'est pas l'unique possibilité d'interconnexion de Publik vers les autres applications, tous les échanges de Publik se font en HTTPS, cela permet à toute application tierce, en exploitant les actions, formats et API de Publik, d'interopérer directement avec celui-ci, sans connecteur particulier, ou à travers d'un module indépendant (ESB, proxy d'API externe, etc.). h2. Démarrage Il faut d'abord créer un répertoire qui contiendra le connecteur, appelons ce répertoire "passerelle-test" (@mkdir passerelle-test@). Dans ce répertoire (@cd passerelle-test@), il faut créer le module Python qui correspondra au connecteur, ici les tirets ne sont pas autorisés dans les noms, appelons-le donc "passerelle_test", un module Python est un simple répertoire (@mkdir passerelle_test@) contenant un fichier @__init__.py@, qui peut être vide (@touch passerelle_test/__init__.py@). On a alors cette arborescence de fichier :
passerelle-test/
passerelle-test/passerelle_test/
passerelle-test/passerelle_test/__init__.py   # fichier vide
h3. Code du connecteur Le code du connecteur se crée dans le fichier @passerelle-test/passerelle_test/models.py@ :
passerelle-test/
passerelle-test/passerelle_test/
passerelle-test/passerelle_test/__init__.py
passerelle-test/passerelle_test/models.py      # code du connecteur
Ce fichier @models.py@ décrit un "modèle Django":https://docs.djangoproject.com/fr/1.11/topics/db/models/, dans Passerelle on le fait hériter du modèle "BaseResource" qui contient déjà tout ce qui structure un connecteur :

from passerelle.base.models import BaseResource

class TestConnector(BaseResource):
    category = 'Divers'

    class Meta:
        verbose_name = 'Connecteur de test'
La catégorie où se range le connecteur se définit dans l'attribut @category@ et le nom du connecteur va lui dans les métadonnées du modèle. Très clairement ce connecteur ne fait encore rien mais il y a désormais assez de code pour l'instancier ; mais avant cela, il faut qu'il puisse être installé sur le système. h3. Installation du connecteur Pour que le connecteur puisse être installé, il faut créer un fichier @setup.py@ à la racine du projet (i.e. directement dans le répertoire @passerelle_test/@); son contenu peut être minimal :

#! /usr/bin/env python

from setuptools import setup, find_packages

setup(
    name='passerelle-test',
    author='John Doe',
    author_email='john.doe@example.net',
    url='http://example.net/',
    packages=find_packages(),
)
Le code d'installation exige la présence d'un fichier de documentation README, il doit donc être créé pour l'occasion (il peut très bien être vide, @touch README@). À ce stade, l'arborescence du projet est donc celle-ci :
passerelle-test/
passerelle-test/README
passerelle-test/setup.py
passerelle-test/passerelle_test/
passerelle-test/passerelle_test/__init__.py
passerelle-test/passerelle_test/models.py
Il est maintenant possible de lancer l'installation, en mode développement pour permettre à nos modifications ultérieures d'être prises en compte sans demander à chaque fois une réinstallation (/home/utilisateur/envs/publik-env-py3/bin/python setup.py develop@).
$ /home/utilisateur/envs/publik-env-py3/bin/python setup.py develop
running develop
running egg_info
writing passerelle_test.egg-info/PKG-INFO
writing top-level names to passerelle_test.egg-info/top_level.txt
writing dependency_links to passerelle_test.egg-info/dependency_links.txt
reading manifest file 'passerelle_test.egg-info/SOURCES.txt'
writing manifest file 'passerelle_test.egg-info/SOURCES.txt'
running build_ext
Creating /home/utilisateur/envs/publik-env-py3/lib/python3.7/site-packages/dist-packages/passerelle-test.egg-link (link to .)
Adding passerelle-test 0.0.0 to easy-install.pth file

Installed /home/test/passerelle-test
Processing dependencies for passerelle-test==0.0.0
Finished processing dependencies for passerelle-test==0.0.0
h3. Activation du connecteur Le connecteur a désormais tout le nécessaire pour être activé, il reste juste à le déclarer dans la configuration, pour cela il faut créer un fichier @/home/utilisateur/.config/publik/settings/passerelle/settings.d/connecteur.py@ (ou tout autre nom en @.py@), avec ces deux lignes :
INSTALLED_APPS += ('passerelle_test',)
TENANT_APPS += ('passerelle_test',)
h3. Migration initiale Django dispose d'un système automatique de "migration des données":https://docs.djangoproject.com/fr/1.11/topics/migrations/ des différents modèles, qui facilite grandement les mises à jour. Dans la majorité des cas ce système est automatique, il est temps de l'initialiser en créant une première migration :
$ /home/utilisateur/envs/publik-env-py3/bin/passerelle-manage --forceuser makemigrations passerelle_test
Migrations for 'passerelle_test':
  0001_initial.py:
    - Create model TestConnector
Si cette commande échoue sur un problème d'accès, vérifiez que votre utilisateur est bien dans le groupe "passerelle". h3. Instanciation du connecteur Passerelle peut désormais être rédémarré, @sudo supervisorctl restart passerelle@. Et voilà, il est maintenant possible de naviguer, le nouveau connecteur apparaîtra dans la liste des connecteurs disponibles. Créons-le et donnons-lui le nom de "démo", sa première instance devient disponible : https://passerelle.dev.publik.love/passerelle-test/demo/ h2. Endpoints C'est ici que tout commence car bien sûr un connecteur vide n'a aucun intérêt, il faut désormais lui ajouter le code qui le rendra utile, sous forme de "endpoints", les URL qui seront utilisées pour exposer les webservices. Il faut importer le code permettant de déclarer les endpoints @from passerelle.utils.api import endpoint@ en haut du fichier @models.py@, et ensuite, dans le modèle @TestConnector@ créé plus haut, nous pouvons alors déclarer un premier endpoint,

class TestConnector(BaseResource):
    […]

    @endpoint()
    def info(self, request):
        return {'hello': 'world'}
Redémarrons Passerelle pour que le code soit pris en compte (@sudo supervisorctl restart passerelle@) et voilà, accéder à l'URL du endpoint, https://passerelle.dev.publik.love/passerelle-test/demo/info retourne la donnée au format JSON :

{
  "hello": "world"
}
Il est aussi possible pour un endpoint d'attendre des paramètres, déclarons donc un deuxième endpoint, faisant l'addition de nombres entiers et retournant le résultat :

class TestConnector(BaseResource):
    […]

    @endpoint()
    def addition(self, request, a, b):
        return {'total': int(a) + int(b)}
Visiter https://passerelle.dev.publik.love/passerelle-test/demo/addition?a=2&b=3 retournera au format JSON :

{
  "total": 5
}
h2. Journalisation Passerelle suit la configuration de la journalisation établie au niveau de Django (cf "Journalisation":https://docs.djangoproject.com/fr/1.11/topics/logging/ dans la documentation officielle de Django) et ajoute à celle-ci une journalisation interne, consultable depuis la page de visualisation du connecteur. L'objet @logger@ d'un connecteur dispose des méthodes standards de log (debug, info, warning, critical, error, fatal); ainsi un connecteur opérant une division pourrait logguer au niveau "debug" toutes les demandes et logguer au niveau "error" les divisions par zéro :

class TestConnector(BaseResource):
    […]

    @endpoint()
    def division(self, request, a, b):
        self.logger.debug('division de %s par %s', a, b)
        try:
            return {'result': int(a) / int(b)}
        except ZeroDivisionError:
            self.logger.error('division par zéro')
            raise
h2. Autorisations d'accès Par défaut les endpoints ajoutés sont privés, limités à une permission @can_access@, un tableau de gestion des autorisations d'accès est automatiquement inclus à la page de gestion du connecteur. @can_access@ est le nom par défaut utilisé pour limiter l'accès mais il est possible d'augmenter la granularité des autorisations, on pourrait ainsi avoir un autre endpoint dont l'accès serait différencié :

class TestConnector(BaseResource):
    […]

    _can_compute_description = 'Accès aux fonctions de calcul'

    @endpoint(perm='can_compute')
    def addition(self, request, a, b):
        return {'total': int(a) + int(b)}
À noter également l'ajout dans la classe d'un attribut @_can_compute_description@, qui servira au niveau de l'interface à fournir une description pour cette autorisation d'accès. Il est possible de créer des accès ouverts, en passant @OPEN@ comme permission d’accès :

class TestConnector(BaseResource):
    […]

    @endpoint(perm='OPEN')
    def info(self, request):
        return {'hello': 'world'}
h2. Documentation Pour renforcer l'utilité de la page d'information d'un connecteur il est important de documenter le connecteur et ses différents endpoints. Le connecteur en lui-même peut être décrit en ajoutant un attribut @api_description@. Pour les endpoints, cela passe par l'ajout de paramètres à la déclaration @@endpoint@ :

class TestConnector(BaseResource):
    […]
    api_description = "Ce connecteur propose quelques opération arithmétiques élémentaires."

    @endpoint(description='Addition de deux nombres entiers',
              parameters={
                'a': {'description': 'Un premier nombre', 'example_value': '7'},
                'b': {'description': 'Un second nombre', 'example_value': '2'}
              })
    def addition(self, request, a, b):
        return {'total': int(a) + int(b)}
Dans @description@ se place donc une description général de l'endpoint et dans @parameters@ se place les informations sur les différents paramètres autorisés par l'appel, pour chacun d'eux une description et une valeur qui sera reprise en exemple peuvent être fournies. h2. Paramétrages supplémentaires À l'instanciation d'un connecteur quelques paramètres sont demandés, un titre et une description, ainsi que le niveau de journalisation souhaité. Il est possible d'ajouter d'autres paramètres, il faut pour cela ajouter de nouveaux attributs à la classe du connecteur. Comme noté en début de document cette classe correspond à un "modèle Django":https://docs.djangoproject.com/fr/1.11/topics/db/models/, il s'agit ainsi d'y ajouter des "champs":https://docs.djangoproject.com/fr/1.11/topics/db/models/#fields. Comme exemple, créons un connecteur dont le rôle sera de nous renseigner sur le bon état de fonctionnement, ou pas, d'une adresse, en faisant en sorte que cette adresse soit un paramètre supplémentaire du connecteur. Il faut donc ajouter un attribut au modèle, @url = models.URLField(...)@ et cette information sera par la suite disponible dans @self.url@ :

import requests
from passerelle.base.models import BaseResource

class TestConnector(BaseResource):
    category = 'Divers'
    url = models.URLField('URL', default='http://www.example.net')

    class Meta:
        verbose_name = 'Connecteur de test'

    @endpoint(description='Teste une adresse')
    def up(self, request):
        try:
            response = self.requests.get(self.url)
            response.raise_for_status()
        except requests.RequestException:
            return {'result': '%s est en panne' % self.url}
        return {'result': '%s est ok' % self.url}
Comme le modèle se trouve modifié, il est nécessaire de prévoir pour la base de données une migration qui ajoutera une colonne pour ce champ @url@, @/home/utilisateur/envs/publik-env-py3/bin/passerelle-manage --forceuser makemigrations passerelle_test@. Cette migration sera exécutée automatiquement au redémarrage de Passerelle, qui est donc nécessaire à cette étape. Note : @self.requests@ utilisé ici est un wrapper léger au-dessus du module "Requests":http://www.python-requests.org/, qui ajoute une journalisation et une expiration automatique des appels. h2. Exécution de tâches planifiées (cron) Un connecteur peut souhaiter exécuter des tâches de manière régulière, comme par exemple lancer une synchronisation avec un référentiel externe. Passerelle prend en charge quatre fréquences, une tâche peut être planifiée pour s'exécuter toutes les heures, tous les jours, toutes les semaines ou tous les mois. Pour ce faire il faut définir dans le connecteur une fonction avec la tâche à exécuter et la nommer selon la fréquence (hourly / daily / weekly / monthly).

from passerelle.base.models import BaseResource

class TestConnector(BaseResource):
    […]

    def hourly(self):
        pass  # code exécuté toutes les heures
⚠ Les tâches planifiées sont lancées automatiquement dans un environnement de production debian mais pas dans un environnement de développement local. Pour lancer manuellement une tâche planifiée dans un environnement de développement local (remplacer hourly pas daily / weekly / monthly selon le cas) :
/home/utilisateur/envs/publik-env-py3/bin/passerelle-manage tenant_command cron -d passerelle.dev.publik.love hourly
h2. Suivi de la disponibilité Dans le prolongement des tâches planifiées se trouve le suivi de la disponibilité. Un connecteur peut régulièrement (toutes les cinq minutes) interroger le service distant pour s'assurer de sa disponibilité et marquer celui-ci si jamais ce n'était pas le cas. Il suffit pour assurer cela de définir une méthode @check_status@, le service sera considéré indisponible quand la méthode lèvera une exception.

from passerelle.base.models import BaseResource

class TestConnector(BaseResource):
    […]

    def check_status(self):
        response = self.requests.get('http://example.net')
        response.raise_for_status()
⚠ Le suivi de disponibilité est lancé automatiquement dans un environnement de production debian mais pas dans un environnement de développement local. Pour lancer manuellement le suivi de disponibilité dans un environnement de développement local :
/home/utilisateur/envs/publik-env-py3/bin/passerelle-manage tenant_command cron -d passerelle.dev.publik.love availability
h2. Tâches asynchrones Les tâches planifiées s'exécutent de manière régulière, il peut également être utile d'exécuter des tâches de manière ponctuelle, par exemple pour transférer en tâche de fond un fichier important. La méthode @add_job@ permet d'ajouter une tâche de fond, qui sera exécutée après la requête.

class TestConnector(BaseResource):
    […]

    @endpoint(description='Transfert de données',
              parameters={
                  'src': {'description': 'Source'},
                  'dst': {'description': 'Destination'},
              })
    def transfert(self, request, src, dst):
        self.add_job('execution_transfert', src=src, dst=dst)
        return {'msg': 'Transfert planifié', 'err': 0}

    def execution_transfert(self, src, dst):
        # ici obtenir les données depuis la source puis les transférer à destination
        pass
La méthode @add_job@ prend comme paramètres le nom de la méthode à exécuter puis une série libre de paramètres nommés, qui seront utilisés lors de l'exécution. Quand des tâches existent, la page du connecteur les reprend dans un tableau. ⚠ Dans un environnement de développement local, les tâche asynchrones ne sont exécutées que si passerelle utilise le serveur "UWSGI":https://doc-publik.entrouvert.com/dev/installation-developpeur/#Utilisation-du-serveur-web-UWSGI . h2. Tests unitaires Ils sont indispensables pour dormir tranquille, ils se créent dans un nouveau répertoire, @tests@ et utilisent les modules "pytest":https://docs.pytest.org/, "pytest-django":https://pytest-django.readthedocs.io/ et "django-webtest":https://github.com/django-webtest/django-webtest (ces trois modules doivent donc être installés). Il y a d'abord à définir un fichier @tests/settings.py@ particulier, qui assurera la présence du connecteur :

INSTALLED_APPS += ('passerelle_test',)
Puis ensuite créer un fichier pour les tests en eux-mêmes, ça peut être @tests/test_connecteur.py@, il commencera par les imports des modules souhaités puis la définition des objets qui seront ensuite utiles.

# -*- coding: utf-8 -*-

import pytest
import django_webtest

from django.contrib.contenttypes.models import ContentType
from passerelle_test.models import TestConnector
from passerelle.base.models import ApiUser, AccessRight

@pytest.fixture
def app(request):
    # création de l'application Django
    return django_webtest.DjangoTestApp()

@pytest.fixture
def connector(db):
    # création du connecteur et ouverture de la permission "can_access" sans authentification.
    connector = TestConnector.objects.create(slug='test')
    api = ApiUser.objects.create(username='all', keytype='', key='')
    obj_type = ContentType.objects.get_for_model(connector)
    AccessRight.objects.create(
            codename='can_access', apiuser=api,
            resource_type=obj_type, resource_pk=connector.pk)
    return connector
Vient alors enfin le temps de tester le connecteur, pour un test sur la fonction d'addition :

def test_addition(app, connector):
    resp = app.get('/passerelle-test/test/addition?a=5&b=3')
    assert resp.json.get('total') == 8
Les tests s'exécutent ensuite via la commande @py.test@, en pointant le fichier @settings.py@ créé spécialement :
 $ DJANGO_SETTINGS_MODULE=passerelle.settings PASSERELLE_SETTINGS_FILE=tests/settings.py py.test 
======================================= test session starts =======================================
platform linux2 -- Python 2.7.14+, pytest-3.3.1, ...
cachedir: .cache
Django settings: passerelle.settings (from environment variable)
rootdir: ..., inifile:
plugins: ...
collected 1 item

tests/test_connecteur.py::test_addition PASSED                                              [100%]

==================================== 1 passed in 0.48 seconds =====================================
h2. Au-delà * gestion des POST. * pattern sur les endpoints. * Squelette HTML de description du connecteur.