Chapitre 5

Développement

Cette section contient la documentation pour démarrer avec le développement de plugin et la contribution “noyau” d’Hermes.

Journalisation

Une instance Logger est disponible via la variable “__hermes__.logger”. Comme cette variable est déclarée comme builtin, elle est toujours disponible et ne nécessite aucun import ou appel à logging.getLogger().

Contribuer

Avant de proposer une pull request pour fusionner du code dans Hermes, vous devez vous assurer que votre code :

  1. fournit des docstrings et des annotations de type
  2. a été formaté avec black
  3. est conforme à Flake8
  4. passe la suite de tests

tox peut être utilisé pour valider les trois dernières conditions, en exécutant l’une des commandes ci-dessous :

# # Test séquentiel (lent mais plus détaillé) uniquement sur la version Python par défaut de votre système
tox run -e linters,tests
# Test en parallèle (plus rapide, mais sans détails) uniquement sur la version Python par défaut de votre système
tox run-parallel -e linters,tests

# Test séquentiel (lent mais plus détaillé) sur toutes les versions Python compatibles - elles doivent être disponibles sur votre système
tox run
# Test en parallèle (plus rapide, mais sans détails) sur toutes les versions Python compatibles - elles doivent être disponibles sur votre système
tox run-parallel
Astuce

tox >= 4 doit être installé mais est probablement disponible dans les dépôts de votre distribution

Boris Lechner 2025-05-05 e022507882f1c7d53ec4dc72b08922261dfdd25f

Sous-sections de Développement

Plugins

Quel que soit son type, un plugin est toujours un dossier nommé ‘plugin_name’ contenant au moins les 4 fichiers suivants :

Code source du plugin

Hermes va essayer d’importer le fichier plugin_name.py. Il est possible de découper le code du plugin en plusieurs fichiers et dossiers, mais le plugin sera toujours importé à partir de ce fichier.

Pour plus de détails sur l’API du plugin, veuillez consulter les sections suivantes :

Astuce

Certains modules utilitaires sont disponibles dans helpers :

  • helpers.command : pour exécuter des commandes locales sur l’hôte du client
  • helpers.ldaphashes: pour générer les hachages LDAP à partir de mots de passe en clair
  • helpers.randompassword : pour générer des mots de passe aléatoires avec des contraintes spécifiques

Schéma de configuration du plugin

Selon le type de plugin, le fichier de schéma de configuration varie légèrement.

Schéma de configuration du plugin pour les plugins clients

Hermes va essayer de valider la configuration du plugin avec un schéma de validation Cerberus spécifié dans un fichier YAML : config-schema-client-plugin_name.yml.

Le fichier de validation des plugins clients doit être vide ou ne contenir qu’une seule clé de premier niveau qui doit être le nom du plugin préfixé par hermes-client-.

Exemple pour le nom du plugin usersgroups_flatfiles_emails_of_groups :

# https://docs.python-cerberus.org/validation-rules.html

hermes-client-usersgroups_flatfiles_emails_of_groups:
  type: dict
  required: true
  empty: false
  schema:
    destDir:
      type: string
      required: true
      empty: false
    onlyTheseGroups:
      type: list
      required: true
      nullable: false
      default: []
      schema:
        type: string

Schéma de configuration du plugin pour les autres types de plugins

Hermes va essayer de valider la configuration du plugin avec un schéma de validation Cerberus spécifié dans un fichier YAML : config-schema-plugin-plugin_name.yml.

Même si le plugin ne nécessite aucune configuration, il nécessite tout de même un fichier de validation vide.

Exemple pour le nom de plugin ldapPasswordHash :

# https://docs.python-cerberus.org/validation-rules.html

default_hash_types:
  type: list
  required: false
  nullable: false
  empty: true
  default: []
  schema:
    type: string
    allowed:
      - MD5
      - SHA
      - SMD5
      - SSHA
      - SSHA256
      - SSHA512

Fichier README.md du plugin

La documentation doit être écrite dans README.md et doit contenir les sections suivantes :

# `plugin_name` attribute plugin

## Description

## Configuration

## Usage
Uniquement pour les plugins `attributes` et `datasources`.

## Datamodel
Uniquement pour les plugins `clients`.

Dépendances du plugin : requirements.txt

