├── .gitignore ├── README.md ├── example └── pythonShellNode.js ├── package-lock.json ├── package.json ├── src ├── PythonShellNode.js ├── pythonshell.html └── pythonshell.js ├── test ├── PythonShellNode_Spec.js ├── sample-file-read.py ├── sample-loop.py ├── sample-need-venv-file-read.py ├── sample-need-venv.py ├── sample-python3.py ├── sample-with-arg.py ├── sample.py ├── stdin-data.py └── test.txt └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_store 2 | .config.json 3 | .dist 4 | .jshintignore 5 | .npm 6 | .project 7 | .sessions.json 8 | .settings 9 | .tern-project 10 | *.backup 11 | *_cred* 12 | coverage 13 | credentials.json 14 | flows*.json 15 | nodes/node-red-nodes/ 16 | node_modules 17 | public 18 | locales/zz-ZZ 19 | nodes/core/locales/zz-ZZ 20 | 21 | test/venv -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Executing a python script from node-red. Input to the node will become the argument for the python script, output of the script will be sent to output of the node. 2 | 3 | * Now supporting continuous std in data as the input to the script. If the node is configured to receive continuous data from its input, clicking on the trigger will terminate the script. 4 | 5 | * Now supporting executing within a virtual environment. Specify the path to the virtualenv folder in node configuration. 6 | 7 | Example flow: 8 | 9 | ``` 10 | [{"id":"a1b2b31b.65fe7","type":"tab","label":"Flow 1"},{"id":"3df34b3a.b6bb8c","type":"pythonshell in","z":"a1b2b31b.65fe7","name":"","pyfile":"/Users/namtrang/main.py","x":341.5,"y":154,"wires":[["f811cd5c.e9dfe8"]]},{"id":"f4dcbeae.1da998","type":"inject","z":"a1b2b31b.65fe7","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":140.5,"y":76,"wires":[["3df34b3a.b6bb8c"]]},{"id":"f811cd5c.e9dfe8","type":"debug","z":"a1b2b31b.65fe7","name":"","active":true,"console":"false","complete":"false","x":537.5,"y":233,"wires":[]}] 11 | ``` 12 | 13 | And this is the content of the python script: 14 | 15 | ``` 16 | import sys 17 | print "Got arguments: ", sys.argv 18 | ``` 19 | -------------------------------------------------------------------------------- /example/pythonShellNode.js: -------------------------------------------------------------------------------- 1 | var PythonshellNode = require('../src/PythonShellNode'); 2 | 3 | var pyNode = new PythonshellNode({ 4 | pyfile: "./test/sample.py", 5 | virtualEnv: "./test/venv", 6 | }); 7 | 8 | pyNode.onInput({payload: ""}, console.log, console.log); -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-pythonshell", 3 | "version": "1.4.3", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 11 | "dev": true 12 | }, 13 | "brace-expansion": { 14 | "version": "1.1.11", 15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 16 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 17 | "dev": true, 18 | "requires": { 19 | "balanced-match": "1.0.0", 20 | "concat-map": "0.0.1" 21 | } 22 | }, 23 | "browser-stdout": { 24 | "version": "1.3.1", 25 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 26 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 27 | "dev": true 28 | }, 29 | "commander": { 30 | "version": "2.15.1", 31 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 32 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 33 | "dev": true 34 | }, 35 | "concat-map": { 36 | "version": "0.0.1", 37 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 38 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 39 | "dev": true 40 | }, 41 | "debug": { 42 | "version": "3.1.0", 43 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 44 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 45 | "dev": true, 46 | "requires": { 47 | "ms": "2.0.0" 48 | } 49 | }, 50 | "diff": { 51 | "version": "3.5.0", 52 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 53 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 54 | "dev": true 55 | }, 56 | "escape-string-regexp": { 57 | "version": "1.0.5", 58 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 59 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 60 | "dev": true 61 | }, 62 | "fs.realpath": { 63 | "version": "1.0.0", 64 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 65 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 66 | "dev": true 67 | }, 68 | "glob": { 69 | "version": "7.1.2", 70 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 71 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 72 | "dev": true, 73 | "requires": { 74 | "fs.realpath": "1.0.0", 75 | "inflight": "1.0.6", 76 | "inherits": "2.0.3", 77 | "minimatch": "3.0.4", 78 | "once": "1.4.0", 79 | "path-is-absolute": "1.0.1" 80 | } 81 | }, 82 | "growl": { 83 | "version": "1.10.5", 84 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 85 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 86 | "dev": true 87 | }, 88 | "has-flag": { 89 | "version": "3.0.0", 90 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 91 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 92 | "dev": true 93 | }, 94 | "he": { 95 | "version": "1.1.1", 96 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 97 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 98 | "dev": true 99 | }, 100 | "inflight": { 101 | "version": "1.0.6", 102 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 103 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 104 | "dev": true, 105 | "requires": { 106 | "once": "1.4.0", 107 | "wrappy": "1.0.2" 108 | } 109 | }, 110 | "inherits": { 111 | "version": "2.0.3", 112 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 113 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 114 | "dev": true 115 | }, 116 | "minimatch": { 117 | "version": "3.0.4", 118 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 119 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 120 | "dev": true, 121 | "requires": { 122 | "brace-expansion": "1.1.11" 123 | } 124 | }, 125 | "minimist": { 126 | "version": "0.0.8", 127 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 128 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 129 | "dev": true 130 | }, 131 | "mkdirp": { 132 | "version": "0.5.1", 133 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 134 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 135 | "dev": true, 136 | "requires": { 137 | "minimist": "0.0.8" 138 | } 139 | }, 140 | "mocha": { 141 | "version": "5.2.0", 142 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 143 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 144 | "dev": true, 145 | "requires": { 146 | "browser-stdout": "1.3.1", 147 | "commander": "2.15.1", 148 | "debug": "3.1.0", 149 | "diff": "3.5.0", 150 | "escape-string-regexp": "1.0.5", 151 | "glob": "7.1.2", 152 | "growl": "1.10.5", 153 | "he": "1.1.1", 154 | "minimatch": "3.0.4", 155 | "mkdirp": "0.5.1", 156 | "supports-color": "5.4.0" 157 | } 158 | }, 159 | "ms": { 160 | "version": "2.0.0", 161 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 162 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 163 | "dev": true 164 | }, 165 | "once": { 166 | "version": "1.4.0", 167 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 168 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 169 | "dev": true, 170 | "requires": { 171 | "wrappy": "1.0.2" 172 | } 173 | }, 174 | "path-is-absolute": { 175 | "version": "1.0.1", 176 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 177 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 178 | "dev": true 179 | }, 180 | "supports-color": { 181 | "version": "5.4.0", 182 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 183 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 184 | "dev": true, 185 | "requires": { 186 | "has-flag": "3.0.0" 187 | } 188 | }, 189 | "wrappy": { 190 | "version": "1.0.2", 191 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 192 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 193 | "dev": true 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-contrib-pythonshell", 3 | "version": "1.5.4", 4 | "description": "nodes used to interact with python processes", 5 | "scripts": { 6 | "test": "./node_modules/mocha/bin/mocha" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/namgk/node-red-contrib-pythonshell.git" 11 | }, 12 | "keywords": [ 13 | "distributed", 14 | "python", 15 | "node", 16 | "node-red" 17 | ], 18 | "author": "Nam Giang", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/namgk/node-red-contrib-pythonshell/issues" 22 | }, 23 | "homepage": "https://github.com/namgk/node-red-contrib-pythonshell#readme", 24 | "dependencies": {}, 25 | "node-red": { 26 | "nodes": { 27 | "pythonshell": "src/pythonshell.js" 28 | } 29 | }, 30 | "devDependencies": { 31 | "mocha": "^5.0.4" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/PythonShellNode.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | 3 | function PythonshellInNode(config) { 4 | if (!config.pyfile){ 5 | throw 'pyfile not present'; 6 | } 7 | this.pythonExec = config.python3 ? "python3" : "python"; 8 | this.pyfile = config.pyfile; 9 | this.virtualenv = config.virtualenv; 10 | 11 | if (!fs.existsSync(this.pyfile)) { 12 | throw 'pyfile not exist'; 13 | } 14 | 15 | if (this.virtualenv && !fs.existsSync(this.virtualenv)){ 16 | throw 'configured virtualenv not exist, consider remove or change'; 17 | } 18 | 19 | this.stdInData = config.stdInData; 20 | this.continuous = this.stdInData ? true : config.continuous; 21 | this.pydir = this.pyfile.substring(0, this.pyfile.lastIndexOf('/')); 22 | this.pyfile = this.pyfile.substring(this.pyfile.lastIndexOf('/') + 1, this.pyfile.length); 23 | this.spawn = require('child_process').spawn; 24 | this.onStatus = ()=>{} 25 | } 26 | 27 | PythonshellInNode.prototype.onInput = function(msg, out, err) { 28 | payload = msg.payload || ''; 29 | if (typeof payload === 'object'){ 30 | payload = JSON.stringify(payload); 31 | } else if (typeof payload !== 'string'){ 32 | payload = payload.toString(); 33 | } 34 | 35 | if (payload === 'pythonshell@close'){ 36 | if (this.py != null){ 37 | this.onClose() 38 | return 39 | } else { 40 | // trigger new execution 41 | payload = '' 42 | } 43 | } 44 | 45 | if (this.continuous && !this.stdInData && this.py != null){ 46 | this.onStatus({fill:"yellow",shape:"dot",text:"Not accepting input"}) 47 | return 48 | } 49 | 50 | var spawnCmd = (this.virtualenv ? this.virtualenv + '/bin/' : '') + this.pythonExec 51 | 52 | if (this.stdInData){ 53 | if (!this.py){ 54 | this.py = this.spawn(spawnCmd, ['-u', this.pyfile], { 55 | cwd: this.pydir, 56 | detached: true 57 | }); 58 | this.firstExecution = true 59 | } else { 60 | this.firstExecution = false 61 | } 62 | } else { 63 | this.py = this.spawn(spawnCmd, ['-u', this.pyfile, payload], { 64 | cwd: this.pydir 65 | }); 66 | } 67 | 68 | this.onStatus({fill:"green",shape:"dot",text:"Standby"}) 69 | 70 | // subsequence message, no need to setup callbacks 71 | if (this.stdInData && !this.firstExecution){ 72 | this.py.stdin.write(payload + '\n') 73 | return 74 | } 75 | 76 | var py = this.py; 77 | var dataString = ''; 78 | var errString = ''; 79 | 80 | py.stdout.on('data', data => { 81 | clearTimeout(this.standbyTimer) 82 | 83 | this.onStatus({fill:"green",shape:"dot",text:"Processing data"}) 84 | 85 | let dataStr = data.toString(); 86 | 87 | dataString += dataStr; 88 | 89 | if (dataString.endsWith("\n")){ 90 | if (this.continuous){ 91 | msg.payload = dataString; 92 | out(msg); 93 | dataString = '' 94 | } 95 | } 96 | 97 | this.standbyTimer = setTimeout(()=>{ 98 | this.onStatus({fill:"green",shape:"dot",text:"Standby"}) 99 | }, 2000) 100 | 101 | }); 102 | 103 | py.stderr.on('data', data => { 104 | errString += String(data);// just a different way to do it 105 | this.onStatus({fill:"red",shape:"dot",text:"Error: " + errString}) 106 | }); 107 | 108 | py.stderr.on('error', console.log) 109 | py.stdout.on('error', console.log) 110 | py.stdin.on('error', console.log) 111 | py.on('error', console.log) 112 | 113 | py.on('close', code =>{ 114 | if (code){ 115 | err('exit code: ' + code + ', ' + errString); 116 | this.onStatus({fill:"red",shape:"dot",text:"Exited: " + code}) 117 | } else if (!this.continuous){ 118 | msg.payload = dataString.trim(); 119 | out(msg); 120 | this.onStatus({fill:"green",shape:"dot",text:"Done"}) 121 | } else { 122 | this.onStatus({fill:"yellow",shape:"dot",text:"Script Closed"}) 123 | } 124 | this.py = null 125 | setTimeout(()=>{ 126 | this.onStatus({}) 127 | }, 2000) 128 | }); 129 | 130 | if (this.stdInData){ 131 | py.stdin.write(payload + '\n') 132 | } 133 | }; 134 | 135 | PythonshellInNode.prototype.onClose = function() { 136 | if (this.py){ 137 | this.py.kill() 138 | this.py = null 139 | } 140 | }; 141 | 142 | PythonshellInNode.prototype.setStatusCallback = function(callback) { 143 | this.onStatus = callback 144 | }; 145 | 146 | 147 | module.exports = PythonshellInNode -------------------------------------------------------------------------------- /src/pythonshell.html: -------------------------------------------------------------------------------- 1 | f 16 | 54 | 55 | 69 | 70 | 83 | 84 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /src/pythonshell.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Sense Tecnic Systems, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | **/ 16 | 17 | var util = require("util"); 18 | var httpclient; 19 | var PythonshellNode = require('./PythonShellNode'); 20 | 21 | module.exports = function(RED) { 22 | "use strict"; 23 | 24 | function PythonshellInNode(n) { 25 | RED.nodes.createNode(this,n); 26 | 27 | var node = this; 28 | node.config = n; // copy config to the backend so that down bellow we can have a reference 29 | 30 | var pyNode = new PythonshellNode(n); 31 | 32 | pyNode.setStatusCallback(node.status.bind(node)) 33 | 34 | node.on("input",function(msg) { 35 | pyNode.onInput(msg, function(result){ 36 | node.send(result); 37 | }, function(err){ 38 | node.error(err); 39 | }); 40 | }); 41 | 42 | node.on('close', ()=>pyNode.onClose()); 43 | } 44 | 45 | RED.nodes.registerType("pythonshell in", PythonshellInNode); 46 | 47 | RED.httpAdmin.post("/pythonshell/:id", RED.auth.needsPermission("pythonshell.query"), function(req,res) { 48 | var node = RED.nodes.getNode(req.params.id); 49 | if (node != null) { 50 | try { 51 | if (node.config.continuous){// see above comment 52 | node.receive({payload: 'pythonshell@close'}) 53 | } else { 54 | node.receive(); 55 | } 56 | res.sendStatus(200); 57 | } catch(err) { 58 | res.sendStatus(500); 59 | node.error(RED._("pythonshell.failed",{error:err.toString()})); 60 | } 61 | } else { 62 | res.sendStatus(404); 63 | } 64 | }); 65 | 66 | } 67 | -------------------------------------------------------------------------------- /test/PythonShellNode_Spec.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs') 2 | let assert = require('assert'); 3 | let spawn = require('child_process').spawn 4 | let net = require("net"); 5 | 6 | let PythonshellNode = require('../src/PythonShellNode'); 7 | 8 | describe('Pythonshell Node', function() { 9 | let venv = "/venv"; 10 | 11 | before(function(done){ 12 | this.timeout(10000); 13 | 14 | if (fs.existsSync(__dirname + venv)) { 15 | done(); 16 | return; 17 | } 18 | 19 | console.log('creating virtual environment for testing') 20 | 21 | let spawn = require('child_process').spawn; 22 | let ve; 23 | try { 24 | ve = spawn('virtualenv', [__dirname + venv]); 25 | } catch (e){ 26 | done(e); 27 | } 28 | 29 | ve.stdout.on('data', d=>console.log(d.toString())); 30 | ve.stderr.on('data', d=>console.log(d.toString())); 31 | 32 | ve.on('close', function(code) { 33 | if (code){ 34 | done(code); 35 | } else{ 36 | try { 37 | let pipInstall = spawn(__dirname + venv + '/bin/pip', ['install', 'lxml']); 38 | pipInstall.stdout.on('data', d=>console.log(d.toString())); 39 | pipInstall.stderr.on('data', d=>console.log(d.toString())); 40 | pipInstall.on('close', done) 41 | } catch (e){ 42 | done(e); 43 | } 44 | } 45 | }); 46 | }); 47 | 48 | describe('Failing cases', function(){ 49 | it('should throw an error for empty config', function(done) { 50 | try { 51 | let pyNode = new PythonshellNode(); 52 | done(1) 53 | } catch (e){ 54 | done() 55 | } 56 | }); 57 | 58 | it('should throw an error for empty config', function(done) { 59 | try { 60 | let pyNode = new PythonshellNode({}); 61 | done(1) 62 | } catch (e){ 63 | done() 64 | } 65 | }); 66 | 67 | it('should throw an error for config without python file', function(done) { 68 | try { 69 | let pyNode = new PythonshellNode({virtualenv: __dirname + venv}); 70 | done(1) 71 | } catch (e){ 72 | done() 73 | } 74 | }); 75 | 76 | it('should throw an error for non existing python file', function(done) { 77 | try { 78 | let pyNode = new PythonshellNode({pyfile: __dirname + "/sample.p"}); 79 | done(1) 80 | } catch (e){ 81 | done() 82 | } 83 | }); 84 | 85 | it('should throw an error for non existing python virtualenv', function(done) { 86 | try { 87 | let pyNode = new PythonshellNode({ 88 | pyfile: __dirname + "/sample.py", 89 | virtualenv: __dirname + "/awefaewaf" 90 | }); 91 | done(1) 92 | } catch (e){ 93 | done() 94 | } 95 | }); 96 | 97 | it('should throw an error when importing external libraries without venv', function(done) { 98 | let pyNode = new PythonshellNode({pyfile: __dirname + "/sample-need-venv.py"}); 99 | 100 | pyNode.onInput({payload: ""}, function(result){ 101 | done(1) 102 | }, function(err){ 103 | done() 104 | }); 105 | }); 106 | }) 107 | 108 | 109 | describe('Run Python script', function() { 110 | it('should return the script result', function(done) { 111 | let pyNode = new PythonshellNode({ 112 | pyfile: __dirname + "/sample.py" 113 | }); 114 | 115 | pyNode.onInput({payload: ""}, function(result){ 116 | assert.notEqual(result.payload, null); 117 | assert.equal(result.payload, 'hi'); 118 | done() 119 | }, function(err){ 120 | done(err) 121 | }); 122 | }); 123 | 124 | it('should forward the whole message', function(done) { 125 | let pyNode = new PythonshellNode({ 126 | pyfile: __dirname + "/sample.py" 127 | }); 128 | 129 | pyNode.onInput({payload: "", otherPayload: "testval"}, function(result){ 130 | assert.notEqual(result.payload, null); 131 | assert.equal(result.payload, 'hi'); 132 | assert.equal(result.otherPayload, "testval") 133 | done() 134 | }, function(err){ 135 | done(err) 136 | }); 137 | }) 138 | 139 | it('should output script ongoing result', function(done) { 140 | this.timeout(10000); 141 | 142 | let runs = 0; 143 | 144 | let pyNode = new PythonshellNode({ 145 | pyfile: __dirname + "/sample-loop.py", 146 | continuous: true 147 | }); 148 | 149 | pyNode.onInput({payload: ""}, function(result){ 150 | assert.notEqual(result.payload, null); 151 | assert.equal(result.payload.trim(), 'on going') 152 | runs++; 153 | 154 | if (runs >= 3){ 155 | pyNode.onClose() 156 | done(); 157 | } 158 | }, function(err){ 159 | done(err) 160 | }); 161 | }); 162 | 163 | it('should not accepting input when is producing result', function(done) { 164 | this.timeout(10000); 165 | 166 | let ins = 0; 167 | let runner; 168 | 169 | let pyNode = new PythonshellNode({ 170 | pyfile: __dirname + "/sample-loop.py", 171 | continuous: true 172 | }); 173 | 174 | pyNode.setStatusCallback(status=>{ 175 | if (ins === 2 && status.text === "Not accepting input"){ 176 | clearInterval(runner) 177 | pyNode.onClose() 178 | done() 179 | } 180 | }) 181 | 182 | runner = setInterval(()=>{ 183 | ins++ 184 | pyNode.onInput({payload: "arg"},(result)=>{}, (err)=>{done(err)}) 185 | }, 500) 186 | 187 | // TODO: to double check, look at ps aux | grep python 188 | }); 189 | 190 | it('should pass arguments to script', function(done) { 191 | let pyNode = new PythonshellNode({ 192 | pyfile: __dirname + "/sample-with-arg.py" 193 | }); 194 | 195 | pyNode.onInput({payload: "firstArg secondArg"}, function(result){ 196 | assert.notEqual(result.payload, null); 197 | assert.equal(result.payload, 'firstArg secondArg'); 198 | done() 199 | }, function(err){ 200 | done(err) 201 | }); 202 | }); 203 | 204 | it('should support file read', function(done) { 205 | let pyNode = new PythonshellNode({ 206 | pyfile: __dirname + "/sample-file-read.py" 207 | }); 208 | 209 | pyNode.onInput({payload: ""}, function(result){ 210 | assert.notEqual(result.payload, null); 211 | assert.equal(result.payload, fs.readFileSync(__dirname + '/test.txt', 'utf8')); 212 | done() 213 | }, function(err){ 214 | done(err) 215 | }); 216 | }); 217 | 218 | it('should support virtual env', function(done) { 219 | let pyNode = new PythonshellNode({ 220 | pyfile: __dirname + "/sample-need-venv.py", 221 | virtualenv: __dirname + venv 222 | }); 223 | 224 | pyNode.onInput({payload: ""}, function(result){ 225 | assert.notEqual(result.payload, null); 226 | assert.equal(result.payload, 'hi from venv'); 227 | done() 228 | }, function(err){ 229 | done(err) 230 | }); 231 | }); 232 | 233 | it('should support python3', function(done) { 234 | let pyNode = new PythonshellNode({ 235 | pyfile: __dirname + "/sample-python3.py", 236 | python3: true 237 | }); 238 | pyNode.onInput({payload: ""}, function(result){ 239 | assert.notEqual(result.payload, null); 240 | assert.equal(result.payload, '0 1 2 3 4 5 6 7 8 9'); 241 | done() 242 | }, function(err){ 243 | done(err) 244 | }); 245 | }); 246 | 247 | it('should support virtual env and file read', function(done) { 248 | let pyNode = new PythonshellNode({ 249 | pyfile: __dirname + "/sample-python3.py", 250 | virtualenv: __dirname + venv, 251 | python3: true 252 | }); 253 | pyNode.onInput({payload: ""}, function(result){ 254 | assert.notEqual(result.payload, null); 255 | assert.equal(result.payload, '0 1 2 3 4 5 6 7 8 9'); 256 | done() 257 | }, function(err){ 258 | done(err) 259 | }); 260 | }); 261 | 262 | it('should support virtual env and file read', function(done) { 263 | let pyNode = new PythonshellNode({ 264 | pyfile: __dirname + "/sample-need-venv-file-read.py", 265 | virtualenv: __dirname + venv 266 | }); 267 | 268 | pyNode.onInput({payload: ""}, function(result){ 269 | assert.notEqual(result.payload, null); 270 | assert.equal(result.payload, fs.readFileSync(__dirname + '/test.txt', 'utf8')); 271 | done() 272 | }, function(err){ 273 | done(err) 274 | }); 275 | }); 276 | }); 277 | 278 | describe('piping using unix socket', () => { 279 | 280 | it.skip('pipe', function(done) { 281 | let client 282 | let spawnCmd = 'python'//__dirname + '/' + venv + '/bin/' + 'python' 283 | let py1File = __dirname + "/sample-loop.py" 284 | let py2File = __dirname + "/stdin-data.py" 285 | 286 | let py1 = spawn(spawnCmd, ['-u', py1File]) 287 | let py2 = spawn(spawnCmd, ['-u', py2File]) 288 | 289 | py2.stdout.pipe(process.stdout) 290 | 291 | py1.stdout.on('data', d => { 292 | if (client) 293 | client.write(d) 294 | }) 295 | 296 | let pipeServer = net.createServer(stream => { 297 | stream.on('data', d => { 298 | py2.stdin.write(d) 299 | }) 300 | }) 301 | pipeServer.listen('./abc') 302 | 303 | client = net.connect('./abc', console.log) 304 | }) 305 | 306 | it.skip('work stdin-data', function(done) { 307 | this.timeout(10000); 308 | 309 | let spawnCmd = __dirname + '/' + venv + '/bin/' + 'python' 310 | let stdinDataFile = __dirname + "/stdin-data.py" 311 | 312 | let child = spawn(spawnCmd, ['-u', stdinDataFile]) 313 | 314 | setInterval(()=>{ 315 | child.stdin.write("abc\n") 316 | },1000) 317 | 318 | child.stdout.pipe(process.stdout); 319 | }); 320 | 321 | it('send data to python script stdin', function(done) { 322 | // TODO: here test just one input 323 | 324 | let pyNode = new PythonshellNode({ 325 | pyfile: __dirname + "/stdin-data.py", 326 | stdInData: true 327 | }); 328 | 329 | pyNode.onInput({payload: "abc\n"}, function(result){ 330 | assert.equal(result.payload.trim(), "abc"); 331 | done() 332 | }, function(err){ 333 | done(err) 334 | }); 335 | 336 | setTimeout(()=>{ 337 | pyNode.onClose() 338 | }, 1000) 339 | }); 340 | }) 341 | }); -------------------------------------------------------------------------------- /test/sample-file-read.py: -------------------------------------------------------------------------------- 1 | f = open("test.txt", "r") 2 | print(f.read()) 3 | -------------------------------------------------------------------------------- /test/sample-loop.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | 4 | start = time.time() 5 | 6 | i = 0 7 | while True: 8 | now = time.time() 9 | lapsed = now - start 10 | if lapsed > 4: 11 | print( "loop ended") 12 | break 13 | else: 14 | print("on going") 15 | i += 1 16 | time.sleep(1) 17 | -------------------------------------------------------------------------------- /test/sample-need-venv-file-read.py: -------------------------------------------------------------------------------- 1 | import lxml 2 | f = open("test.txt", "r") 3 | print(f.read()) 4 | -------------------------------------------------------------------------------- /test/sample-need-venv.py: -------------------------------------------------------------------------------- 1 | import lxml 2 | 3 | print 'hi from venv' -------------------------------------------------------------------------------- /test/sample-python3.py: -------------------------------------------------------------------------------- 1 | # test by running an operation that does not work on python2 2 | a, b, *rest = range(10) 3 | print(a,b,*rest) -------------------------------------------------------------------------------- /test/sample-with-arg.py: -------------------------------------------------------------------------------- 1 | import sys 2 | print sys.argv[1] -------------------------------------------------------------------------------- /test/sample.py: -------------------------------------------------------------------------------- 1 | print 'hi' -------------------------------------------------------------------------------- /test/stdin-data.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | while True: 4 | line = sys.stdin.readline() 5 | print line -------------------------------------------------------------------------------- /test/test.txt: -------------------------------------------------------------------------------- 1 | python file read content -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | balanced-match@^1.0.0: 6 | version "1.0.0" 7 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 8 | 9 | brace-expansion@^1.1.7: 10 | version "1.1.11" 11 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 12 | dependencies: 13 | balanced-match "^1.0.0" 14 | concat-map "0.0.1" 15 | 16 | browser-stdout@1.3.1: 17 | version "1.3.1" 18 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" 19 | 20 | commander@2.11.0: 21 | version "2.11.0" 22 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" 23 | 24 | concat-map@0.0.1: 25 | version "0.0.1" 26 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 27 | 28 | debug@3.1.0: 29 | version "3.1.0" 30 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 31 | dependencies: 32 | ms "2.0.0" 33 | 34 | diff@3.5.0: 35 | version "3.5.0" 36 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" 37 | 38 | escape-string-regexp@1.0.5: 39 | version "1.0.5" 40 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 41 | 42 | fs.realpath@^1.0.0: 43 | version "1.0.0" 44 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 45 | 46 | glob@7.1.2: 47 | version "7.1.2" 48 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 49 | dependencies: 50 | fs.realpath "^1.0.0" 51 | inflight "^1.0.4" 52 | inherits "2" 53 | minimatch "^3.0.4" 54 | once "^1.3.0" 55 | path-is-absolute "^1.0.0" 56 | 57 | growl@1.10.3: 58 | version "1.10.3" 59 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" 60 | 61 | has-flag@^2.0.0: 62 | version "2.0.0" 63 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" 64 | 65 | he@1.1.1: 66 | version "1.1.1" 67 | resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" 68 | 69 | inflight@^1.0.4: 70 | version "1.0.6" 71 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 72 | dependencies: 73 | once "^1.3.0" 74 | wrappy "1" 75 | 76 | inherits@2: 77 | version "2.0.3" 78 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 79 | 80 | minimatch@^3.0.4: 81 | version "3.0.4" 82 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 83 | dependencies: 84 | brace-expansion "^1.1.7" 85 | 86 | minimist@0.0.8: 87 | version "0.0.8" 88 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 89 | 90 | mkdirp@0.5.1: 91 | version "0.5.1" 92 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 93 | dependencies: 94 | minimist "0.0.8" 95 | 96 | mocha@^5.0.4: 97 | version "5.0.4" 98 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.0.4.tgz#6b7aa328472da1088e69d47e75925fd3a3bb63c6" 99 | dependencies: 100 | browser-stdout "1.3.1" 101 | commander "2.11.0" 102 | debug "3.1.0" 103 | diff "3.5.0" 104 | escape-string-regexp "1.0.5" 105 | glob "7.1.2" 106 | growl "1.10.3" 107 | he "1.1.1" 108 | mkdirp "0.5.1" 109 | supports-color "4.4.0" 110 | 111 | ms@2.0.0: 112 | version "2.0.0" 113 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 114 | 115 | once@^1.3.0: 116 | version "1.4.0" 117 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 118 | dependencies: 119 | wrappy "1" 120 | 121 | path-is-absolute@^1.0.0: 122 | version "1.0.1" 123 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 124 | 125 | supports-color@4.4.0: 126 | version "4.4.0" 127 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" 128 | dependencies: 129 | has-flag "^2.0.0" 130 | 131 | wrappy@1: 132 | version "1.0.2" 133 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 134 | --------------------------------------------------------------------------------