diff --git a/docs/source/api.rst b/docs/source/api.rst index 98797f4a6..57059f442 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -633,6 +633,11 @@ A :class:`neo4j.Result` is attached to an active connection, through a :class:`n **This is experimental.** + .. automethod:: value + + .. automethod:: values + + .. automethod:: data See https://neo4j.com/docs/driver-manual/current/cypher-workflow/#driver-type-mapping for more about type mapping. @@ -698,6 +703,8 @@ Record Derive a sub-record based on a start and end index. All keys and values within those bounds will be copied across in the same order as in the original record. + .. automethod:: keys + .. describe:: record[key] Obtain a value from the record by key. @@ -705,15 +712,13 @@ Record .. automethod:: get(key, default=None) - .. automethod:: value(key=0, default=None) - .. automethod:: index(key) - .. automethod:: keys + .. automethod:: items - .. automethod:: values + .. automethod:: value(key=0, default=None) - .. automethod:: items + .. automethod:: values .. automethod:: data diff --git a/neo4j/data.py b/neo4j/data.py index 04ac87c49..bc9f173c7 100644 --- a/neo4j/data.py +++ b/neo4j/data.py @@ -110,9 +110,9 @@ def get(self, key, default=None): """ Obtain a value from the record by key, returning a default value if the key does not exist. - :param key: - :param default: - :return: + :param key: a key + :param default: default value + :return: a value """ try: index = self.__keys.index(str(key)) @@ -126,8 +126,9 @@ def get(self, key, default=None): def index(self, key): """ Return the index of the given item. - :param key: - :return: + :param key: a key + :return: index + :rtype: int """ if isinstance(key, int): if 0 <= key < len(self.__keys): @@ -146,9 +147,9 @@ def value(self, key=0, default=None): index or key is specified, the first value is returned. If the specified item does not exist, the default value is returned. - :param key: - :param default: - :return: + :param key: an index or key + :param default: default value + :return: a single value """ try: index = self.index(key) @@ -171,6 +172,7 @@ def values(self, *keys): :param keys: indexes or keys of the items to include; if none are provided, all values will be included :return: list of values + :rtype: list """ if keys: d = [] @@ -187,7 +189,8 @@ def values(self, *keys): def items(self, *keys): """ Return the fields of the record as a list of key and value tuples - :return: + :return: a list of value tuples + :rtype: list """ if keys: d = [] diff --git a/neo4j/work/result.py b/neo4j/work/result.py index 4921249b7..af6a62d24 100644 --- a/neo4j/work/result.py +++ b/neo4j/work/result.py @@ -227,6 +227,7 @@ def keys(self): """The keys for the records in this result. :returns: tuple of key names + :rtype: tuple """ return self._keys @@ -249,7 +250,7 @@ def single(self): A warning is generated if more than one record is available but the first of these is still returned. - :returns: the next :class:`.Record` or :const:`None` if none remain + :returns: the next :class:`neo4j.Record` or :const:`None` if none remain :warns: if more than one record is available """ records = list(self) # TODO: exhausts the result with self.consume if there are more records. @@ -277,42 +278,47 @@ def peek(self): return None - # See Record class for available methods. - - # NOT IN THE API - def graph(self): - """Return a Graph instance containing all the graph objects + """Return a :class:`neo4j.graph.Graph` instance containing all the graph objects in the result. After calling this method, the result becomes detached, buffering all remaining records. - :returns: result graph + :returns: a result graph + :rtype: :class:`neo4j.graph.Graph` """ self._buffer_all() return self._hydrant.graph - # def value(self, item=0, default=None): - # """Return the remainder of the result as a list of values. - # - # :param item: field to return for each remaining record - # :param default: default value, used if the index of key is unavailable - # :returns: list of individual values - # """ - # return [record.value(item, default) for record in self._records()] - - # def values(self, *items): - # """Return the remainder of the result as a list of tuples. - # - # :param items: fields to return for each remaining record - # :returns: list of value tuples - # """ - # return [record.values(*items) for record in self._records()] - - # def data(self, *items): - # """Return the remainder of the result as a list of dictionaries. - # - # :param items: fields to return for each remaining record - # :returns: list of dictionaries - # """ - # return [record.data(*items) for record in self] + def value(self, key=0, default=None): + """Helper function that return the remainder of the result as a list of values. + + See :class:`neo4j.Record.value` + + :param key: field to return for each remaining record. Obtain a single value from the record by index or key. + :param default: default value, used if the index of key is unavailable + :returns: list of individual values + :rtype: list + """ + return [record.value(key, default) for record in self] + + def values(self, *keys): + """Helper function that return the remainder of the result as a list of values lists. + See :class:`neo4j.Record.values` + + :param keys: fields to return for each remaining record. Optionally filtering to include only certain values by index or key. + :returns: list of values lists + :rtype: list + """ + return [record.values(*keys) for record in self] + + def data(self, *keys): + """Helper function that return the remainder of the result as a list of dictionaries. + + See :class:`neo4j.Record.data` + + :param keys: fields to return for each remaining record. Optionally filtering to include only certain values by index or key. + :returns: list of dictionaries + :rtype: list + """ + return [record.data(*keys) for record in self] diff --git a/tests/integration/examples/test_driver_introduction_example.py b/tests/integration/examples/test_driver_introduction_example.py new file mode 100644 index 000000000..942ab19c0 --- /dev/null +++ b/tests/integration/examples/test_driver_introduction_example.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +# Copyright (c) 2002-2020 "Neo4j," +# Neo4j Sweden AB [http://neo4j.com] +# +# This file is part of Neo4j. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import pytest + +from contextlib import redirect_stdout +from io import StringIO + +# tag::driver-introduction-example-import[] +from neo4j import GraphDatabase +import logging +from neo4j.exceptions import ServiceUnavailable +# end::driver-introduction-example-import[] + +from neo4j._exceptions import BoltHandshakeError + + +# python -m pytest tests/integration/examples/test_aura_example.py -s -v + +# tag::driver-introduction-example[] +class App: + + def __init__(self, uri, user, password): + # Aura queries use an encrypted connection + self.driver = GraphDatabase.driver(uri, auth=(user, password), encrypted=True) + + def close(self): + # Don't forget to close the driver connection when you are finished with it + self.driver.close() + + def create_friendship(self, person1_name, person2_name): + with self.driver.session() as session: + # Write transactions allow the driver to handle retries and transient errors + result = session.write_transaction( + self._create_and_return_friendship, person1_name, person2_name) + for row in result: + print("Created friendship between: {p1}, {p2}".format(p1=row['p1'], p2=row['p2'])) + + @staticmethod + def _create_and_return_friendship(tx, person1_name, person2_name): + # To learn more about the Cypher syntax, see https://neo4j.com/docs/cypher-manual/current/ + # The Reference Card is also a good resource for keywords https://neo4j.com/docs/cypher-refcard/current/ + query = """ + CREATE (p1:Person { name: $person1_name }) + CREATE (p2:Person { name: $person2_name }) + CREATE (p1)-[:KNOWS]->(p2) + RETURN p1, p2 + """ + result = tx.run(query, person1_name=person1_name, person2_name=person2_name) + try: + return [{"p1": row["p1"]["name"], "p2": row["p2"]["name"]} + for row in result] + # Capture any errors along with the query and data for traceability + except ServiceUnavailable as exception: + logging.error("{query} raised an error: \n {exception}".format( + query=query, exception=exception)) + raise + + def find_person(self, person_name): + with self.driver.session() as session: + result = session.read_transaction(self._find_and_return_person, person_name) + for row in result: + print("Found person: {row}".format(row=row)) + + @staticmethod + def _find_and_return_person(tx, person_name): + query = """ + MATCH (p:Person) + WHERE p.name = $person_name + RETURN p.name AS name + """ + result = tx.run(query, person_name=person_name) + return [row["name"] for row in result] + +if __name__ == "__main__": + # Aura uses the "bolt+routing" protocol + bolt_url = "%%BOLT_URL_PLACEHOLDER%%" + user = "" + password = "" + app = App(bolt_url, user, password) + app.create_friendship("Alice", "David") + app.find_person("Alice") + app.close() +# end::driver-introduction-example[] + + +def test_driver_introduction_example(uri, auth): + try: + s = StringIO() + with redirect_stdout(s): + app = App(uri, auth[0], auth[1]) + app.create_friendship("Alice", "David") + app.find_person("Alice") + app.close() + + assert s.getvalue().startswith("Found person: Alice") + except ServiceUnavailable as error: + if isinstance(error.__cause__, BoltHandshakeError): + pytest.skip(error.args[0]) diff --git a/tests/integration/test_result.py b/tests/integration/test_result.py index f539c4de4..250ba8c89 100644 --- a/tests/integration/test_result.py +++ b/tests/integration/test_result.py @@ -291,3 +291,21 @@ def test_single_indexed_values(session): def test_single_keyed_values(session): result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z") assert result.single().values("z", "x") == [3, 1] + + +def test_result_with_helper_function_value(session): + + def f(tx): + result = tx.run("UNWIND range(1, 3) AS n RETURN n") + assert result.value(0) == [1, 2, 3] + + session.read_transaction(f) + + +def test_result_with_helper_function_values(session): + + def f(tx): + result = tx.run("UNWIND range(1, 3) AS n RETURN n, 0") + assert result.values(0, 1) == [[1, 0], [2, 0], [3, 0]] + + session.read_transaction(f) diff --git a/tests/integration/test_result_data.py b/tests/integration/test_result_data.py index 29923ef2e..c166c8627 100644 --- a/tests/integration/test_result_data.py +++ b/tests/integration/test_result_data.py @@ -25,6 +25,11 @@ def test_data_with_one_key_and_no_records(session): assert data == [] +def test_data_with_one_key_and_no_records_with_helper_function(session): + result = session.run("UNWIND range(1, 0) AS n RETURN n") + assert result.data() == [] + + def test_multiple_data(session): result = session.run("UNWIND range(1, 3) AS n " "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") @@ -32,6 +37,12 @@ def test_multiple_data(session): assert data == [{"x": 1, "y": 2, "z": 3}, {"x": 2, "y": 4, "z": 6}, {"x": 3, "y": 6, "z": 9}] +def test_multiple_data_with_helper_function(session): + result = session.run("UNWIND range(1, 3) AS n " + "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") + assert result.data() == [{"x": 1, "y": 2, "z": 3}, {"x": 2, "y": 4, "z": 6}, {"x": 3, "y": 6, "z": 9}] + + def test_multiple_indexed_data(session): result = session.run("UNWIND range(1, 3) AS n " "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") @@ -39,6 +50,12 @@ def test_multiple_indexed_data(session): assert data == [{"x": 1, "z": 3}, {"x": 2, "z": 6}, {"x": 3, "z": 9}] +def test_multiple_indexed_data_with_helper_function(session): + result = session.run("UNWIND range(1, 3) AS n " + "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z") + assert result.data(2, 0) == [{"x": 1, "z": 3}, {"x": 2, "z": 6}, {"x": 3, "z": 9}] + + def test_multiple_keyed_data(session): result = session.run("UNWIND range(1, 3) AS n " "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")