Version de développement
Cette documentation décrit la version « next » (develop), non encore publiée. Pour la version stable, basculez sur « v5 » dans l'en-tête.
API

Temps réel (WebSocket / Socket.IO)

Comment utiliser et étendre le serveur Socket.IO Python de Dodock pour envoyer des événements en temps réel et définir des handlers personnalisés dans votre application.

Temps réel — WebSocket & Socket.IO

Dodock intègre un serveur Socket.IO écrit en Python qui permet d'envoyer des événements en temps réel à un ou plusieurs utilisateurs connectés. Ce serveur remplace progressivement l'ancien serveur Node.js : il s'exécute directement sous le processus Gunicorn, ce qui réduit la consommation mémoire (~45 Mo économisés) et simplifie l'architecture.

Compatibilité : le serveur Node.js reste disponible en parallèle pendant la période de transition. Le serveur Python est activé via la configuration du site.

Architecture

  1. Le processus web (Gunicorn) publie un message via frappe.publish_realtime().
  2. Ce message transite par Redis Pub/Sub.
  3. Le serveur Socket.IO Python reçoit le message et l'émet vers le(s) client(s) concerné(s).

Configuration

Le serveur Python Socket.IO se configure dans common_site_config.json :

{
  "socketio_port": 9000,
  "use_python_socketio": true
}
CléValeur par défautDescription
socketio_port9000Port d'écoute du serveur Socket.IO
use_python_socketiofalseActive le serveur Python (désactive Node.js)

Publier un événement depuis le serveur

frappe.publish_realtime()

C'est la fonction principale pour envoyer un événement depuis n'importe quel code Python (contrôleur, tâche planifiée, hook…).

frappe.publish_realtime(
    event="mon_evenement",
    message={"statut": "terminé", "avancement": 100},
    user="utilisateur@example.com",   # ou room=...
)

Paramètres :

ParamètreTypeDescription
eventstrNom de l'événement Socket.IO
messagedictDonnées envoyées au client
userstr | NoneEnvoie à un utilisateur précis
roomstr | NoneEnvoie à une room Socket.IO
doctypestr | NoneCible les abonnés au document
docnamestr | NoneIdentifiant du document
task_idstr | NoneLié à une tâche de fond
after_commitboolSi True, publie après le commit BDD

Helpers nommés

Pour les cas d'usage courants, des fonctions dédiées sont disponibles :

from frappe.realtime import (
    publish_to_user,
    publish_to_room,
    publish_to_doctype,
    publish_to_document,
    publish_to_task,
    publish_to_all,
)

# Vers un utilisateur spécifique
publish_to_user("admin@example.com", "alerte", {"texte": "Nouveau message"})

# Vers tous les abonnés d'un document
publish_to_document("Project", "PROJ-0001", "mise_a_jour", {"avancement": 75})

# Vers tous les utilisateurs connectés
publish_to_all("maintenance", {"message": "Redémarrage dans 5 minutes"})

Définir des handlers personnalisés (API développeur)

Vous pouvez réagir à des événements Socket.IO envoyés par le navigateur en définissant des handlers dans votre application.

Enregistrer un handler avec @realtime.on

# monapp/realtime.py
import frappe
from frappe.realtime import Socket, realtime


@realtime.on("project_subscribe")
def project_subscribe(socket: Socket, project: str) -> None:
    """L'utilisateur demande à rejoindre la room d'un projet."""
    if socket.has_permission("Project", project):
        socket.join(f"project:{project}")

Le décorateur @realtime.on("<nom_evenement>") enregistre automatiquement la fonction dans le registre de Dodock au démarrage du serveur. Aucune configuration supplémentaire n'est nécessaire.

Découverte automatique

Dodock découvre les handlers en cherchant le module <monapp>.realtime dans chaque application installée. Il suffit de créer le fichier monapp/realtime.py contenant vos handlers décorés.

Options du décorateur

@realtime.on(
    "mon_evenement",
    frappe_context=True,   # Ouvre un contexte Frappe (BDD + session) — défaut : True
    allow_guest=False,     # Si False, refuse les sockets non authentifiés — défaut : False
)
def mon_handler(socket: Socket, **data) -> None:
    ...
OptionTypeDéfautDescription
frappe_contextboolTrueInitialise frappe.init() et ouvre la BDD avant d'appeler le handler
allow_guestboolFalseAutorise les connexions de type Invité (Guest)