Même si le plugin n’a pas de dépendance Python, veuillez créer un fichier pip requirements.txt commençant par un commentaire contenant le chemin du plugin et se terminant par une ligne vide.

Exemple :

# plugins/attributes/crypto_RSA_OAEP
pycryptodomex==3.21.0
 

Boris Lechner 2025-05-05 e022507882f1c7d53ec4dc72b08922261dfdd25f

Sous-sections de Plugins

Attributs

Description

Un plugin d’attribut est simplement une classe fille de AbstractAttributePlugin conçue pour implémenter un filtre Jinja.

Conditions requises

Voici une implémentation de plugin minimale commentée qui ne fera rien.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Nécessaire pour hériter de la classe AbstractAttributePlugin
from lib.plugins import AbstractAttributePlugin

# Nécessaire pour utiliser le statut Jinja Undefined
from jinja2 import Undefined

# Nécessaire pour les annotations de type
from typing import Any

# Nécessaire pour indiquer à hermes quelle classe il devra instancier
HERMES_PLUGIN_CLASSNAME = "MyPluginClassName"

class MyPluginClassName(AbstractAttributePlugin):
    def __init__(self, settings: dict[str, any]):
        # Crée une nouvelle instance du plugin et stocke une copie de
        # son dictionnaire de paramètres dans self._settings
        super().__init__(settings)
        # ... code d'initialisation du plugin

    def filter(self, value: Any | None | Undefined) -> Any:
        # Filtre qui ne fait rien
        return value

Méthode filter

Vous devriez consulter la documentation officielle de Jinja sur les filtres personnalisés.

La méthode filter() prend toujours au moins un paramètre value et peut en avoir d’autres.

Son prototype générique est :

def filter(self, value: Any | None | Undefined, *args: Any, **kwds: Any) -> Any:

En Jinja, on l’utilise ainsi :

"{{ value | filter }}"
"{{ value | filter(otherarg1, otherarg2) }}"
"{{ value | filter(otherarg1=otherarg1_value, otherarg2=otherarg2_value) }}"

Les expressions ci-dessus seront remplacées par la valeur de retour du filtre.

Exemple : le plugin d’attribut datetime_format

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Nécessaire pour hériter de la classe AbstractAttributePlugin
from lib.plugins import AbstractAttributePlugin

# Nécessaire pour utiliser le statut Jinja Undefined
from jinja2 import Undefined

# Nécessaire pour les annotations de type
from typing import Any

from datetime import datetime

# Nécessaire pour indiquer à hermes quelle classe il devra instancier
HERMES_PLUGIN_CLASSNAME = "DatetimeFormatPlugin"

class DatetimeFormatPlugin(AbstractAttributePlugin):
    def filter(self, value:Any, format:str="%H:%M %d-%m-%y") -> str | Undefined:
        if isinstance(value, Undefined):
            return value

        if not isinstance(value, datetime):
            raise TypeError(f"""Invalid type '{type(value)}' for datetime_format value: must be a datetime""")

        return value.strftime(format)

Le filtre peut désormais être utilisé ainsi :

"{{ a_datetime_attribute | datetime_format }}"
"{{ a_datetime_attribute | datetime_format('%m/%d/%Y, %H:%M:%S') }}"
"{{ a_datetime_attribute | datetime_format(format='%m/%d/%Y') }}"

Boris Lechner 2025-05-05 e022507882f1c7d53ec4dc72b08922261dfdd25f

Clients

Description

Un plugin client est simplement une classe fille de GenericClient conçue pour implémenter des gestionnaires d’événements simples et pour diviser leurs tâches en sous-tâches atomiques afin de garantir un retraitement cohérent en cas d’erreur.

Conditions requises

Voici une implémentation de plugin minimale commentée qui ne fera rien, car elle n’implémente pas encore de gestionnaires d’événements.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Nécessaire pour hériter de la classe GenericClient
from clients import GenericClient

# Nécessaire pour les annotations de type des gestionnaires d'événements
from lib.config import HermesConfig # uniquement si le plugin implémente une méthode __init__()
from lib.datamodel.dataobject import DataObject
from typing import Any

# Nécessaire pour indiquer à hermes quelle classe il devra instancier
HERMES_PLUGIN_CLASSNAME = "MyPluginClassName"

