Пример #1
0
 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
Пример #2
0
 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
Пример #3
0
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()
Пример #4
0
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()
Пример #5
0
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")
Пример #6
0
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
Пример #7
0
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")