# Извикайте Python програма, която остава пусната, с PYPELINE()

> Стартирайте Python скрипт от приложението, изпращайте му JSON съобщения през сокет и получавайте отговорите му, като скриптът се рестартира, ако умре.

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

`PYPELINE(name, [options])` стартира `pypelines/<name>.py` с `python3` и поддържа връзка с него, така че приложението да може да използва Python библиотеки (данни, модели, обработка на изображения), без да стартира нов процес за всяка заявка. Връща обект с `send(data)` за изпращане на съобщение, `on('data', fn)` за получаване на отговорите и `close()` за спиране на скрипта. Име, което започва с `~`, е вместо това пътят на файл (`~/opt/job.py`), а текст, който е код (има нови редове или прилича на Python), се изпълнява като вграден код с `python3 -c`.

- **Протоколът.** Скриптът се стартира като `python3 -u script.py <socket>`: **последният аргумент е пътят на Unix сокет**, на който приложението слуша, и скриптът се свързва към него. Всяко съобщение, в двете посоки, е кадър: 4 байта с дължината (big endian), следвани от тялото, което по подразбиране е JSON.
- **Опции:** `type` (`'json'` по подразбиране, `'text'` или нещо друго за суров `Buffer`), `autorestart` (`false`, за да не се рестартира скриптът, когато приключи; по подразбиране се стартира отново), `threads` и `socket: false` (без сокет, само стартиране на програмата).
- Съобщенията, изпратени преди скриптът да се е свързал, чакат и се доставят, когато той се свърже.
- Събития: `open` (скриптът се свърза), `data` (съобщение), `restart` и `close`.
- **Пътят на сокета има ограничение.** Сокетът е файл в папката `tmp` на приложението и системата ограничава целия път (около 104 знака в macOS и 108 в Linux). В проект, който е дълбоко на диска, сървърът се проваля с `listen EINVAL`, скриптът никога не се свързва и съобщенията чакат вечно. Пътят е свойството `pypeline.socket`: задайте го веднага след `PYPELINE()` (файлът се проверява и сървърът се стартира след това) на кратък, както прави примерът.
- Python програмата е **обикновена програма, която работи с правата на приложението**: тя е толкова сигурна, колкото е кодът на файла, и трябва да е инсталиран Python 3 (`python3` в `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 });
}
```
