├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── neo4j ├── __init__.py ├── connection.py ├── contextmanager.py ├── cursor.py ├── strings.py └── test │ ├── __init__.py │ ├── performance.py │ ├── test_connection.py │ ├── test_contextmanager.py │ ├── test_cursor.py │ ├── test_exceptions.py │ └── test_types.py ├── pavement.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | *.egg-info 4 | .DS_store 5 | paver-minilib.zip 6 | __pycache__ 7 | *.pyc 8 | *.swp 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "2.6" 5 | - "2.7" 6 | - "3.2" 7 | - "3.3" 8 | # command to install dependencies 9 | install: 10 | - pip install paver 11 | - paver sdist 12 | 13 | # command to run tests 14 | script: 15 | - paver start_server 16 | - paver nosetests 17 | - paver stop_server 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include paver-minilib.zip -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Neo4jDB 3 | ======= 4 | 5 | Implements the Python DB API 2.0 for Neo4j, compatible with python 2.6, 2.7, 6 | 3.2 and 3.3 and Neo4j >= 2.0.0 7 | 8 | http://legacy.python.org/dev/peps/pep-0249/ 9 | 10 | .. image:: https://travis-ci.org/jakewins/neo4jdb-python.svg?branch=master 11 | :target: https://travis-ci.org/jakewins/neo4jdb-python 12 | 13 | 14 | Installing 15 | ---------- 16 | 17 | :: 18 | 19 | pip install neo4jdb 20 | 21 | Minimum viable snippet 22 | ---------------------- 23 | 24 | :: 25 | 26 | import neo4j 27 | 28 | connection = neo4j.connect("http://localhost:7474") 29 | 30 | cursor = connection.cursor() 31 | for name, age in cursor.execute("MATCH (n:User) RETURN n.name, n.age"): 32 | print name, age 33 | 34 | Usage 35 | ----- 36 | 37 | The library generally adheres to the DB API, please refer to the documentation 38 | for the DB API for detailed usage. 39 | 40 | :: 41 | 42 | # Write statements 43 | cursor.execute("CREATE (n:User {name:'Stevie Brook'}") 44 | connection.commit() # Or connection.rollback() 45 | 46 | # With positional parameters 47 | cursor.execute("CREATE (n:User {name:{0}})", "Bob") 48 | # or 49 | l = ['Bob'] 50 | cursor.execute("CREATE (n:User {name:{0}})", *l) 51 | 52 | # With named parameters 53 | cursor.execute("CREATE (n:User {name:{name}})", name="Bob") 54 | # or 55 | d = {'name': 'Bob'} 56 | cursor.execute("CREATE (n:User {name:{name}})", **d) 57 | # or 58 | d = {'node': {'name': 'Bob'}} 59 | cursor.execute("CREATE (n:User {node})", **d) 60 | 61 | 62 | If you ask Cypher to return Nodes or Relationships, these are represented as Node and Relationship types, which 63 | are `dict` objects with additional metadata for id, labels, type, end_id and start_id. 64 | 65 | :: 66 | 67 | # Retrieve and access a node 68 | for node, in cursor.execute("MATCH (n) RETURN n"): 69 | print node.id, node.labels 70 | print node['a_property'] 71 | 72 | # Retrieve and access a relationship 73 | for rel, in cursor.execute("MATCH ()-[r]->() RETURN r"): 74 | print rel.id, rel.type, rel.start_id, rel.end_id 75 | print rel['a_property'] 76 | 77 | 78 | Using the context manager. Any exception in the context will result in the exception being thrown and the transaction to be rolled back. 79 | 80 | :: 81 | 82 | from neo4j.contextmanager import Neo4jDBConnectionManager 83 | manager = contextmanager.Neo4jDBConnectionManager('http://localhost:7474') 84 | 85 | with manager.read as r: # r is just a cursor 86 | for name, age in r.execute("MATCH (n:User) RETURN n.name, n.age"): 87 | print name, age 88 | # When leaving read context the transaction will be rolled back 89 | 90 | with manager.write as w: 91 | w.execute("CREATE (n:User {name:{name}})", name="Bob") 92 | # When leaving write context the transaction will be committed 93 | 94 | # When using transaction a new connection will be created 95 | with manager.transaction as t: 96 | t.execute("CREATE (n:User {name:{name}})", name="Bob") 97 | # When leaving transaction context the transaction will be 98 | # committed and the connection will be closed 99 | 100 | # Rolling back or commit in contexts 101 | with manager.transaction as t: 102 | t.execute("CREATE (n:User {name:{name}})", name="Bob") 103 | if something == True: 104 | t.connection.commit() # This will commit the transaction 105 | else: 106 | t.connection.rollback() # This will rollback the transaction 107 | 108 | 109 | Building & Testing 110 | ------------------ 111 | 112 | Neo4jDB uses paver as its build system. To install paver:: 113 | 114 | pip install paver 115 | 116 | Then you can build Neo4jDB with:: 117 | 118 | paver build 119 | 120 | And install it with:: 121 | 122 | paver install 123 | 124 | 125 | Running tests requires a Neo4j server running on localhost. Paver can handle 126 | this for you:: 127 | 128 | paver start_server 129 | paver nosetests 130 | paver stop_server 131 | 132 | 133 | Incompliance with the spec 134 | -------------------------- 135 | 136 | **Parameters** 137 | 138 | The library delegates to Neo4j for parameter substitution, which means it does 139 | not use any of the standard parameter substitution types defined in the spec. 140 | 141 | Instead it uses curly brackets with named and/or positional parameters:: 142 | 143 | {0} or {identifier} 144 | 145 | 146 | **Type system** 147 | 148 | Because the wire format for Neo4j is JSON, the library does not support the 149 | date or binary value types. This may change in the future as the wire format 150 | for Neo4j evolves. 151 | 152 | In a similar vein, because Neo4j is a schema-optional database, it may return 153 | arbitrary types in each cell in the result table. As such, the description of the 154 | result table always marks each column type as neo4j.MIXED. 155 | 156 | 157 | License 158 | ------- 159 | 160 | http://opensource.org/licenses/MIT 161 | -------------------------------------------------------------------------------- /neo4j/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from neo4j.connection import Connection 3 | 4 | apilevel = '2.0' 5 | threadsafety = 1 6 | 7 | # This is non-standard, it uses neos built-in params. 8 | paramstyle = 'curly' 9 | 10 | 11 | def connect(dsn, username=None, password=None): 12 | con = Connection(dsn) 13 | if username and password: 14 | con.authorization(username, password) 15 | return con 16 | 17 | # 18 | # Types 19 | # Neo4j is a schema-optional database, which means that 20 | # result rows can contain arbitrary types, and the database 21 | # does not know in advance what types it will be dealing with. 22 | # Because of this, we always describe return types as neo4j.MIXED, 23 | # and we don't currently allow using the richer type set defined 24 | # by the spec, since the transport format is JSON. This will change 25 | # when the transport format changes. 26 | # 27 | 28 | class Node(dict): 29 | 30 | def __init__(self, node_id, labels, properties): 31 | self.id = node_id 32 | self.labels = labels 33 | for k,v in properties.items(): 34 | self[k] = v 35 | 36 | 37 | class Relationship(dict): 38 | 39 | def __init__(self, rel_id, rel_type, start_node_id, end_node_id, properties): 40 | self.id = rel_id 41 | self.type = rel_type 42 | self.start_id = start_node_id 43 | self.end_id = end_node_id 44 | 45 | for k,v in properties.items(): 46 | self[k] = v 47 | 48 | 49 | class TypeCode(object): 50 | 51 | def __init__(self, code): 52 | self._code = code 53 | 54 | def __eq__(self, other): 55 | return other is self 56 | 57 | def __unicode__(self): 58 | return self._code 59 | 60 | def __repr__(self): 61 | return self._code 62 | 63 | def __str__(self): 64 | return self._code 65 | 66 | 67 | class TypeObject(object): 68 | 69 | def __init__(self, *args, **kwargs): 70 | raise NotSupportedError("Complex types are not yet supported.") 71 | 72 | Date = TypeObject 73 | Time = TypeObject 74 | Timestamp = TypeObject 75 | DateFromTicks = TypeObject 76 | TimeFromTicks = TypeObject 77 | TimestampFromTicks = TypeObject 78 | Binary = TypeObject 79 | 80 | STRING = TypeCode("STRING") 81 | BINARY = TypeCode("BINARY") 82 | NUMBER = TypeCode("NUMBER") 83 | DATETIME = TypeCode("DATETIME") 84 | ROWID = TypeCode("ROWID") 85 | 86 | MIXED = TypeCode("MIXED") 87 | 88 | # 89 | # Exceptions 90 | # 91 | 92 | Error = Connection.Error 93 | Warning = Connection.Warning 94 | InterfaceError = Connection.InterfaceError 95 | DatabaseError = Connection.DatabaseError 96 | InternalError = Connection.InternalError 97 | OperationalError = Connection.OperationalError 98 | ProgrammingError = Connection.ProgrammingError 99 | IntegrityError = Connection.IntegrityError 100 | DataError = Connection.DataError 101 | NotSupportedError = Connection.NotSupportedError -------------------------------------------------------------------------------- /neo4j/connection.py: -------------------------------------------------------------------------------- 1 | 2 | import json 3 | import base64 4 | 5 | from neo4j.cursor import Cursor 6 | from neo4j.strings import ustr 7 | 8 | try: 9 | from http import client as http 10 | from urllib.parse import urlparse 11 | StandardError = Exception 12 | except ImportError: 13 | import httplib as http 14 | from urlparse import urlparse 15 | from exceptions import StandardError 16 | 17 | TX_ENDPOINT = "/db/data/transaction" 18 | 19 | 20 | def neo_code_to_error_class(code): 21 | if code.startswith('Neo.ClientError.Schema'): 22 | return Connection.IntegrityError 23 | elif code.startswith('Neo.ClientError'): 24 | return Connection.ProgrammingError 25 | return Connection.InternalError 26 | 27 | 28 | def default_error_handler(connection, cursor, errorclass, errorvalue): 29 | if errorclass != Connection.Warning: 30 | raise errorclass(errorvalue) 31 | 32 | 33 | class Connection(object): 34 | 35 | class Error(StandardError): 36 | rollback = True 37 | 38 | class Warning(StandardError): 39 | rollback = False 40 | 41 | class InterfaceError(Error): 42 | pass 43 | 44 | class DatabaseError(Error): 45 | pass 46 | 47 | class InternalError(DatabaseError): 48 | pass 49 | 50 | class OperationalError(DatabaseError): 51 | pass 52 | 53 | class ProgrammingError(DatabaseError): 54 | rollback = False 55 | 56 | class IntegrityError(DatabaseError): 57 | rollback = False 58 | 59 | class DataError(DatabaseError): 60 | pass 61 | 62 | class NotSupportedError(DatabaseError): 63 | pass 64 | 65 | _COMMON_HEADERS = {"Content-Type": "application/json", "Accept": "application/json", "Connection": "keep-alive"} 66 | 67 | def __init__(self, db_uri): 68 | self.errorhandler = default_error_handler 69 | self._host = urlparse(db_uri).netloc 70 | self._http = http.HTTPConnection(self._host) 71 | self._tx = TX_ENDPOINT 72 | self._messages = [] 73 | self._cursors = set() 74 | self._cursor_ids = 0 75 | 76 | def authorization(self, username, password): 77 | basic_auth = '%s:%s' % (username, password) 78 | try: # Python 2 79 | auth = base64.encodestring(basic_auth) 80 | except TypeError: # Python 3 81 | auth = base64.encodestring(bytes(basic_auth, 'utf-8')).decode() 82 | self._COMMON_HEADERS.update({"Authorization": "Basic %s" % auth.strip()}) 83 | 84 | def commit(self): 85 | self._messages = [] 86 | pending = self._gather_pending() 87 | 88 | if self._tx != TX_ENDPOINT or len(pending) > 0: 89 | payload = None 90 | if len(pending) > 0: 91 | payload = {'statements': [{'statement': s, 'parameters': p} for (s, p) in pending]} 92 | response = self._deserialize(self._http_req("POST", self._tx + "/commit", payload)) 93 | self._tx = TX_ENDPOINT 94 | self._handle_errors(response, self, None) 95 | 96 | def rollback(self): 97 | self._messages = [] 98 | self._gather_pending() # Just used to clear all pending requests 99 | if self._tx != TX_ENDPOINT: 100 | try: 101 | response = self._deserialize(self._http_req("DELETE", self._tx)) 102 | self._tx = TX_ENDPOINT 103 | self._handle_errors(response, self, None) 104 | except self.OperationalError: 105 | # Neo.ClientError.Transaction.UnknownId 106 | # Unrecognized transaction id. Transaction may have timed out and been rolled back. 107 | pass 108 | 109 | def cursor(self): 110 | self._messages = [] 111 | cursor = Cursor(self._next_cursor_id(), self, self._execute) 112 | self._cursors.add(cursor) 113 | return cursor 114 | 115 | def close(self): 116 | self._messages = [] 117 | if hasattr(self, '_http') and self._http is not None: 118 | self._http.close() 119 | self._http = None 120 | 121 | def __del__(self): 122 | self.close() 123 | 124 | @property 125 | def messages(self): 126 | return self._messages 127 | 128 | def _next_cursor_id(self): 129 | self._cursor_ids += 1 130 | return self._cursor_ids 131 | 132 | def _gather_pending(self): 133 | pending = [] 134 | for cursor in self._cursors: 135 | if len(cursor._pending) > 0: 136 | pending.extend(cursor._pending) 137 | cursor._pending = [] 138 | return pending 139 | 140 | def _execute(self, cursor, statements): 141 | """" 142 | Executes a list of statements, returning an iterator of results sets. Each 143 | statement should be a tuple of (statement, params). 144 | """ 145 | payload = [{'statement': s, 'parameters': p, 'resultDataContents':['rest']} for (s, p) in statements] 146 | http_response = self._http_req("POST", self._tx, {'statements': payload}) 147 | 148 | if self._tx == TX_ENDPOINT: 149 | self._tx = http_response.getheader('Location') 150 | 151 | response = self._deserialize(http_response) 152 | 153 | self._handle_errors(response, cursor, cursor) 154 | 155 | return response['results'][-1] 156 | 157 | def _http_req(self, method, path, payload=None, retries=2): 158 | serialized_payload = json.dumps(payload) if payload is not None else None 159 | 160 | try: 161 | self._http.request(method, path, serialized_payload, self._COMMON_HEADERS) 162 | http_response = self._http.getresponse() 163 | except (http.BadStatusLine, http.CannotSendRequest): 164 | self._http = http.HTTPConnection(self._host) 165 | if retries > 0: 166 | return self._http_req(method, path, payload, retries-1) 167 | self._handle_error(self, None, Connection.OperationalError, "Connection has expired.") 168 | 169 | if not http_response.status in [200, 201]: 170 | message = "Server returned unexpected response: " + ustr(http_response.status) + ustr(http_response.read()) 171 | self._handle_error(self, None, Connection.OperationalError, message) 172 | 173 | return http_response 174 | 175 | def _handle_errors(self, response, owner, cursor): 176 | for error in response['errors']: 177 | error_class = neo_code_to_error_class(error['code']) 178 | error_value = ustr(error['code']) + ": " + ustr(error['message']) 179 | self._handle_error(owner, cursor, error_class, error_value) 180 | 181 | def _handle_error(self, owner, cursor, error_class, error_value): 182 | if error_class.rollback: 183 | self._tx = TX_ENDPOINT 184 | self._gather_pending() # Just used to clear all pending requests 185 | owner._messages.append((error_class, error_value)) 186 | owner.errorhandler(self, cursor, error_class, error_value) 187 | 188 | def _deserialize(self, response): 189 | # TODO: This is exceptionally annoying, python 3 has improved byte array handling, but that means the JSON 190 | # parser no longer supports deserializing these things in a streaming manner, so we have to decode the whole 191 | # thing first. 192 | return json.loads(response.read().decode('utf-8')) 193 | -------------------------------------------------------------------------------- /neo4j/contextmanager.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | from neo4j import connect 3 | 4 | 5 | class Neo4jDBConnectionManager: 6 | 7 | """ 8 | Every new connection is a transaction. To minimize new connection overhead for many reads we try to reuse a single 9 | connection. If this seem like a bad idea some kind of connection pool might work better. 10 | 11 | Neo4jDBConnectionManager.read() 12 | When using with Neo4jDBConnectionManager.read(): we will always rollback the transaction. All exceptions will be 13 | thrown. 14 | 15 | Neo4jDBConnectionManager.write() 16 | When using with Neo4jDBConnectionManager.write() we will always commit the transaction except when we see an 17 | exception. If we get an exception we will rollback the transaction and throw the exception. 18 | 19 | Neo4jDBConnectionManager.transaction() 20 | When we don't want to share a connection (transaction context) we can set up a new connection which will work 21 | just as the write context manager above but with it's own connection. 22 | 23 | >>> manager = Neo4jDBConnectionManager("http://localhost:7474") 24 | >>> with manager.write() as w: 25 | ... w.execute("CREATE (TheMatrix:Movie {title:'The Matrix', tagline:'Welcome to the Real World'})") 26 | ... 27 | 28 | >>> 29 | >>> with manager.read() as r: 30 | ... for n in r.execute("MATCH (n:Movie) RETURN n LIMIT 1"): 31 | ... print n 32 | "({'tagline': 'Welcome to the Real World', 'title': 'The Matrix'},)" 33 | 34 | Commits in batches can be achieved by: 35 | >>> with manager.write() as w: 36 | ... w.execute("CREATE (TheMatrix:Movie {title:'The Matrix Reloaded', tagline:'Free your mind.'})") 37 | ... w.connection.commit() # The Matric Reloaded will be committed 38 | ... w.execute("CREATE (TheMatrix:Movie {title:'Matrix Revolutions', tagline:'Everything that has a beginning has an end.'})") 39 | """ 40 | 41 | def __init__(self, dsn, username=None, password=None): 42 | self.dsn = dsn 43 | self.connection = connect(dsn, username, password) 44 | 45 | @contextmanager 46 | def _read(self): 47 | cursor = self.connection.cursor() 48 | try: 49 | yield cursor 50 | finally: 51 | cursor.close() 52 | self.connection.rollback() 53 | read = property(_read) 54 | 55 | @contextmanager 56 | def _write(self): 57 | cursor = self.connection.cursor() 58 | try: 59 | yield cursor 60 | except self.connection.Error as e: 61 | cursor.close() 62 | self.connection.rollback() 63 | raise e 64 | else: 65 | cursor.close() 66 | self.connection.commit() 67 | finally: 68 | pass 69 | write = property(_write) 70 | 71 | @contextmanager 72 | def _transaction(self): 73 | connection = connect(self.dsn) 74 | cursor = connection.cursor() 75 | try: 76 | yield cursor 77 | except self.connection.Error as e: 78 | connection.rollback() 79 | raise e 80 | else: 81 | connection.commit() 82 | finally: 83 | cursor.close() 84 | connection.close() 85 | transaction = property(_transaction) 86 | -------------------------------------------------------------------------------- /neo4j/cursor.py: -------------------------------------------------------------------------------- 1 | 2 | import neo4j 3 | from neo4j.strings import ustr 4 | 5 | 6 | class Cursor(object): 7 | 8 | def __init__( self, cursorid, connection, execute_statements ): 9 | self.connection = connection 10 | self.lastrowid = None 11 | self.arraysize = 1 12 | self.errorhandler = connection.errorhandler 13 | 14 | self._id = cursorid 15 | 16 | self._pending = [] 17 | self._execute = execute_statements 18 | self._rows = None 19 | self._rowcount = -1 20 | self._cursor = 0 21 | self._messages = [] 22 | 23 | def execute(self, statement, *args, **kwargs): 24 | for i in range(len(args)): 25 | kwargs[i] = args[i] 26 | 27 | self._messages = [] 28 | self._rows = None 29 | self._rowcount = 0 30 | 31 | self._pending.append((statement, kwargs)) 32 | return self 33 | 34 | def fetchone(self): 35 | self._execute_pending() 36 | row = self._rows[self._cursor] 37 | self._cursor += 1 38 | return self._map_row(row['rest']) 39 | 40 | def fetchmany(self, size=None): 41 | self._execute_pending() 42 | if size is None: 43 | size = self.arraysize 44 | result = [self._map_row(r['rest']) for r in self._rows[self._cursor:self._cursor + size]] 45 | self._cursor += size 46 | return result 47 | 48 | def fetchall(self): 49 | self._execute_pending() 50 | result = [self._map_row(r['rest']) for r in self._rows[self._cursor:]] 51 | self._cursor += self.rowcount 52 | return result 53 | 54 | def __iter__(self): 55 | self._execute_pending() 56 | return self 57 | 58 | def __next__(self): 59 | try: 60 | return self.fetchone() 61 | except IndexError: 62 | raise StopIteration() 63 | 64 | def next(self): 65 | return self.__next__() 66 | 67 | def scroll(self, value, mode='relative'): 68 | self._execute_pending() 69 | if value < 0: 70 | raise self.connection.NotSupportedError() 71 | if mode == 'relative': 72 | self._cursor += value 73 | elif mode == 'absolute': 74 | self._cursor = value 75 | 76 | if self._cursor >= self.rowcount: 77 | self._cursor = self.rowcount 78 | raise IndexError() 79 | 80 | @property 81 | def description(self): 82 | self._execute_pending() 83 | return self._description 84 | 85 | @property 86 | def rowcount(self): 87 | self._execute_pending() 88 | return self._rowcount 89 | 90 | @property 91 | def messages(self): 92 | self._execute_pending() 93 | return self._messages 94 | 95 | def nextset(self): 96 | pass 97 | 98 | def setinputsizes(self, sizes): 99 | pass 100 | 101 | def setoutputsizes(self, size, column=None): 102 | pass 103 | 104 | def close(self): 105 | self._rows = None 106 | self._rowcount = -1 107 | self._messages = [] 108 | self._description = None 109 | self.connection._cursors.discard(self) 110 | 111 | def __del__(self): 112 | self.close() 113 | 114 | def __hash__(self): 115 | return self._id 116 | 117 | def __eq__(self, other): 118 | return self._id == other._id 119 | 120 | def _map_row(self, row): 121 | return tuple(self._map_value(row)) 122 | 123 | def _map_value(self, value): 124 | ''' Maps a raw deserialized row to proper types ''' 125 | # TODO: Once we've gotten here, we've done the following: 126 | # -> Recieve the full response, copy it from network buffer it into a ByteBuffer (copy 1) 127 | # -> Copy all the data into a String (copy 2) 128 | # -> Deserialize that string (copy 3) 129 | # -> Map the deserialized JSON to our response format (copy 4, what we are doing in this method) 130 | # This should not bee needed. Technically, this mapping from transport format to python types should require exactly one copy, 131 | # from the network buffer into the python VM. 132 | if isinstance(value, list): 133 | out = [] 134 | for c in value: 135 | out.append(self._map_value( c )) 136 | return out 137 | elif isinstance(value, dict) and 'metadata' in value and 'labels' in value['metadata'] and 'self' in value: 138 | return neo4j.Node(ustr(value['metadata']['id']), value['metadata']['labels'], value['data']) 139 | elif isinstance(value, dict) and 'metadata' in value and 'type' in value and 'self' in value: 140 | return neo4j.Relationship(ustr(value['metadata']['id']), value['type'], value['start'].split('/')[-1], value['end'].split('/')[-1], value['data']) 141 | elif isinstance(value, dict): 142 | out = {} 143 | for k,v in value.items(): 144 | out[k] = self._map_value( v ) 145 | return out 146 | elif isinstance(value, str): 147 | return ustr(value) 148 | else: 149 | return value 150 | 151 | 152 | def _execute_pending(self): 153 | if len(self._pending) > 0: 154 | pending = self._pending 155 | 156 | # Clear these, in case the request fails. 157 | self._pending = [] 158 | self._rows = [] 159 | self._rowcount = 0 160 | self._description = [] 161 | 162 | result = self._execute(self, pending) 163 | 164 | self._rows = result['data'] 165 | self._rowcount = len(self._rows) 166 | self._description = [(name, neo4j.MIXED, None, None, None, None, True) for name in result['columns']] 167 | self._cursor = 0 168 | -------------------------------------------------------------------------------- /neo4j/strings.py: -------------------------------------------------------------------------------- 1 | 2 | # From Nigel Smalls util library 3 | try: 4 | unicode 5 | except NameError: 6 | # Python 3 7 | def ustr(s, encoding="utf-8"): 8 | if isinstance(s, str): 9 | return s 10 | try: 11 | return s.decode(encoding) 12 | except AttributeError: 13 | return str(s) 14 | 15 | unicode_type = str 16 | else: 17 | # Python 2 18 | def ustr(s, encoding="utf-8"): 19 | if isinstance(s, str): 20 | return s.decode(encoding) 21 | else: 22 | return unicode(s) 23 | 24 | unicode_type = unicode -------------------------------------------------------------------------------- /neo4j/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakewins/neo4jdb-python/cd78cb8397885f219500fb1080d301f0c900f7be/neo4j/test/__init__.py -------------------------------------------------------------------------------- /neo4j/test/performance.py: -------------------------------------------------------------------------------- 1 | 2 | import neo4j 3 | import time 4 | 5 | 6 | def main(): 7 | conn = neo4j.connect("http://localhost:7474") 8 | conn.authorization('neo4j', 'testing') 9 | start = time.time() 10 | iterations = 10000 11 | for it in xrange(iterations): 12 | cursor = conn.cursor() 13 | for i in xrange(50): 14 | cursor.execute("CREATE (n:User)") 15 | conn.commit() 16 | delta = time.time() - start 17 | print('Tx/s: ' + str(iterations / delta)) 18 | 19 | if __name__ == "__main__": 20 | main() 21 | -------------------------------------------------------------------------------- /neo4j/test/test_connection.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import neo4j 4 | 5 | 6 | class TestConnection(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.conn = neo4j.connect("http://localhost:7474") 10 | self.conn.authorization('neo4j', 'testing') 11 | 12 | def test_commit(self): 13 | # Given 14 | cursor = self.conn.cursor() 15 | cursor.execute("CREATE (n:TestCommit {name:1337})") 16 | 17 | # When 18 | self.conn.commit() 19 | 20 | # Then other cursors should see it 21 | cursor = self.conn.cursor() 22 | cursor.execute("MATCH (n:TestCommit) RETURN n.name") 23 | self.assertEqual(cursor.fetchone(), (1337,)) 24 | 25 | # And other connections should see it 26 | cursor = neo4j.connect("http://localhost:7474").cursor() 27 | cursor.execute("MATCH (n:TestCommit) RETURN n.name") 28 | self.assertEqual(cursor.fetchone(), (1337,)) 29 | 30 | def test_read_and_commit(self): 31 | # Given 32 | cursor = self.conn.cursor() 33 | cursor.execute("CREATE (n:TestCommit {name:1337})") 34 | cursor.rowcount # Force client to execute 35 | 36 | # When 37 | self.conn.commit() 38 | 39 | # Then other cursors should see it 40 | cursor = self.conn.cursor() 41 | cursor.execute("MATCH (n:TestCommit) RETURN n.name") 42 | self.assertEqual(cursor.fetchone(), (1337,)) 43 | 44 | # And other connections should see it 45 | cursor = neo4j.connect("http://localhost:7474").cursor() 46 | cursor.execute("MATCH (n:TestCommit) RETURN n.name") 47 | self.assertEqual(cursor.fetchone(), (1337,)) 48 | 49 | def test_rollback(self): 50 | # Given 51 | cursor = self.conn.cursor() 52 | cursor.execute("CREATE (n:TestRollback {name:1337})") 53 | 54 | # When 55 | self.conn.rollback() 56 | 57 | # Then the same cursor should not see it 58 | cursor.execute("MATCH (n:TestRollback) RETURN n.name") 59 | self.assertEqual(cursor.rowcount, 0) 60 | 61 | # And other connections should see it 62 | cursor = neo4j.connect("http://localhost:7474").cursor() 63 | cursor.execute("MATCH (n:TestRollback) RETURN n.name") 64 | self.assertEqual(cursor.rowcount, 0) 65 | 66 | 67 | if __name__ == '__main__': 68 | unittest.main() -------------------------------------------------------------------------------- /neo4j/test/test_contextmanager.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from neo4j.contextmanager import Neo4jDBConnectionManager 4 | 5 | 6 | class TestConnectionManager(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.manager = Neo4jDBConnectionManager("http://localhost:7474", "neo4j", "testing") 10 | 11 | def test_commit(self): 12 | # Given 13 | with self.manager.write as cursor: 14 | cursor.execute("CREATE (n:TestCommit {name:1337})") 15 | 16 | # Other cursors should see it 17 | with self.manager.read as cursor: 18 | cursor.execute("MATCH (n:TestCommit) RETURN n.name") 19 | self.assertEqual(cursor.fetchone(), (1337,)) 20 | 21 | # And other connections should see it 22 | with self.manager.transaction as cursor: 23 | cursor.execute("MATCH (n:TestCommit) RETURN n.name") 24 | self.assertEqual(cursor.fetchone(), (1337,)) 25 | 26 | def test_read_and_commit(self): 27 | # Given 28 | with self.manager.write as cursor: 29 | cursor.execute("CREATE (n:TestCommit {name:1337})") 30 | cursor.rowcount # Force client to execute 31 | 32 | # Then other cursors should see it 33 | with self.manager.read as cursor: 34 | cursor.execute("MATCH (n:TestCommit) RETURN n.name") 35 | self.assertEqual(cursor.fetchone(), (1337,)) 36 | 37 | # And other connections should see it 38 | with self.manager.transaction as cursor: 39 | cursor.execute("MATCH (n:TestCommit) RETURN n.name") 40 | self.assertEqual(cursor.fetchone(), (1337,)) 41 | 42 | def test_rollback(self): 43 | # Given 44 | with self.manager.write as cursor: 45 | cursor.execute("CREATE (n:TestRollback {name:1337})") 46 | # When 47 | cursor.connection.rollback() 48 | # Then the same cursor should not see it 49 | cursor.execute("MATCH (n:TestRollback) RETURN n.name") 50 | self.assertEqual(cursor.rowcount, 0) 51 | 52 | # And other connections should see it 53 | with self.manager.transaction as cursor: 54 | cursor.execute("MATCH (n:TestRollback) RETURN n.name") 55 | self.assertEqual(cursor.rowcount, 0) 56 | 57 | 58 | if __name__ == '__main__': 59 | unittest.main() -------------------------------------------------------------------------------- /neo4j/test/test_cursor.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import neo4j 4 | 5 | 6 | class TestCursor(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.conn = neo4j.connect("http://localhost:7474") 10 | self.conn.authorization('neo4j', 'testing') 11 | 12 | def test_description(self): 13 | # Given 14 | cursor = self.conn.cursor() 15 | 16 | # When 17 | cursor.execute("CREATE (n) RETURN 1 AS hello, 'str' AS str, 3") 18 | 19 | # Then 20 | self.assertEqual(cursor.description, [ 21 | ('hello', neo4j.MIXED, None, None, None, None, True), 22 | ('str', neo4j.MIXED, None, None, None, None, True), 23 | ('3', neo4j.MIXED, None, None, None, None, True) 24 | ]) 25 | 26 | def test_positional_parameters(self): 27 | # Given 28 | cursor = self.conn.cursor() 29 | 30 | # When 31 | cursor.execute("CREATE (n:Params {name:{0}})", "Bob") 32 | 33 | # Then 34 | self.assertEqual(list(cursor.execute("MATCH (n:Params) RETURN n")), [({'name': 'Bob'},)]) 35 | 36 | def test_named_parameters(self): 37 | # Given 38 | cursor = self.conn.cursor() 39 | 40 | # When 41 | cursor.execute("CREATE (n:Params {name:{name}})", name="Bob") 42 | 43 | # Then 44 | self.assertEqual(list(cursor.execute("MATCH (n:Params) RETURN n")), [({'name': 'Bob'},)]) 45 | 46 | def test_fetch_one(self): 47 | # Given 48 | cursor = self.conn.cursor() 49 | 50 | # When 51 | cursor.execute("CREATE (n) RETURN 1,2,3") 52 | 53 | # Then 54 | self.assertEqual(cursor.rowcount, 1) 55 | self.assertEqual(cursor.fetchone(), (1, 2, 3)) 56 | 57 | def test_fetch_many(self): 58 | # Given 59 | cursor = self.conn.cursor() 60 | 61 | # When 62 | cursor.execute("""FOREACH (n IN [1,2,3,4,5,6,7]| CREATE (:Test { id:n })) 63 | WITH 1 AS p 64 | MATCH (k:Test) 65 | RETURN k.id AS id ORDER BY id""") 66 | 67 | # Then 68 | self.assertEqual(cursor.rowcount, 7) 69 | self.assertEqual(cursor.fetchmany(2), [(1,), (2,)]) 70 | self.assertEqual(cursor.fetchmany(3), [(3,), (4,), (5,)]) 71 | self.assertEqual(cursor.fetchmany(5), [(6,), (7,)]) 72 | 73 | def test_fetch_all(self): 74 | # Given 75 | cursor = self.conn.cursor() 76 | 77 | # When 78 | cursor.execute("""FOREACH (n IN [1,2,3,4,5,6,7]| CREATE (:Test { id:n })) 79 | WITH 1 AS p 80 | MATCH (k:Test) 81 | RETURN k.id AS id ORDER BY id""") 82 | 83 | # Then 84 | self.assertEqual(cursor.fetchone(), (1,)) 85 | self.assertEqual(cursor.fetchall(), [(2,), (3,), (4,), (5,), (6,), (7,)]) 86 | self.assertEqual(cursor.fetchall(), []) 87 | 88 | def test_iter(self): 89 | # Given 90 | cursor = self.conn.cursor() 91 | 92 | # When 93 | cursor.execute("""FOREACH (n IN [1,2,3,4,5,6,7]| CREATE (:Test { id:n })) 94 | WITH 1 AS p 95 | MATCH (k:Test) 96 | RETURN k.id AS id ORDER BY id""") 97 | 98 | # Then 99 | cells = [] 100 | for cell, in cursor: 101 | cells.append(cell) 102 | self.assertEqual(cells, [1, 2, 3, 4, 5, 6, 7]) 103 | self.assertEqual(cursor.fetchall(), []) 104 | 105 | def test_scroll(self): 106 | # Given 107 | cursor = self.conn.cursor() 108 | cursor.execute("""FOREACH (n IN [1,2,3,4,5,6,7]| CREATE (:Test { id:n })) 109 | WITH 1 AS p 110 | MATCH (k:Test) 111 | RETURN k.id AS id ORDER BY id""") 112 | # When 113 | cursor.scroll(3) 114 | 115 | # Then 116 | self.assertEqual(cursor.fetchone(), (4,)) 117 | 118 | # And When 119 | cursor.scroll(5, 'absolute') 120 | 121 | # Then 122 | self.assertEqual(cursor.fetchone(), (6,)) 123 | 124 | def test_scroll_overflow(self): 125 | # Given 126 | cursor = self.conn.cursor() 127 | cursor.execute("""CREATE (n) RETURN 1""") 128 | 129 | # When 130 | try: 131 | cursor.scroll(10) 132 | raise Exception("Should not have reached here.") 133 | except IndexError: 134 | # Then 135 | pass 136 | 137 | 138 | if __name__ == '__main__': 139 | unittest.main() -------------------------------------------------------------------------------- /neo4j/test/test_exceptions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import neo4j 4 | 5 | 6 | class TestExceptions(unittest.TestCase): 7 | 8 | def setUp(self): 9 | self.conn = neo4j.connect("http://localhost:7474") 10 | self.conn.authorization('neo4j', 'testing') 11 | 12 | def test_syntax_error(self): 13 | # Given 14 | cursor = self.conn.cursor() 15 | 16 | # When 17 | try: 18 | cursor.execute("this is not valid syntax") 19 | cursor.rowcount # Force client to talk to server 20 | raise Exception("Should not have reached here.") 21 | except neo4j.ProgrammingError as e: 22 | # Then 23 | self.assertEqual(str(e), "Neo.ClientError.Statement.InvalidSyntax: Invalid input \'t\': expected (line 1, column 1 (offset: 0))\n\"this is not valid syntax\"\n ^") 24 | self.assertEqual(cursor.messages, [(neo4j.ProgrammingError, "Neo.ClientError.Statement.InvalidSyntax: Invalid input \'t\': expected (line 1, column 1 (offset: 0))\n\"this is not valid syntax\"\n ^")]) 25 | 26 | def test_cursor_clears_errors(self): 27 | # Given 28 | cursor = self.conn.cursor() 29 | try: 30 | cursor.execute("this is not valid syntax") 31 | cursor.rowcount # Force client to talk to server 32 | except neo4j.ProgrammingError as e: 33 | pass 34 | self.conn.rollback() 35 | 36 | # When 37 | cursor.execute("CREATE (n)") 38 | 39 | # Then 40 | msg = cursor.messages 41 | self.assertEqual(msg, []) 42 | 43 | if __name__ == '__main__': 44 | unittest.main() -------------------------------------------------------------------------------- /neo4j/test/test_types.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import neo4j 4 | from neo4j.strings import unicode_type 5 | 6 | 7 | class TestTypes(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.conn = neo4j.connect("http://localhost:7474") 11 | self.conn.authorization('neo4j', 'testing') 12 | 13 | def test_nodes(self): 14 | # Given 15 | cursor = self.conn.cursor() 16 | cursor.execute("CREATE (n:Params {name:{0}})", "Bob") 17 | 18 | # When 19 | res = list(cursor.execute("MATCH (n:Params) RETURN n")) 20 | 21 | # Then 22 | self.assertEqual(len(res), 1) 23 | node = res[0][0] 24 | self.assertEqual(node.labels, ['Params']) 25 | self.assertEqual(node['name'], "Bob") 26 | 27 | def test_rels(self): 28 | # Given 29 | cursor = self.conn.cursor() 30 | cursor.execute("CREATE (n:Types)-[r:KNOWS]->() SET r.name={0}", "Bob") 31 | 32 | # When 33 | res = list(cursor.execute("MATCH (:Types)-[r]-() RETURN r")) 34 | 35 | # Then 36 | self.assertEqual(len(res), 1) 37 | rel = res[0][0] 38 | self.assertEqual(rel.type, 'KNOWS') 39 | self.assertEqual(rel['name'], "Bob") 40 | 41 | def test_nested_structures(self): 42 | # Given 43 | cursor = self.conn.cursor() 44 | cursor.execute("CREATE (n:Types {name:{0}})-[r:KNOWS]->() SET r.name={0}", "Bob") 45 | 46 | # When 47 | res = list(cursor.execute("MATCH (n:Types)-[r]-() RETURN [{rel:r, node:n}]")) 48 | 49 | # Then 50 | self.assertEqual(len(res), 1) 51 | 52 | complicated = res[0][0] 53 | rel = complicated[0]['rel'] 54 | node = complicated[0]['node'] 55 | 56 | self.assertEqual(rel.type, 'KNOWS') 57 | self.assertEqual(rel['name'], "Bob") 58 | 59 | self.assertTrue( isinstance(rel.start_id, unicode_type) ) 60 | self.assertTrue( isinstance(rel.end_id, unicode_type) ) 61 | self.assertTrue( isinstance(rel.id, unicode_type) ) 62 | 63 | self.assertEqual(node.labels, ['Types']) 64 | self.assertEqual(node['name'], "Bob") 65 | self.assertTrue( isinstance(node.id, unicode_type) ) 66 | 67 | 68 | 69 | if __name__ == '__main__': 70 | unittest.main() -------------------------------------------------------------------------------- /pavement.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tarfile 3 | import base64 4 | import json 5 | from time import sleep 6 | from subprocess import call 7 | from paver.easy import * 8 | from paver.setuputils import setup, find_packages 9 | 10 | try: 11 | from http import client as http 12 | from urllib.request import urlretrieve 13 | except ImportError: 14 | from urllib import urlretrieve 15 | import httplib as http 16 | 17 | setup( 18 | name='neo4jdb', 19 | version='0.0.8', 20 | author='Jacob Hansson', 21 | author_email='jakewins@gmail.com', 22 | packages=find_packages(), 23 | py_modules=['setup'], 24 | include_package_data=True, 25 | install_requires=[], 26 | url='https://github.com/jakewins/neo4jdb-python', 27 | description='DB API 2.0 driver for the Neo4j graph database.', 28 | long_description=open('README.rst').read(), 29 | classifiers=[ 30 | 'Programming Language :: Python :: 2.6', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3.2', 33 | 'Programming Language :: Python :: 3.3', 34 | ], 35 | ) 36 | 37 | BUILD_DIR = 'build' 38 | NEO4J_VERSION = '2.3.1' 39 | DEFAULT_USERNAME = 'neo4j' 40 | DEFAULT_PASSWORD = 'neo4j' 41 | 42 | 43 | @task 44 | @needs('generate_setup', 'minilib', 'setuptools.command.sdist') 45 | def sdist(): 46 | """Overrides sdist to make sure that our setup.py is generated.""" 47 | pass 48 | 49 | 50 | @task 51 | def start_server(): 52 | if not os.path.exists(BUILD_DIR): 53 | os.makedirs(BUILD_DIR) 54 | 55 | if not path(BUILD_DIR + '/neo4j.tar.gz').access(os.R_OK): 56 | print("Downloading Neo4j Server") 57 | urlretrieve("http://dist.neo4j.org/neo4j-community-%s-unix.tar.gz" % NEO4J_VERSION, BUILD_DIR + "/neo4j.tar.gz") 58 | 59 | if not path(BUILD_DIR + '/neo4j').access(os.R_OK): 60 | print("Unzipping Neo4j Server..") 61 | call(['tar', '-xf', BUILD_DIR + "/neo4j.tar.gz", '-C', BUILD_DIR]) 62 | os.rename(BUILD_DIR + "/neo4j-community-%s" % NEO4J_VERSION, BUILD_DIR + "/neo4j") 63 | 64 | call([BUILD_DIR + "/neo4j/bin/neo4j", "start"]) 65 | change_password() 66 | 67 | 68 | @task 69 | def stop_server(): 70 | if path(BUILD_DIR + '/neo4j').access(os.R_OK): 71 | call([BUILD_DIR + "/neo4j/bin/neo4j", "stop"]) 72 | 73 | 74 | @task 75 | def change_password(): 76 | """ 77 | Changes the standard password from neo4j to testing to be able to run the test suite. 78 | """ 79 | basic_auth = '%s:%s' % (DEFAULT_USERNAME, DEFAULT_PASSWORD) 80 | try: # Python 2 81 | auth = base64.encodestring(basic_auth) 82 | except TypeError: # Python 3 83 | auth = base64.encodestring(bytes(basic_auth, 'utf-8')).decode() 84 | 85 | headers = { 86 | "Content-Type": "application/json", 87 | "Accept": "application/json", 88 | "Authorization": "Basic %s" % auth.strip() 89 | } 90 | 91 | response = None 92 | retry = 0 93 | while not response: # Retry if the server is not ready yet 94 | sleep(1) 95 | con = http.HTTPConnection('localhost:7474', timeout=10) 96 | try: 97 | con.request('GET', 'http://localhost:7474/user/neo4j', headers=headers) 98 | response = json.loads(con.getresponse().read().decode('utf-8')) 99 | except ValueError: 100 | con.close() 101 | retry += 1 102 | if retry > 10: 103 | print("Could not change password for user neo4j") 104 | break 105 | if response and response.get('password_change_required', None): 106 | payload = json.dumps({'password': 'testing'}) 107 | con.request('POST', 'http://localhost:7474/user/neo4j/password', payload, headers) 108 | print("Password changed for user neo4j") 109 | con.close() 110 | 111 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | import paver.tasks 3 | except ImportError: 4 | from os.path import exists 5 | if exists("paver-minilib.zip"): 6 | import sys 7 | sys.path.insert(0, "paver-minilib.zip") 8 | import paver.tasks 9 | 10 | paver.tasks.main() 11 | --------------------------------------------------------------------------------