class MyPluginClassName(GenericClient):
    def __init__(self, config: HermesConfig):
        # La variable 'config' ne doit être ni utilisée ni modifiée par le plugin
        super().__init__(config)
        # ... code d'initialisation du plugin

Méthodes de gestion d’événements

Gestionnaires d’événements

Pour chaque type de données configuré dans le modèle de données client, le plugin peut implémenter un gestionnaire pour chacun des 5 types d’événements possibles :

  • added : lorsqu’un objet est ajouté
  • recycled : lorsqu’un objet est restauré depuis la corbeille (ne sera jamais appelé si la corbeille est désactivée)
  • modified : lorsqu’un objet est modifié
  • trashed : lorsqu’un objet est placé dans la corbeille (ne sera jamais appelé si la corbeille est désactivée)
  • removed : lorsqu’un objet est supprimé

Si un événement est reçu par un client, mais que son gestionnaire n’est pas implémenté, il sera ignoré silencieusement.

Chaque gestionnaire doit être nommé on_datatypename_eventtypename.

Exemple pour un type de données Mydatatype :

    def on_Mydatatype_added(
        self,
        objkey: Any,
        eventattrs: "dict[str, Any]",
        newobj: DataObject,
    ):
        pass

    def on_Mydatatype_recycled(
        self,
        objkey: Any,
        eventattrs: "dict[str, Any]",
        newobj: DataObject,
    ):
        pass

    def on_Mydatatype_modified(
        self,
        objkey: Any,
        eventattrs: "dict[str, Any]",
        newobj: DataObject,
        cachedobj: DataObject,
    ):
        pass

    def on_Mydatatype_trashed(
        self,
        objkey: Any,
        eventattrs: "dict[str, Any]",
        cachedobj: DataObject,
    ):
        pass

    def on_Mydatatype_removed(
        self,
        objkey: Any,
        eventattrs: "dict[str, Any]",
        cachedobj: DataObject,
    ):
        pass

Arguments des gestionnaires d’événements

  • objkey : la clé primaire de l’objet affecté par l’événement

  • eventattrs : un dictionnaire contenant les nouveaux attributs de l’objet. Son contenu dépend du type d’événement :

    • added / recycled : contient tous les noms d’attributs comme clé, et leurs valeurs respectives comme valeur
      • modified : contient toujours trois clés :
        • added : attributs qui n’étaient pas définis auparavant, mais qui ont maintenant une valeur. Noms d’attributs comme clé, et leurs valeurs respectives comme valeur
        • modified : attributs qui étaient définis auparavant, mais dont la valeur a changé. Noms d’attributs comme clé, et leurs nouvelles valeurs respectives comme valeur
        • removed : attributs qui étaient définis auparavant, mais qui n’ont plus de valeur. Noms d’attributs comme clé, et None comme valeur
    • trashed / removed : toujours un dictionnaire vide {}
  • newobj : une instance DataObject contenant toutes les valeurs mises à jour de l’objet affecté par l’événement (voir Instances de DataObject ci-dessous)

  • cachedobj : une instance DataObject contenant toutes les valeurs précédentes (mises en cache) de l’objet affecté par l’événement (voir Instances de DataObject ci-dessous)

Instances de DataObject

Chaque objet de type de données peut être utilisé intuitivement via une instance DataObject. Utilisons un exemple simple avec ces valeurs d’objet User (sans adresse email) du modèle de données ci-dessous :

{
    "user_pkey": 42,
    "uid": "jdoe",
    "givenname": "John",
    "sn": "Doe"
}
hermes-client:
  datamodel:
    Users:
      hermesType: SRVUsers
      attrsmapping:
        user_pkey: srv_user_id
        uid: srv_login
        givenname: srv_firstname
        sn: srv_lastname
        mail: srv_mail

Maintenant, si cet objet est stocké dans une instance de DataObject newobj :

>>> newobj.getPKey()
42

>>> newobj.user_pkey
42

>>> newobj.uid
'jdoe'

>>> newobj.givenname
'John'

>>> newobj.sn
'Doe'

>>> newobj.mail
AttributeError: 'Users' object has no attribute 'mail'

>>> hasattr(newobj, 'sn')
True

>>> hasattr(newobj, 'mail')
False

