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.
#!/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') }}"
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.
Propriétés et méthodes de GenericClient
Propriétés
-
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.
-
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.
-
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
-
Boucle principale du client
Avertissement
Appelée par Hermes, pour démarrer le client. Ne doit jamais être appelée ni surchargée
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 :
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
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 :
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
Méthodes
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 :
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.