def __init__(self, graph, path): self.graph = graph self.remote = Resource(remote(self.graph.dbms).uri.resolve(path)) try: self.remote.get() except GraphError: raise NotImplementedError("No extension found at path %r on " "graph <%s>" % (path, remote(self.graph).uri))
def test_can_handle_other_error_from_delete(self): with patch.object(_Resource, "delete") as mocked: mocked.side_effect = DodgyServerError resource = Resource("http://localhost:7474/db/data/node/spam") try: resource.delete() except GraphError as error: assert isinstance(error.__cause__, DodgyServerError) else: assert False
class UnmanagedExtension(object): """ Base class for unmanaged extensions. """ def __init__(self, graph, path): self.graph = graph self.remote = Resource(remote(self.graph.dbms).uri.resolve(path)) try: self.remote.get() except GraphError: raise NotImplementedError("No extension found at path %r on " "graph <%s>" % (path, remote(self.graph).uri))
def test_can_handle_404(self): node_id = self.get_non_existent_node_id() resource = Resource("http://localhost:7474/db/data/node/%s" % node_id) try: resource.get() except GraphError as error: self.assert_error( error, (GraphError, ), "org.neo4j.server.rest.web.NodeNotFoundException", (_ClientError, _Response), 404) else: assert False
def test_can_handle_400(self): resource = Resource("http://localhost:7474/db/data/cypher") try: resource.post() except GraphError as error: try: self.assert_error( error, (GraphError, ), "org.neo4j.server.rest.repr.BadInputException", (_ClientError, _Response), 400) except AssertionError: self.assert_error( error, (GraphError, ), "org.neo4j.server.rest.repr.InvalidArgumentsException", (_ClientError, _Response), 400) else: assert False
def __init__(self, content_type, uri, name=None): self._content_type = content_type key_value_pos = uri.find("/{key}/{value}") if key_value_pos >= 0: self._searcher = ResourceTemplate(uri) self.__remote__ = Resource(uri[:key_value_pos]) else: self.__remote__ = Resource(uri) self._searcher = ResourceTemplate(uri.string + "/{key}/{value}") uri = remote(self).uri self._create_or_fail = Resource( uri.resolve("?uniqueness=create_or_fail")) self._get_or_create = Resource( uri.resolve("?uniqueness=get_or_create")) self._query_template = ResourceTemplate(uri.string + "{?query,order}") self._name = name or uri.path.segments[-1] self.__searcher_stem_cache = {} self.graph = Graph(uri.resolve("/db/data/").string)
class BatchRunner(object): """ Resource for batch execution. """ def __init__(self, uri): self.resource = Resource(uri) def post(self, batch): num_jobs = len(batch) plural = "" if num_jobs == 1 else "s" log.info("> Sending batch request with %s job%s", num_jobs, plural) data = [] for i, job in enumerate(batch): if job.finished: raise BatchFinished(job) else: job.finished = True log.info("> {%s} %s", i, job) data.append(dict(job, id=i)) response = self.resource.post(data) log.info("< Received batch response for %s job%s", num_jobs, plural) return response def run(self, batch): """ Execute a collection of jobs and return all results. :param batch: A :class:`.Batch` of jobs. :rtype: :class:`list` """ response = self.post(batch) try: results = [] for result_data in response.content: result = JobResult.hydrate(result_data, batch) log.info("< %s", result) results.append(result) return results except ValueError: # Here, we're looking to gracefully handle a Neo4j server bug # whereby a response is received with no content and # 'Content-Type: application/json'. Given that correct JSON # technically needs to contain {} at minimum, the JSON # parser fails with a ValueError. if response.content_length == 0: from sys import exc_info from traceback import extract_tb type_, value, traceback = exc_info() for filename, line_number, function_name, text in extract_tb( traceback): if "json" in filename and "decode" in function_name: return [] raise finally: response.close()
class BatchRunner(object): """ Resource for batch execution. """ def __init__(self, uri): self.resource = Resource(uri) def post(self, batch): num_jobs = len(batch) plural = "" if num_jobs == 1 else "s" log.info("> Sending batch request with %s job%s", num_jobs, plural) data = [] for i, job in enumerate(batch): if job.finished: raise BatchFinished(job) else: job.finished = True log.info("> {%s} %s", i, job) data.append(dict(job, id=i)) response = self.resource.post(data) log.info("< Received batch response for %s job%s", num_jobs, plural) return response def run(self, batch): """ Execute a collection of jobs and return all results. :param batch: A :class:`.Batch` of jobs. :rtype: :class:`list` """ response = self.post(batch) try: results = [] for result_data in response.content: result = JobResult.hydrate(result_data, batch) log.info("< %s", result) results.append(result) return results except ValueError: # Here, we're looking to gracefully handle a Neo4j server bug # whereby a response is received with no content and # 'Content-Type: application/json'. Given that correct JSON # technically needs to contain {} at minimum, the JSON # parser fails with a ValueError. if response.content_length == 0: from sys import exc_info from traceback import extract_tb type_, value, traceback = exc_info() for filename, line_number, function_name, text in extract_tb(traceback): if "json" in filename and "decode" in function_name: return [] raise finally: response.close()
def __init__(self, graph, name): self.graph = graph self.name = name remote_graph = remote(self.graph) extensions = remote_graph.metadata["extensions"] try: self.resources = { key: Resource(value) for key, value in extensions[self.name].items() } except KeyError: raise LookupError("No plugin named %r found on graph <%s>" % (self.name, remote_graph.uri))
def _index_manager(self, content_type): """ Fetch the index management resource for the given `content_type`. :param content_type: :return: """ if content_type is Node: uri = remote(self.graph).metadata["node_index"] elif content_type is Relationship: uri = remote(self.graph).metadata["relationship_index"] else: raise TypeError("Indexes can manage either Nodes or Relationships") return Resource(uri)
def __init__(self, content_type, uri, name=None): self._content_type = content_type key_value_pos = uri.find("/{key}/{value}") if key_value_pos >= 0: self._searcher = ResourceTemplate(uri) self.__remote__ = Resource(uri[:key_value_pos]) else: self.__remote__ = Resource(uri) self._searcher = ResourceTemplate(uri.string + "/{key}/{value}") uri = remote(self).uri self._create_or_fail = Resource(uri.resolve("?uniqueness=create_or_fail")) self._get_or_create = Resource(uri.resolve("?uniqueness=get_or_create")) self._query_template = ResourceTemplate(uri.string + "{?query,order}") self._name = name or uri.path.segments[-1] self.__searcher_stem_cache = {} self.graph = Graph(uri.resolve("/db/data/").string)
def test_can_raise_unauthorized_on_delete(self): with self.assertRaises(Unauthorized): _ = Resource( "http://*****:*****@127.0.0.1:7474/db/data/").delete().content
class ServerErrorTestCase(GraphTestCase): def setUp(self): self.non_existent_resource = Resource( "http://localhost:7474/db/data/x") def test_can_handle_json_error_from_get(self): try: self.non_existent_resource.get() except GraphError as error: cause = error.__cause__ assert isinstance(cause, _ClientError) assert isinstance(cause, _Response) assert cause.status_code == 404 else: assert False def test_can_handle_json_error_from_put(self): try: self.non_existent_resource.put("") except GraphError as error: cause = error.__cause__ assert isinstance(cause, _ClientError) assert isinstance(cause, _Response) assert cause.status_code == 404 else: assert False def test_can_handle_json_error_from_post(self): try: self.non_existent_resource.post("") except GraphError as error: cause = error.__cause__ assert isinstance(cause, _ClientError) assert isinstance(cause, _Response) assert cause.status_code == 404 else: assert False def test_can_handle_json_error_from_delete(self): try: self.non_existent_resource.delete() except GraphError as error: cause = error.__cause__ assert isinstance(cause, _ClientError) assert isinstance(cause, _Response) assert cause.status_code == 404 else: assert False def test_can_handle_other_error_from_get(self): with patch.object(_Resource, "get") as mocked: mocked.side_effect = DodgyServerError resource = Resource("http://localhost:7474/db/data/node/spam") try: resource.get() except GraphError as error: assert isinstance(error.__cause__, DodgyServerError) else: assert False def test_can_handle_other_error_from_put(self): with patch.object(_Resource, "put") as mocked: mocked.side_effect = DodgyServerError resource = Resource("http://localhost:7474/db/data/node/spam") try: resource.put() except GraphError as error: assert isinstance(error.__cause__, DodgyServerError) else: assert False def test_can_handle_other_error_from_post(self): with patch.object(_Resource, "post") as mocked: mocked.side_effect = DodgyServerError resource = Resource("http://localhost:7474/db/data/node/spam") try: resource.post() except GraphError as error: assert isinstance(error.__cause__, DodgyServerError) else: assert False def test_can_handle_other_error_from_delete(self): with patch.object(_Resource, "delete") as mocked: mocked.side_effect = DodgyServerError resource = Resource("http://localhost:7474/db/data/node/spam") try: resource.delete() except GraphError as error: assert isinstance(error.__cause__, DodgyServerError) else: assert False
class ManualIndex(object): """ Searchable database index which can contain either nodes or relationships. """ def __init__(self, content_type, uri, name=None): self._content_type = content_type key_value_pos = uri.find("/{key}/{value}") if key_value_pos >= 0: self._searcher = ResourceTemplate(uri) self.__remote__ = Resource(uri[:key_value_pos]) else: self.__remote__ = Resource(uri) self._searcher = ResourceTemplate(uri.string + "/{key}/{value}") uri = remote(self).uri self._create_or_fail = Resource( uri.resolve("?uniqueness=create_or_fail")) self._get_or_create = Resource( uri.resolve("?uniqueness=get_or_create")) self._query_template = ResourceTemplate(uri.string + "{?query,order}") self._name = name or uri.path.segments[-1] self.__searcher_stem_cache = {} self.graph = Graph(uri.resolve("/db/data/").string) def __repr__(self): return "{0}({1}, {2})".format(self.__class__.__name__, self._content_type.__name__, repr(remote(self).uri.string)) def _searcher_stem_for_key(self, key): if key not in self.__searcher_stem_cache: stem = self._searcher.uri_template.string.partition("{key}")[0] self.__searcher_stem_cache[key] = stem + percent_encode(key) + "/" return self.__searcher_stem_cache[key] def add(self, key, value, entity): """ Add an entity to this index under the `key`:`value` pair supplied. Note that while Neo4j indexes allow multiple entities to be added under a particular key:value, the same entity may only be represented once; this method is therefore idempotent. :param key: :param value: :param entity: """ remote(self).post({ "key": key, "value": value, "uri": remote(entity).uri.string, }) return entity def add_if_none(self, key, value, entity): """ Add an entity to this index under the `key`:`value` pair supplied if no entry already exists at that point. If added, this method returns the entity, otherwise :py:const:`None` is returned. """ rs = self._get_or_create.post({ "key": key, "value": value, "uri": remote(entity).uri.string, }) if rs.status_code == CREATED: return entity else: return None @property def content_type(self): """ Return the type of entity contained within this index. Will return either :py:class:`Node` or :py:class:`Relationship`. """ return self._content_type @property def name(self): """ Return the name of this index. """ return self._name def get(self, key, value): """ Fetch a list of all entities from the index which are associated with the `key`:`value` pair supplied. :param key: :param value: """ return [ self.graph._hydrate(result) for result in self._searcher.expand( key=key, value=value).get().content ] def create(self, key, value, abstract): """ Create and index a new node or relationship using the abstract provided. :param key: :param value: :param abstract: """ batch = ManualIndexWriteBatch(self.graph) if self._content_type is Node: batch.create(abstract) batch.add_to_index(Node, self, key, value, 0) elif self._content_type is Relationship: batch.create(abstract) batch.add_to_index(Relationship, self, key, value, 0) else: raise TypeError(self._content_type) entity, index_entry = batch.run() return entity def _create_unique(self, key, value, abstract): """ Internal method to support `get_or_create` and `create_if_none`. """ if self._content_type is Node: body = {"key": key, "value": value, "properties": abstract} elif self._content_type is Relationship: body = { "key": key, "value": value, "start": abstract[0].uri.string, "type": abstract[1], "end": abstract[2].uri.string, "properties": abstract[3] if len(abstract) > 3 else None } else: raise TypeError(self._content_type) return self._get_or_create.post(body) def get_or_create(self, key, value, abstract): """ Fetch a single entity from the index which is associated with the `key`:`value` pair supplied, creating a new entity with the supplied details if none exists. """ return self.graph._hydrate( self._create_unique(key, value, abstract).content) def create_if_none(self, key, value, abstract): """ Create a new entity with the specified details within the current index, under the `key`:`value` pair supplied, if no such entity already exists. If creation occurs, the new entity will be returned, otherwise :py:const:`None` will be returned. """ rs = self._create_unique(key, value, abstract) if rs.status_code == CREATED: return self.graph._hydrate(rs.content) else: return None def remove(self, key=None, value=None, entity=None): """ Remove any entries from the index which match the parameters supplied. The allowed parameter combinations are: `key`, `value`, `entity` remove a specific entity indexed under a given key-value pair `key`, `value` remove all entities indexed under a given key-value pair `key`, `entity` remove a specific entity indexed against a given key but with any value `entity` remove all occurrences of a specific entity regardless of key and value """ if key and value and entity: t = ResourceTemplate( remote(self).uri.string + "/{key}/{value}/{entity}") t.expand(key=key, value=value, entity=remote(entity)._id).delete() elif key and value: uris = [ URI(remote(entity).metadata["indexed"]) for entity in self.get(key, value) ] batch = ManualIndexWriteBatch(self.graph) for uri in uris: batch.append_delete(uri) batch.run() elif key and entity: t = ResourceTemplate(remote(self).uri.string + "/{key}/{entity}") t.expand(key=key, entity=remote(entity)._id).delete() elif entity: t = ResourceTemplate(remote(self).uri.string + "/{entity}") t.expand(entity=remote(entity)._id).delete() else: raise TypeError("Illegal parameter combination for index removal") def query(self, query): """ Query the index according to the supplied query criteria, returning a list of matched entities. The query syntax used should be appropriate for the configuration of the index being queried. For indexes with default configuration, this should be Apache Lucene query syntax. """ resource = self._query_template.expand(query=query) for result in resource.get().content: yield self.graph._hydrate(result) def _query_with_score(self, query, order): resource = self._query_template.expand(query=query, order=order) for result in resource.get().content: yield self.graph._hydrate(result), result["score"] def query_by_index(self, query): return self._query_with_score(query, "index") def query_by_relevance(self, query): return self._query_with_score(query, "relevance") def query_by_score(self, query): return self._query_with_score(query, "score")
def setUp(self): self.non_existent_resource = Resource( "http://localhost:7474/db/data/x")
def __init__(self, uri): self.resource = Resource(uri)
class ManualIndex(object): """ Searchable database index which can contain either nodes or relationships. """ def __init__(self, content_type, uri, name=None): self._content_type = content_type key_value_pos = uri.find("/{key}/{value}") if key_value_pos >= 0: self._searcher = ResourceTemplate(uri) self.__remote__ = Resource(uri[:key_value_pos]) else: self.__remote__ = Resource(uri) self._searcher = ResourceTemplate(uri.string + "/{key}/{value}") uri = remote(self).uri self._create_or_fail = Resource(uri.resolve("?uniqueness=create_or_fail")) self._get_or_create = Resource(uri.resolve("?uniqueness=get_or_create")) self._query_template = ResourceTemplate(uri.string + "{?query,order}") self._name = name or uri.path.segments[-1] self.__searcher_stem_cache = {} self.graph = Graph(uri.resolve("/db/data/").string) def __repr__(self): return "{0}({1}, {2})".format( self.__class__.__name__, self._content_type.__name__, repr(remote(self).uri.string) ) def _searcher_stem_for_key(self, key): if key not in self.__searcher_stem_cache: stem = self._searcher.uri_template.string.partition("{key}")[0] self.__searcher_stem_cache[key] = stem + percent_encode(key) + "/" return self.__searcher_stem_cache[key] def add(self, key, value, entity): """ Add an entity to this index under the `key`:`value` pair supplied. Note that while Neo4j indexes allow multiple entities to be added under a particular key:value, the same entity may only be represented once; this method is therefore idempotent. :param key: :param value: :param entity: """ remote(self).post({ "key": key, "value": value, "uri": remote(entity).uri.string, }) return entity def add_if_none(self, key, value, entity): """ Add an entity to this index under the `key`:`value` pair supplied if no entry already exists at that point. If added, this method returns the entity, otherwise :py:const:`None` is returned. """ rs = self._get_or_create.post({ "key": key, "value": value, "uri": remote(entity).uri.string, }) if rs.status_code == CREATED: return entity else: return None @property def content_type(self): """ Return the type of entity contained within this index. Will return either :py:class:`Node` or :py:class:`Relationship`. """ return self._content_type @property def name(self): """ Return the name of this index. """ return self._name def get(self, key, value): """ Fetch a list of all entities from the index which are associated with the `key`:`value` pair supplied. :param key: :param value: """ return [ self.graph._hydrate(result) for result in self._searcher.expand(key=key, value=value).get().content ] def create(self, key, value, abstract): """ Create and index a new node or relationship using the abstract provided. :param key: :param value: :param abstract: """ batch = ManualIndexWriteBatch(self.graph) if self._content_type is Node: batch.create(abstract) batch.add_to_index(Node, self, key, value, 0) elif self._content_type is Relationship: batch.create(abstract) batch.add_to_index(Relationship, self, key, value, 0) else: raise TypeError(self._content_type) entity, index_entry = batch.run() return entity def _create_unique(self, key, value, abstract): """ Internal method to support `get_or_create` and `create_if_none`. """ if self._content_type is Node: body = { "key": key, "value": value, "properties": abstract } elif self._content_type is Relationship: body = { "key": key, "value": value, "start": abstract[0].uri.string, "type": abstract[1], "end": abstract[2].uri.string, "properties": abstract[3] if len(abstract) > 3 else None } else: raise TypeError(self._content_type) return self._get_or_create.post(body) def get_or_create(self, key, value, abstract): """ Fetch a single entity from the index which is associated with the `key`:`value` pair supplied, creating a new entity with the supplied details if none exists. """ return self.graph._hydrate(self._create_unique(key, value, abstract).content) def create_if_none(self, key, value, abstract): """ Create a new entity with the specified details within the current index, under the `key`:`value` pair supplied, if no such entity already exists. If creation occurs, the new entity will be returned, otherwise :py:const:`None` will be returned. """ rs = self._create_unique(key, value, abstract) if rs.status_code == CREATED: return self.graph._hydrate(rs.content) else: return None def remove(self, key=None, value=None, entity=None): """ Remove any entries from the index which match the parameters supplied. The allowed parameter combinations are: `key`, `value`, `entity` remove a specific entity indexed under a given key-value pair `key`, `value` remove all entities indexed under a given key-value pair `key`, `entity` remove a specific entity indexed against a given key but with any value `entity` remove all occurrences of a specific entity regardless of key and value """ if key and value and entity: t = ResourceTemplate(remote(self).uri.string + "/{key}/{value}/{entity}") t.expand(key=key, value=value, entity=remote(entity)._id).delete() elif key and value: uris = [ URI(remote(entity).metadata["indexed"]) for entity in self.get(key, value) ] batch = ManualIndexWriteBatch(self.graph) for uri in uris: batch.append_delete(uri) batch.run() elif key and entity: t = ResourceTemplate(remote(self).uri.string + "/{key}/{entity}") t.expand(key=key, entity=remote(entity)._id).delete() elif entity: t = ResourceTemplate(remote(self).uri.string + "/{entity}") t.expand(entity=remote(entity)._id).delete() else: raise TypeError("Illegal parameter combination for index removal") def query(self, query): """ Query the index according to the supplied query criteria, returning a list of matched entities. The query syntax used should be appropriate for the configuration of the index being queried. For indexes with default configuration, this should be Apache Lucene query syntax. """ resource = self._query_template.expand(query=query) for result in resource.get().content: yield self.graph._hydrate(result) def _query_with_score(self, query, order): resource = self._query_template.expand(query=query, order=order) for result in resource.get().content: yield self.graph._hydrate(result), result["score"] def query_by_index(self, query): return self._query_with_score(query, "index") def query_by_relevance(self, query): return self._query_with_score(query, "relevance") def query_by_score(self, query): return self._query_with_score(query, "score")