Gestion d’erreur

Toute exception non gérée levée dans un gestionnaire d’événements sera gérée par GenericClient, qui ajoutera l’événement à sa file d’erreurs. GenericClient essaiera alors de re-traiter l’événement régulièrement jusqu’à ce qu’il réussisse, et appellera donc le gestionnaire d’événements.

Mais parfois, un gestionnaire d’événements doit procéder à plusieurs opérations sur la cible. Imaginons un gestionnaire comme celui-ci :

    def on_Mydatatype_added(
        self,
        objkey: Any,
        eventattrs: "dict[str, Any]",
        newobj: DataObject,
    ):
        if condition:
            operation1()  # "condition" vaut False, operation1() n'est pas appelée
        operation2()  # aucune erreur ne se produit
        operation3()  # celle-ci lève une exception

A chaque nouvelle tentative la fonction operation2() sera à nouveau appelée, mais ce n’est probablement pas souhaitable.

Il est possible de découper les étapes d’un gestionnaire d’événements en utilisant l’attribut currentStep hérité de GenericClient, afin de retenter les traitements des événements en erreur depuis l’étape qui avait échouée.

currentStep démarre toujours à 0 lors du traitement normal des événements. L’évolution de sa valeur doit être gérée par les plugins.

Lorsqu’une erreur se produit, la valeur currentStep est enregistrée dans la file d’erreurs avec l’événement.

Les re-tentatives de traitement de la file d’erreurs restaureront toujours la valeur de currentStep avant d’appeler le gestionnaire d’événements.

Ainsi en l’implémentant comme ci-dessous, operation2() ne sera appelée qu’une seule fois.

    def on_Mydatatype_added(
        self,
        objkey: Any,
        eventattrs: "dict[str, Any]",
        newobj: DataObject,
    ):
        if self.currentStep == 0:
            if condition:
                operation1()  # "condition" vaut False, operation1() n'est pas appelée
                # Indique que des modifications ont été propagées sur la cible
                self.isPartiallyProcessed = True
            self.currentStep += 1
        
        if self.currentStep == 1:
            operation2()  # aucune erreur ne se produit
            # Indique que des modifications ont été propagées sur la cible
            self.isPartiallyProcessed = True
            self.currentStep += 1

        if self.currentStep == 2:
            operation3()  # celle-ci lève une exception
            # Indique que des modifications ont été propagées sur la cible
            self.isPartiallyProcessed = True
            self.currentStep += 1
Comprendre l’attribut isPartiallyProcessed

L’attribut isPartiallyProcessed hérité de GenericClient indique si le traitement de l’événement en cours a déjà propagé des modifications sur la cible. Il doit donc être mis à True dès que la moindre modification a été propagée sur la cible. Il permet à l’autoremédiation de fusionner les événements dont currentStep est différent de 0 mais dont les étapes précédentes n’ont rien modifié sur la cible.

isPartiallyProcessed vaut toujours False lors d’un traitement d’événement normal. L’évolution de sa valeur doit être gérée par les plugins.

Avec l’exemple d’implémentation ci-dessus, et une exception levée par operation3(), l’autoremédiation ne tenterait pas de fusionner cet événement partiellement traité avec d’éventuels événements ultérieurs, car isPartiallyProcessed vaut True.

Avec l’exemple d’implémentation ci-dessus, mais une exception levée par operation2(), l’autoremédiation essaierait de fusionner cet événement non traité avec d’éventuels événements ultérieurs, car isPartiallyProcessed vaudrait encore False.

Gestionnaire d’événement on_save

Un gestionnaire d’événements spécial qui peut être implémenté lorsque Hermes vient de sauvegarder ses fichiers cache : une fois que certains événements ont été traités et qu’aucun événement n’est en attente sur le bus de messages, ou avant la fin.

Avertissement

Comme ce gestionnaire n’est pas un gestionnaire d’événements standard, GenericClient ne peut pas gérer les exceptions pour lui et procéder à une nouvelle tentative ultérieurement.

Toute exception non gérée levée dans ce gestionnaire d’événements mettra immédiatement fin au client.

Il appartient à l’implémentation d’éviter les erreurs.

    def on_save(self):
        pass

Propriétés et méthodes de GenericClient

