[{"data":1,"prerenderedAt":1400},["ShallowReactive",2],{"\u002Fv5\u002Fdodock\u002Fdeveloppeurs\u002Fserveur-temps-reel-python":3,"section:\u002Fv5\u002Fdodock\u002Fdeveloppeurs\u002Fserveur-temps-reel-python":1399},{"id":4,"title":5,"body":6,"description":1379,"extension":1380,"meta":1381,"navigation":482,"path":1395,"seo":1396,"stem":1397,"__hash__":1398},"docs\u002F1.v5\u002F3.dodock\u002F10.developpeurs\u002Fserveur-temps-reel-python.md","Serveur temps réel Python (Socket.IO)",{"type":7,"value":8,"toc":1360},"minimark",[9,13,27,30,57,62,73,83,88,107,134,151,178,182,189,331,338,407,430,434,445,459,463,466,522,525,544,547,558,720,727,780,788,805,819,823,864,870,982,986,1007,1124,1128,1139,1147,1150,1167,1207,1329,1334,1338,1356],[10,11,5],"h1",{"id":12},"serveur-temps-réel-python-socketio",[14,15,16,17,21,22,26],"p",{},"Dodock fournit un serveur temps réel Socket.IO écrit en Python (asyncio et uvicorn). Il est ",[18,19,20],"strong",{},"optionnel"," : le serveur Node.js (",[23,24,25],"code",{},"socketio.js",") reste le serveur par défaut et rien ne change sur un bench tant que vous ne choisissez pas le serveur Python.",[14,28,29],{},"Cette page s'adresse aux développeurs et aux administrateurs de bench. Elle explique comment lancer le serveur, comment une application y enregistre ses propres événements, et comment le code serveur publie des messages vers les navigateurs connectés.",[31,32,35],"callout",{"color":33,"icon":34},"blue","i-mdi-information-outline",[14,36,37,38,41,42,41,45,48,49,52,53,56],{},"Sur Dodock v5, aucun processus du bench ne démarre ce serveur automatiquement : vous le lancez vous-même (voir ci-dessous). Les dépendances ",[23,39,40],{},"python-socketio",", ",[23,43,44],{},"python-engineio",[23,46,47],{},"uvicorn"," et ",[23,50,51],{},"httpx"," sont installées avec Dodock, mais seul le serveur Python les importe. ",[23,54,55],{},"import frappe"," et le processus web ne les chargent pas.",[58,59,61],"h2",{"id":60},"lancer-le-serveur-python","Lancer le serveur Python",[14,63,64,65,68,69,72],{},"Le serveur est un processus séparé du serveur web (Gunicorn). Pour l'utiliser à la place du serveur Node.js, remplacez la ligne ",[23,66,67],{},"socketio:"," du ",[23,70,71],{},"Procfile"," du bench par :",[74,75,81],"pre",{"className":76,"code":78,"language":79,"meta":80},[77],"language-text","socketio: .\u002Fenv\u002Fbin\u002Fpython -m frappe.realtime.server\n","text","",[23,82,78],{"__ignoreMap":80},[84,85,87],"h3",{"id":86},"en-production-supervisor-et-nginx","En production (Supervisor et Nginx)",[14,89,90,91,94,95,98,99,102,103,106],{},"L'outil ",[23,92,93],{},"bench"," ne connaît pas le serveur Python : ",[23,96,97],{},"bench setup supervisor"," génère toujours le programme ",[23,100,101],{},"\u003Cbench>-node-socketio"," avec le serveur Node.js. Dans le fichier ",[23,104,105],{},"config\u002Fsupervisor.conf"," du bench, remplacez la commande de ce programme :",[74,108,112],{"className":109,"code":110,"language":111,"meta":80,"style":80},"language-ini shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","[program:mon-bench-node-socketio]\ncommand=\u002Fhome\u002Fdokos\u002Fmon-bench\u002Fenv\u002Fbin\u002Fpython -m frappe.realtime.server\ndirectory=\u002Fhome\u002Fdokos\u002Fmon-bench\n","ini",[23,113,114,122,128],{"__ignoreMap":80},[115,116,119],"span",{"class":117,"line":118},"line",1,[115,120,121],{},"[program:mon-bench-node-socketio]\n",[115,123,125],{"class":117,"line":124},2,[115,126,127],{},"command=\u002Fhome\u002Fdokos\u002Fmon-bench\u002Fenv\u002Fbin\u002Fpython -m frappe.realtime.server\n",[115,129,131],{"class":117,"line":130},3,[115,132,133],{},"directory=\u002Fhome\u002Fdokos\u002Fmon-bench\n",[14,135,136,137,140,141,144,145,48,147,150],{},"Rechargez ensuite Supervisor (",[23,138,139],{},"sudo supervisorctl reread"," puis ",[23,142,143],{},"sudo supervisorctl update","). ",[23,146,97],{},[23,148,149],{},"bench setup production"," réécrivent ce fichier : refaites la modification après chaque exécution.",[14,152,153,154,157,158,161,162,165,166,169,170,173,174,177],{},"La configuration Nginx ne change pas : le bloc ",[23,155,156],{},"location \u002Fsocket.io"," envoie déjà les connexions vers ",[23,159,160],{},"127.0.0.1:\u003Csocketio_port>",", le port sur lequel le serveur Python écoute. Si vous utilisez ",[23,163,164],{},"socketio_uds",", modifiez vous-même l'",[23,167,168],{},"upstream"," ",[23,171,172],{},"\u003Cbench>-socketio-server"," du fichier Nginx pour pointer vers ",[23,175,176],{},"unix:\u003Cchemin du socket>",".",[84,179,181],{"id":180},"configuration","Configuration",[14,183,184,185,188],{},"Le serveur lit sa configuration dans ",[23,186,187],{},"sites\u002Fcommon_site_config.json"," :",[190,191,192,208],"table",{},[193,194,195],"thead",{},[196,197,198,202,205],"tr",{},[199,200,201],"th",{},"Clé",[199,203,204],{},"Rôle",[199,206,207],{},"Valeur par défaut",[209,210,211,227,239,255,274,288,303,318],"tbody",{},[196,212,213,219,222],{},[214,215,216],"td",{},[23,217,218],{},"socketio_port",[214,220,221],{},"Port d'écoute (toutes les interfaces)",[214,223,224],{},[23,225,226],{},"9000",[196,228,229,233,236],{},[214,230,231],{},[23,232,164],{},[214,234,235],{},"Chemin d'un socket Unix à utiliser à la place du port",[214,237,238],{},"aucune",[196,240,241,246,249],{},[214,242,243],{},[23,244,245],{},"redis_queue",[214,247,248],{},"URL du Redis de la file d'attente, où le serveur écoute les messages publiés",[214,250,251,254],{},[23,252,253],{},"redis:\u002F\u002F127.0.0.1:11311"," si la clé est absente",[196,256,257,262,272],{},[214,258,259],{},[23,260,261],{},"default_site",[214,263,264,265,268,269],{},"Site utilisé quand la connexion vient de ",[23,266,267],{},"localhost"," ou ",[23,270,271],{},"127.0.0.1",[214,273,238],{},[196,275,276,281,284],{},[214,277,278],{},[23,279,280],{},"webserver_host",[214,282,283],{},"Hôte du serveur web que le serveur temps réel appelle pour authentifier et vérifier les droits",[214,285,286],{},[23,287,271],{},[196,289,290,295,298],{},[214,291,292],{},[23,293,294],{},"webserver_port",[214,296,297],{},"Port de ce serveur web",[214,299,300],{},[23,301,302],{},"8000",[196,304,305,310,313],{},[214,306,307],{},[23,308,309],{},"socketio_worker_threads",[214,311,312],{},"Nombre de threads pour le travail bloquant (authentification, gestionnaires synchrones)",[214,314,315],{},[23,316,317],{},"4",[196,319,320,325,328],{},[214,321,322],{},[23,323,324],{},"developer_mode",[214,326,327],{},"Active le relais « ouvrir dans l'éditeur » du mode développeur",[214,329,330],{},"désactivé",[14,332,333,334,337],{},"Exemple fictif de ",[23,335,336],{},"common_site_config.json"," pour un bench dont le serveur web écoute sur le port 8001 :",[74,339,343],{"className":340,"code":341,"language":342,"meta":80,"style":80},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"socketio_port\": 9000,\n  \"webserver_port\": 8001,\n  \"socketio_worker_threads\": 8\n}\n","json",[23,344,345,351,372,387,401],{"__ignoreMap":80},[115,346,347],{"class":117,"line":118},[115,348,350],{"class":349},"sMK4o","{\n",[115,352,353,356,359,362,365,369],{"class":117,"line":124},[115,354,355],{"class":349},"  \"",[115,357,218],{"class":358},"spNyl",[115,360,361],{"class":349},"\"",[115,363,364],{"class":349},":",[115,366,368],{"class":367},"sbssI"," 9000",[115,370,371],{"class":349},",\n",[115,373,374,376,378,380,382,385],{"class":117,"line":130},[115,375,355],{"class":349},[115,377,294],{"class":358},[115,379,361],{"class":349},[115,381,364],{"class":349},[115,383,384],{"class":367}," 8001",[115,386,371],{"class":349},[115,388,390,392,394,396,398],{"class":117,"line":389},4,[115,391,355],{"class":349},[115,393,309],{"class":358},[115,395,361],{"class":349},[115,397,364],{"class":349},[115,399,400],{"class":367}," 8\n",[115,402,404],{"class":117,"line":403},5,[115,405,406],{"class":349},"}\n",[14,408,409,410,413,414,417,418,421,422,425,426,429],{},"Les clients se connectent sur le namespace ",[23,411,412],{},"\u002F{site}"," et doivent présenter soit le cookie ",[23,415,416],{},"sid",", soit un en-tête ",[23,419,420],{},"Authorization",". Le serveur délègue l'authentification au serveur web : il appelle la méthode ",[23,423,424],{},"frappe.realtime.get_user_info"," avec un secret partagé, stocké dans Redis (clé ",[23,427,428],{},"socketio_auth_secret",") et comparé en temps constant. La connexion est refusée si l'origine ne correspond pas à l'hôte, si le namespace n'est pas le site, ou si l'utilisateur n'est pas reconnu.",[58,431,433],{"id":432},"écrire-des-gestionnaires-dans-une-application","Écrire des gestionnaires dans une application",[14,435,436,437,440,441,444],{},"Créez le module ",[23,438,439],{},"realtime\u002Fhandlers.py"," dans le paquet Python de votre application (",[23,442,443],{},"mon_app.realtime.handlers","). Au démarrage, le serveur importe ce module pour chaque application installée du bench :",[446,447,448,452],"ul",{},[449,450,451],"li",{},"si le module n'existe pas, l'application n'a simplement aucun gestionnaire ;",[449,453,454,455,458],{},"si le module existe mais lève une erreur à l'import, le serveur ",[18,456,457],{},"échoue au démarrage"," : l'erreur n'est pas masquée.",[84,460,462],{"id":461},"exemple","Exemple",[14,464,465],{},"Exemple fictif : un utilisateur s'abonne aux mises à jour d'un projet s'il a le droit de le lire.",[74,467,471],{"className":468,"code":469,"language":470,"meta":80,"style":80},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","from frappe.realtime import Socket, realtime\n\n\n@realtime.on(\"project_subscribe\")\nasync def project_subscribe(socket: Socket, project: str) -> dict:\n    allowed = await socket.has_permission(\"Project\", project)\n    if allowed:\n        await socket.join(f\"project:{project}\")\n    return {\"subscribed\": allowed}\n","python",[23,472,473,478,484,488,493,498,504,510,516],{"__ignoreMap":80},[115,474,475],{"class":117,"line":118},[115,476,477],{},"from frappe.realtime import Socket, realtime\n",[115,479,480],{"class":117,"line":124},[115,481,483],{"emptyLinePlaceholder":482},true,"\n",[115,485,486],{"class":117,"line":130},[115,487,483],{"emptyLinePlaceholder":482},[115,489,490],{"class":117,"line":389},[115,491,492],{},"@realtime.on(\"project_subscribe\")\n",[115,494,495],{"class":117,"line":403},[115,496,497],{},"async def project_subscribe(socket: Socket, project: str) -> dict:\n",[115,499,501],{"class":117,"line":500},6,[115,502,503],{},"    allowed = await socket.has_permission(\"Project\", project)\n",[115,505,507],{"class":117,"line":506},7,[115,508,509],{},"    if allowed:\n",[115,511,513],{"class":117,"line":512},8,[115,514,515],{},"        await socket.join(f\"project:{project}\")\n",[115,517,519],{"class":117,"line":518},9,[115,520,521],{},"    return {\"subscribed\": allowed}\n",[14,523,524],{},"Côté serveur, le code qui modifie le projet publie ensuite vers la même salle :",[74,526,528],{"className":468,"code":527,"language":470,"meta":80,"style":80},"from frappe.realtime import publish_to_room\n\npublish_to_room(\"project:PROJ-0001\", \"project_updated\", {\"status\": \"Open\"})\n",[23,529,530,535,539],{"__ignoreMap":80},[115,531,532],{"class":117,"line":118},[115,533,534],{},"from frappe.realtime import publish_to_room\n",[115,536,537],{"class":117,"line":124},[115,538,483],{"emptyLinePlaceholder":482},[115,540,541],{"class":117,"line":130},[115,542,543],{},"publish_to_room(\"project:PROJ-0001\", \"project_updated\", {\"status\": \"Open\"})\n",[14,545,546],{},"Le nom de salle publié doit être identique à celui rejoint par le gestionnaire.",[14,548,549,550,553,554,557],{},"Côté navigateur, émettez l'événement avec ",[23,551,552],{},"frappe.realtime.emit",", et écoutez les messages publiés avec ",[23,555,556],{},"frappe.realtime.on",". Les arguments qui suivent le nom de l'événement sont transmis au gestionnaire. Une fonction placée en dernier argument reçoit l'accusé de réception (la valeur retournée par le gestionnaire) :",[74,559,563],{"className":560,"code":561,"language":562,"meta":80,"style":80},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","frappe.realtime.emit(\"project_subscribe\", \"PROJ-0001\", (result) => {\n  console.log(result.subscribed);\n});\nfrappe.realtime.on(\"project_updated\", (data) => {\n  console.log(data.status);\n});\n","js",[23,564,565,622,647,656,691,712],{"__ignoreMap":80},[115,566,567,571,573,576,578,582,585,587,591,593,596,599,602,604,606,609,613,616,619],{"class":117,"line":118},[115,568,570],{"class":569},"sTEyZ","frappe",[115,572,177],{"class":349},[115,574,575],{"class":569},"realtime",[115,577,177],{"class":349},[115,579,581],{"class":580},"s2Zo4","emit",[115,583,584],{"class":569},"(",[115,586,361],{"class":349},[115,588,590],{"class":589},"sfazB","project_subscribe",[115,592,361],{"class":349},[115,594,595],{"class":349},",",[115,597,598],{"class":349}," \"",[115,600,601],{"class":589},"PROJ-0001",[115,603,361],{"class":349},[115,605,595],{"class":349},[115,607,608],{"class":349}," (",[115,610,612],{"class":611},"sHdIc","result",[115,614,615],{"class":349},")",[115,617,618],{"class":358}," =>",[115,620,621],{"class":349}," {\n",[115,623,624,627,629,632,635,637,639,642,644],{"class":117,"line":124},[115,625,626],{"class":569},"  console",[115,628,177],{"class":349},[115,630,631],{"class":580},"log",[115,633,584],{"class":634},"swJcz",[115,636,612],{"class":569},[115,638,177],{"class":349},[115,640,641],{"class":569},"subscribed",[115,643,615],{"class":634},[115,645,646],{"class":349},";\n",[115,648,649,652,654],{"class":117,"line":130},[115,650,651],{"class":349},"}",[115,653,615],{"class":569},[115,655,646],{"class":349},[115,657,658,660,662,664,666,669,671,673,676,678,680,682,685,687,689],{"class":117,"line":389},[115,659,570],{"class":569},[115,661,177],{"class":349},[115,663,575],{"class":569},[115,665,177],{"class":349},[115,667,668],{"class":580},"on",[115,670,584],{"class":569},[115,672,361],{"class":349},[115,674,675],{"class":589},"project_updated",[115,677,361],{"class":349},[115,679,595],{"class":349},[115,681,608],{"class":349},[115,683,684],{"class":611},"data",[115,686,615],{"class":349},[115,688,618],{"class":358},[115,690,621],{"class":349},[115,692,693,695,697,699,701,703,705,708,710],{"class":117,"line":403},[115,694,626],{"class":569},[115,696,177],{"class":349},[115,698,631],{"class":580},[115,700,584],{"class":634},[115,702,684],{"class":569},[115,704,177],{"class":349},[115,706,707],{"class":569},"status",[115,709,615],{"class":634},[115,711,646],{"class":349},[115,713,714,716,718],{"class":117,"line":500},[115,715,651],{"class":349},[115,717,615],{"class":569},[115,719,646],{"class":349},[84,721,723,724],{"id":722},"règles-du-décorateur-realtimeon","Règles du décorateur ",[23,725,726],{},"realtime.on",[446,728,729,736,739,767,770,773],{},[449,730,731,732,735],{},"Le premier argument du gestionnaire est toujours le ",[23,733,734],{},"Socket",". Les arguments suivants sont ceux envoyés par le client, dans l'ordre.",[449,737,738],{},"Le décorateur retourne la fonction inchangée : vous pouvez l'appeler directement dans un test.",[449,740,741,742,745,746,749,750,745,753,755,756,759,760,763,764,177],{},"Options : ",[23,743,744],{},"frappe_context"," (défaut ",[23,747,748],{},"False",") et ",[23,751,752],{},"allow_guest",[23,754,748],{},"). Avec ",[23,757,758],{},"allow_guest=False",", l'événement est ignoré si ",[23,761,762],{},"socket.user"," vaut ",[23,765,766],{},"Guest",[449,768,769],{},"Une application qui n'est pas installée sur le site de la connexion ne voit pas ses gestionnaires exécutés. L'application propriétaire est détectée automatiquement à l'import.",[449,771,772],{},"Une exception levée par un gestionnaire est journalisée ; les autres gestionnaires du même événement s'exécutent quand même.",[449,774,775,776,779],{},"Si plusieurs gestionnaires répondent au même événement, le dernier résultat différent de ",[23,777,778],{},"None"," est renvoyé comme accusé de réception au client qui a émis avec une fonction de rappel.",[84,781,783,784,787],{"id":782},"gestionnaire-async-ou-synchrone","Gestionnaire ",[23,785,786],{},"async"," ou synchrone",[14,789,790,791,794,795,798,799,801,802,804],{},"Écrivez de préférence des gestionnaires ",[23,792,793],{},"async def"," : ils s'exécutent sur la boucle d'événements et utilisent ",[23,796,797],{},"await"," pour les appels au ",[23,800,734],{},". Ne bloquez jamais la boucle depuis un gestionnaire ",[23,803,786],{}," : une entrée\u002Fsortie bloquante ou un calcul long figerait tous les autres sockets.",[14,806,807,808,811,812,815,816,818],{},"Un gestionnaire ",[23,809,810],{},"def"," ordinaire est accepté. Il s'exécute dans un thread de travail et reçoit un ",[23,813,814],{},"SyncSocket",", dont l'API est identique mais sans ",[23,817,797],{},". Utilisez-le quand le corps doit bloquer (bibliothèque sans client asynchrone, contexte Frappe).",[84,820,822],{"id":821},"vérifier-les-droits-deux-méthodes","Vérifier les droits : deux méthodes",[824,825,826,843],"ol",{},[449,827,828,169,831,834,835,268,838,840,841,177],{},[18,829,830],{},"Appel HTTP (recommandé).",[23,832,833],{},"await socket.has_permission(doctype, name=None)"," interroge le serveur web et renvoie ",[23,836,837],{},"True",[23,839,748],{},". Aucune connexion à la base de données n'est ouverte dans le processus temps réel. En cas d'erreur, la méthode renvoie ",[23,842,748],{},[449,844,845,848,849,852,853,856,857,859,860,863],{},[18,846,847],{},"Contexte Frappe."," Avec ",[23,850,851],{},"@realtime.on(\"project_subscribe\", frappe_context=True)",", le corps du gestionnaire s'exécute dans un contexte Frappe complet (base de données et session de l'utilisateur) : vous pouvez appeler ",[23,854,855],{},"frappe.has_permission"," directement. Chaque événement paie alors l'initialisation du contexte et ouvre une connexion à la base : réservez cette option aux cas qui en ont besoin. Le gestionnaire doit être une fonction ordinaire ; marquer un ",[23,858,793],{}," avec ",[23,861,862],{},"frappe_context=True"," lève une erreur à l'import.",[84,865,867,868],{"id":866},"lobjet-socket","L'objet ",[23,869,734],{},[190,871,872,881],{},[193,873,874],{},[196,875,876,879],{},[199,877,878],{},"Membre",[199,880,204],{},[209,882,883,907,922,936,950,963,973],{},[196,884,885,898],{},[214,886,887,41,890,41,892,41,895],{},[23,888,889],{},"socket.site",[23,891,762],{},[23,893,894],{},"socket.user_type",[23,896,897],{},"socket.installed_apps",[214,899,900,901,763,904,906],{},"Identité du socket, en lecture seule (",[23,902,903],{},"user",[23,905,766],{}," pour un anonyme)",[196,908,909,917],{},[214,910,911,41,914],{},[23,912,913],{},"socket.sid",[23,915,916],{},"socket.namespace",[214,918,919,920,615],{},"Identifiant du socket et namespace (",[23,921,412],{},[196,923,924,933],{},[214,925,926,929,930],{},[23,927,928],{},"await socket.join(room)"," \u002F ",[23,931,932],{},"await socket.leave(room)",[214,934,935],{},"Ajouter ou retirer le socket d'une salle",[196,937,938,943],{},[214,939,940],{},[23,941,942],{},"await socket.emit(event, data=None, room=None)",[214,944,945,946,949],{},"Émettre vers une salle, ou vers ce client si ",[23,947,948],{},"room"," est omis",[196,951,952,960],{},[214,953,954,929,957],{},[23,955,956],{},"await socket.set(key, value)",[23,958,959],{},"socket.get(key, default=None)",[214,961,962],{},"État transitoire du socket, effacé à la déconnexion",[196,964,965,970],{},[214,966,967],{},[23,968,969],{},"socket.participants(room)",[214,971,972],{},"Identifiants des sockets présents dans une salle",[196,974,975,979],{},[214,976,977],{},[23,978,833],{},[214,980,981],{},"Vérification des droits via le serveur web",[58,983,985],{"id":984},"événements-fournis-par-dodock","Événements fournis par Dodock",[14,987,988,989,992,993,48,996,999,1000,1003,1004,177],{},"Le module ",[23,990,991],{},"frappe.realtime.handlers"," enregistre les événements du cœur. À la connexion, le socket rejoint les salles ",[23,994,995],{},"user:{utilisateur}",[23,997,998],{},"website",", ainsi que ",[23,1001,1002],{},"all"," si l'utilisateur est de type ",[23,1005,1006],{},"System User",[190,1008,1009,1019],{},[193,1010,1011],{},[196,1012,1013,1016],{},[199,1014,1015],{},"Événement client",[199,1017,1018],{},"Effet",[209,1020,1021,1034,1051,1067,1083,1095,1112],{},[196,1022,1023,1028],{},[214,1024,1025],{},[23,1026,1027],{},"ping",[214,1029,1030,1031],{},"Répond par ",[23,1032,1033],{},"pong",[196,1035,1036,1044],{},[214,1037,1038,929,1041],{},[23,1039,1040],{},"doctype_subscribe",[23,1042,1043],{},"doctype_unsubscribe",[214,1045,1046,1047,1050],{},"Rejoint ou quitte la salle ",[23,1048,1049],{},"doctype:{doctype}"," (droit de lecture vérifié à l'abonnement)",[196,1052,1053,1061],{},[214,1054,1055,929,1058],{},[23,1056,1057],{},"doc_subscribe",[23,1059,1060],{},"doc_unsubscribe",[214,1062,1046,1063,1066],{},[23,1064,1065],{},"doc:{doctype}\u002F{nom}"," (droit de lecture vérifié)",[196,1068,1069,1077],{},[214,1070,1071,929,1074],{},[23,1072,1073],{},"task_subscribe",[23,1075,1076],{},"progress_subscribe",[214,1078,1079,1080],{},"Rejoint la salle ",[23,1081,1082],{},"task_progress:{task_id}",[196,1084,1085,1090],{},[214,1086,1087],{},[23,1088,1089],{},"task_unsubscribe",[214,1091,1092,1093],{},"Quitte la salle ",[23,1094,1082],{},[196,1096,1097,1105],{},[214,1098,1099,929,1102],{},[23,1100,1101],{},"doc_open",[23,1103,1104],{},"doc_close",[214,1106,1107,1108,1111],{},"Suit les utilisateurs qui ouvrent un document et émet ",[23,1109,1110],{},"doc_viewers"," avec la liste des utilisateurs",[196,1113,1114,1119],{},[214,1115,1116],{},[23,1117,1118],{},"open_in_editor",[214,1120,1121,1122],{},"Réservé au mode développeur : relaie la demande vers le canal Redis ",[23,1123,1118],{},[58,1125,1127],{"id":1126},"publier-des-événements-depuis-le-code-serveur","Publier des événements depuis le code serveur",[14,1129,1130,1131,1134,1135,1138],{},"Le processus web ne parle pas directement aux sockets. Il publie dans Redis (",[23,1132,1133],{},"publish_realtime",", inchangé) et le serveur temps réel relaie chaque message du canal ",[23,1136,1137],{},"events"," vers les clients connectés :",[446,1140,1141,1144],{},[449,1142,1143],{},"avec une salle : le message est émis dans cette salle, sur le namespace du site ;",[449,1145,1146],{},"sans salle : le message est diffusé à tous les sites connectés (événements de build).",[14,1148,1149],{},"Le serveur se reconnecte seul à Redis en cas de coupure et ignore les messages mal formés.",[14,1151,988,1152,1155,1156,1158,1159,1162,1163,1166],{},[23,1153,1154],{},"frappe.realtime"," fournit des fonctions nommées, qui s'appuient sur ",[23,1157,1133],{},". Toutes acceptent l'option ",[23,1160,1161],{},"after_commit=False"," (mot-clé uniquement) : avec ",[23,1164,1165],{},"after_commit=True",", la publication attend la validation de la transaction en cours.",[74,1168,1170],{"className":468,"code":1169,"language":470,"meta":80,"style":80},"publish_to_user(user, event, message=None, *, after_commit=False)\npublish_to_doc(doctype, docname, event, message=None, *, after_commit=False)\npublish_to_doctype(doctype, event, message=None, *, after_commit=False)\npublish_task_progress(task_id, message=None, *, after_commit=False)\npublish_to_website(event, message=None, *, after_commit=False)\npublish_to_all(event, message=None, *, after_commit=False)\npublish_to_room(room, event, message=None, *, after_commit=False)\n",[23,1171,1172,1177,1182,1187,1192,1197,1202],{"__ignoreMap":80},[115,1173,1174],{"class":117,"line":118},[115,1175,1176],{},"publish_to_user(user, event, message=None, *, after_commit=False)\n",[115,1178,1179],{"class":117,"line":124},[115,1180,1181],{},"publish_to_doc(doctype, docname, event, message=None, *, after_commit=False)\n",[115,1183,1184],{"class":117,"line":130},[115,1185,1186],{},"publish_to_doctype(doctype, event, message=None, *, after_commit=False)\n",[115,1188,1189],{"class":117,"line":389},[115,1190,1191],{},"publish_task_progress(task_id, message=None, *, after_commit=False)\n",[115,1193,1194],{"class":117,"line":403},[115,1195,1196],{},"publish_to_website(event, message=None, *, after_commit=False)\n",[115,1198,1199],{"class":117,"line":500},[115,1200,1201],{},"publish_to_all(event, message=None, *, after_commit=False)\n",[115,1203,1204],{"class":117,"line":506},[115,1205,1206],{},"publish_to_room(room, event, message=None, *, after_commit=False)\n",[190,1208,1209,1222],{},[193,1210,1211],{},[196,1212,1213,1216,1219],{},[199,1214,1215],{},"Fonction",[199,1217,1218],{},"Salle",[199,1220,1221],{},"Destinataires",[209,1223,1224,1239,1254,1268,1285,1299,1316],{},[196,1225,1226,1231,1236],{},[214,1227,1228],{},[23,1229,1230],{},"publish_to_user",[214,1232,1233],{},[23,1234,1235],{},"user:{user}",[214,1237,1238],{},"Tous les sockets de cet utilisateur sur ce site",[196,1240,1241,1246,1251],{},[214,1242,1243],{},[23,1244,1245],{},"publish_to_doc",[214,1247,1248],{},[23,1249,1250],{},"doc:{doctype}\u002F{docname}",[214,1252,1253],{},"Sockets abonnés à ce document",[196,1255,1256,1261,1265],{},[214,1257,1258],{},[23,1259,1260],{},"publish_to_doctype",[214,1262,1263],{},[23,1264,1049],{},[214,1266,1267],{},"Sockets abonnés à ce type de document",[196,1269,1270,1275,1279],{},[214,1271,1272],{},[23,1273,1274],{},"publish_task_progress",[214,1276,1277],{},[23,1278,1082],{},[214,1280,1281,1282,615],{},"Sockets qui suivent cette tâche (pas d'argument ",[23,1283,1284],{},"event",[196,1286,1287,1292,1296],{},[214,1288,1289],{},[23,1290,1291],{},"publish_to_website",[214,1293,1294],{},[23,1295,998],{},[214,1297,1298],{},"Tous les sockets de ce site",[196,1300,1301,1306,1310],{},[214,1302,1303],{},[23,1304,1305],{},"publish_to_all",[214,1307,1308],{},[23,1309,1002],{},[214,1311,1312,1313,1315],{},"Utilisateurs système (",[23,1314,1006],{},") de ce site, pas les invités",[196,1317,1318,1323,1326],{},[214,1319,1320],{},[23,1321,1322],{},"publish_to_room",[214,1324,1325],{},"la salle passée en argument",[214,1327,1328],{},"Les sockets de cette salle",[14,1330,1331,1333],{},[23,1332,1305],{}," est limité au site courant ; ce n'est pas la diffusion sans salle utilisée pour les événements de build.",[58,1335,1337],{"id":1336},"voir-aussi","Voir aussi",[446,1339,1340,1350],{},[449,1341,1342,1347,1348,177],{},[1343,1344,1346],"a",{"href":1345},"\u002Fv5\u002Fdodock\u002Fdeveloppeurs\u002Farchitecture\u002Farchitecture-technique","Architecture technique"," : les composants du bench et le rôle de ",[23,1349,245],{},[449,1351,1352],{},[1343,1353,1355],{"href":1354},"\u002Fv5\u002Fadministrer\u002Fexploitation\u002Fbench\u002Fcommandes","Commandes Bench",[1357,1358,1359],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}",{"title":80,"searchDepth":118,"depth":124,"links":1361},[1362,1366,1376,1377,1378],{"id":60,"depth":124,"text":61,"children":1363},[1364,1365],{"id":86,"depth":130,"text":87},{"id":180,"depth":130,"text":181},{"id":432,"depth":124,"text":433,"children":1367},[1368,1369,1371,1373,1374],{"id":461,"depth":130,"text":462},{"id":722,"depth":130,"text":1370},"Règles du décorateur realtime.on",{"id":782,"depth":130,"text":1372},"Gestionnaire async ou synchrone",{"id":821,"depth":130,"text":822},{"id":866,"depth":130,"text":1375},"L'objet Socket",{"id":984,"depth":124,"text":985},{"id":1126,"depth":124,"text":1127},{"id":1336,"depth":124,"text":1337},"Lancer le serveur temps réel Python optionnel de Dodock, écrire des gestionnaires d'événements Socket.IO dans une application et publier des événements vers les navigateurs.","md",{"icon":1382,"sources":1383,"generated":482,"generated_at":1392,"source_mrs":1393,"reviewed":482},"i-mdi-lightning-bolt-outline",[1384,1385,1386,1387,1388,1389,1390,1391],"dodock@v5:frappe\u002Frealtime\u002F__init__.py","dodock@v5:frappe\u002Frealtime\u002Fserver.py","dodock@v5:frappe\u002Frealtime\u002Fconfig.py","dodock@v5:frappe\u002Frealtime\u002Fregistry.py","dodock@v5:frappe\u002Frealtime\u002Fhandlers.py","dodock@v5:frappe\u002Frealtime\u002Fsocket.py","dodock@v5:frappe\u002Frealtime\u002Fauth.py","dodock@v5:frappe\u002Frealtime\u002Fbridge.py","2026-11-13",[1394],"https:\u002F\u002Fgitlab.com\u002Fdokos\u002Fdodock\u002F-\u002Fmerge_requests\u002F12044","\u002Fv5\u002Fdodock\u002Fdeveloppeurs\u002Fserveur-temps-reel-python",{"title":5,"description":1379},"1.v5\u002F3.dodock\u002F10.developpeurs\u002Fserveur-temps-reel-python","dlPXNGSLESX7zNx1hrVog9oG8MMZ9dp7nKhLf4QzEIw",{},1794662845300]