diff options
| -rw-r--r-- | res/etc/rhizi-backend.conf | 3 | ||||
| -rw-r--r-- | src-py/db_controller.py | 179 | ||||
| -rw-r--r-- | src-py/neo4j_util.py | 49 | ||||
| -rw-r--r-- | src-py/rhizi_webapp.py | 70 |
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) |
