├── requirements.txt ├── test_python.py ├── .gitignore ├── client ├── views │ ├── error.pug │ └── index.pug └── public │ ├── js │ └── client.js │ └── stylesheets │ └── style.css ├── Utils.js ├── PythonServer.py ├── README.md ├── package.json ├── LICENSE ├── PythonConnector.js └── server.js /requirements.txt: -------------------------------------------------------------------------------- 1 | zerorpc -------------------------------------------------------------------------------- /test_python.py: -------------------------------------------------------------------------------- 1 | def test_me(param): 2 | return 'Hello World! - from python' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/npm-debug.log 3 | *.pyc 4 | .vscode/settings.json 5 | *.DS_Store 6 | *~ 7 | -------------------------------------------------------------------------------- /client/views/error.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title=title 4 | link(rel='stylesheet', href='/stylesheets/style.css') 5 | body 6 | h2=error.status 7 | p=error.message || 'Something unexpected happened :(' -------------------------------------------------------------------------------- /Utils.js: -------------------------------------------------------------------------------- 1 | var promisify = (fn, ctx, ...args) => { 2 | if (!ctx) { 3 | ctx = fn; 4 | } 5 | 6 | return new Promise((resolve, reject) => { 7 | args.push((err, data) => { 8 | if (err) { 9 | reject(err); 10 | } 11 | else { 12 | resolve(data); 13 | } 14 | }); 15 | 16 | fn.apply(ctx, args) 17 | }); 18 | }; 19 | 20 | 21 | module.exports = { 22 | promisify 23 | }; -------------------------------------------------------------------------------- /PythonServer.py: -------------------------------------------------------------------------------- 1 | import zerorpc 2 | 3 | from test_python import test_me 4 | 5 | PORT = 4242 6 | 7 | class PythonServer(object): 8 | def listen(self): 9 | print(f'Python Server started listening on {PORT} ...') 10 | 11 | def test(self, param): 12 | return test_me(param) 13 | 14 | try: 15 | s = zerorpc.Server(PythonServer()) 16 | s.bind(f'tcp://0.0.0.0:{PORT}') 17 | s.run() 18 | print('PythonServer running...') 19 | except Exception as e: 20 | print('unable to start PythonServer:', e) 21 | raise e -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node-Python communication 2 | Starter code to use nodejs with a python layer as the backend 3 | 4 | (useful for deploying Machine Learning models if you want to use nodejs as your web framework) 5 | 6 | ### Setup 7 | (in the root folder) 8 | ``` 9 | $ npm install 10 | $ pip install -r requirements.txt 11 | ``` 12 | 13 | ### Demo 14 | ``` 15 | $ npm start 16 | ``` 17 | 18 | ### Examples 19 | * Image Classification: [https://github.com/navjotts/node-python/tree/example_ml_image_classification](https://github.com/navjotts/node-python/tree/example_ml_image_classification) -------------------------------------------------------------------------------- /client/views/index.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title=title 4 | link(rel='stylesheet', href='/stylesheets/style.css') 5 | script(src='/js/client.js') 6 | body 7 | div(class='center') 8 | div(class='title')='nodejs - python' 9 | div(class='content') 10 | button(id='test-button', 11 | class='test-button', 12 | type='button', 13 | onclick='fireTest()')='Test Python' 14 | div(class='result-label') 15 | label(id='result-label') 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-python", 3 | "version": "1.0.0", 4 | "description": "communicate between node and python", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git@personal:navjotts/node-python.git" 13 | }, 14 | "author": "Navjot", 15 | "license": "MIT", 16 | "dependencies": { 17 | "body-parser": "^1.18.3", 18 | "express": "^4.16.3", 19 | "pug": "^2.0.3", 20 | "zmq": "^2.15.3", 21 | "zerorpc": "^0.9.7" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/public/js/client.js: -------------------------------------------------------------------------------- 1 | var loc = window.location; 2 | const HOSTURL = `${loc.protocol}//${loc.hostname}:${loc.port}`; 3 | 4 | function fireTest() { 5 | document.getElementById('test-button').innerHTML = 'Testing...'; 6 | var xhr = new XMLHttpRequest(); 7 | xhr.open('GET', `${HOSTURL}/test`, true); 8 | xhr.onerror = function() {alert (xhr.responseText);} 9 | xhr.onload = function(e) { 10 | if (this.readyState === 4) { 11 | var response = JSON.parse(e.target.responseText); 12 | document.getElementById('result-label').innerHTML = `${response['result']}`; 13 | } 14 | document.getElementById('test-button').innerHTML = 'Test Python'; 15 | } 16 | xhr.send(); 17 | } 18 | -------------------------------------------------------------------------------- /client/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #fff; 3 | } 4 | 5 | .center { 6 | margin: auto; 7 | padding: 10px; 8 | padding-left:50px; 9 | padding-right:50px; 10 | text-align: center; 11 | font-size: 14px; 12 | } 13 | 14 | .title { 15 | font-size: 30px; 16 | margin-top: 1em; 17 | margin-bottom: 1em; 18 | color: #262626; 19 | } 20 | 21 | .content { 22 | margin-top: 10em; 23 | } 24 | 25 | button.test-button { 26 | width: 200px; 27 | height: 40px; 28 | border: solid 1px #4a90e2; 29 | border-radius: 2px; 30 | background-color: #4a90e2; 31 | font-size: 14px; 32 | color: #ffffff; 33 | } 34 | 35 | button:focus {outline:0;} 36 | 37 | .result-label { 38 | margin-top: 0.5em; 39 | padding: 10px; 40 | font-size: 13px; 41 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Navjot 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PythonConnector.js: -------------------------------------------------------------------------------- 1 | const spawn = require('child_process').spawn; 2 | const path = require('path'); 3 | 4 | const zerorpc = require('zerorpc'); 5 | const Utils = require('./Utils.js'); 6 | 7 | const TIMEOUT = 60; // in seconds (adjust as per how long your server calls can take) 8 | const IP = '127.0.0.1'; 9 | const PORT = '4242'; 10 | 11 | class PythonConnector { 12 | static server() { 13 | if (!PythonConnector.connected) { 14 | console.log('PythonConnector – making a new connection to the python layer'); 15 | PythonConnector.zerorpcProcess = spawn('python3', ['-u', path.join(__dirname, 'PythonServer.py')]); 16 | PythonConnector.zerorpcProcess.stdout.on('data', function(data) { 17 | console.info('python:', data.toString()); 18 | }); 19 | PythonConnector.zerorpcProcess.stderr.on('data', function(data) { 20 | console.error('python:', data.toString()); 21 | }); 22 | 23 | PythonConnector.zerorpc = new zerorpc.Client({'timeout': TIMEOUT, 'heartbeatInterval': TIMEOUT*1000}); 24 | PythonConnector.zerorpc.connect('tcp://' + IP + ':' + PORT); 25 | PythonConnector.connected = true; 26 | } 27 | 28 | return PythonConnector.zerorpc; 29 | } 30 | 31 | static async invoke(method, ...args) { 32 | var zerorpc = PythonConnector.server(); 33 | return await Utils.promisify(zerorpc.invoke, zerorpc, method, ...args); 34 | } 35 | } 36 | 37 | module.exports = PythonConnector; 38 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | 5 | const PythonConnector = require('./PythonConnector.js'); 6 | 7 | var app = express(); 8 | 9 | app.set('views', path.join(__dirname, 'client', 'views')); 10 | app.set('view engine', 'pug'); 11 | app.use(express.static(path.join(__dirname, 'client', 'public'))); 12 | 13 | app.use(bodyParser.json()); 14 | app.use(bodyParser.urlencoded({extended: true})); 15 | 16 | app.use(function (req, res, next) { 17 | res.header('Access-Control-Allow-Origin', '*'); 18 | res.header('Access-Control-Allow-Headers', 'X-Requested-With'); 19 | res.header('Access-Control-Allow-Headers', 'Content-Type'); 20 | next(); 21 | }); 22 | 23 | app.get('/', function (req, res) { 24 | console.log(req.url); 25 | res.render('index', {title: 'Demo App'}); 26 | }); 27 | 28 | app.get('/test', async function (req, res, next) { 29 | console.log(req.url); 30 | try { 31 | var pyRes = await PythonConnector.invoke('test', 'None'); 32 | var data = {result: pyRes} 33 | res.json(data); 34 | } 35 | catch (e) { 36 | console.log('error in /test', e); 37 | res.send(404); 38 | } 39 | }); 40 | 41 | app.get('*', function (req, res, next) { 42 | var err = new Error(); 43 | err.status = 404; 44 | next(err); 45 | }); 46 | 47 | app.use(function (err, req, res, next) { 48 | if (err.status !== 404) { 49 | return next(err); 50 | } 51 | 52 | res.status(500); 53 | res.render('error', { error: err }); 54 | }); 55 | 56 | const PORT = process.env.PORT || 3030; 57 | app.listen(PORT, async () => { 58 | console.log(`Started listening on port ${PORT} ...`); 59 | await PythonConnector.invoke('listen'); 60 | }); --------------------------------------------------------------------------------