Skip to content

Commit 216448a

Browse files
author
Arnau Orriols
committed
Init repo
0 parents  commit 216448a

6 files changed

+390
-0
lines changed

Diff for: LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2016 Arnau Orriols
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.

Diff for: README.md

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Python Function NodeRED Node
2+
=============================
3+
4+
Node-RED is a wonderful tool, but Javascript can be a rather painful language to write.
5+
Being able to write functions with the language of your choice, and not just Javascript,
6+
might just be the last piece of functionality missing to make Node-RED perfect. Or not.
7+
In any case, this quick hacked node will let you write functions using Python instead of Javascript!
8+
How cool is that? Too cool to be used in production, that is for sure.
9+
10+
I repeat, don't use this in production. And when you do (cause you will), please tell your manager
11+
I already told you so.
12+
13+
Install
14+
-------
15+
16+
Requires Python 2.7 installed in the system.
17+
18+
`rpm install -g node-red-contrib-python-function`
19+
20+
Usage
21+
-----
22+
23+
Just like the plain-old function node, but writting Python instead of Javascript.
24+
The msg is a dictionary, (almost) all the Node-RED helper functions are avaiable, and its behaviour
25+
is expected to be exactly the same (at some point in the future at least).
26+
27+
Caveats
28+
-------
29+
30+
- Although it will accept virtually any msg you give it, any non-JSON data type will be silently dropped from the msg permanently.
31+
- Somethings is wrong with the current implementation of the IPC, that makes it blow up when there are +2 messages waiting to be processed
32+
- Python is by default a synchronous runtime. The function is run in a dedicated child process,
33+
therefore it won't block the NodeJS main process, but in any case only 1 message is processed at a time. That is, of course, unless you
34+
use any of the concurrency features available in Python, like multithreading, multiprocessing, Tornado, Twisted...
35+
- No sandboxing has been attempted whatsoever. After all, this is just to have some fun, not to be used in production, remember...?

Diff for: lib/.python-function.html.swp

16 KB
Binary file not shown.

