diff options
| author | Alon Levy <alon@pobox.com> | 2014-12-16 17:37:42 +0200 |
|---|---|---|
| committer | Alon Levy <alon@pobox.com> | 2014-12-16 17:37:42 +0200 |
| commit | ade19de410e0a789a921ec1666b278b2a64176d6 (patch) | |
| tree | c4c54b106de93609ca7bb70a20fd4be696715dbe /src | |
| parent | c7d026b1d504323a8c61d51b726e5e8d2337fc4b (diff) | |
moving files around after repository merger
Diffstat (limited to 'src')
70 files changed, 2577 insertions, 0 deletions
diff --git a/src/client-tests/anotherScript.js b/src/client-tests/anotherScript.js new file mode 100644 index 00000000..93da9fbf --- /dev/null +++ b/src/client-tests/anotherScript.js @@ -0,0 +1,2 @@ +debugger +console.log(window.__myObject); diff --git a/src/client-tests/base.js b/src/client-tests/base.js new file mode 100644 index 00000000..320edccf --- /dev/null +++ b/src/client-tests/base.js @@ -0,0 +1,87 @@ +var fs = require('fs'), + jsdom = require('jsdom'), + addScript = require('./test_util').addScript; + +function dump_nodes(window) { + var nodes = window.force.nodes(); + for(var i = 0 ; i < nodes.length; ++i) { + console.log('nodes[' + nodes[i].id + '/' + nodes[i].type + '].[xy] = (' + nodes[i].x + ',' + nodes[i].y + ')'); + } +} + +function dump_graphviz(force) { + var i; + var nodes = force.nodes(); + var links = force.links(); + var q = function(s) { + if (s.search(' ') == -1) { + return s; + } + return '"' + s + '"'; + }; + + console.log('digraph {'); + for (i = 0 ; i < nodes.length; ++i) { + if (nodes[i].type == 'bubble') { + console.log(' BUBBLE;'); + } else { + console.log(' ' + q(nodes[i].name) + ';'); + } + } + for (i = 0 ; i < links.length; ++i) { + var link = links[i]; + console.log(' ' + q(link.__src.name) + ' -> ' + q(link.__dst.name) + ' [label=' + link.name + '];'); + } + console.log('}'); +} + +function run_tests(settings) { + var document = jsdom.jsdom('<html><body></body></html>'); + var window = document.parentWindow; + + window.console.log = console.log; // slightly evil + window.process = process; // more evil + window.is_node = true; + if (settings.created) { + settings.created(undefined, window); + } + debugger + addScript(window, '../src/external/require.js') + .load_next('../src/app.js') + .done(function () { + console.log('test harness: starting script loading with requirejs'); + window.require.config(window.rhizi_require_config); + window.requirejs(['main'], function(main) { + console.log('test harness: main loaded'); + main.main(); + if (settings.done) { + settings.done(undefined, window); + } + process.quit(); + }); + }); + //window.setInterval(function() { console.log('.'); return true; }, 100); +} + +function reset(window) { + window.require('rz_core').graph.clear(); +} + +function nodes(window) { + return window.require('rz_core').force.nodes().map(function (n) { + return n.name; + }); +} + +function links(window) { + return window.require('rz_core').force.links().map(function (l) { + debugger; + return [l.__src.name, l.__dst.name, l.name]; + }); +} + +exports.run_tests = run_tests; +exports.dump_graphviz = dump_graphviz; +exports.reset = reset; +exports.links = links; +exports.nodes = nodes; diff --git a/src/client-tests/commit_run_tests_output_update b/src/client-tests/commit_run_tests_output_update new file mode 100755 index 00000000..5912fb8d --- /dev/null +++ b/src/client-tests/commit_run_tests_output_update @@ -0,0 +1,3 @@ +#!/bin/bash +git add output +git commit -m "update run_tests.js output" diff --git a/src/client-tests/cri.json b/src/client-tests/cri.json new file mode 100644 index 00000000..c53c7a3d --- /dev/null +++ b/src/client-tests/cri.json @@ -0,0 +1,5 @@ +{ + "nodes":[{"name":"Jean- Christophe Thalabard","type":"person","state":"perm"},{"name":"Owen Cornec","type":"person","state":"perm"},{"name":"Vincent ALexandrine","type":"person","state":"perm"}, {"name":"Maéva Vignes","type":"person","state":"perm"}, {"name":"Abdel El Abed","type":"person","state":"perm"}, {"name":"Valérie Taly","type":"person","state":"perm"}, {"name":"Sébastien Dutreuil","type":"person","state":"perm"}, {"name":"Antoine BERGEL","type":"person","state":"perm"}, {"name":"Antoine Angot","type":"person","state":"perm"}, {"name":"Jérôme Feret","type":"person","state":"perm"}, {"name":"Charlène Gayrard","type":"person","state":"perm"}, {"name":"Hugo Jimenez Perez","type":"person","state":"perm"}, {"name":"Caterina Urban","type":"person","state":"perm"}, {"name":"Gaëlle Chevalon","type":"person","state":"perm"}, {"name":"Ian Marcus","type":"person","state":"perm"}, {"name":"Antoine TALY","type":"person","state":"perm"}, {"name":"Chantal LOTTON","type":"person","state":"perm"}, {"name":"Ana-Maria Lennon-Duménil","type":"person","state":"perm"}, {"name":"Frédérique Carlier-Grynkorn","type":"person","state":"perm"}, {"name":"Pascal Martin","type":"person","state":"perm"}, {"name":"Tamara Milosevic","type":"person","state":"perm"}, {"name":"Nicolas Carpi","type":"person","state":"perm"}, {"name":"Stéphane Daoudy","type":"person","state":"perm"}, {"name":"Danijela Matic Vignjevic","type":"person","state":"perm"}, {"name":"Eugenio Cinquemani","type":"person","state":"perm"}, {"name":"Vincent DAHIREL","type":"person","state":"perm"}, {"name":"Martin Lenz","type":"person","state":"perm"}, {"name":"Maïlys Chassagne","type":"person","state":"perm"}, {"name":"Anne Schmidt","type":"person","state":"perm"}, {"name":"Sophie Sacquin Mora","type":"person","state":"perm"}, {"name":"Richard-Emmanuel Eastes","type":"person","state":"perm"}, {"name":"Michel Morange","type":"person","state":"perm"}, {"name":"Ewa Zlotek-Zlotkiewicz","type":"person","state":"perm"}, {"name":"A.m.o.d.s.e.n C.h.o.t.i.a","type":"person","state":"perm"}, {"name":"Alexandre Vaugoux","type":"person","state":"perm"}, {"name":"Annemiek JM Cornelissen","type":"person","state":"perm"}, {"name":"Clément Nizak","type":"person","state":"perm"}, {"name":"Antoine Frenoy","type":"person","state":"perm"}, {"name":"Ariel B. Lindner","type":"person","state":"perm"}, {"name":"Benjamin Brogniart","type":"person","state":"perm"}, {"name":"Christophe Zimmer","type":"person","state":"perm"}, {"name":"Claire Ribrault","type":"person","state":"perm"}, {"name":"David Tareste","type":"person","state":"perm"}, {"name":"Denis Lafeuille","type":"person","state":"perm"}, {"name":"Dor Garbash","type":"person","state":"perm"}, {"name":"Dusan MISEVIC","type":"person","state":"perm"}, {"name":"Edda Nitschke","type":"person","state":"perm"}, {"name":"François Taddei","type":"person","state":"perm"}, {"name":"Gregory Batt","type":"person","state":"perm"}, {"name":"Jean Luc Lebrun","type":"person","state":"perm"}, {"name":"Jesse Himmelstein","type":"person","state":"perm"}, {"name":"Kevin Lhoste","type":"person","state":"perm"}, {"name":"Laura Ciriani","type":"person","state":"perm"}, {"name":"Livio Riboli-Sasco","type":"person","state":"perm"}, {"name":"Nathalie Sussfeld","type":"person","state":"perm"}, {"name":"Jake Edwin Wintermute","type":"person","state":"perm"}, {"name":"Marlyne Nogbou","type":"person","state":"perm"}, {"name":"Matthieu Piel","type":"person","state":"perm"}, {"name":"Pascal Hersen","type":"person","state":"perm"}, {"name":"Pierre-Yves Bourguignon","type":"person","state":"perm"}, {"name":"Timo Betz","type":"person","state":"perm"}, {"name":"Raphaël Goujet","type":"person","state":"perm"}, {"name":"Stéphane Debove","type":"person","state":"perm"}, {"name":"Vincent Danos","type":"person","state":"perm"}, {"name":"Tam Kien Duong","type":"person","state":"perm"}], + "links":[{"__src":"Jean- Christophe Thalabard","__dst":"Maéva Vignes","name":"works with"}], + "projects":[{"name":"Rhizi","type":"project","state":"perm"},{"name":"RedWire","type":"project","state":"perm"},{"name":"IdStorm","type":"project","state":"perm"},{"name":"Savanturiers","type":"project","state":"perm"},{"name":"Hero.coli","type":"project","state":"perm"}] +} diff --git a/src/client-tests/output/test_analyzer.js.stderr b/src/client-tests/output/test_analyzer.js.stderr new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/client-tests/output/test_analyzer.js.stderr diff --git a/src/client-tests/output/test_analyzer.js.stdout b/src/client-tests/output/test_analyzer.js.stdout new file mode 100644 index 00000000..7e15edcb --- /dev/null +++ b/src/client-tests/output/test_analyzer.js.stdout @@ -0,0 +1,82 @@ +addScript: loaded ../src/external/require.js +app: running under node +addScript: loaded ../src/app.js +test harness: starting script loading with requirejs +test harness: main loaded +Rhizi main started +rhizi: init drag-n-drop +analyzing #a hello there #b +not same size: new/old 2 / 0; 1 / 0 +bug: temp node creation on finalize +expected: a,b +got: a|b +expected: a,b,hello there +got: a,b,hello there +digraph { + a; + b; + a -> b [label=hello there]; +} +analyzing #c and #d like #e +not same size: new/old 3 / 0; 3 / 0 +bug: temp node creation on finalize +expected: c,d,e +got: c|d|e +expected: c,e,like,d,e,like +got: c,d,and,d,e,like,c,e,like +digraph { + c; + d; + e; + c -> d [label=and]; + d -> e [label=like]; + c -> e [label=like]; +} +analyzing #f and #g and #h are cool +not same size: new/old 5 / 0; 3 / 0 +bug: temp node creation on finalize +bug: temp node creation on finalize +expected: f,g,h,f and g and h are cool +got: f|g|h|f and g and h are cool +expected: f,f and g and h are cool,,g,f and g and h are cool,,h,f and g and h are cool, +got: f,f and g and h are cool,,g,f and g and h are cool,,h,f and g and h are cool, +digraph { + f; + g; + h; + "f and g and h are cool"; + f -> "f and g and h are cool" [label=]; + g -> "f and g and h are cool" [label=]; + h -> "f and g and h are cool" [label=]; +} +analyzing #i likes #j and #k +not same size: new/old 3 / 0; 3 / 0 +bug: temp node creation on finalize +expected: i,j,k +got: i|j|k +expected: i,j,likes +got: i,j,likes,j,k,and,i,k,likes +digraph { + i; + j; + k; + i -> j [label=likes]; + j -> k [label=and]; + i -> k [label=likes]; +} +analyzing #q likes #r but doesn't like #l +not same size: new/old 4 / 0; 3 / 0 +bug: temp node creation on finalize +expected: q,r,l +got: q|r|l|q likes r but doesn't like l +expected: +got: q,q likes r but doesn't like l,,r,q likes r but doesn't like l,,l,q likes r but doesn't like l, +digraph { + q; + r; + l; + "q likes r but doesn't like l"; + q -> "q likes r but doesn't like l" [label=]; + r -> "q likes r but doesn't like l" [label=]; + l -> "q likes r but doesn't like l" [label=]; +} diff --git a/src/client-tests/output/test_app.js.stderr b/src/client-tests/output/test_app.js.stderr new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/client-tests/output/test_app.js.stderr diff --git a/src/client-tests/output/test_app.js.stdout b/src/client-tests/output/test_app.js.stdout new file mode 100644 index 00000000..2d2c7940 --- /dev/null +++ b/src/client-tests/output/test_app.js.stdout @@ -0,0 +1 @@ +test_app: running under node diff --git a/src/client-tests/output/test_globals.js.stderr b/src/client-tests/output/test_globals.js.stderr new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/client-tests/output/test_globals.js.stderr diff --git a/src/client-tests/output/test_globals.js.stdout b/src/client-tests/output/test_globals.js.stdout new file mode 100644 index 00000000..9aa7f822 --- /dev/null +++ b/src/client-tests/output/test_globals.js.stdout @@ -0,0 +1,11 @@ +before fields count: 187 +addScript: loaded ../src/external/require.js +app: running under node +addScript: loaded ../src/app.js +test harness: starting script loading with requirejs +test harness: main loaded +Rhizi main started +rhizi: init drag-n-drop +after fields count: 195 +new fields count: 8 +requirejs,require,define,rhizi_require_config,$,jQuery,d3,saveAs diff --git a/src/client-tests/output/test_require_js.js.stderr b/src/client-tests/output/test_require_js.js.stderr new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/client-tests/output/test_require_js.js.stderr diff --git a/src/client-tests/output/test_require_js.js.stdout b/src/client-tests/output/test_require_js.js.stdout new file mode 100644 index 00000000..82ea089e --- /dev/null +++ b/src/client-tests/output/test_require_js.js.stdout @@ -0,0 +1,17 @@ +addScript: loaded ../src/external/require.js +test_app: running under node +addScript: loaded test_app.js +callback after test_app.js loading +{ paths: + { jquery: 'external/jquery', + 'jquery-ui': 'external/jquery-ui', + caret: 'external/caret', + d3: 'external/d3/d3', + autocomplete: 'external/autocomplete', + FileSaver: 'external/FileSaver' }, + baseUrl: '../src/' } +hello from test factory +here we are after test_app prerequisites +42 +Rhizi main started +rhizi: init drag-n-drop diff --git a/src/client-tests/output/test_script_jsdom.js.stderr b/src/client-tests/output/test_script_jsdom.js.stderr new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/client-tests/output/test_script_jsdom.js.stderr diff --git a/src/client-tests/output/test_script_jsdom.js.stdout b/src/client-tests/output/test_script_jsdom.js.stdout new file mode 100644 index 00000000..02b09a7b --- /dev/null +++ b/src/client-tests/output/test_script_jsdom.js.stdout @@ -0,0 +1 @@ +{ foo: 'bar' } diff --git a/src/client-tests/output/test_util.js.stderr b/src/client-tests/output/test_util.js.stderr new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/client-tests/output/test_util.js.stderr diff --git a/src/client-tests/output/test_util.js.stdout b/src/client-tests/output/test_util.js.stdout new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/client-tests/output/test_util.js.stdout diff --git a/src/client-tests/run_tests.js b/src/client-tests/run_tests.js new file mode 100644 index 00000000..bfc129eb --- /dev/null +++ b/src/client-tests/run_tests.js @@ -0,0 +1,31 @@ +var fs = require('fs'); +var spawn = require('child_process').spawn; + +var tests = fs.readdirSync('.').filter(function (x) { return /^test_.*\.js$/.test(x); }); +var i; +var testname; + +function store_output(proc, args, stdout, stderr) +{ + var out = fs.openSync(stdout, 'w+'); + var err = fs.openSync(stderr, 'w+'); + var p = spawn(proc, args); + + p.stdout.on('data', function (data) { + console.log(args + ' gives ' + data.length); + fs.appendFileSync(stdout, data); + }); + p.stderr.on('data', function (data) { + console.log(args + ' gives ' + data.length + ' (err)'); + fs.appendFileSync(stderr, data); + }); + p.on('close', function (code, signal) { + console.log(proc + '(' + args.join(', ') + ') exited with ' + code); + }); + +} + +for (i in tests) { + testname = tests[i]; + store_output('/usr/bin/node', [__dirname + '/' + testname], 'output/' + testname + '.stdout', 'output/' + testname + '.stderr'); +} diff --git a/src/client-tests/test2.js b/src/client-tests/test2.js new file mode 100644 index 00000000..0f8db81f --- /dev/null +++ b/src/client-tests/test2.js @@ -0,0 +1,4 @@ +define('test2', function() { + console.log('hello from test2 factory'); + return {'me': 'not you'}; +}); diff --git a/src/client-tests/test_analyzer.js b/src/client-tests/test_analyzer.js new file mode 100644 index 00000000..441f0dde --- /dev/null +++ b/src/client-tests/test_analyzer.js @@ -0,0 +1,49 @@ +var base = require('./base'); + +var data = [ +["#a hello there #b", ["a", "b"], [["a", "b", "hello there"]], +["#a hello there2 #ba", ["a", "ba"], ["a", "ba", "hello there2"]]], +["#c and #d like #e", ["c", "d", "e"], [["c", "e", "like"], ["d", "e", "like"]]], +// issue 86 +["#f and #g and #h are cool", ["f", "g", "h", "f and g and h are cool"], [["f", "f and g and h are cool", ""], ["g", "f and g and h are cool", ""], ["h", "f and g and h are cool", ""]]], +["#i likes #j and #k", ["i", "j", "k"], ["i", "j", "likes"], ["i", "k", "likes"]], +["#q likes #r but doesn't like #l", ["q", "r", "l"], []], +]; +/* +#t and #u like #v +#w #x #y +sometimes #z and #ab aren't friends +I like to work with #a +*/ + +base.run_tests({ + done: function (errors, window) { + debugger; + var analyzeSentence = window.require('textanalysis.ui').analyzeSentence; + if (process.argv.length > 2) { + analyzeSentence(process.argv.slice(2).join(" "), true); + base.dump_graphviz(window.require('rz_core').force); + } else { + for (var k = 0; k < data.length; ++k) { + var sentence = data[k][0]; + var expected_nodes = data[k][1]; + var expected_links = data[k][2]; + base.reset(window); + console.log('analyzing ' + sentence); + analyzeSentence(sentence, true); + var nodes = base.nodes(window); + var links = base.links(window); + if (nodes != expected_nodes) { + console.log('expected: ' + expected_nodes); + console.log('got: ' + nodes.join('|')); + } + if (links != expected_links) { + console.log('expected: ' + expected_links); + console.log('got: ' + links); + } + base.dump_graphviz(window.require('rz_core').force); + } + } + process.exit(); + } +}); diff --git a/src/client-tests/test_app.js b/src/client-tests/test_app.js new file mode 100644 index 00000000..6696c1fc --- /dev/null +++ b/src/client-tests/test_app.js @@ -0,0 +1,32 @@ +// mocks just to all run_tests to succeed +if (typeof define == 'undefined') { + function define(mod, cb) { + } +} +if (typeof document == 'undefined') { + var document = {}; +} + +(function() { +var config = { + //urlArgs: "bust=" + (new Date()).getTime(), // NOTE: useful for debugging + paths: { + jquery: 'external/jquery', + 'jquery-ui': 'external/jquery-ui', + caret: 'external/caret', + 'd3': 'external/d3/d3', + autocomplete: 'external/autocomplete', + FileSaver: 'external/FileSaver', + } +} + +define('test', function() { + console.log('hello from test factory'); + return {f:function(){console.log(42);}}; +}); + +console.log('test_app: running under node'); +config.baseUrl = '../src/'; + +document.config = config; +}()); diff --git a/src/client-tests/test_globals.js b/src/client-tests/test_globals.js new file mode 100644 index 00000000..03dd1cf9 --- /dev/null +++ b/src/client-tests/test_globals.js @@ -0,0 +1,30 @@ +var base = require('./base'); +var util = require('../src/util'); + +var count; + +function object_length(obj) { + var count = 0; + for (var k in obj) { + count += 1; + } + return count; +} + +var win_before; +var win_after; + +base.run_tests({ + created: function(errors, window) { + count = object_length(window); + console.log('before fields count: ' + count); + win_before = util.set_from_object(window); + }, + done:function (errors, window) { + var new_count = object_length(window); + console.log('after fields count: ' + new_count); + console.log('new fields count: ' + (new_count - count)); + win_after = util.set_from_object(window); + console.log(util.set_diff(win_after, win_before).a_b.join(',')); + } +}); diff --git a/src/client-tests/test_require_js.js b/src/client-tests/test_require_js.js new file mode 100644 index 00000000..aa71018e --- /dev/null +++ b/src/client-tests/test_require_js.js @@ -0,0 +1,25 @@ +var jsdom = require("jsdom").jsdom; +var document = jsdom(); +var window = document.parentWindow; +var addScript = require('./test_util').addScript; + +window.console.log = console.log; +window.is_node = true; + +addScript(window, '../src/external/require.js') + .load_next('test_app.js') + .done(function () { + var config = document.config + var requirejs = window.requirejs; + var require = window.require; + console.log('callback after test_app.js loading'); + console.log(config); + require.config(config); + requirejs(['require', 'test', './test2.js', 'main'], function(require, test, test2, main) { + console.log('here we are after test_app prerequisites'); + debugger; + test.f(); + main.main(); + process.exit(); + }); + }); diff --git a/src/client-tests/test_script_jsdom.js b/src/client-tests/test_script_jsdom.js new file mode 100644 index 00000000..1ae5835f --- /dev/null +++ b/src/client-tests/test_script_jsdom.js @@ -0,0 +1,9 @@ +var jsdom = require("jsdom").jsdom; +var window = jsdom().parentWindow; + +window.__myObject = { foo: "bar" }; + +var scriptEl = window.document.createElement("script"); +scriptEl.src = "../tests/anotherScript.js"; +window.console.log = console.log; +window.document.body.appendChild(scriptEl); diff --git a/src/client-tests/test_util.js b/src/client-tests/test_util.js new file mode 100644 index 00000000..7780b0ad --- /dev/null +++ b/src/client-tests/test_util.js @@ -0,0 +1,44 @@ +function new_deffer() { + var deffer = { + cb: null, + call_cb: false, + }; + deffer.done = function(cb) { + deffer.cb = cb; + if (deffer.call_cb) { + // warning: going down callstack - should probably use setInterval + deffer.call_cb = false; // first do this to avoid endless recursion + return deffer.cb(); + } + }; + deffer.on_done = function() { + if (this.cb) { + this.call_cb = false; + this.cb(); + } else { + this.call_cb = true; + } + }; + return deffer; +} + +function addScript(window, name) { + var scriptEl = window.document.createElement("script"); + scriptEl.src = name; + var deffer = new_deffer(); + deffer.load_next = function(script) { + var sec_deffer = new_deffer(); + addScript(window, script).done( + function() { sec_deffer.on_done(); }) + return sec_deffer; + } + function onload_cb() { + console.log('addScript: loaded ' + name); + deffer.on_done(); + } + scriptEl.onload = onload_cb; + window.document.body.appendChild(scriptEl); + return deffer; +} + +exports.addScript = addScript; diff --git a/src/client-tests/util/RhiziHTTPServer.py b/src/client-tests/util/RhiziHTTPServer.py new file mode 100644 index 00000000..29e8980d --- /dev/null +++ b/src/client-tests/util/RhiziHTTPServer.py @@ -0,0 +1,78 @@ +""" +Inherits from python-2 SimpleHTTPServer (same bug exists in python3 +http.server) to fix issue where query argument is mistakenly treated as a +improperly non slash terminated path. +""" + + +import os +import BaseHTTPServer +import SimpleHTTPServer + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + + +class MyHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): + + """ + Fix lack of handling for query parameters in SimpleHTTPRequestHandler + """ + + def send_head(self): + """Common code for GET and HEAD commands. + + This sends the response code and MIME headers. + + Return value is either a file object (which has to be copied + to the outputfile by the caller unless the command was HEAD, + and must be closed by the caller under all circumstances), or + None, in which case the caller has nothing further to do. + + """ + path = self.translate_path(self.path) + f = None + if os.path.isdir(path): + if not path.endswith('/'): + # redirect browser - doing basically what apache does + self.send_response(301) + self.send_header("Location", self.path + "/") + self.end_headers() + return None + for index in "index.html", "index.htm": + index = os.path.join(path, index) + if os.path.exists(index): + path = index + break + else: + return self.list_directory(path) + ctype = self.guess_type(path) + try: + # Always read in binary mode. Opening files in text mode may cause + # newline translations, making the actual size of the content + # transmitted *less* than the content-length! + f = open(path, 'rb') + except IOError: + self.send_error(404, "File not found") + return None + try: + self.send_response(200) + self.send_header("Content-type", ctype) + fs = os.fstat(f.fileno()) + self.send_header("Content-Length", str(fs[6])) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + self.end_headers() + return f + except: + f.close() + raise + +def test(HandlerClass = MyHTTPRequestHandler, + ServerClass = BaseHTTPServer.HTTPServer): + BaseHTTPServer.test(HandlerClass, ServerClass) + + +if __name__ == '__main__': + test() diff --git a/src/client-tests/weizmann.json b/src/client-tests/weizmann.json new file mode 100644 index 00000000..fac5fe08 --- /dev/null +++ b/src/client-tests/weizmann.json @@ -0,0 +1 @@ +{"nodes":[{"id":"Oren","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Hila","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Yuval","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Pareto Morphology","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Avi","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"circuits","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Pablo","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"FCD","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"animals","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"ParTI","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"genotypes","type":"project","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Miri","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Jean","type":"person","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Physisist","type":"skill","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Chemist","type":"skill","state":"perm","start":0,"end":0,"status":"unknown"},{"id":"Mathematician","type":"skill","state":"perm","start":0,"end":0,"status":"unknown"}],"links":[{"__src":"Yuval","__dst":"Pareto Morphology","name":"work on "},{"__src":"Oren","__dst":"Pareto Morphology","name":"work on "},{"__src":"Oren","__dst":"Pareto Morphology","name":"work on "},{"__src":"Hila","__dst":"Pareto Morphology","name":"work on "},{"__src":"Hila","__dst":"Pareto Morphology","name":"work on "},{"__src":"Avi","__dst":"Pareto Morphology","name":"works on "},{"__src":"Avi","__dst":"Pareto Morphology","name":"works on "},{"__src":"Avi","__dst":"circuits","name":"works on "},{"__src":"Avi","__dst":"circuits","name":"works on "},{"__src":"Pablo","__dst":"circuits","name":"works on "},{"__src":"Pablo","__dst":"circuits","name":"works on "},{"__src":"Pablo","__dst":"FCD","name":"works on "},{"__src":"Pablo","__dst":"animals","name":"works on "},{"__src":"Pablo","__dst":"ParTI","name":"works on "},{"__src":"Avi","__dst":"circuits","name":"works on "},{"__src":"Avi","__dst":"circuits","name":"works on "},{"__src":"Avi","__dst":"FCD","name":"works on "},{"__src":"Avi","__dst":"animals","name":"works on "},{"__src":"Yuval","__dst":"ParTI","name":"works on "},{"__src":"Yuval","__dst":"ParTI","name":"works on "},{"__src":"Hila","__dst":"circuits","name":"works on "},{"__src":"Hila","__dst":"circuits","name":"works on "},{"__src":"Hila","__dst":"genotypes","name":"works on "},{"__src":"Miri","__dst":"FCD","name":"works on "},{"__src":"Miri","__dst":"FCD","name":"works on "},{"__src":"Hila","__dst":"ParTI","name":"wrote the code and consulted with the math in "},{"__src":"Hila","__dst":"ParTI","name":"wrote the code and consulted with the math in "},{"__src":"Avi","__dst":"ParTI","name":"prepared graphical interface to "},{"__src":"Avi","__dst":"ParTI","name":"prepared graphical interface to "},{"__src":"Jean","__dst":"ParTI","name":"helped with biological interpretations in the project "},{"__src":"Jean","__dst":"ParTI","name":"helped with biological interpretations in the project "},{"__src":"Pablo","__dst":"Physisist","name":"is a "},{"__src":"Physisist","__dst":"Chemist","name":" and a "},{"__src":"Pablo","__dst":"Physisist","name":"is a "},{"__src":"Pablo","__dst":"Chemist","name":"is a "},{"__src":"Yuval","__dst":"Physisist","name":"is a "},{"__src":"Physisist","__dst":"Mathematician","name":" and a "},{"__src":"Hila","__dst":"Physisist","name":"is a "},{"__src":"Hila","__dst":"Mathematician","name":"is a "},{"__src":"Yuval","__dst":"Physisist","name":"is a "},{"__src":"Yuval","__dst":"Mathematician","name":"is a "}]}
\ No newline at end of file diff --git a/src/ScrollTo.js b/src/client/ScrollTo.js index a015f08c..a015f08c 100644 --- a/src/ScrollTo.js +++ b/src/client/ScrollTo.js diff --git a/src/app.js b/src/client/app.js index 39acdde7..39acdde7 100644 --- a/src/app.js +++ b/src/client/app.js diff --git a/src/buttons.js b/src/client/buttons.js index 860c8573..860c8573 100644 --- a/src/buttons.js +++ b/src/client/buttons.js diff --git a/src/consts.js b/src/client/consts.js index 32167811..32167811 100644 --- a/src/consts.js +++ b/src/client/consts.js diff --git a/src/drag_n_drop.js b/src/client/drag_n_drop.js index da166ad4..da166ad4 100644 --- a/src/drag_n_drop.js +++ b/src/client/drag_n_drop.js diff --git a/src/history.js b/src/client/history.js index ab864d1c..ab864d1c 100644 --- a/src/history.js +++ b/src/client/history.js diff --git a/src/main.js b/src/client/main.js index 863d0440..863d0440 100644 --- a/src/main.js +++ b/src/client/main.js diff --git a/src/model/core.js b/src/client/model/core.js index 5c643ea1..5c643ea1 100644 --- a/src/model/core.js +++ b/src/client/model/core.js diff --git a/src/model/diff.js b/src/client/model/diff.js index 8903b161..8903b161 100644 --- a/src/model/diff.js +++ b/src/client/model/diff.js diff --git a/src/model/graph.js b/src/client/model/graph.js index b3c8e33f..b3c8e33f 100644 --- a/src/model/graph.js +++ b/src/client/model/graph.js diff --git a/src/model/util.js b/src/client/model/util.js index d938fbc9..d938fbc9 100644 --- a/src/model/util.js +++ b/src/client/model/util.js diff --git a/src/robot.js b/src/client/robot.js index 700e41b6..700e41b6 100644 --- a/src/robot.js +++ b/src/client/robot.js diff --git a/src/rz_api_backend.js b/src/client/rz_api_backend.js index da073f9f..da073f9f 100644 --- a/src/rz_api_backend.js +++ b/src/client/rz_api_backend.js diff --git a/src/rz_api_mesh.js b/src/client/rz_api_mesh.js index 9014dbac..9014dbac 100644 --- a/src/rz_api_mesh.js +++ b/src/client/rz_api_mesh.js diff --git a/src/rz_bus.js b/src/client/rz_bus.js index 38033bc4..38033bc4 100644 --- a/src/rz_bus.js +++ b/src/client/rz_bus.js diff --git a/src/rz_config.js b/src/client/rz_config.js index e55e74d0..e55e74d0 100644 --- a/src/rz_config.js +++ b/src/client/rz_config.js diff --git a/src/rz_core.js b/src/client/rz_core.js index 7ec19692..7ec19692 100644 --- a/src/rz_core.js +++ b/src/client/rz_core.js diff --git a/src/rz_observer.js b/src/client/rz_observer.js index 718fd0a2..718fd0a2 100644 --- a/src/rz_observer.js +++ b/src/client/rz_observer.js diff --git a/src/textanalysis.js b/src/client/textanalysis.js index 830c16c1..830c16c1 100644 --- a/src/textanalysis.js +++ b/src/client/textanalysis.js diff --git a/src/textanalysis.ui.js b/src/client/textanalysis.ui.js index 5f5718bd..5f5718bd 100644 --- a/src/textanalysis.ui.js +++ b/src/client/textanalysis.ui.js diff --git a/src/util.js b/src/client/util.js index 3fd4aecb..3fd4aecb 100644 --- a/src/util.js +++ b/src/client/util.js diff --git a/src/view/completer.js b/src/client/view/completer.js index d2593321..d2593321 100644 --- a/src/view/completer.js +++ b/src/client/view/completer.js diff --git a/src/view/edge_info.js b/src/client/view/edge_info.js index d711e200..d711e200 100644 --- a/src/view/edge_info.js +++ b/src/client/view/edge_info.js diff --git a/src/view/helpers.js b/src/client/view/helpers.js index ad11763a..ad11763a 100644 --- a/src/view/helpers.js +++ b/src/client/view/helpers.js diff --git a/src/view/internal.js b/src/client/view/internal.js index bc18bd75..bc18bd75 100644 --- a/src/view/internal.js +++ b/src/client/view/internal.js diff --git a/src/view/node_info.js b/src/client/view/node_info.js index 44f2fab8..44f2fab8 100644 --- a/src/view/node_info.js +++ b/src/client/view/node_info.js diff --git a/src/view/selection.js b/src/client/view/selection.js index d51d1a4b..d51d1a4b 100644 --- a/src/view/selection.js +++ b/src/client/view/selection.js diff --git a/src/view/tab.js b/src/client/view/tab.js index 97ba1351..97ba1351 100644 --- a/src/view/tab.js +++ b/src/client/view/tab.js diff --git a/src/view/timeline.js b/src/client/view/timeline.js index 0a26c0a7..0a26c0a7 100644 --- a/src/view/timeline.js +++ b/src/client/view/timeline.js diff --git a/src/view/view.js b/src/client/view/view.js index 95bfc09b..95bfc09b 100644 --- a/src/view/view.js +++ b/src/client/view/view.js diff --git a/src/server-tests/neo4j_test_util.py b/src/server-tests/neo4j_test_util.py new file mode 100644 index 00000000..d5aa640a --- /dev/null +++ b/src/server-tests/neo4j_test_util.py @@ -0,0 +1,54 @@ +import uuid +import string +from random import choice +import db_controller as dbc + +def rand_id(): + return str(uuid.uuid4()) + +def rand_label(length=8): + """ + return random label + """ + char_set = string.ascii_lowercase + string.ascii_uppercase + string.digits + return ''.join([choice(string.ascii_lowercase)] + [choice(char_set) for _ in range(length - 1)]) + +def flush_db(db_ctl): + """ + complete DB flush: remove all nodes & links + """ + db_ctl.exec_cypher_query('match (n) optional match (n)-[r]-() delete n,r') + + +def gen_rand_data(db_ctl, lim_n=128, lim_r=256, prob_link_create = 0.3): + """ + generate random DB data + + @return: tuple consisting of the random node,link labels generated + """ + assert 2 <= lim_n + + n_label = rand_label() + r_label = rand_label() + q_arr = ['with 0 as _', # TODO clean: foreach triggers SyntaxException: otherwise + 'foreach (rid in range(0,%d)' % (lim_n - 1), + '|', + 'create (:%s {id:rid, n_attr_0:toInt(%d * rand())}))' % (n_label, lim_n) + ] + + q = ' '.join(q_arr) + op = dbc.DBO_cypher_query(q) + db_ctl.exec_op(op) + + q_arr = ['match (s:%s),(d:%s)' % (n_label, n_label), + 'with s,d', + 'limit %d' % (lim_r - 1), + 'where rand() < %.2f' % (prob_link_create), + 'create (s)-[:%s {l_attr_0:toInt(%d * rand())}]->(d)' % (r_label,lim_r)] + + q = ' '.join(q_arr) + op = dbc.DBO_cypher_query(q) + db_ctl.exec_op(op) + + return (n_label, r_label) + diff --git a/src/server-tests/test_db_controller.py b/src/server-tests/test_db_controller.py new file mode 100644 index 00000000..79be62d2 --- /dev/null +++ b/src/server-tests/test_db_controller.py @@ -0,0 +1,327 @@ +import unittest +import logging +import db_controller as dbc + +from rhizi_server import Config +from neo4j_test_util import rand_id +from neo4j_test_util import flush_db +from neo4j_test_util import gen_rand_data +from neo4j_util import Neo4JException + +from model.graph import Attr_Diff +from model.graph import Topo_Diff +from model.model import Link + +class TestDBController(unittest.TestCase): + + db_ctl = None + log = None + + n_map = { 'Skill': [{'name': 'Kung Fu', 'id': 'skill_00' }, + {'name': 'Judo', 'id': 'skill_01' } + ], + + 'Person': [{'name': 'Bob', 'id': 'person_00', 'age': 128 }, + {'name': 'Alice', 'id': 'person_01', 'age': 256 } + ] + } + + l_map = { 'Knows' : [Link.link_ptr('person_00', 'skill_00'), + Link.link_ptr('person_00', 'skill_01')] } + + @classmethod + def setUpClass(self): + cfg = Config.init_from_file('res/etc/rhizi-server.conf') + self.db_ctl = dbc.DB_Controller(cfg) + self.log = logging.getLogger('rhizi') + self.log.addHandler(logging.StreamHandler()) + + # TODO rm when implemented: neo4j_test_util + self.db_ctl.exec_cypher_query('create index on :Person(id)') + self.db_ctl.exec_cypher_query('create index on :Skill(id)') + + def setUp(self): + flush_db(self.db_ctl) # remove once embedded DB test mode is supported + self.db_ctl.exec_op(dbc.DBO_add_node_set(self.n_map)) + self.db_ctl.exec_op(dbc.DBO_add_link_set(self.l_map)) + + def test_db_op_statement_iteration(self): + s_arr = ['create (b:Book {title: \'foo\'}) return b', + 'match (n) return n', ] + + op = dbc.DB_op() + op.add_statement(s_arr[0]) + op.add_statement(s_arr[1]) + + i = 0 + for _, s, r in op: + # access: second tuple item -> REST-form 'statement' key + self.assertEqual(s_arr[i], s['statement']) + self.assertEqual(None, r) + i = i + 1 + + self.db_ctl.exec_op(op) + + i = 0 + for _, s, r_set in op: + # access: second tuple item -> REST-form 'statement' key + self.assertNotEqual(None, r_set) + for x in r_set: + pass + i = i + 1 + + def test_add_node_set(self): + n_map = { 'T_test_add_node_set': [{'id': rand_id()}, {'id': rand_id()}] } + op = dbc.DBO_add_node_set(n_map) + + self.assertEqual(len(op.statement_set), 1) # assert a single statement is issued + + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 2) + + def test_add_link_set(self): + src_id = rand_id() + dst_id_0 = rand_id() + dst_id_1 = rand_id() + n_map = { 'T_test_add_node_set': [{'id': src_id }, + {'id': dst_id_0 }, + {'id': dst_id_1 }] } + self.db_ctl.exec_op(dbc.DBO_add_node_set(n_map)) + + l_map = { 'T_test_add_link_set' : [{'__src': src_id, '__dst': dst_id_0}, + {'__src': src_id, '__dst': dst_id_1}] } + + op = dbc.DBO_add_link_set(l_map) + self.assertEqual(len(op.statement_set), 2) # no support yet for parameterized statements for link creation + + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 2) + + def test_match_node_set_by_type(self): + op = dbc.DBO_match_node_id_set(filter_label='Person') + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 2) + + op = dbc.DBO_match_node_id_set(filter_label='Nan_Type') + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 0) + + def test_match_node_set_by_attribute(self): + fam = { 'name': ['Bob', u'Judo'], 'age': [128] } + n_set = self.db_ctl.exec_op(dbc.DBO_match_node_id_set(filter_attr_map=fam)) + self.assertEqual(len(n_set), 1) + + fam = { 'age': [128, 256, 404] } + n_set = self.db_ctl.exec_op(dbc.DBO_match_node_id_set(filter_attr_map=fam)) + self.assertEqual(len(n_set), 2) + + def test_match_node_set_by_DB_id(self): + pass # TODO + + def test_match_node_set_by_id_attribute(self): + n_set = self.db_ctl.exec_op(dbc.DBO_match_node_set_by_id_attribute(['skill_00', 'person_01'])) + self.assertEqual(len(n_set), 2) + + def test_match_link_set_by_type(self): + op = dbc.DBO_match_link_id_set(filter_label='Knows') + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 2) + + op = dbc.DBO_match_link_id_set(filter_label='Nan_Type') + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 0) + + def test_load_link_set(self): + + # load by l_ptr + l_ptr = Link.link_ptr(src_id='person_00', dst_id='skill_00') + op = dbc.DBO_load_link_set.init_from_link_ptr(l_ptr) + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 1) + + l_ptr = Link.link_ptr(src_id='person_00') + op = dbc.DBO_load_link_set.init_from_link_ptr(l_ptr) + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 2) + + l_ptr = Link.link_ptr(dst_id='skill_00') + op = dbc.DBO_load_link_set.init_from_link_ptr(l_ptr) + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 1) + + # load by l_ptr sets + l_ptr_set = [Link.link_ptr(s, d) for (s, d) in [('person_00', 'skill_00'), ('person_00', 'skill_01')]] + op = dbc.DBO_load_link_set.init_from_link_ptr_set(l_ptr_set) + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 2) + + # this should return the same link twice + l_ptr_set = [Link.link_ptr(s, d) for (s, d) in [('person_00', 'skill_00'), ('person_00', 'skill_01')]] + l_ptr_set.append(Link.link_ptr(dst_id='skill_00')) + op = dbc.DBO_load_link_set.init_from_link_ptr_set(l_ptr_set) + l_set = self.db_ctl.exec_op(op) + self.assertEqual(len(l_set), 3) + + def test_load_node_set_by_DB_id(self): + """ + test node DB id life cycle + """ + + # create nodes, get DB ids + op = dbc.DBO_add_node_set({'T_test_load_node_set_by_DB_id': [{'name': 'John Doe'}, + {'name': 'John Doe'}]}) + id_set = self.db_ctl.exec_op(op) + + # match against DB ids + op = dbc.DBO_load_node_set_by_DB_id(id_set) + n_set = self.db_ctl.exec_op(op) + self.assertEqual(len(n_set), len(id_set), 'incorrect result size') + + def test_partial_query_set_execution_success(self): + """ + test: + - statement execution stops at first invalid statement + - assert create statement with result data does not actually persist in DB + + From the REST API doc: 'If any errors occur while executing statements, + the server will roll back the transaction.' + """ + n_id = 'test_partial_query_set_execution_success' + + op = dbc.DB_op() + op.add_statement("create (n:Person {id: '%s'}) return n" % (n_id), {}) # valid statement + op.add_statement("match (n) return n", {}) # valid statement + op.add_statement("non-valid statement #1", {}) + op.add_statement("non-valid statement #2", {}) + + self.assertRaises(Neo4JException, self.db_ctl.exec_op, op) + + self.assertEqual(len(op.result_set), 2) + self.assertEqual(len(op.error_set), 1) + + # assert node creation did not persist + n_set = self.db_ctl.exec_op(dbc.DBO_match_node_set_by_id_attribute([n_id])) + self.assertEqual(len(n_set), 0) + + def test_topo_diff_commit(self): + n_0_id = rand_id() + n_1_id = rand_id() + n_2_id = rand_id() + n_T = 'T_test_topo_diff_commit' + + n_set = [{'__type': n_T, 'id': n_0_id }, + {'__type': n_T, 'id': n_1_id }, + {'__type': n_T, 'id': n_2_id }] + l_set = [{'__type': n_T, '__src_id': n_0_id, '__dst_id': n_1_id}, + {'__type': n_T, '__src_id': n_1_id, '__dst_id': n_0_id}] + + topo_diff = Topo_Diff(node_set_add=n_set, + link_set_add=l_set) + + op = dbc.DBO_topo_diff_commit(topo_diff) + op_ret = self.db_ctl.exec_op(op) + self.assertEqual(len(op_ret), 2) # to id-sets, nodes & links + self.assertEqual(len(op_ret[0]), 3) # expect id-set of length 3 + self.assertEqual(len(op_ret[1]), 2) # expect id-set of length 2 + + id_set = self.db_ctl.exec_op(dbc.DBO_match_node_set_by_id_attribute([n_0_id, n_1_id])) + self.assertEqual(len(id_set), 2) + + l_ptr = Link.link_ptr(src_id=n_0_id, dst_id=n_1_id) + id_set = self.db_ctl.exec_op(dbc.DBO_load_link_set.init_from_link_ptr(l_ptr)) + self.assertEqual(len(id_set), 1) + + l_ptr = Link.link_ptr(src_id=n_1_id, dst_id=n_0_id) + id_set = self.db_ctl.exec_op(dbc.DBO_load_link_set.init_from_link_ptr(l_ptr)) + self.assertEqual(len(id_set), 1) + + id_set_rm = [n_2_id] + topo_diff = Topo_Diff(node_set_rm=id_set_rm) + op = dbc.DBO_topo_diff_commit(topo_diff) + self.db_ctl.exec_op(op) + op = dbc.DBO_match_node_set_by_id_attribute(id_set_rm) + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 0) + + def test_attr_diff_commit(self): + # create test node + n_id = rand_id() + topo_diff = Topo_Diff(node_set_add=[{'__type': 'T_test_attr_diff_commit', 'id': n_id, 'attr_rm': 0}]) + op = dbc.DBO_topo_diff_commit(topo_diff) + self.db_ctl.exec_op(op) + + # apply attr_diff + attr_diff = Attr_Diff() + attr_diff.add_node_attr_write(n_id, 'attr_0', 0) + attr_diff.add_node_attr_write(n_id, 'attr_1', 'a') + attr_diff.add_node_attr_rm(n_id, 'attr_rm') + + op = dbc.DBO_attr_diff_commit(attr_diff) + n_map = self.db_ctl.exec_op(op) + self.assertEqual(len(n_map), 1) + n = n_map.get(n_id) + self.assertTrue(None != n) + self.assertTrue(None == n.get('attr_rm')) + self.assertEqual(0, n.get('attr_0')) + self.assertEqual('a', n.get('attr_1')) + + # attr-set only + attr_diff = Attr_Diff() + attr_diff.add_node_attr_write(n_id, 'attr_2', 0) + + op = dbc.DBO_attr_diff_commit(attr_diff) + n_map = self.db_ctl.exec_op(op) + + # attr-remove only + attr_diff = Attr_Diff() + attr_diff.add_node_attr_rm(n_id, 'attr_2') + + op = dbc.DBO_attr_diff_commit(attr_diff) + n_map = self.db_ctl.exec_op(op) + + def test_rm_node_set(self): + n_0_id = rand_id() + n_1_id = rand_id() + n_2_id = rand_id() + n_3_id = rand_id() + n_T = 'T_test_rm_node_set' + + n_set = [{'__type': n_T, 'id': n_0_id }, + {'__type': n_T, 'id': n_1_id }, + {'__type': n_T, 'id': n_2_id }, + {'__type': n_T, 'id': n_3_id }] + l_set = [{'__type': n_T, '__src_id': n_2_id, '__dst_id': n_2_id}, + {'__type': n_T, '__src_id': n_2_id, '__dst_id': n_3_id}] + + topo_diff = Topo_Diff(node_set_add=n_set, + link_set_add=l_set) + + op = dbc.DBO_topo_diff_commit(topo_diff) + self.db_ctl.exec_op(op) + + op = dbc.DBO_rm_node_set([n_0_id, n_1_id]) + self.db_ctl.exec_op(op) + + op = dbc.DBO_rm_node_set([n_2_id, n_3_id], rm_links=True) + self.db_ctl.exec_op(op) + + # assert all deleted + op = dbc.DBO_match_node_id_set(filter_label=n_T) + id_set = self.db_ctl.exec_op(op) + self.assertEqual(len(id_set), 0) + + def test_rz_clone(self): + l_n, l_r = gen_rand_data(self.db_ctl, lim_n=8, lim_r=16, prob_link_create=0.7) + op = dbc.DBO_rz_clone(filter_label=l_n, limit=32) + ret = self.db_ctl.exec_op(op) + n_set = ret['node_set'] + l_set = ret['link_set'] + + # TODO improve assertions + self.assertTrue(0 < len(n_set)) + self.assertTrue(0 < len(l_set)) + + def tearDown(self): pass + +if __name__ == "__main__": + unittest.main() diff --git a/src/server-tests/test_rhizi_api.py b/src/server-tests/test_rhizi_api.py new file mode 100644 index 00000000..c404edba --- /dev/null +++ b/src/server-tests/test_rhizi_api.py @@ -0,0 +1,84 @@ +import unittest +import db_controller as dbc +import rhizi_api +import json +import logging + +from rhizi_server import Config +from werkzeug.test import EnvironBuilder +from werkzeug.test import Client + +from db_controller import DB_Driver_Embedded + +class TestRhiziAPI(unittest.TestCase): + + def setUp(self): + self.flush_db() + + @classmethod + def setUpClass(self): + cfg = Config.init_from_file('res/etc/rhizi-server.conf') + self.db_ctl = dbc.DB_Controller(cfg) + rhizi_api.db_ctl = self.db_ctl + + # TODO extract to superclass + log = logging.getLogger('rhizi') + log.setLevel(logging.DEBUG) + log_handler_c = logging.StreamHandler() + log.addHandler(log_handler_c) + + def flush_db(self): + """ + complete DB flush: remove all nodes & links + """ + self.db_ctl.exec_cypher_query('match (n) optional match (n)-[r]-() delete n,r') + + def test_add_node_set(self): + """ + add node set test + """ + node_map = { 'Skill': [{ 'name': 'kung-fu' }, { 'name': 'judo' }] } + with rhizi_api.webapp.test_client() as c: + req = c.post('/add/node-set', + content_type='application/json', + data=json.dumps(dict(node_map=node_map))) + id_set = json.loads(req.data)['data'] + self.assertEqual(2, len(id_set)) + self.assertTrue(isinstance(id_set[0], int)) + + def test_load_node_non_existing(self): + """ + loading a non existing node test + """ + id_set = ['non_existing_id'] + with rhizi_api.webapp.test_client() as c: + req = c.post('/load/node-set-by-id', + content_type='application/json', + data=json.dumps({ 'id_set': id_set})) + req_data = json.loads(req.data) + rz_data = req_data['data'] + rz_err = req_data['error'] + self.assertEqual(None, rz_err) + self.assertEqual(0, len(rz_data)) + + def test_load_node_set_by_id_existing(self): + """ + loading an existing node test + """ + id_set = ['skill_00'] + self.db_ctl.exec_cypher_query('create (s:Skill {id: \'skill_00\'} )') + + with rhizi_api.webapp.test_client() as c: + req = c.post('/load/node-set-by-id', + content_type='application/json', + data=json.dumps({ 'id_set': id_set})) + n_set = json.loads(req.data)['data'] + + self.assertEqual(1, len(n_set)) + self.assertEqual(n_set[0]['id'], id_set[0]) + + def test_load_node_set(self): + pass + +if __name__ == "__main__": + unittest.main() diff --git a/src/server/crypt_util.py b/src/server/crypt_util.py new file mode 100644 index 00000000..464fac94 --- /dev/null +++ b/src/server/crypt_util.py @@ -0,0 +1,46 @@ +import pickle +import hashlib, uuid +import os +import logging + +log = logging.getLogger('rhizi') + +def add_user_login(config, u, p): + htpasswd_path = config.htpasswd_path + + if False == os.path.exists(htpasswd_path): + with open(htpasswd_path, 'wb') as f: + pickle.dump({}, f) + + with open(htpasswd_path, 'rb') as f: + data = f.read() + pw_db = pickle.loads(data) + + with open(htpasswd_path, 'wb') as f: + salt = config.secret_key + pw_db[u] = hash_pw(str(p), salt) + pickle.dump(pw_db, f) + + log.info('htpasswd db: added entry: user: %s, pw: %s...' % (u, pw_db[u][:5])) + +def hash_pw(pw_str, salt_str): + salt = hashlib.sha512(salt_str).hexdigest() + ret = hashlib.sha512(pw_str + salt).hexdigest() + return ret + +def validate_login(config, u, p): + htpasswd_path = config.htpasswd_path + + salt = config.secret_key + + with open(htpasswd_path) as f: + pw_db = pickle.load(f) + + existing_pw_hash = pw_db.get(u) + if None == existing_pw_hash: + raise Exception('Not autorhized') + + if hash_pw(p, salt) != existing_pw_hash: + raise Exception('Not autorhized') + + diff --git a/src/server/db_controller.py b/src/server/db_controller.py new file mode 100644 index 00000000..c17fd0cd --- /dev/null +++ b/src/server/db_controller.py @@ -0,0 +1,495 @@ +#!/usr/bin/python + +import json +import logging +import os +import re +import traceback + +from db_driver import DB_Driver_REST, DB_Driver_Base +from model.graph import Attr_Diff +from model.graph import Topo_Diff +from neo4j_util import DB_result_set +from neo4j_util import cfmt +import neo4j_util as db_util +from model.model import Link + +log = logging.getLogger('rhizi') + +class DB_op(object): + """ + tx wrapped DB operation possibly composing multiple DB queries + """ + def __init__(self): + self.statement_set = [] + self.result_set = [] + self.error_set = None + self.tx_id = None + self.tx_commit_url = None # cached from response to tx begin + + def parse_tx_id(self, tx_commit_url): + m = re.search('/(?P<id>\d+)/commit$', tx_commit_url) + id_str = m.group('id') + self.tx_id = int(id_str) + + def add_statement(self, query, query_params={}): + """ + add a DB query language statement + @return: statement index (zero based) + """ + s = db_util.statement_to_REST_form(query, query_params) + self.statement_set.append(s) + return len(self.statement_set) + + def __iter__(self): + """ + iterate over (statement_index, statement, result, error) + where result & error are mutually exclusive + + note: statement_index is zero based + + TODO: handle partial iteration due to error_set being non-empty + """ + i = 0 + r_set_len = len(self.result_set) + for s in self.statement_set: + r_set = None # row-set + if i < r_set_len: # support partial result recovery + r_set = DB_result_set(self.result_set[i]) + yield (i, s, r_set) + i = i + 1 + + def parse_multi_statement_response_data(self, data): + pass + + @property + def name(self): + return self.__class__.__name__ + + def process_result_set(self): + """ + DB op can issue complex sets of quries all at once - this helper method + assists in parsing response data from a single query. + """ + ret = [] + for _, _, r_set in self: + for row in r_set: + for col in row: + ret.append(col) + return ret + +class DB_composed_op(DB_op): + def __init__(self): + super(DB_composed_op, self).__init__() + self.sub_op_set = [] + + def __assert_false_statement_access(self): + assert False, "composed_op may not contain statements, only sub-ops" + + def add_statement(self, query, query_params={}): + self.__assert_false_statement_access() + + def add_sub_op(self, op): + self.sub_op_set.append(op) + + def __getattribute__(self, attr): + """ + intercept 'statement_set' attr get + """ + if attr == 'statement_set': + self.__assert_false_statement_access() + + return object.__getattribute__(self, attr) + + def __iter__(self): + """ + iterate over sub_op_set + """ + for s_op in self.sub_op_set: + yield s_op + + def process_result_set(self): + ret = [] + for s_op in self: + s_result_set = s_op.process_result_set() + ret.append(s_result_set) + return ret + +class DBO_cypher_query(DB_op): + """ + freeform cypher query + """ + def __init__(self, q, q_params={}): + super(DBO_cypher_query, self).__init__() + self.add_statement(q, q_params) + +class DBO_topo_diff_commit(DB_composed_op): + """ + commit a Topo_Diff + """ + def __init__(self, topo_diff): + super(DBO_topo_diff_commit, self).__init__() + + n_add_map = db_util.meta_attr_list_to_meta_attr_map(topo_diff.node_set_add) + l_add_map = db_util.meta_attr_list_to_meta_attr_map(topo_diff.link_set_add) + l_rm_set = topo_diff.link_set_rm + n_rm_set = topo_diff.node_set_rm + + # + # [!] order critical + # + if len(n_add_map) > 0: + op = DBO_add_node_set(n_add_map) + self.add_sub_op(op) + + if len(l_add_map) > 0: + op = DBO_add_link_set(l_add_map) + self.add_sub_op(op) + + if len(l_rm_set) > 0: + op = DBO_rm_link_set(l_rm_set) + self.add_sub_op(op) + + if len(n_rm_set) > 0: + op = DBO_rm_node_set(n_rm_set) + self.add_sub_op(op) + +class DBO_attr_diff_commit(DB_op): + """ + commit a Attr_Diff + """ + def __init__(self, attr_diff): + super(DBO_attr_diff_commit, self).__init__() + + for id_attr, n_attr_diff in attr_diff.type__node.items(): + # TODO parameterize multiple attr removal + r_attr_set = n_attr_diff['__attr_remove'] + w_attr_set = n_attr_diff['__attr_write'] + + assert len(r_attr_set) > 0 or len(w_attr_set) > 0 + + q_arr = ["match (n {id: {id}}) ", + "return n.id, n"] + q_param_set = {'id': id_attr} + + if len(r_attr_set) > 0: + stmt_attr_rm = "remove " + ', '.join(['n.' + attr for attr in r_attr_set]) + q_arr.insert(1, stmt_attr_rm) + + if len(w_attr_set) > 0: + stmt_attr_set = "set n += {attr_set}" + q_arr.insert(1, stmt_attr_set) + q_param_set['attr_set'] = w_attr_set + + q = " ".join(q_arr) + self.add_statement(q, q_param_set) + + for id_attr, n_attr_diff in attr_diff.type__link.items(): + pass # TODO: handl link attr_diffs + + def process_result_set(self): + ret = {} + for _, _, r_set in self: + for row in r_set: + n_id, n = [v for v in row] # we expect a [n_id, n] array + ret[n_id] = n + return ret + +class DBO_add_node_set(DB_op): + def __init__(self, node_map): + """ + DB op: add node set + + @param node_map: node-type to node-set map + @return: set of new node DB ids + """ + super(DBO_add_node_set, self).__init__() + for q, q_param_set in db_util.gen_query_create_from_node_map(node_map): + self.add_statement(q, q_param_set) + + def process_result_set(self): + id_set = [] + for _, _, row_set in self: + for row in row_set: + for clo in row: + id_set.append(clo) + + return id_set + +class DBO_add_link_set(DB_op): + def __init__(self, link_map): + """ + @param link_map: is a link-type to link-set map - see model.link + @return: set of new node DB ids + """ + super(DBO_add_link_set, self).__init__() + for q, q_params in db_util.gen_query_create_from_link_map(link_map): + self.add_statement(q, q_params) + + def process_result_set(self): + id_set = [] + for _, _, r_set in self: + for row in r_set: + for col_val in row: + id_set.append(col_val) + + return id_set + +class DBO_load_node_set_by_DB_id(DB_op): + def __init__(self, id_set): + """ + load a set of nodes whose DB id is in id_set + + @param id_set: DB node id set + @return: loaded node set or an empty set if no match was found + """ + super(DBO_load_node_set_by_DB_id, self).__init__() + q = "start n=node({id_set}) return n" + self.add_statement(q, { 'id_set': id_set}) + +class DBO_match_node_id_set(DB_op): + + def __init__(self, filter_label=None, filter_attr_map={}): + """ + match a set of nodes by type / attr_map + + @param filter_label: node type filter + @param filter_attr_map: is a filter_key to filter_value_set map of + possible attributes to match against, eg.: + { 'id':[0,1], 'color: ['red','blue'] } + @return: a set of node DB id's + """ + super(DBO_match_node_id_set, self).__init__() + + q = "match (n{filter_label}) {where_clause} return id(n)" + q = cfmt(q, filter_label="" if not filter_label else ":" + filter_label) + q = cfmt(q, where_clause=db_util.gen_clause_where_from_filter_attr_map(filter_attr_map)) + + q_params = filter_attr_map + + self.add_statement(q, q_params) + +class DBO_match_node_set_by_id_attribute(DBO_match_node_id_set): + def __init__(self, id_set): + """ + convenience op: load a set of nodes by their 'id' attribute != DB node id + """ + assert isinstance(id_set, list) + + super(DBO_match_node_set_by_id_attribute, self).__init__(filter_attr_map={'id': id_set}) + + +class DBO_load_link_set(DB_op): + def __init__(self, link_ptr_set): + """ + match a set of sets of links by source/target node id attributes + + This class should be instantiated through a static factory function + + @link_ptr_set link pointer set + @return: a set of loaded links + """ + super(DBO_load_link_set, self).__init__() + + for l_ptr in link_ptr_set: + if not l_ptr.src_id: + q = "match ()-[r]->({id: {dst_id}}) return r" + q_params = {'dst_id': l_ptr.dst_id} + elif not l_ptr.dst_id: + q = "match ({id: {src_id}})-[r]->() return r" + q_params = {'src_id': l_ptr.src_id} + else: + q = "match ({id: {src_id}})-[r]->({id: {dst_id}}) return r" + q_params = {'src_id': l_ptr.src_id, 'dst_id': l_ptr.dst_id} + + self.add_statement(q, q_params) + + @staticmethod + def init_from_link_ptr(l_ptr): + return DBO_load_link_set([l_ptr]) + + @staticmethod + def init_from_link_ptr_set(l_ptr_set): + return DBO_load_link_set(l_ptr_set) + +class DBO_match_link_id_set(DB_op): + def __init__(self, filter_label=None, filter_attr_map={}): + """ + load an id-set of links + + @param filter_label: link type filter + @param filter_attr_map: is a filter_key to filter_value_set map of + attributes to match link properties against + @return: a set of loaded link ids + """ + super(DBO_match_link_id_set, self).__init__() + + q_arr = ['match ()-[r{filter_label} {filter_attr}]->()', + 'return id(r)' + ] + q = ' '.join(q_arr) + q = cfmt(q, filter_label="" if not filter_label else ":" + filter_label) + q = cfmt(q, filter_attr=db_util.gen_clause_attr_filter_from_filter_attr_map(filter_attr_map)) + q_params = {k: v[0] for (k, v) in filter_attr_map.items()} # pass on only first value from each value set + + self.add_statement(q, q_params) + +class DBO_rm_node_set(DB_op): + def __init__(self, id_set, rm_links=False): + """ + remove node set + """ + assert len(id_set) > 0, __name__ + ': empty id set' + + super(DBO_rm_node_set, self).__init__() + + if rm_links: + q_arr = ['match (n)', + 'where n.id in {id_set}', + 'optional match (n)-[r]-()', + 'delete n,r', + 'return {id_set}' + ] + else: + q_arr = ['match (n)', + 'where n.id in {id_set}', + 'delete n', + 'return {id_set}' + ] + + q = ' '.join(q_arr) + q_params = {'id_set': id_set} + self.add_statement(q, q_params) + +class DBO_rm_link_set(DB_op): + def __init__(self, id_set): + """ + remove link set + + [!] when removing as a result of node removal, use DBO_rm_node_set + along with rm_links=True + """ + assert len(id_set) > 0, __name__ + ': empty id set' + + super(DBO_rm_link_set, self).__init__() + + q_arr = ['match ()-[r]->()', + 'where r.id in {id_set}', + 'delete r', + 'return {id_set}' + ] + + q = ' '.join(q_arr) + q_params = {'id_set': id_set} + self.add_statement(q, q_params) + +class DBO_rz_clone(DB_op): + def __init__(self, filter_label=None, limit=128): + """ + clone rhizi + + @return: a dict: {'node_set': n_set, + 'link_set': l_set } + where l_set is a list of (src.id, dst.id, link) tuples + """ + super(DBO_rz_clone, self).__init__() + + self.limit = limit + self.skip = 0 + + q_arr = ['match (n)' if not filter_label else 'match (n:%s)' % (filter_label), + 'optional match (n)-[r]->(m)', + 'with n,r,m', + 'order by n.id', + 'skip %d' % (self.skip), + 'limit %d' % (self.limit), + 'return n,labels(n),collect([m.id, r, type(r)])'] + + q = ' '.join(q_arr) + self.add_statement(q) + + def process_result_set(self): + ret_n_set = [] + ret_l_set = [] + for _, _, row_set in self: + for row in row_set: + n, n_lbl_set, l_set = row.items() # see query return statement + + # reconstruct nodes + assert None != n['id'] + + n['__label_set'] = n_lbl_set + ret_n_set.append(n) + + # reconstruct links from link tuples + for l_tuple in l_set: + assert 3 == len(l_tuple) # see query return statement + + if None == l_tuple[0]: # check if link dst is None + # as link matching is optional, collect may yield empty sets + continue + + l = l_tuple[1] + l['__src_id'] = n['id'] + l['__dst_id'] = l_tuple[0] + l['__label_set'] = [l_tuple[2]] # box single value returned by type() + + ret_l_set.append(l) + + return {'node_set': ret_n_set, + 'link_set': ret_l_set } + +class DB_Controller: + """ + neo4j DB controller + """ + def __init__(self, config, db_driver_class=None): + self.config = config + if not db_driver_class: + self.db_driver = DB_Driver_REST(self.config.db_base_url) + else: + self.db_driver = db_driver_class() + assert isinstance(self.db_driver, DB_Driver_Base) + + def exec_op(self, op): + """ + execute operation within a DB transaction + """ + if isinstance(op, DB_composed_op): + # construct a list comprehension composed of all sup_op statements + for s_op in op.sub_op_set: + self.exec_op(s_op) + return op.process_result_set() + + try: + self.db_driver.begin_tx(op) + self.db_driver.exec_statement_set(op) + self.db_driver.commit_tx(op) + + ret = op.process_result_set() + + log.debug('exec_op:' + op.name + ': return value: ' + str(ret)) + return ret + except Exception as e: + # here we watch for IOExecptions, etc - not db errors + # these are returned in the db response itself + log.error(e.message) + log.error(traceback.print_exc()) + raise e + + def create_db_op(self, f_work, f_cont): + ret = DB_op(f_work, f_cont) + return ret + + def exec_cypher_query(self, q): + """ + @deprecated: use DBO_cypher_query + """ + + # call post and not db_util.post_neo4j to avoid response key errors + try: + db_util.post(self.config.db_base_url + '/db/data/cypher', {"query" : q}) + except Exception as e: + log.error(e.message) + log.error(traceback.print_exc()) + raise e diff --git a/src/server/db_driver.py b/src/server/db_driver.py new file mode 100644 index 00000000..0532e512 --- /dev/null +++ b/src/server/db_driver.py @@ -0,0 +1,91 @@ +import logging + +from neo4j_util import Neo4JException +import neo4j_util as db_util + + +log = logging.getLogger('rhizi') + +class DB_Driver_Base(): + + def log_committed_queries(self, statement_set): + for sp_dict in statement_set['statements']: + if None != sp_dict['parameters']: + msg = '\tq: {0}\n\tp: {1}'.format(sp_dict['statement'], + sp_dict['parameters']) + else: + msg = '\tq: {0}'.format(sp_dict['statement']) + log.debug(msg) + +class DB_Driver_Embedded(DB_Driver_Base): + def __init__(self, db_base_url): + self.tx_base_url = db_base_url + '/db/data/transaction' + + from org.rhizi.db.neo4j.util import EmbeddedNeo4j + self.edb = EmbeddedNeo4j.createDb() + self.edb.createDb() + + def begin_tx(self, op): + pass + + def exec_statement_set(self, op): + s_set = op.statement_set + self.edb.executeCypherQury() + + def commit_tx(self, op): + pass + +class DB_Driver_REST(DB_Driver_Base): + def __init__(self, db_base_url): + self.tx_base_url = db_base_url + '/db/data/transaction' + + def begin_tx(self, op): + tx_open_url = self.tx_base_url + + try: + # + # [!] neo4j seems picky about receiving an additional empty statement list + # + data = data = db_util.statement_set_to_REST_form([]) + ret = db_util.post_neo4j(tx_open_url, data) + tx_commit_url = ret['commit'] + op.parse_tx_id(tx_commit_url) + + log.debug('tx-open: id: {0}, commit-url: {1}'.format(op.tx_id, tx_commit_url)) + except Exception as e: + raise Exception('failed to open transaction:' + e.message) + + def exec_statement_set(self, op): + + tx_url = "{0}/{1}".format(self.tx_base_url, op.tx_id) + statement_set = db_util.statement_set_to_REST_form(op.statement_set) + + try: + post_ret = db_util.post_neo4j(tx_url, statement_set) + op.result_set = post_ret['results'] + op.error_set = post_ret['errors'] + if 0 != len(op.error_set): + raise Neo4JException(op.error_set) + + self.log_committed_queries(statement_set) + except Neo4JException as e: + raise e + except Exception as e: + raise Exception('failed exec op statements: err: {0}, url: {1}'.format(e.message, tx_url)) + + def commit_tx(self, op): + tx_commit_url = "{0}/{1}/commit".format(self.tx_base_url, op.tx_id) + + try: + # + # [!] neo4j seems picky about receiving an additional empty statement list + # + data = db_util.statement_set_to_REST_form([]) + ret = db_util.post(tx_commit_url, data) + + log.debug('tx-commit: id: {0}, commit-url: {1}'.format(op.tx_id, tx_commit_url)) + + return ret + except Exception as e: + raise Exception('failed to commit transaction:' + e.message) + diff --git a/src/server/model/__init__.py b/src/server/model/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/server/model/__init__.py diff --git a/src/server/model/graph.py b/src/server/model/graph.py new file mode 100644 index 00000000..f197921d --- /dev/null +++ b/src/server/model/graph.py @@ -0,0 +1,105 @@ +class Attr_Diff(dict): + """ + Represents a change to note attributes, where nodes can represent + either logical nodes or logical links, and attributes can be added, + changed or removed + + Example: + attr_diff = {'__type_node' : {n_id: {'__attr_write': {'attr_0': 0, + 'attr_1': 'a'}, + '__attr_remove': ['attr_2'] }} + '__type_link' : {l_id: ... } + } + """ + def __init__(self): + self['__type_node'] = {} + self['__type_link'] = {} + + def init_node_attr_diff(self, n_id): + ret = {'__attr_write': {}, + '__attr_remove': []} + self['__type_node'][n_id] = ret + return ret + + @staticmethod + def from_json_dict(json_dict): + ret = Attr_Diff() + for obj_type in ret.keys(): + obj_ad_set = json_dict.get(obj_type) + if None != obj_ad_set: + for o_id, ad in obj_ad_set.items(): + if None != ad.get('__attr_write'): + for k, v in ad['__attr_write'].items(): + ret.add_node_attr_write(o_id, k, v) + if None != ad.get('__attr_remove'): + for k in ad['__attr_remove']: + ret.add_node_attr_rm(o_id, k) + return ret + + @property + def type__node(self): + return self['__type_node'] + + @property + def type__link(self): + return self['__type_link'] + + def add_node_attr_write(self, n_id, attr_name, attr_val): + + assert 'id' != attr_name.lower(), 'Attr_Diff: attempt to write to \'id\' attribute' + + n_attr_diff = self['__type_node'].get(n_id) + if None == n_attr_diff: + n_attr_diff = self.init_node_attr_diff(n_id) + n_attr_diff['__attr_write'][attr_name] = attr_val + + def add_node_attr_rm(self, n_id, attr_name): + n_attr_diff = self['__type_node'].get(n_id) + if None == n_attr_diff: + n_attr_diff = self.init_node_attr_diff(n_id) + n_attr_diff['__attr_remove'].append(attr_name) + + def add_link_attr_write(self, l_id, attr_name, attr_val): + assert False, 'unimplemented' + + def add_link_attr_rm(self, l_id, attr_name): + assert False, 'unimplemented' + +class Topo_Diff(object): + """ + Represents a change to the graph topology + """ + def __init__(self, link_set_rm=[], + node_set_rm=[], + node_set_add=[], + link_set_add=[]): + + self.link_set_rm = link_set_rm + self.node_set_rm = node_set_rm + self.node_set_add = node_set_add + self.link_set_add = link_set_add + + def __str__(self): + return __name__ + ': ' + ', '.join('%s: %s' % (k, v) for k, v in self.__dict__.items()) + + def check_validity(self, topo_diff_dict): + """ + Topo_Diff may represent invalid operations, eg. adding a link while + removing it's end-point - this stub should check for that + """ + pass + + @staticmethod + def from_json_dict(json_dict): + """ + construct from dict - no node/link constructor set must be provided + """ + ret = Topo_Diff() + + # merge keys - this allows constructor argument omission (link_set_rm, + # node_set_rm, etc.) such as when constructing from POST JSON data + for k, _ in ret.__dict__.items(): + v = json_dict.get(k) + if None != v: + ret.__dict__[k] = v + return ret diff --git a/src/server/model/model.py b/src/server/model/model.py new file mode 100644 index 00000000..33cf9e6a --- /dev/null +++ b/src/server/model/model.py @@ -0,0 +1,37 @@ +class Link(): + """ + documentation anchor - this class currently carries no implementation + and only acts as a documentation anchor + + link['__src'] - meta attribute for link source object + link['__dst'] - meta attribute for link destination object + """ + + def __init__(self, src=None, dst=None): + assert False, 'currently unused' + + class Link_Ptr(dict): + """ + link['__src_id'] - meta attribute for link source id + link['__dst_id'] - meta attribute for link destination id + """ + def __init__(self, src_id=None, dst_id=None): + assert None != src_id or None != dst_id + + self['__src_id'] = src_id + self['__dst_id'] = dst_id + + @property + def src_id(self): + return self['__src_id'] + + @property + def dst_id(self): + return self['__dst_id'] + + @staticmethod + def link_ptr(src_id=None, dst_id=None): + """ + init from src_id or dst_id attributes - at least one must be provided + """ + return Link.Link_Ptr(src_id, dst_id) diff --git a/src/server/neo4j_util.py b/src/server/neo4j_util.py new file mode 100644 index 00000000..cf2f962e --- /dev/null +++ b/src/server/neo4j_util.py @@ -0,0 +1,260 @@ +""" + Utility code in speaking the neo4j REST api +""" + +import json +import six +from six.moves.urllib import request +import six.moves.urllib_error as urllib_error +import model +import string +import time + +from util import debug_log_duration + +class Neo4JException(Exception): + def __init__(self, error_set): + self.error_set = error_set + + def __str__(self): + return 'neo4j error set: ' + str(self.error_set) + +class DB_row(object): + def __init__(self, data): + self.data = data + + def __iter__(self): + for column_val in self.data: + yield column_val + + def items(self): + return [x for x in self] + +class DB_result_set(object): + def __init__(self, data): + self.data = data + + def __iter__(self): + for db_row_dict in self.data['data']: + # example: dict: {u'row': [{u'title': u'foo'}]} + assert None != db_row_dict['row'] + + yield DB_row(db_row_dict['row']) + + def items(self): + return [x for x in self] + +class Cypher_String_Formatter(string.Formatter): + """ + Despite parameter support in Cypher, we sometimes do engage in query string building + - as both Cypher & Python use brackets to wrap parameters, escaping them in Python makes + queries less readable. This customized formatter will simply ignore unavailable keyworded + formatting arguments, allowing the use of non-escaped parameter designation, eg: + q = cfmt("match (a:{type} {cypher_param})", type='Book') + """ + + def get_field(self, field_name, args, kwargs): + # ignore key not found, return bracket wrapped key + try: + val = super(Cypher_String_Formatter, self).get_field(field_name, args, kwargs) + except (KeyError, AttributeError): + val = "{" + field_name + "}", field_name + return val + +def cfmt(fmt_str, *args, **kwargs): + return Cypher_String_Formatter().format(fmt_str, *args, **kwargs) + +def post_neo4j(url, data): + """ + @return dict object from the neo4j json POST response + """ + ret = post(url, data) + ret_data = json.load(ret) + + # [!] do not raise exception if ret_data['errors'] is not empty - + # this allows query-sets to partially succeed + + return ret_data + +def post(url, data): + assert(isinstance(data, dict)) # make sure we're not handed json strings + + post_data_json = json.dumps(data) + + req = request.Request(url) + req.add_header('User-Agent', 'rhizi-server/0.1') + req.add_header('Accept', 'application/json; charset=UTF-8') + req.add_header('Content-Type', 'application/json') + + req.add_header('X-Stream', 'true') # enable neo4j JSON streaming + + try: + ret = request.urlopen(req, post_data_json) + except urllib_error.HTTPError as e: + raise Exception('post request failed: code: {0}, reason: {1}'.format(e.code, e.reason)) + + return ret + +def statement_to_REST_form(query, parameters={}): + """ + turn cypher query to neo4j json API format + """ + assert isinstance(query, six.string_types) + if isinstance(parameters, list): + for v in parameters: + assert isinstance(v, dict) + else: + assert isinstance(parameters, dict) + + return {'statement' : query, 'parameters': parameters} + +def statement_set_to_REST_form(statement_set): + assert isinstance(statement_set, list) + + return {'statements': statement_set} + +def gen_clause_attr_filter_from_filter_attr_map(filter_attr_map, node_label="n"): + if not filter_attr_map: + return "{}" + + __type_check_filter_attr_map(filter_attr_map) + + filter_arr = [] + for attr_name in filter_attr_map.keys(): + # create a cypher query parameter place holder for each attr set + # eg. n.foo in {foo}, where foo is passed as a query parameter + f_attr = cfmt("{attr_name}: {{{attr}}}", attr_name=attr_name) + filter_arr.append(f_attr) + + filter_str = "{{{0}}}".format(', '.join(filter_arr)) + return filter_str + +def gen_clause_where_from_filter_attr_map(filter_attr_map, node_label="n"): + """ + convert a filter attribute map to a parameterized Cypher where clause, eg. + in: { 'att_foo': [ 'a', 'b' ], 'att_goo': [1,2] } + out: {att_foo: {att_foo}, att_goo: {att_goo}, ...} + + this function will essentially ignore all but the first value in the value list + + @param filter_attr_map: may be None or empty + """ + if not filter_attr_map: + return "" + + __type_check_filter_attr_map(filter_attr_map) + + filter_arr = [] + for attr in filter_attr_map.keys(): + # create a cypher query parameter place holder for each attr set + # eg. n.foo in {foo}, where foo is passed as a query parameter + f_attr = cfmt("{node_label}.{attr} in {{{attr}}}", node_label=node_label, attr=attr) + filter_arr.append(f_attr) + filter_str = "where {0}".format(' and '.join(filter_arr)) + return filter_str + +def gen_query_create_from_node_map(node_map, input_to_DB_property_map=lambda _: _): + """ + generate a set of node create queries + + @param node_map: is a node-type to node map + @input_to_DB_property_map: optional function which takes a map of input properties and returns a map of DB properties - use to map input schemas to DB schemas + + @return: a (query, query_parameteres) set of create queries + """ + __type_check_link_or_node_map(node_map) + + ret = [] + for label, n_set in node_map.items(): + + assert len(label) > 2 and label[0].isupper() and label[1:].islower(), 'malformed label: ' + label + + q_arr = ['create (n:%s {node_attr})' % (label), + 'return n.id' + ] + + q = ' '.join(q_arr) + q_params_set = [] + for n_prop_set in n_set: + + assert None != n_prop_set['id'], 'node create query: node id attribute not set' + + q_params = input_to_DB_property_map(n_prop_set) + q_params_set.append(q_params) + ret.append((q, {'node_attr': q_params_set})) + return ret + +def gen_query_create_from_link_map(link_map, input_to_DB_property_map=lambda _: _): + """ + generate a set of link create queries + + @param link_map: is a link-type to link map - see model.link + """ + __type_check_link_or_node_map(link_map) + + ret = [] + for l_type, l_set in link_map.items(): + q = "match (src {id: {src}.id}),(dst {id: {dst}.id}) " + \ + "create (src)-[r:%(__type)s {link_attr}]->(dst) " + \ + "return id(r)" + q = q % {'__type':l_type} + + for link in l_set: + __type_check_link(link) + + src_id = link['__src_id'] + dst_id = link['__dst_id'] + + # TODO: use object based link representation + prop_dict = link.copy() + del prop_dict['__dst_id'] + del prop_dict['__src_id'] + + q_params = {'src': { 'id': src_id} , + 'dst': { 'id': dst_id} , + 'link_attr' : input_to_DB_property_map(prop_dict)} + ret.append((q, q_params)) + + return ret + +def meta_attr_list_to_meta_attr_map(e_set, meta_attr='__label_set'): + """ + convert a list of maps each containing a meta_attr key into a + meta_attr-mapped collection of lists with the meta_attr removed - eg: + + in: [{'id':0, '__type': 'T'}, {'id':1, '__type': 'T'}] + out: { 'T', [{'id':0}, {'id':1}] } + """ + ret = {} + for v in e_set: + assert None != v[meta_attr], 'missing type meta-attribute' + assert 1 == len(v[meta_attr]), 'only single-type mapping currently suppoerted' + + v_type = v[meta_attr][0] + if None == ret.get(v_type): # init type list if necessary + ret[v_type] = [] + + v_no_meta = v.copy() + del v_no_meta[meta_attr] + + ret[v_type].append(v_no_meta) + + return ret + +def __type_check_link(link): + assert link.has_key('__src_id') + assert link.has_key('__dst_id') + +def __type_check_link_or_node_map(x_map): + for k, v in x_map.iteritems(): # do some type sanity checking + assert isinstance(k, six.string_types) + assert isinstance(v, list) + +def __type_check_filter_attr_map(filter_attr_map): + """ + # type sanity check an attribute filter map + """ + assert isinstance(filter_attr_map, dict) + for k, v in filter_attr_map.items(): + assert isinstance(k, six.string_types) + assert isinstance(v, list) diff --git a/src/server/rhizi_api.py b/src/server/rhizi_api.py new file mode 100644 index 00000000..579d41e1 --- /dev/null +++ b/src/server/rhizi_api.py @@ -0,0 +1,256 @@ +""" +Rhizi web API +""" +import os +import db_controller as dbc +import json +import logging +import traceback +import crypt_util + +import flask +from flask import jsonify +from flask import Flask +from flask import request +from flask import make_response +from flask import session +from flask import redirect +from flask import escape +from flask import url_for +from flask import render_template +from flask import send_from_directory + +from model.graph import Topo_Diff +from model.graph import Attr_Diff +from model.model import Link +from datetime import datetime + +log = logging.getLogger('rhizi') + +# injected: DB controller +db_ctl = None + +def __sanitize_input(*args, **kw_args): + pass + +def sanitize_input__node(n): + """ + provide a control point as to which node fields are persisted + """ + assert None != n['id'], 'invalid input: node: missing id' + +def sanitize_input__link(l): + """ + provide a control point as to which node fields are persisted + """ + assert None != l['id'], 'invalid input: link: missing id' + assert None != l['__src_id'], 'invalid input: link: missing src id' + assert None != l['__dst_id'], 'invalid input: link: missing dst id' + +def sanitize_input__topo_diff(topo_diff): + for n in topo_diff.node_set_add: + sanitize_input__node(n) + for l in topo_diff.link_set_add: + sanitize_input__link(l) + +def sanitize_input__attr_diff(attr_diff): + pass # TODO: impl + +def __response_wrap(data=None, error=None): + """ + wrap response data/errors as dict - this should always be used when returning + data to allow easy return of list objects, assist in error case distinction, etc. + """ + return dict(data=data, error=error) + +def __common_resp_handle(data=None, error=None): + """ + provide common response handling + """ + ret_data = __response_wrap(data, error) + resp = jsonify(ret_data) + + resp.headers['Access-Control-Allow-Origin'] = '*' + + # more response processing + + return resp + +def __common_exec(op, on_success=__common_resp_handle): + try: + op_ret = db_ctl.exec_op(op) + return on_success(op_ret) + except Exception as e: + log.error(e.message) + log.error(traceback.print_exc()) + return __common_resp_handle('error occurred') + +def load_node_set_by_id_attr(): + """ + load node-set by ID attribute + + @param id_set: list of node ids to match id attribute against + @return: a list of nodes whose id attribute matches 'id' or + an empty list if the requested node is not found + @raise exception: on error + """ + req_json = request.get_json() + id_set = req_json['id_set'] + + __sanitize_input(id_set) + + return __load_node_set_by_id_attr_common(id_set) + +def __load_node_set_by_id_attr_common(id_set): + """ + @param f_k: optional attribute filter key + @param f_vset: possible key values to match against + """ + op = dbc.DBO_match_node_set_by_id_attribute(id_set=id_set) + try: + n_set = db_ctl.exec_op(op) + return __common_resp_handle(data=n_set) + except Exception as e: + log.exception(e) + return __common_resp_handle(error='unable to load node with ids: {0}'.format(id_set)) + +def match_node_set_by_attr_filter_map(attr_filter_map): + """ + @param attr_filter_map + + @return: a set of node DB id's + """ + op = dbc.DBO_match_node_id_set(attr_filter_map) + return __common_exec(op) + +def load_link_set_by_link_ptr_set(): + + def deserialize_param_set(param_json): + l_ptr_set_raw = param_json['link_ptr_set'] + + __sanitize_input(l_ptr_set_raw) + + l_ptr_set = [] + for lptr_dict in l_ptr_set_raw: + src_id = lptr_dict.get('__src_id') + dst_id = lptr_dict.get('__dst_id') + l_ptr_set += [Link.Link_Ptr(src_id=src_id, dst_id=dst_id) ] + + return l_ptr_set + + l_ptr_set = deserialize_param_set(request.get_json()) + + op = dbc.DBO_load_link_set.init_from_link_ptr_set(l_ptr_set) + return __common_exec(op) + +def rz_clone(): + op = dbc.DBO_rz_clone() + return __common_exec(op) + +def diff_commit__set(): + """ + commit a diff set + """ + def sanitize_input(req): + diff_set_dict = request.get_json()['diff_set'] + topo_diff_dict = diff_set_dict['__diff_set_topo'][0] + topo_diff = Topo_Diff.from_json_dict(topo_diff_dict) + + sanitize_input__topo_diff(topo_diff) + return topo_diff; + + topo_diff = sanitize_input(request) + op = dbc.DBO_topo_diff_commit(topo_diff) + return __common_exec(op) + +def diff_commit__topo(): + """ + commit a graph topology diff + """ + def sanitize_input(req): + topo_diff_dict = request.get_json()['topo_diff'] + topo_diff = Topo_Diff.from_json_dict(topo_diff_dict) + + sanitize_input__topo_diff(topo_diff) + return topo_diff; + + topo_diff = sanitize_input(request) + op = dbc.DBO_topo_diff_commit(topo_diff) + return __common_exec(op) + +def diff_commit__attr(): + """ + commit a graph attribute diff + """ + def sanitize_input(req): + attr_diff_dict = request.get_json()['attr_diff'] + attr_diff = Attr_Diff.from_json_dict(attr_diff_dict) + + sanitize_input__attr_diff(attr_diff) + return attr_diff; + + attr_diff = sanitize_input(request) + op = dbc.DBO_attr_diff_commit(attr_diff) + return __common_exec(op) + +def diff_commit__vis(): + pass + +def add_node_set(): + """ + @deprecated: use topo_attr_commit + + @param node_map: node type to node map, eg. { 'Skill': { 'name': 'kung-fu' } } + """ + node_map = request.get_json()['node_map'] + __sanitize_input(node_map) + + op = dbc.DBO_add_node_set(node_map) + return __common_exec(op) + +def monitor__server_info(): + """ + server monitor stub + """ + dt = datetime.now() + return "<html><body>" + \ + "<h1>Rhizi Server v0.1</h1><p>" + \ + "date: " + dt.strftime("%Y-%m-%d") + "<br>" + \ + "time: " + dt.strftime("%H:%M:%S") + "<br>" + \ + "</p></body></html>" + +def index(): + username = escape(session.get('username')) + return render_template('index.html', username=username) + +def login(): + + def sanitize_input(req): + req_json = request.get_json() + u = req_json['username'] + p = req_json['password'] + return u, p + + if request.method == 'POST': + try: + u, p = sanitize_input(request) + crypt_util.validate_login(flask.current_app.rz_config, u, p) + except Exception as e: + # login failed + log.warn('login: unauthorized: user: %s' % (u)) + return render_template('login.html', login_failed=True) + + # login successful + session['username'] = u + log.debug('login: success: user: %s' % (u)) + return redirect(url_for('index')) + + if request.method == 'GET': + return render_template('login.html') + +def logout(): + # remove the username from the session if it's there + u = session.pop('username', None) + log.debug('logout: success: user: %s' % (u)) + return redirect(url_for('login')) + diff --git a/src/server/rhizi_server.py b/src/server/rhizi_server.py new file mode 100644 index 00000000..168e2f53 --- /dev/null +++ b/src/server/rhizi_server.py @@ -0,0 +1,263 @@ +#!/usr/bin/python + +import logging +import json +import util +import os +import neo4j_util +import argparse +import db_controller as dbc +import rhizi_api +import flask +import crypt_util +import re + +from flask import Flask +from flask import session +from flask import redirect +from flask import request +from flask import send_from_directory + +from functools import wraps + +class Config(object): + """ + rhizi-server configuration + + TODO: config option documentation + + htpasswd_path + listen_address + listen_port + neo4j_url + root_path + """ + + @staticmethod + def init_from_file(file_path): + + if False == os.path.exists(file_path): + raise Exception('config file not found: ' + file_path) + + # apply defaults + cfg = {} + cfg['access_control'] = True + cfg['config_dir'] = os.path.abspath(os.path.dirname(file_path)) # bypass prop restriction + cfg['development_mode'] = False + cfg['listen_address'] = '127.0.0.1' + cfg['listen_port'] = 8080 + cfg['root_path'] = os.getcwd() + cfg['static_url_path'] = '/static' + + # Flask keys + cfg['SECRET_KEY'] = '' + + with open(file_path, 'r') as f: + for line in f: + if re.match('(^#)|(\s+$)', line): + continue + + kv_arr = line.split('=') + if 2 != len(kv_arr): + raise Exception('failed to parse config line: ' + line) + + k, v = map(str.strip, kv_arr) + + if None != cfg.get(k): + # apply type conversion based on default value type + type_f = type(cfg[k]) + if bool == type_f: + v = v in ("True", "true") # workaround bool('false') = True + else: + v = type_f(v) + + # [!] we can't use k.lower() as we are loading Flask configuration + # keys which are expected to be capitalized + cfg[k] = v + + ret = Config() + ret.__dict__ = cfg # allows setting of @property attributes + + # validate config + if False == os.path.isabs(ret.root_path): + ret.root_path = os.path.abspath(ret.root_path) + + return ret + + def __str__(self): + return '\n'.join('%s: %s' % (k, v) for k, v in self.__dict__.items()) + + @property + def db_base_url(self): + return self.neo4j_url + + @property + def tx_api_path(self): + return '/db/data/transaction' + + @property + def config_dir_path(self): + return self.config_dir + + @property + def secret_key(self): + return self.SECRET_KEY + +class FlaskExt(Flask): + """ + Flask server customization + """ + + def __init__(self, import_name, *args, **kwargs): + """ + reserved for future use + """ + super(FlaskExt, self).__init__(import_name, *args, **kwargs) + + def before_request(self, *args, **kwargs): + # TODO impl + pass + + def make_default_options_response(self): + ret = Flask.make_default_options_response(self) + + ret.headers['Access-Control-Allow-Origin'] = 'http://rhizi.net' + ret.headers['Access-Control-Allow-Headers'] = "Accept, Authorization, Content-Type, Origin" + ret.headers['Access-Control-Allow-Credentials'] = 'true' + + # ret.headers['Access-Control-Allow-Methods'] = ', '.join(m_list) + return ret + +def init_log(cfg): + """ + init log file, location derived from configuration + """ + log = logging.getLogger('rhizi') + log.setLevel(logging.DEBUG) + log_handler_c = logging.StreamHandler() + log_handler_f = logging.FileHandler(cfg.log_path) + + log.addHandler(log_handler_c) + log.addHandler(log_handler_f) + return log + +def init_rest_api(cfg, flask_webapp): + """ + map REST API calls + """ + + def rest_entry(path, f, flask_args={'methods': ['POST']}): + return (path, f, flask_args) + + def dev_mode__resend_from_static(static_url): + """ + redirect broken-on-local-deploy links: + - /src -> '': handle root based files, eg. app.js + - /res, /lib -> res, lib + """ + static_folder = flask.current_app.static_folder + + static_path = request.path + if static_path.startswith('/src'): + # TODO: clean - /src/... links should not exist + static_path = static_path.replace('/src', '') + if static_path.startswith('/'): # convert to relative path + static_path = static_path[1:] + return send_from_directory(static_folder, static_path) + + def login_decorator(f): + """ + [!] security boundary: asserd logged-in user before executing REST api call + """ + @wraps(f) + def wrapped_function(*args, **kw): + if not 'username' in session: + return redirect('/login') + return f(*args, **kw) + + return wrapped_function + + rest_entry_set = [ + rest_entry('/add/node-set' , rhizi_api.add_node_set), + rest_entry('/graph/clone', rhizi_api.rz_clone), + rest_entry('/graph/diff-commit-set', rhizi_api.diff_commit__set), + rest_entry('/graph/diff-commit-topo', rhizi_api.diff_commit__topo), + rest_entry('/graph/diff-commit-attr', rhizi_api.diff_commit__attr), + rest_entry('/graph/diff-commit-vis', rhizi_api.diff_commit__vis), + rest_entry('/index', rhizi_api.index, {'methods': ['GET']}), + rest_entry('/load/node-set-by-id', rhizi_api.load_node_set_by_id_attr), + rest_entry('/load/link-set/by_link_ptr_set', rhizi_api.load_link_set_by_link_ptr_set), + rest_entry('/login', rhizi_api.login, {'methods': ['GET', 'POST']}), + rest_entry('/logout', rhizi_api.logout, {'methods': ['GET', 'POST']}), + rest_entry('/match/node-set', rhizi_api.match_node_set_by_attr_filter_map), + rest_entry('/monitor/server-info', rhizi_api.monitor__server_info), + ] + + if cfg.development_mode: + dev_path_set = ['/src', '/res', '/lib'] + rest_dev_entry_set = [] + for dev_path in dev_path_set: + rest_dev_entry_set.append(rest_entry(dev_path + '/<path:static_url>', + dev_mode__resend_from_static, + {'methods': ['GET']})) + rest_entry_set += rest_dev_entry_set + + if False == cfg.access_control: + log.warn('access control disabled, public access set on all URLs') + + for re_entry in rest_entry_set: + rest_path, f, flask_args = re_entry + + if cfg.access_control and '/login' != rest_path: + # currently require login on all but /login paths + f = login_decorator(f) + + # [!] order seems important - apply route decorator last + route_dec = flask_webapp.route(rest_path, **flask_args) + f = route_dec(f) + + flask_webapp.f = f # assign decorated function + +def init_webapp(cfg): + root_path = cfg.root_path + webapp = FlaskExt(__name__, + static_folder='static', + template_folder=os.path.join(root_path, 'templates'), + static_url_path=cfg.static_url_path) + webapp.config.from_object(cfg) + webapp.root_path = root_path # for some reason calling config.from_xxx() does not have effect + + db_ctl = dbc.DB_Controller(cfg) + rhizi_api.db_ctl = db_ctl + + webapp.rz_config = cfg + return webapp + +def init_config(cfg_dir): + cfg_path = os.path.join(cfg_dir, 'rhizi-server.conf') + cfg = Config.init_from_file(cfg_path) + return cfg + + +if __name__ == "__main__": + + p = argparse.ArgumentParser(description='rhizi-server') + p.add_argument('--config-dir', help='path to Rhizi config dir', default='res/etc') + p.add_argument('--init-htpasswd-db', help='init login htpasswd db', action='store_const', const=True) + args = p.parse_args() + + cfg = init_config(args.config_dir) + log = init_log(cfg) + log.debug('loaded configuration:\n%s' % cfg) + + if args.init_htpasswd_db: + init_pw_db(cfg) + exit(0) + + webapp = init_webapp(cfg) + init_rest_api(cfg, webapp) + + log.info('launching webapp via Flask development server') + webapp.run(host=cfg.listen_address, + port=cfg.listen_port) + diff --git a/src/server/rhizi_server_fcgi.py b/src/server/rhizi_server_fcgi.py new file mode 100755 index 00000000..f0b9a04b --- /dev/null +++ b/src/server/rhizi_server_fcgi.py @@ -0,0 +1,25 @@ +#!/usr/bin/python + +from flup.server.fcgi import WSGIServer +import os +import sys +import cgitb +import rhizi_server + +# sys.path.insert(0, '/srv/www/rhizi/rhizi.net/src-py') + +# enable debugging +cgitb.enable() + +if __name__ == '__main__': + cfg_dir = '/etc/rhizi' + + cfg = rhizi_server.init_config(os.path.join(cfg_dir, 'rhizi-server.conf')) + log = rhizi_server.init_log() + + webapp = rhizi_server.init_webapp(cfg) + rhizi_server.init_rest_api(cfg, webapp) + + log.info('launching webapp via flup.server.fcgi.WSGIServer') + + WSGIServer(webapp).run() diff --git a/src/server/util.py b/src/server/util.py new file mode 100644 index 00000000..e8840114 --- /dev/null +++ b/src/server/util.py @@ -0,0 +1,22 @@ +""" +code with no better place to go +""" +import time + +def debug_log_duration(method): + """ + dubug call durations - use example: + + neo4j_util.post = util.debug_log_duration(neo4j_util.post) + """ + + def timed(*args, **kw): + t_0 = time.time() + result = method(*args, **kw) + t_1 = time.time() + dt = t_1 - t_0 + + print ('%2.2f sec, function: %r' % (dt, method.__name__)) + return result + + return timed |