Propriétés

  • currentStep: int

    Numéro d’étape de l’événement en cours de traitement. Nécessaire pour permettre aux clients de reprendre un événement où il a échoué.

  • isPartiallyProcessed: bool

    Indique si le traitement de l’événement en cours a déjà propagé des modifications sur la cible.
    Doit être défini à True dès que la moindre modification a été propagée sur la cible.
    Il permet à l’autoremédiation de fusionner les événements dont currentStep est différent de 0 mais dont les étapes précédentes n’ont rien modifié sur la cible.

  • isAnErrorRetry: bool

    Attribut en lecture seule qui peut permettre au gestionnaire d’événements de savoir si l’événement actuel est en cours de traitement dans le cadre d’une nouvelle tentative suite à une erreur. Cela peut être utile par exemple pour effectuer des vérifications complémentaires lorsqu’une bibliothèque génère des exceptions même si elle a correctement traité les modifications demandées, comme le fait parfois python-ldap.

  • config: dict[str, Any]

    Dictionnaire contenant la configuration du plugin client.

Méthodes

  • def getDataobjectlistFromCache(objtype: str) -> DataObjectList

    Renvoie le cache du type d’objet spécifié, par référence. Lève IndexError si objtype n’est pas valide

    Avertissement

    Toute modification du contenu du cache corrompra votre client !!!

  • def getObjectFromCache(objtype: str, objpkey: Any ) -> DataObject

    Renvoie une copie complète (deepcopy) d’un objet depuis le cache. Lève une erreur IndexError si objtype n’est pas valide ou si objpkey n’est pas trouvée

  • def mainLoop() -> None

    Boucle principale du client

    Avertissement

    Appelée par Hermes, pour démarrer le client. Ne doit jamais être appelée ni surchargée

Boris Lechner 2025-05-05 e022507882f1c7d53ec4dc72b08922261dfdd25f

Consommateurs de bus de messages

Description

Un plugin consommateur de bus de messages est simplement une classe fille de AbstractMessageBusConsumerPlugin conçue pour permettre à hermes-client d’interroger n’importe quel bus de messages.

Il faudra y implémenter des méthodes pour se connecter et se déconnecter au bus de messages, et pour consommer les événements disponibles.

Fonctionnalités requises du bus de messages

  • Permettre de spécifier une clé/catégorie de message (producteurs) et de filtrer les messages d’une clé/catégorie spécifiée (consommateurs)
  • Permettre de consommer un même message plusieurs fois
  • Implémenter un offset de message, permettant aux consommateurs de rechercher le prochain message attendu. Comme il sera stocké dans le cache des clients, cet offset doit être de l’un des types Python ci-dessous :
    • int
    • float
    • str
    • bytes

Conditions requises

Voici une implémentation de plugin minimale commentée qui ne fera rien.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Nécessaire pour hériter de la classe AbstractMessageBusConsumerPlugin
from lib.plugins import AbstractMessageBusConsumerPlugin
# Required to return Event
from lib.datamodel.event import Event

# Nécessaire pour les annotations de type
from typing import Any, Iterable

# Nécessaire pour indiquer à hermes quelle classe il devra instancier
HERMES_PLUGIN_CLASSNAME = "MyMessagebusConsumerPluginClassName"

class MyMessagebusConsumerPluginClassName(AbstractMessageBusConsumerPlugin):
    def __init__(self, settings: dict[str, Any]):
        # Crée une nouvelle instance du plugin et stocke une copie de
        # son dictionnaire de paramètres dans self._settings
        super().__init__(settings)
        # ... code d'initialisation du plugin

    def open(self) -> Any:
        """Établit la connexion avec le bus de message"""

    def close(self):
        """Ferme la connexion avec le bus de message"""

    def seekToBeginning(self):
        """Rechercher le premier événement (le plus ancien) dans la file du bus
        de messages"""

    def seek(self, offset: Any):
        """Rechercher l'événement de l'offset spécifié dans la file du bus de
        messages"""

    def setTimeout(self, timeout_ms: int | None):
        """Définit le délai d'attente (en millisecondes) avant d'interrompre
        l'attente du prochain événement. Si None, attend indéfiniment"""

    def findNextEventOfCategory(self, category: str) -> Event | None:
        """Recherche le premier message avec la catégorie spécifiée et le renvoie,
        ou renvoie None si aucun n'a été trouvé"""

    def __iter__(self) -> Iterable:
        """Itère sur le bus de messages en renvoyant chaque événement, en
        commençant à l'offset courant.
        Lorsque chaque événement a été consommé, attends le message suivant
        jusqu'à ce que le délai d'expiration défini avec setTimeout() soit
        atteint"""