Diff for: lib/python-function.html

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/ace.js"></script>
2+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.3/ext-language_tools.js"></script>
3+
<script type="text/javascript">
4+
RED.nodes.registerType('python-function',{
5+
category: 'function',
6+
color: '#fdd0a2',
7+
defaults: {
8+
name: {value: ""},
9+
func: {value: "\nreturn msg"},
10+
outputs: {value: 1} // Ofuscated way to persist the number of outputs of the node
11+
},
12+
inputs: 1,
13+
outputs: 1,
14+
icon: "function.png",
15+
label: function() {
16+
return this.name;
17+
},
18+
oneditprepare: function() {
19+
$( "#node-input-outputs" ).spinner({
20+
min:1
21+
});
22+
var langTools = ace.require('ace/ext/language_tools');
23+
this.editor = ace.edit('node-input-func-editor');
24+
this.editor.setTheme('ace/theme/tomorrow');
25+
this.editor.getSession().setMode('ace/mode/python');
26+
this.editor.setValue($("#node-input-func").val(), -1);
27+
this.editor.setOptions({
28+
enableBasicAutocompletion: true,
29+
enableLiveAutocompletion: true,
30+
highlightSelectedWord: true,
31+
useSoftTabs: true,
32+
tabSize: 4,
33+
});
34+
var noderedKeywords = [
35+
'msg', 'msg.payload', 'node', 'node.send',
36+
'node.log', 'node.warn', 'node.error', 'node.status'
37+
];
38+
this.editor.completers.push({
39+
getCompletions: function (state, session, pos, prefix, callback) {
40+
callback(null, noderedKeywords.map(function (word) {
41+
return {
42+
name: word,
43+
value: word,
44+
score: 0,
45+
meta: 'Node-RED'
46+
};
47+
}));
48+
}
49+
});
50+
this.editor.focus();
51+
},
52+
oneditsave: function() {
53+
var annot = this.editor.getSession().getAnnotations();
54+
this.noerr = 0;
55+
$("#node-input-noerr").val(0);
56+
for (var k=0; k < annot.length; k++) {
57+
//console.log(annot[k].type,":",annot[k].text, "on line", annot[k].row);
58+
if (annot[k].type === "error") {
59+
$("#node-input-noerr").val(annot.length);
60+
this.noerr = annot.length;
61+
}
62+
}
63+
$("#node-input-func").val(this.editor.getValue());
64+
delete this.editor;
65+
},
66+
oneditresize: function(size) {
67+
var rows = $("#dialog-form>div:not(.node-text-editor-row)");
68+
var height = $("#dialog-form").height();
69+
for (var i=0;i<rows.size();i++) {
70+
height -= $(rows[i]).outerHeight(true);
71+
}
72+
var editorRow = $("#dialog-form>div.node-text-editor-row");
73+
height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
74+
$(".node-text-editor").css("height",height+"px");
75+
this.editor.resize();
76+
}
77+
});
78+
</script>
79+
80+
<script type="text/x-red" data-template-name="python-function">
81+
<div class="form-row">
82+
<label for="node-input-name"><i class="fa fa-tag"></i> <span>Name</span></label>
83+
<input type="text" id="node-input-name" placeholder="Name">
84+
</div>
85+
<div class="form-row" style="margin-bottom: 0px;">
86+
<label for="node-input-func"><i class="fa fa-wrench"></i> <span>Function</span></label>
87+
<input type="hidden" id="node-input-func" autofocus="autofocus">
88+
<input type="hidden" id="node-input-noerr">
89+
</div>
90+
<div class="form-row node-text-editor-row">
91+
<div style="height: 250px; min-height:150px;" class="node-text-editor" id="node-input-func-editor" ></div>
92+
</div>
93+
<div class="form-row">
94+
<label for="node-input-outputs"><i class="fa fa-random"></i> <span>Outputs</span></label>
95+
<input id="node-input-outputs" style="width: 60px;" value="1">
96+
</div>
97+
<div class="form-tips"><span>See the Info tab for help writing Python functions</span></div>
98+
</script>
99+
100+
<script type="text/x-red" data-help-name="python-function">
101+
<p>Just like the plain old Javascript function node, but writting Python!</p>
102+
<p>All functionality of the javascript function node is to be expected (now or in the near future, or in a parallel universe)</p>
103+
<p>Virtually all posible incoming messages are supported, available as a Python dictionary. However bear in mind that all data inside msg that is not a valid JSON data type will be dropped from it permanently</p>
104+
<p><pre>THIS IS NOT READY FOR PRODUCTION YET!</pre></p>
105+
106+
<h2>Usage</h2>
107+
<p>A function block where you can write code to do more interesting things.</p>
108+
<p>The message is passed in as a Python dictionary called <code>msg</code>.</p>
109+
<p>By convention it will have a <code>msg['payload']</code> key containing
110+
the body of the message.</p>
111+
<h4>Logging and Error Handling</h4>
112+
<p>To log any information, or report an error, the following functions are available:</p>
113+
<ul>
114+
<li><code>node.log("Log")</code></li>
115+
<li><code>node.warn("Warning")</code></li>
116+
<li><code>node.error("Error")</code></li>
117+
</ul>
118+
</p>
119+
<p>The Catch node can also be used to handle errors. To invoke a Catch node,
120+
pass <code>msg</code> as a second argument to <code>node.error</code>:</p>
121+
<pre>node.error("Error msg", msg)</pre>
122+
<h4>Sending messages</h4>
123+
<p>The function can either return the messages it wants to pass on to the next nodes
124+
in the flow, or can call <code>node.send(messages)</code>.</p>
125+
<p>It can return/send:</p>
126+
<ul>
127+
<li>a single message dictionary - passed to nodes connected to the first output</li>
128+
<li>a list of message dictionaries - passed to nodes connected to the corresponding outputs</li>
129+
</ul>
130+
<p>If any element of the list is itself a list of messages, multiple
131+
messages are sent to the corresponding output.</p>
132+
<p>If null is returned, either by itself or as an element of the list, no
133+
message is passed on.</p>
134+
<p>See the <a target="_new" href="http://nodered.org/docs/writing-functions.html">online documentation</a> for more help.</p>
135+
136+
</script>

Diff for: lib/python-function.js

