summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--res/etc/rhizi-backend.conf3
-rw-r--r--src-py/db_controller.py179
-rw-r--r--src-py/neo4j_util.py49
-rw-r--r--src-py/rhizi_webapp.py70
4 files changed, 301 insertions, 0 deletions
diff --git a/res/etc/rhizi-backend.conf b/res/etc/rhizi-backend.conf
new file mode 100644
index 00000000..ae321264
--- /dev/null
+++ b/res/etc/rhizi-backend.conf
@@ -0,0 +1,3 @@
+{
+ "neo4j_url": "http://127.0.0.1:50442"
+} \ No newline at end of file
diff --git a/src-py/db_controller.py b/src-py/db_controller.py
new file mode 100644
index 00000000..6ad77b01
--- /dev/null
+++ b/src-py/db_controller.py
@@ -0,0 +1,179 @@
+#!/usr/bin/python
+
+import os
+import json
+import re
+import logging
+import traceback
+
+import urllib2
+
+import neo4j_util as dbu
+
+log = logging.getLogger('rhizi')
+
+class DB_op(object):
+ """
+ tx wrapped DB operation possibly composing multiple DB queries
+ """
+
+ def __init__(self):
+ self.s_id = 0 # statement id counter
+ self.id_to_statement_map = {} # zero based id to statement map
+ self.tx_id = None
+ self.tx_commit_url = None # cached from response to tx begin
+
+ def begin(self, tx_open_url):
+ try:
+ #
+ # [!] neo4j seems picky about receiving an additional empty statement list
+ #
+ data = data = dbu.statement_set_to_REST_form([])
+ ret = dbu.post_neo4j(tx_open_url, data)
+ tx_commit_url = ret['commit']
+ self.parse_tx_id(tx_commit_url)
+ self.tx_commit_url = tx_commit_url
+
+ log.debug('tx-open: id: {0}, commit-url: {1}'.format(self.tx_id, tx_commit_url))
+ except Exception as e:
+ raise Exception('failed to open transaction:' + e.message)
+
+ 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 commit(self):
+ try:
+ #
+ # [!] neo4j seems picky about receiving an additional empty statement list
+ #
+ data = dbu.statement_set_to_REST_form([])
+ ret = dbu.post(self.tx_commit_url, data)
+ except Exception as e:
+ raise Exception('failed to commit transaction:' + e.message)
+
+ log.debug('tx-commit: id: {0}, commit-url: {1}'.format(self.tx_id, self.tx_commit_url))
+
+ def add_statement(self, cypher_query, params={}):
+ """
+ add a DB query language statement
+ @return: statement id
+ """
+ ret = self.s_id
+ self.id_to_statement_map[self.s_id] = dbu.statement_to_REST_form(cypher_query, params)
+ self.s_id = self.s_id + 1
+ return ret
+
+ @property
+ def statement_set(self):
+ return self.id_to_statement_map.values()
+
+ def on_success(self, data):
+ pass
+
+ def on_error(self):
+ pass
+
+class DBO_add_node_set(DB_op):
+ """
+ DB op: add node set
+
+ @param node_map: type to node list map
+ """
+ def __init__(self, node_map):
+ super(DBO_add_node_set, self).__init__()
+ self.node_map = node_map
+
+ for type, n_set in self.node_map.items():
+ q = "create (n:{0} {{prop_dict}}) return id(n)".format(type)
+ for n in n_set:
+ #
+ # any translation between how we accept node data
+ # and how we store them should go here
+ #
+ p = {'prop_dict' : { 'name' : n['name']}}
+ self.add_statement(q, p)
+
+ def on_success(self, data):
+ # [!] fragile - parse results
+ # sample input: dict: {u'errors': [], u'results': [{u'data': [{u'row': [20]}], u'columns': [u'id(n)']}]}
+ id_set = []
+ for r in data['results']:
+ columns = r['columns']
+ for k in r['data']:
+ nid = k['row'][0]
+ id_set.append(nid)
+
+ log.debug('node-set added: ids: ' + str(id_set))
+ return id_set
+
+class DBO_load_node_id_set(DB_op):
+ """
+ load node id set, filter by type / properties
+ """
+ def __init__(self, filter_type, filter_prop=None):
+ super(DBO_load_node_id_set, self).__init__()
+
+ # build where clause if necessary
+ filter_prop_str = ""
+ if filter_prop:
+ filter_prop_arr = []
+ for k, v in filter_prop:
+ v_str = str(v)
+ if isinstance(v, str):
+ # quote string values
+ v_str = "'{0}'".format(v_str)
+ filter_prop_arr.append("n.{0} = {1} and ".format(k, v_str))
+ filter_prop_str = " where " + " and ".join(filter_prop_arr)
+
+ q = "match (n:{0}){1} return id(n)".format(filter_type, filter_prop_str)
+ self.add_statement(q)
+
+ def on_success(self, data):
+ # [!] fragile - parse results
+ # sample input: dict: {u'errors': [], u'results': [{u'data': [{u'row': [20]}], u'columns': [u'id(n)']}]}
+ id_set = []
+ for r in data['results']:
+ columns = r['columns']
+ for k in r['data']:
+ nid = k['row'][0]
+ id_set.append(nid)
+
+ log.debug('loaded node-set: ids: ' + str(id_set))
+ return id_set
+
+class DB_Controller:
+ """
+ neo4j DB controller
+ """
+ def __init__(self, config):
+ self.config = config
+
+ def exec_op(self, op):
+ """
+ execute operation within a DB transaction
+ """
+ tx_base_url = self.config.db_base_url + '/db/data/transaction'
+ data = dbu.statement_set_to_REST_form(op.statement_set)
+
+ try:
+ op.begin(tx_base_url)
+ tx_url = "{0}/{1}".format(tx_base_url, op.tx_id)
+ ret_tx = dbu.post_neo4j(tx_url, data)
+ op.commit()
+ return op.on_success(ret_tx)
+ except Exception as e:
+ log.error(e.message)
+ log.error(traceback.print_exc())
+ op.on_error()
+
+ 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 transaction based api
+ """
+ self.post_neo4j('/db/data/cypher', {"query" : q})
diff --git a/src-py/neo4j_util.py b/src-py/neo4j_util.py
new file mode 100644
index 00000000..009fbe5f
--- /dev/null
+++ b/src-py/neo4j_util.py
@@ -0,0 +1,49 @@
+"""
+ Utility code in speaking the neo4j REST api
+"""
+
+import json
+import urllib2
+
+def post_neo4j(url, data):
+ """
+ return dict translation of the json string returned by neo4j, raise Exception if the 'errors' key is not empty
+ """
+ ret = post(url, data)
+ ret_data = json.load(ret)
+
+ if ret_data['errors']:
+ raise Exception('neo4j exception: ' + str(ret_data['errors']))
+
+ 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 = urllib2.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')
+
+ try:
+ ret = urllib2.urlopen(req, post_data_json)
+ except urllib2.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, str)
+ 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}
diff --git a/src-py/rhizi_webapp.py b/src-py/rhizi_webapp.py
new file mode 100644
index 00000000..b63407d6
--- /dev/null
+++ b/src-py/rhizi_webapp.py
@@ -0,0 +1,70 @@
+"""
+Rhizi webapp
+"""
+import os
+import json
+import logging
+
+from flask import Flask, request, url_for
+import db_controller as dbc
+
+cwd = os.getcwd()
+app = Flask(__name__)
+app.debug = True
+
+class Config:
+ """
+ rhizi-server configuration
+ """
+
+ @staticmethod
+ def init_from_file(file_path):
+ ret = Config()
+
+ with open(file_path, 'r') as f:
+ cfg = json.loads(f.read())
+ ret.db_base_url = cfg['neo4j_url']
+
+ return ret
+
+ @property
+ def db_base_url(self):
+ return self.neo4j_url
+
+ @property
+ def tx_api_path(self):
+ return '/db/data/transaction'
+
+@app.route("/add-node-set")
+def foo():
+ pass
+
+def init_logging():
+ global log
+
+ log = logging.getLogger('rhizi')
+ log.setLevel(logging.DEBUG)
+ log_handler_c = logging.StreamHandler()
+ log_handler_f = logging.FileHandler('/tmp/rhizi-backend.log')
+
+ log.addHandler(log_handler_c)
+ log.addHandler(log_handler_f)
+
+def test_DB_controller_api():
+ db_ctl = dbc.DB_Controller(cfg)
+
+ n_map = { 'Skill': [{'name': 'kung fu' },
+ {'name': 'judo' }
+ ],
+ 'Person': [{'name': 'Bob' }, {'name': 'Alice' }]
+ }
+
+ db_ctl.exec_op(dbc.DBO_add_node_set(n_map))
+ id_set = db_ctl.exec_op(dbc.DBO_load_node_id_set(filter_type='Skill'))
+
+if __name__ == "__main__":
+ cfg = Config.init_from_file('res/etc/rhizi-backend.conf')
+ init_logging()
+
+ test_DB_controller_api()
+ # app.run(host='127.0.0.1', port=rhizi_backend_cfg['port'], ssl_context=ctx)