Méthodes à implémenter

Méthodes de connexion

Comme elles ne prennent aucun argument, les méthodes open et close doivent s’appuyer sur les paramètres du plugin.

Méthode seekToBeginning

Rechercher le premier événement (le plus ancien) dans la file du bus de messages.

Méthode seek

Rechercher l’événement de l’offset spécifié dans la file du bus de messages.

Méthode setTimeout

Définit le délai d’attente (en millisecondes) avant d’interrompre l’attente du prochain événement. Si None, attend indéfiniment.

Méthode findNextEventOfCategory

Recherche le premier message avec la catégorie spécifiée et le renvoie, ou renvoie None si aucun n’a été trouvé.

Comme cette méthode parcourt le bus de messages, l’offset courant sera modifié.

Méthode __iter__

Renvoie un Iterable qui génère (yield) tous les événements disponibles sur le bus de messages, à partir de l’offset courant.

Ces attributs non-sérialisables de l’instance Event doivent être définis avant de le générer (yield) :

  • offset (int | float | str | bytes) : offset de l’événement dans le bus de messages
  • timestamp (dattime.datetime) : horodatage de l’événement

Propriétés et méthodes de la classe Event

Méthodes

  • @staticmethod
    def from_json(jsondata: str | dict[Any, Any]) -> Event

    Désérialise une chaîne ou un dictionnaire JSON en une nouvelle instance Event et la renvoie

Boris Lechner 2025-05-05 e022507882f1c7d53ec4dc72b08922261dfdd25f

Producteurs de bus de messages

Description

Un plugin producteurs de bus de messages est simplement une classe fille de AbstractMessageBusProducerPlugin conçue pour permettre à hermes-server d’émettre des événements vers n’importe quel bus de messages.

Il faudra y implémenter des méthodes pour se connecter et se déconnecter au bus de messages, et pour y produire (émettre) des événements.

Fonctionnalités requises du bus de messages

  • Permettre de spécifier une clé/catégorie de message (producteurs) et de filtrer les messages d’une clé/catégorie spécifiée (consommateurs)
  • Permettre de consommer un même message plusieurs fois
  • Implémenter un offset de message, permettant aux consommateurs de rechercher le prochain message attendu. Comme il sera stocké dans le cache des clients, cet offset doit être de l’un des types Python ci-dessous :
    • int
    • float
    • str
    • bytes

Conditions requises

Voici une implémentation de plugin minimale commentée qui ne fera rien.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Nécessaire pour hériter de la classe AbstractMessageBusProducerPlugin
from lib.plugins import AbstractMessageBusProducerPlugin

# Nécessaire pour les annotations de type
from lib.datamodel.event import Event
from typing import Any

# Nécessaire pour indiquer à hermes quelle classe il devra instancier
HERMES_PLUGIN_CLASSNAME = "MyMessagebusProducerPluginClassName"

class MyMessagebusProducerPluginClassName(AbstractMessageBusProducerPlugin):
    def __init__(self, settings: dict[str, Any]):
        # Crée une nouvelle instance du plugin et stocke une copie de
        # son dictionnaire de paramètres dans self._settings
        super().__init__(settings)
        # ... code d'initialisation du plugin

    def open(self) -> Any:
        """Établit la connexion avec le bus de message"""

    def close(self):
        """Ferme la connexion avec le bus de message"""

    def _send(self, event: Event):
        """Émet l'événement spécifié sur le bus de message"""

Méthodes à implémenter

Méthodes de connexion

Comme elles ne prennent aucun argument, les méthodes open et close doivent s’appuyer sur les paramètres du plugin.

Méthode _send

Remarque

Attention à surcharger la méthode _send() et non pas send().

La méthode send() est un wrapper qui gère les exceptions lors de l’appel à _send().

Envoie un message contenant l’événement spécifié.

