# Appeler un programme Python qui reste en marche avec PYPELINE()

> Démarrez un script Python depuis l'application, envoyez-lui des messages JSON par un socket et recevez ses réponses, le script étant relancé s'il meurt.

Framework: total5
Product: Total.js 5 (total5@0.0.19)
Language: fr
Content-SHA256: e2c689751625abc8082898edc80d5a738749bc3812de67ed8fc7b6a6fefc6637
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded

`PYPELINE(name, [options])` démarre `pypelines/<name>.py` avec `python3` et garde une connexion avec lui, de sorte que l'application peut utiliser des bibliothèques Python (données, modèles, traitement d'images) sans démarrer un nouveau processus pour chaque requête. Il renvoie un objet avec `send(data)` pour envoyer un message, `on('data', fn)` pour recevoir les réponses et `close()` pour arrêter le script. Un nom qui commence par `~` est au contraire le chemin d'un fichier (`~/opt/job.py`), et un texte qui est du code (il a des sauts de ligne ou ressemble à du Python) est exécuté comme un programme en ligne avec `python3 -c`.

- **Le protocole.** Le script est démarré comme `python3 -u script.py <socket>` : le **dernier argument est le chemin d'un socket Unix** sur lequel l'application écoute, et le script s'y connecte. Chaque message, dans les deux sens, est une trame : 4 octets avec la longueur (big endian) suivis du corps, qui est du JSON par défaut.
- **Options :** `type` (`'json'` par défaut, `'text'`, ou toute autre valeur pour des `Buffer` bruts), `autorestart` (`false` pour ne pas relancer le script quand il se termine ; par défaut il est relancé), `threads` et `socket: false` (pas de socket, seulement démarrer le programme).
- Les messages envoyés avant que le script se soit connecté attendent et sont remis quand il se connecte.
- Événements : `open` (le script s'est connecté), `data` (un message), `restart` et `close`.
- **Le chemin du socket a une limite.** Le socket est un fichier dans le dossier `tmp` de l'application et le système limite le chemin entier (environ 104 caractères sur macOS et 108 sur Linux). Dans un projet profondément enfoui sur le disque, le serveur échoue avec `listen EINVAL`, le script ne se connecte jamais et les messages attendent indéfiniment. Le chemin est la propriété `pypeline.socket` : définissez-la juste après `PYPELINE()` (le fichier est vérifié et le serveur démarré ensuite) à un chemin court, comme le fait l'exemple.
- Le programme Python est un **programme normal qui s'exécute avec les droits de l'application** : il n'est aussi sûr que le code du fichier, et Python 3 doit être installé (`python3` dans le `PATH`).

```python pypelines/shout.py
import json
import socket
import struct
import sys

# The last argument is the socket that the application listens on
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(sys.argv[-1])

def read(count):
    data = b''
    while len(data) < count:
        chunk = sock.recv(count - len(data))
        if not chunk:
            raise SystemExit(0)
        data += chunk
    return data

while True:
    size = struct.unpack('>I', read(4))[0]
    message = json.loads(read(size).decode('utf-8'))
    answer = json.dumps({ 'id': message['id'], 'upper': message['text'].upper(), 'words': len(message['text'].split()) }).encode('utf-8')
    sock.sendall(struct.pack('>I', len(answer)) + answer)
```

```js controllers/py.js
exports.install = function() {
	ROUTE('POST /shout/ @json', shout);
};

const pypeline = PYPELINE('shout');

// A short path for the socket: the folder of a project can be too deep for a Unix socket
pypeline.socket = require('os').tmpdir() + '/forge-shout-' + process.pid + '.socket';
const pending = {};
let counter = 0;

// Every answer says which message it is for
pypeline.on('data', function(answer) {
	const callback = pending[answer.id];
	if (callback) {
		delete pending[answer.id];
		callback(answer);
	}
});

function shout($) {
	const id = ++counter;
	const timer = setTimeout(() => $.invalid('The script did not answer'), 5000);
	pending[id] = function(answer) {
		clearTimeout(timer);
		$.json(answer);
	};
	pypeline.send({ id: id, text: $.body.text });
}
```