+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
module.exports = function (RED) {
2+
var spawn = require('child_process').spawn;
3+
var util = require('util');
4+
5+
function indentLines(fnCode, depth) {
6+
return fnCode.split('\n').map((line) => Array(depth).join(' ') + line).join('\n')
7+
}
8+
9+
function spawnFn(self) {
10+
self.child = spawn('python', ['-uc', self.func.code], {stdio: ['pipe', 'pipe', 'pipe', 'ipc']});
11+
self.child.stdout.on('data', function (data) {
12+
self.log(data.toString());
13+
});
14+
self.child.stderr.on('data', function (data) {
15+
self.error(data.toString());
16+
});
17+
self.child.on('close', function (exitCode) {
18+
if (exitCode) {
19+
self.error(`Python Function process exited with code ${exitCode}`);
20+
if (self.func.attempts) {
21+
spawnFn(self);
22+
self.func.attempts--;
23+
} else {
24+
self.error(`Function '${self.name}' has failed more than 10 times. Fix it and deploy again`)
25+
self.status({fill: 'red', shape: 'dot', text: 'Stopped, see debug panel'});
26+
}
27+
}
28+
});
29+
self.child.on('message', function (response) {
30+
switch (response.ctx) {
31+
case 'send':
32+
sendResults(self, response.msgid, response.value);
33+
break;
34+
case 'log':
35+
case 'warn':
36+
case 'error':
37+
case 'status':
38+
self[response.ctx].apply(self, response.value);
39+
break;
40+
default:
41+
throw new Error(`Don't know what to do with ${response.ctx}`);
42+
}
43+
});
44+
self.log(`Python function '${self.name}' running on PID ${self.child.pid}`);
45+
self.status({fill: 'green', shape: 'dot', text: 'Running'});
46+
}
47+
48+
function sendResults(self, _msgid, msgs) {
49+
if (msgs == null) {
50+
return;
51+
} else if (!util.isArray(msgs)) {
52+
msgs = [msgs];
53+
}
54+
var msgCount = 0;
55+
for (var m=0;m<msgs.length;m++) {
56+
if (msgs[m]) {
57+
if (util.isArray(msgs[m])) {
58+
for (var n=0; n < msgs[m].length; n++) {
59+
msgs[m][n]._msgid = _msgid;
60+
msgCount++;
61+
}
62+
} else {
63+
msgs[m]._msgid = _msgid;
64+
msgCount++;
65+
}
66+
}
67+
}
68+
if (msgCount>0) {
69+
self.send(msgs);
70+
}
71+
}
72+
73+
function PythonFunction(config) {
74+
var self = this;
75+
RED.nodes.createNode(self, config);
76+
self.name = config.name;
77+
self.func = {
78+
code: `
79+
import sys
80+
import os
81+
import json
82+
83+
channel = os.fdopen(3, "r+")
84+
85+
86+
class Msg(object):
87+
SEND = 'send'
88+
LOG = 'log'
89+
WARN = 'warn'
90+
ERROR = 'error'
91+
STATUS = 'status'
92+
93+
def __init__(self, ctx, value, msgid):
94+
self.ctx = ctx
95+
self.value = value
96+
self.msgid = msgid
97+
98+
def dumps(self):
99+
return json.dumps(vars(self)) + "\\n"
100+
101+
@classmethod
102+
def loads(cls, json_string):
103+
return cls(**json.loads(json_string))
104+
105+
106+
class Node(object):
107+
def __init__(self, msgid, channel):
108+
self.__msgid = msgid
109+
self.__channel = channel
110+
111+
def send(self, msg):
112+
msg = Msg(Msg.SEND, msg, self.__msgid)
113+
self.send_to_node(msg)
114+
115+
def log(self, *args):
116+
msg = Msg(Msg.LOG, args, self.__msgid)
117+
self.send_to_node(msg)
118+
119+
def warn(self, *args):
120+
msg = Msg(Msg.WARN, args, self.__msgid)
121+
self.send_to_node(msg)
122+
123+
def error(self, *args):
124+
msg = Msg(Msg.ERROR, args, self.__msgid)
125+
self.send_to_node(msg)
126+
127+
def status(self, *args):
128+
msg = Msg(Msg.STATUS, args, self.__msgid)
129+
self.send_to_node(msg)
130+
131+
def send_to_node(self, msg):
132+
self.__channel.write(msg.dumps())
133+
134+
135+
def python_function(msg):
136+
` + indentLines(config.func, 4) +
137+
`
138+
while True:
139+
raw_msg = channel.readline()
140+
if not raw_msg:
141+
raise RuntimeError('Received EOF!')
142+
msg = json.loads(raw_msg)
143+
msgid = msg["_msgid"]
144+
node = Node(msgid, channel)
145+
res_msgs = python_function(msg)
146+
node.send(res_msgs)
147+
`,
148+
attempts: 10
149+
};
150+
spawnFn(self);
151+
self.on('input', function(msg) {
152+
var cache = [];
153+
jsonMsg = JSON.stringify(msg, function(key, value) {
154+
if (typeof value === 'object' && value !== null) {
155+
if (cache.indexOf(value) !== -1) {
156+
// Circular reference found, discard key
157+
return;
158+
}
159+
// Store value in our collection
160+
cache.push(value);
161+
}
162+
return value;
163+
});
164+
cache = null; // Enable garbage collection
165+
self.child.send(JSON.parse(jsonMsg));
166+
});
167+
self.on('close', function () {
168+
self.child.kill();
169+
});
170+
}
171+
RED.nodes.registerType('python-function', PythonFunction);
172+
};

Diff for: package.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "node-red-contrib-python-function",
3+
"version": "0.0.1",
4+
"repository": {
5+
"type": "git",
6+
"url": "https://github.com/arnauorriols/node-red-contrib-python-function"
7+
},
8+
"description": "Define a function with Python instead of Javascript",
9+
"keywords": [
10+
"node-red",
11+
"function",
12+
"Python",
13+
"exec",
14+
"polyglot",
15+
"hack"
16+
],
17+
"author": "Arnau Orriols",
18+
"license": "MIT",
19+
"node-red": {
20+
"nodes": {
21+
"python-function": "lib/python-function.js"
22+
}
23+
},
24+
"dependencies": {
25+
}
26+
}

0 commit comments

Comments
 (0)