Le consommateur aura besoin des propriétés suivantes :

  • evcategory (str) : clé/catégorie de l’événement (stockée dans l’événement)
  • timestamp (dattime.datetime) : horodatage de l’événement
  • offset (int | float | str | bytes) : offset de l’événement dans le bus de messages

Voir Propriétés et méthodes de la classe Event ci-dessous.

Propriétés et méthodes de la classe Event

Propriétés

  • evcategory: str

    Clé/catégorie à attribuer au message

Méthodes

  • def to_json() -> str

    Sérialise un événement dans une chaîne JSON qui pourra être utilisée ultérieurement pour être désérialisée dans une nouvelle instance Event

Boris Lechner 2025-05-05 e022507882f1c7d53ec4dc72b08922261dfdd25f

Sources de données

Description

Un plugin de source de données est simplement une classe fille de AbstractDataSourcePlugin conçue pour permettre à hermes-server d’interroger n’importe quelle source de données.

Il faudra y implémenter des méthodes pour se connecter et se déconnecter de la source de données, et pour récupérer, ajouter, modifier et supprimer des données.

Conditions requises

Voici une implémentation de plugin minimale commentée qui ne fera rien.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Nécessaire pour hériter de la classe AbstractDataSourcePlugin
from lib.plugins import AbstractDataSourcePlugin

# Nécessaire pour les annotations de type
from typing import Any

# Nécessaire pour indiquer à hermes quelle classe il devra instancier
HERMES_PLUGIN_CLASSNAME = "MyDatasourcePluginClassName"

class MyDatasourcePluginClassName(AbstractDataSourcePlugin):
    def __init__(self, settings: dict[str, Any]):
        # Crée une nouvelle instance du plugin et stocke une copie de
        # son dictionnaire de paramètres dans self._settings
        super().__init__(settings)
        # ... code d'initialisation du plugin

    def open(self):
        """Établit la connexion avec la source de données"""

    def close(self):
        """Ferme la connexion avec la source de données"""

    def fetch(
        self,
        query: str | None,
        vars: dict[str, Any],
    ) -> list[dict[str, Any]]:
        """Récupère des données à partir de la source de données avec la requête
        spécifiée et/ou des variables.
        Renvoie une liste de dictionnaires contenant chaque entrée extraite, avec
        REMOTE_ATTRIBUTES comme clés et les valeurs extraites correspondantes
        comme valeurs"""

    def add(self, query: str | None, vars: dict[str, Any]):
        """Ajoute des données à la source de données avec la requête spécifiée
        et/ou des variables"""

    def delete(self, query: str | None, vars: dict[str, Any]):
        """Supprime des données de la source de données avec la requête spécifiée
        et/ou des variables"""

    def modify(self, query: str | None, vars: dict[str, Any]):
        """Modifie des données sur la source de données avec la requête spécifiée
        et/ou des variables"""

Méthodes

Méthodes de connexion

Comme elles ne prennent aucun argument, les méthodes open et close doivent s’appuyer sur les paramètres du plugin. Pour les sources de données sans état, elles peuvent ne rien faire.

Méthode fetch

Cette méthode est appelée pour récupérer des données et les fournir à hermes-server.

Selon l’implémentation du plugin, elle peut s’appuyer sur l’argument query, l’argument vars, ou les deux.

Le résultat doit être renvoyé sous forme de liste de dictionnaires. Chaque élément de la liste est une entrée récupérée stockée dans un dictionnaire, avec le nom de l’attribut comme clé et sa valeur correspondante comme valeur. La valeur doit être de l’un des types Python suivants :

  • None
  • int
  • float
  • str
  • datetime.datetime
  • bytes

Les types itérables autorisés sont :

  • list
  • dict

Les valeurs doivent impérativement être d’un des types mentionnés ci-dessus. Tous les autres types sont invalides.

Méthodes add, delete, et modify

Ces méthodes permettent de modifier le contenu de la source de données, lorsque cela est possible.

Selon les contraintes techniques de la source de données, elles peuvent toutes être implémentées de la même manière ou non.

Selon l’implémentation du plugin, elles peuvent s’appuyer sur l’argument query, l’argument vars, ou les deux.

Gestion d’erreur

Aucune exception ne devrait être interceptée afin de permettre à la gestion d’erreur d’Hermes de fonctionner correctement.