Attention : si frappe_context=True, le handler dispose d'un accès complet à frappe.db, frappe.get_doc(), etc. Si frappe_context=False, ces appels échoueront — à utiliser uniquement pour des handlers purement temps réel sans accès BDD.

L'objet Socket

Le premier argument de chaque handler est un objet Socket qui représente la connexion du client.

from frappe.realtime import Socket

def mon_handler(socket: Socket, **data) -> None:
    # Propriétés
    socket.user          # str : nom d'utilisateur (ex. "admin@example.com")
    socket.site          # str : nom du site Frappe
    socket.sid           # str : identifiant de session Socket.IO

    # Méthodes
    socket.join("ma_room")              # Rejoindre une room
    socket.leave("ma_room")             # Quitter une room
    socket.emit("evenement", {})        # Envoyer un événement à CE client
    socket.is_connected()               # bool : le socket est-il encore connecté ?

    # Vérification de permission
    socket.has_permission(
        doctype="Project",
        docname="PROJ-0001",
        ptype="read",   # "read", "write", "create", "delete"…
    )

Vérification des permissions

socket.has_permission() délègue la vérification au processus web via une requête HTTP interne, ce qui garantit que les règles de permissions Dodock standard sont respectées.

@realtime.on("document_watch")
def document_watch(socket: Socket, doctype: str, docname: str) -> None:
    if socket.has_permission(doctype, docname, ptype="read"):
        socket.join(f"{doctype}::{docname}")
    else:
        socket.emit("error", {"message": "Accès refusé"})

Exemple complet : notifications de projet

Scénario : les utilisateurs s'abonnent à un projet et reçoivent des mises à jour en temps réel quand l'avancement change.

monapp/realtime.py — handler côté serveur :

import frappe
from frappe.realtime import Socket, realtime, publish_to_room


@realtime.on("project_subscribe")
def project_subscribe(socket: Socket, project: str) -> None:
    """Abonne le client aux mises à jour du projet."""
    if socket.has_permission("Project", project, ptype="read"):
        socket.join(f"project:{project}")
        socket.emit("project_subscribed", {"project": project})


@realtime.on("project_unsubscribe")
def project_unsubscribe(socket: Socket, project: str) -> None:
    """Désabonne le client."""
    socket.leave(f"project:{project}")

monapp/controllers/project.py — publication depuis un hook :

from frappe.realtime import publish_to_room


class Project(Document):
    def on_update(self):
        publish_to_room(
            room=f"project:{self.name}",
            event="project_updated",
            message={
                "name": self.name,
                "status": self.status,
                "percent_complete": self.percent_complete,
            },
        )

Côté navigateur (JavaScript) :

// Abonnement
frappe.realtime.emit("project_subscribe", { project: "PROJ-0001" });

// Réception des mises à jour
frappe.realtime.on("project_updated", (data) => {
    console.log("Avancement :", data.percent_complete);
});

Sécurité

  • Authentification à la connexion : le serveur Python délègue la vérification des cookies/tokens de session au processus web Gunicorn. Une connexion sans en-tête Host ou Origin valide est immédiatement rejetée.
  • Isolation par site : chaque connexion est associée à un site précis ; les messages ne peuvent pas traverser les frontières de sites.
  • Invités : par défaut, les handlers personnalisés refusent les connexions de type Guest. Passez allow_guest=True explicitement si votre cas d'usage le requiert.
  • Erreurs d'authentification : les messages d'erreur internes ne sont pas renvoyés au client pour éviter toute fuite d'information.

Dépannage

SymptômeCause probableSolution
Le handler n'est jamais appeléModule monapp/realtime.py introuvableVérifiez que le fichier existe et que l'app est installée
frappe.get_doc() lève une erreurfrappe_context=FalsePassez frappe_context=True (défaut)
Connexion refuséeEn-tête Origin absentVérifiez la configuration du proxy (Nginx)
Événements non reçusServeur Python non activéAjoutez "use_python_socketio": true dans common_site_config.json

Migration depuis Node.js : les deux serveurs (Node.js et Python) peuvent coexister pendant la transition. Pour basculer définitivement sur le serveur Python, activez use_python_socketio: true dans common_site_config.json et redémarrez le service frappe-socketio.