From 19cb12a14f8b2c49d799e714be67d1af1ef19046 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Fri, 18 Nov 2022 12:32:27 +0100 Subject: [PATCH 1/3] General API docs improvements Including * more details * better examples * code formatting * more consistent docstring format --- docs/source/api.rst | 224 ++++++++++++-------- docs/source/async_api.rst | 134 ++++++++---- docs/source/index.rst | 51 +++-- neo4j/_async/driver.py | 9 +- neo4j/_async/io/_bolt.py | 12 +- neo4j/_async/io/_bolt3.py | 2 +- neo4j/_async/io/_bolt4.py | 2 +- neo4j/_async/io/_bolt5.py | 2 +- neo4j/_async/io/_common.py | 1 + neo4j/_async/io/_pool.py | 14 +- neo4j/_async/work/result.py | 76 ++++--- neo4j/_async/work/session.py | 135 ++++-------- neo4j/_async/work/transaction.py | 2 +- neo4j/_async/work/workspace.py | 2 +- neo4j/_async_compat/network/_bolt_socket.py | 8 +- neo4j/_async_compat/network/_util.py | 4 +- neo4j/_codec/hydration/v1/spatial.py | 2 +- neo4j/_codec/hydration/v1/temporal.py | 18 +- neo4j/_codec/hydration/v2/temporal.py | 4 +- neo4j/_conf.py | 4 + neo4j/_data.py | 20 +- neo4j/_routing.py | 2 +- neo4j/_sync/driver.py | 9 +- neo4j/_sync/io/_bolt.py | 12 +- neo4j/_sync/io/_bolt3.py | 2 +- neo4j/_sync/io/_bolt4.py | 2 +- neo4j/_sync/io/_bolt5.py | 2 +- neo4j/_sync/io/_common.py | 1 + neo4j/_sync/io/_pool.py | 14 +- neo4j/_sync/work/result.py | 76 ++++--- neo4j/_sync/work/session.py | 135 ++++-------- neo4j/_sync/work/transaction.py | 2 +- neo4j/_sync/work/workspace.py | 2 +- neo4j/api.py | 20 +- neo4j/debug.py | 14 +- neo4j/exceptions.py | 6 +- neo4j/graph/__init__.py | 16 +- neo4j/spatial/__init__.py | 2 +- neo4j/time/__init__.py | 8 +- neo4j/time/_arithmetic.py | 8 +- neo4j/work/query.py | 7 +- 41 files changed, 567 insertions(+), 499 deletions(-) diff --git a/docs/source/api.rst b/docs/source/api.rst index 220991b98..c9583a346 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -24,6 +24,7 @@ The :class:`neo4j.Driver` construction is done via a ``classmethod`` on the :cla from neo4j import GraphDatabase + uri = "neo4j://example.com:7687" driver = GraphDatabase.driver(uri, auth=("neo4j", "password")) @@ -36,7 +37,7 @@ The :class:`neo4j.Driver` construction is done via a ``classmethod`` on the :cla auth = ("neo4j", "password") - This will implicitly create a :class:`neo4j.Auth` with a ``scheme="basic"``. + This will implicitly create a :class:`neo4j.Auth` with ``scheme="basic"``. Other authentication methods are described under :ref:`auth-ref`. @@ -46,10 +47,10 @@ The :class:`neo4j.Driver` construction is done via a ``classmethod`` on the :cla from neo4j import GraphDatabase + uri = "neo4j://example.com:7687" with GraphDatabase.driver(uri, auth=("neo4j", "password")) as driver: - # use the driver - + ... # use the driver .. _uri-ref: @@ -74,7 +75,7 @@ Available valid URIs: .. code-block:: python - uri = "neo4j://example.com:7687" + uri = "neo4j://example.com:7687?policy=europe" Each supported scheme maps to a particular :class:`neo4j.Driver` subclass that implements a specific behaviour. @@ -118,6 +119,7 @@ Example: import neo4j + auth = neo4j.Auth("basic", "neo4j", "password") @@ -274,6 +276,7 @@ For example: from neo4j import GraphDatabase + def custom_resolver(socket_address): if socket_address == ("example.com", 9999): yield "::1", 7687 @@ -282,6 +285,7 @@ For example: from socket import gaierror raise gaierror("Unexpected socket address %r" % socket_address) + driver = GraphDatabase.driver("neo4j://example.com:9999", auth=("neo4j", "password"), resolver=custom_resolver) @@ -382,6 +386,7 @@ For example: from neo4j import GraphDatabase + class Application: def __init__(self, uri, user, password) @@ -392,7 +397,7 @@ For example: Connection details held by the :class:`neo4j.Driver` are immutable. Therefore if, for example, a password is changed, a replacement :class:`neo4j.Driver` object must be created. -More than one :class:`.Driver` may be required if connections to multiple databases, or connections as multiple users, are required, +More than one :class:`.Driver` may be required if connections to multiple remotes, or connections as multiple users, are required, unless when using impersonation (:ref:`impersonated-user-ref`). :class:`neo4j.Driver` objects are thread-safe but cannot be shared across processes. @@ -455,12 +460,14 @@ To construct a :class:`neo4j.Session` use the :meth:`neo4j.Driver.session` metho from neo4j import GraphDatabase - driver = GraphDatabase(uri, auth=(user, password)) - session = driver.session() - result = session.run("MATCH (a:Person) RETURN a.name AS name") - names = [record["name"] for record in result] - session.close() - driver.close() + + with GraphDatabase(uri, auth=(user, password)) as driver: + session = driver.session() + try: + result = session.run("MATCH (a:Person) RETURN a.name AS name") + names = [record["name"] for record in result] + finally: + session.close() Sessions will often be created and destroyed using a *with block context*. @@ -471,7 +478,7 @@ properly even when an exception is raised. with driver.session() as session: result = session.run("MATCH (a:Person) RETURN a.name AS name") - # do something with the result... + ... # do something with the result Sessions will often be created with some configuration settings, see :ref:`session-configuration-ref`. @@ -480,7 +487,7 @@ Sessions will often be created with some configuration settings, see :ref:`sessi with driver.session(database="example_database", fetch_size=100) as session: result = session.run("MATCH (a:Person) RETURN a.name AS name") - # do something with the result... + ... # do something with the result ******* @@ -540,13 +547,13 @@ Optional :class:`neo4j.Bookmarks`. Use this to causally chain sessions. See :meth:`Session.last_bookmarks` or :meth:`AsyncSession.last_bookmarks` for more information. +:Default: ``None`` + .. deprecated:: 5.0 Alternatively, an iterable of strings can be passed. This usage is deprecated and will be removed in a future release. Please use a :class:`neo4j.Bookmarks` object instead. -:Default: ``None`` - .. _database-ref: @@ -554,15 +561,6 @@ more information. ------------ Name of the database to query. -:Type: ``str``, ``neo4j.DEFAULT_DATABASE`` - - -.. py:attribute:: neo4j.DEFAULT_DATABASE - :noindex: - - This will use the default database on the Neo4j instance. - - .. Note:: The default database can be set on the Neo4j instance settings. @@ -577,9 +575,17 @@ Name of the database to query. from neo4j import GraphDatabase + + # closing of driver and session is omitted for brevity driver = GraphDatabase.driver(uri, auth=(user, password)) session = driver.session(database="system") +.. py:attribute:: neo4j.DEFAULT_DATABASE = None + :noindex: + + This will use the default database on the Neo4j instance. + +:Type: ``str``, ``neo4j.DEFAULT_DATABASE`` :Default: ``neo4j.DEFAULT_DATABASE`` @@ -593,29 +599,27 @@ This means that all actions in the session will be executed in the security context of the impersonated user. For this, the user for which the :class:`Driver` has been created needs to have the appropriate permissions. -:Type: ``str``, None - - -.. py:data:: None - :noindex: - - Will not perform impersonation. - - .. Note:: The server or all servers of the cluster need to support impersonation. Otherwise, the driver will raise :exc:`.ConfigurationError` as soon as it encounters a server that does not. - .. code-block:: python from neo4j import GraphDatabase + + # closing of driver and session is omitted for brevity driver = GraphDatabase.driver(uri, auth=(user, password)) session = driver.session(impersonated_user="alice") +.. py:data:: None + :noindex: + + Will not perform impersonation. + +:Type: ``str``, None :Default: :const:`None` @@ -644,7 +648,13 @@ access mode passed to that session on construction. be executed even when ``neo4j.READ_ACCESS`` is chosen. This behaviour should not be relied upon as it can change with the server. +.. py:attribute:: neo4j.WRITE_ACCESS = "WRITE" + :noindex: +.. py:attribute:: neo4j.READ_ACCESS = "READ" + :noindex: + :Type: ``neo4j.WRITE_ACCESS``, ``neo4j.READ_ACCESS`` + :Default: ``neo4j.WRITE_ACCESS`` @@ -652,7 +662,7 @@ access mode passed to that session on construction. ``fetch_size`` -------------- -The fetch size used for requesting messages from Neo4j. +The fetch size used for requesting records from Neo4j. :Type: ``int`` :Default: ``1000`` @@ -713,28 +723,33 @@ Auto-commit transactions are also the only way to run ``PERIODIC COMMIT`` newer) statements, since those Cypher clauses manage their own transactions internally. -Example: +Write example: .. code-block:: python import neo4j + def create_person(driver, name): - with driver.session(default_access_mode=neo4j.WRITE_ACCESS) as session: - query = "CREATE (a:Person { name: $name }) RETURN id(a) AS node_id" + # default_access_mode defaults to WRITE_ACCESS + with driver.session(database="neo4j") as session: + query = ("CREATE (n:NodeExample {name: $name, id: randomUUID()}) " + "RETURN n.id AS node_id") result = session.run(query, name=name) record = result.single() return record["node_id"] -Example: +Read example: .. code-block:: python import neo4j + def get_numbers(driver): numbers = [] - with driver.session(default_access_mode=neo4j.READ_ACCESS) as session: + with driver.session(database="neo4j", + default_access_mode=neo4j.READ_ACCESS) as session: result = session.run("UNWIND [1, 2, 3] AS x RETURN x") for record in result: numbers.append(record["x"]) @@ -743,8 +758,8 @@ Example: .. _explicit-transactions-ref: -Explicit Transactions -===================== +Explicit Transactions (Unmanaged Transactions) +============================================== Explicit transactions support multiple statements and must be created with an explicit :meth:`neo4j.Session.begin_transaction` call. This creates a new :class:`neo4j.Transaction` object that can be used to run Cypher. @@ -766,7 +781,7 @@ It also gives applications the ability to directly control ``commit`` and ``roll Closing an explicit transaction can either happen automatically at the end of a ``with`` block, or can be explicitly controlled through the :meth:`neo4j.Transaction.commit`, :meth:`neo4j.Transaction.rollback` or :meth:`neo4j.Transaction.close` methods. -Explicit transactions are most useful for applications that need to distribute Cypher execution across multiple functions for the same transaction. +Explicit transactions are most useful for applications that need to distribute Cypher execution across multiple functions for the same transaction or that need to run multiple queries within a single transaction but without the retries provided by managed transactions. Example: @@ -774,25 +789,56 @@ Example: import neo4j - def create_person(driver, name): - with driver.session(default_access_mode=neo4j.WRITE_ACCESS) as session: - tx = session.begin_transaction() - node_id = create_person_node(tx) - set_person_name(tx, node_id, name) - tx.commit() - - def create_person_node(tx): - query = "CREATE (a:Person { name: $name }) RETURN id(a) AS node_id" - name = "default_name" - result = tx.run(query, name=name) - record = result.single() - return record["node_id"] - def set_person_name(tx, node_id, name): - query = "MATCH (a:Person) WHERE id(a) = $id SET a.name = $name" - result = tx.run(query, id=node_id, name=name) - summary = result.consume() - # use the summary for logging etc. + def transfer_to_other_bank(driver, customer_id, other_bank_id, amount): + with driver.session( + database="neo4j", + # optional, defaults to WRITE_ACCESS + default_access_mode=neo4j.WRITE_ACCESS + ) as session: + tx = session.begin_transaction() + # or just use a `with` context instead of try/finally + try: + if not customer_balance_check(tx, customer_id, amount): + # give up + return + other_bank_transfer_api(customer_id, other_bank_id, amount) + # Now the money has been transferred + # => we can't retry or rollback anymore + try: + decrease_customer_balance(tx, customer_id, amount) + tx.commit() + except Exception as e: + request_inspection(customer_id, other_bank_id, amount, e) + raise + finally: + tx.close() # rolls back if not yet committed + + + def customer_balance_check(tx, customer_id, amount): + query = ("MATCH (c:Customer {id: $id}) " + "RETURN c.balance >= $amount AS sufficient") + result = tx.run(query, id=customer_id, amount=amount) + record = result.single(strict=True) + return record["sufficient"] + + + def other_bank_transfer_api(customer_id, other_bank_id, amount): + ... # make some API call to other bank + + + def decrease_customer_balance(tx, customer_id, amount): + query = ("MATCH (c:Customer {id: $id}) " + "SET c.balance = c.balance - $amount") + result = tx.run(query, id=customer_id, amount=amount) + result.consume() + + + def request_inspection(customer_id, other_bank_id, amount, e): + # manual cleanup required; log this or similar + print("WARNING: transaction rolled back due to exception:", repr(e)) + print("customer_id:", customer_id, "other_bank_id:", other_bank_id, + "amount:", amount) .. _managed-transactions-ref: @@ -808,7 +854,7 @@ This function is called one or more times, within a configurable time limit, unt Results should be fully consumed within the function and only aggregate or status values should be returned. Returning a live result object would prevent the driver from correctly managing connections and would break retry guarantees. -This function will receive a :class:`neo4j.ManagedTransaction` object as its first parameter. +The passed function will receive a :class:`neo4j.ManagedTransaction` object as its first parameter. For more details see :meth:`neo4j.Session.execute_write` and :meth:`neo4j.Session.execute_read`. .. autoclass:: neo4j.ManagedTransaction() @@ -822,8 +868,10 @@ Example: with driver.session() as session: node_id = session.execute_write(create_person_tx, name) + def create_person_tx(tx, name): - query = "CREATE (a:Person { name: $name }) RETURN id(a) AS node_id" + query = ("CREATE (a:Person {name: $name, id: randomUUID()}) " + "RETURN a.id AS node_id") result = tx.run(query, name=name) record = result.single() return record["node_id"] @@ -833,7 +881,6 @@ To exert more control over how a transaction function is carried out, the :func: .. autofunction:: neo4j.unit_of_work - ****** Result ****** @@ -882,9 +929,6 @@ Graph .. autoclass:: neo4j.graph.Graph() - A local, self-contained graph object that acts as a container for :class:`.Node` and :class:`neo4j.Relationship` instances. - This is typically obtained via the :meth:`neo4j.Result.graph` method. - .. autoattribute:: nodes .. autoattribute:: relationships @@ -1074,13 +1118,13 @@ Node Checks whether a property key exists for a given node. - .. autoattribute:: graph + .. autoproperty:: graph - .. autoattribute:: id + .. autoproperty:: id - .. autoattribute:: element_id + .. autoproperty:: element_id - .. autoattribute:: labels + .. autoproperty:: labels .. automethod:: get @@ -1130,19 +1174,19 @@ Relationship Returns the type (class) of a relationship. Relationship objects belong to a custom subtype based on the type name in the underlying database. - .. autoattribute:: graph + .. autoproperty:: graph - .. autoattribute:: id + .. autoproperty:: id - .. autoattribute:: element_id + .. autoproperty:: element_id - .. autoattribute:: nodes + .. autoproperty:: nodes - .. autoattribute:: start_node + .. autoproperty:: start_node - .. autoattribute:: end_node + .. autoproperty:: end_node - .. autoattribute:: type + .. autoproperty:: type .. automethod:: get @@ -1179,15 +1223,15 @@ Path Iterates through all the relationships in a path. - .. autoattribute:: graph + .. autoproperty:: graph - .. autoattribute:: nodes + .. autoproperty:: nodes - .. autoattribute:: start_node + .. autoproperty:: start_node - .. autoattribute:: end_node + .. autoproperty:: end_node - .. autoattribute:: relationships + .. autoproperty:: relationships ****************** @@ -1420,6 +1464,14 @@ Warnings The Python Driver uses the built-in :class:`python:DeprecationWarning` class to warn about deprecations. +The Python Driver uses the built-in :class:`python:ResourceWarning` class to warn about not properly closed resources, e.g., Drivers and Sessions. + +.. note:: + Deprecation and resource warnings are not shown by default. One way of enable them is to run the Python interpreter in `development mode`_. + +.. _development mode: https://docs.python.org/3/library/devmode.html#devmode + + The Python Driver uses the :class:`neo4j.ExperimentalWarning` class to warn about experimental features. .. autoclass:: neo4j.ExperimentalWarning @@ -1474,10 +1526,8 @@ not able to connect to the database server or if undesired behavior is observed. There are different ways of enabling logging as listed below. -.. note:: - - For an improved logging experience with the async driver, please see - :ref:`async-logging-ref`. +.. seealso:: + :ref:`async-logging-ref` for an improved logging experience with the async driver. Simple Approach =============== diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index f07ca72cf..4573a89f0 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -33,6 +33,7 @@ The :class:`neo4j.AsyncDriver` construction is done via a ``classmethod`` on the from neo4j import AsyncGraphDatabase + async def main(): uri = "neo4j://example.com:7687" driver = AsyncGraphDatabase.driver(uri, auth=("neo4j", "password")) @@ -59,12 +60,12 @@ The :class:`neo4j.AsyncDriver` construction is done via a ``classmethod`` on the from neo4j import AsyncGraphDatabase + async def main(): uri = "neo4j://example.com:7687" auth = ("neo4j", "password") async with AsyncGraphDatabase.driver(uri, auth=auth) as driver: - # use the driver - ... + ... # use the driver asyncio.run(main()) @@ -164,6 +165,7 @@ For example: from neo4j import AsyncGraphDatabase + async def custom_resolver(socket_address): if socket_address == ("example.com", 9999): yield "::1", 7687 @@ -172,16 +174,18 @@ For example: from socket import gaierror raise gaierror("Unexpected socket address %r" % socket_address) + # alternatively def custom_resolver(socket_address): ... + driver = AsyncGraphDatabase.driver("neo4j://example.com:9999", auth=("neo4j", "password"), resolver=custom_resolver) -:Default: ``None`` +:Default: :const:`None` @@ -196,6 +200,7 @@ For example: from neo4j import AsyncGraphDatabase + class Application: def __init__(self, uri, user, password) @@ -206,7 +211,7 @@ For example: Connection details held by the :class:`neo4j.AsyncDriver` are immutable. Therefore if, for example, a password is changed, a replacement :class:`neo4j.AsyncDriver` object must be created. -More than one :class:`.AsyncDriver` may be required if connections to multiple databases, or connections as multiple users, are required, +More than one :class:`.AsyncDriver` may be required if connections to multiple remotes, or connections as multiple users, are required, unless when using impersonation (:ref:`impersonated-user-ref`). :class:`neo4j.AsyncDriver` objects are safe to be used in concurrent coroutines. @@ -270,13 +275,15 @@ To construct a :class:`neo4j.AsyncSession` use the :meth:`neo4j.AsyncDriver.sess from neo4j import AsyncGraphDatabase + async def main(): - driver = AsyncGraphDatabase(uri, auth=(user, password)) - session = driver.session() - result = await session.run("MATCH (a:Person) RETURN a.name AS name") - names = [record["name"] async for record in result] - await session.close() - await driver.close() + async with AsyncGraphDatabase(uri, auth=(user, password)) as driver: + session = driver.session() + try: + result = await session.run("MATCH (a:Person) RETURN a.name AS name") + names = [record["name"] async for record in result] + finally: + await session.close() asyncio.run(main()) @@ -289,7 +296,7 @@ properly even when an exception is raised. async with driver.session() as session: result = await session.run("MATCH (a:Person) RETURN a.name AS name") - # do something with the result... + ... # do something with the result Sessions will often be created with some configuration settings, see :ref:`async-session-configuration-ref`. @@ -299,7 +306,7 @@ Sessions will often be created with some configuration settings, see :ref:`async async with driver.session(database="example_database", fetch_size=100) as session: result = await session.run("MATCH (a:Person) RETURN a.name AS name") - # do something with the result... + ... # do something with the result ************ @@ -315,7 +322,9 @@ AsyncSession This introduces concurrency and can lead to undefined behavior as :class:`AsyncSession` is not concurrency-safe. - Consider this **wrong** example:: + Consider this **wrong** example + + .. code-block:: python async def dont_do_this(driver): async with driver.session() as session: @@ -330,7 +339,9 @@ AsyncSession In this particular example, the problem could be solved by shielding the whole coroutine ``dont_do_this`` instead of only the - ``session.run``. Like so:: + ``session.run``. Like so + + .. code-block:: python async def thats_better(driver): async def inner() @@ -426,30 +437,32 @@ Auto-commit transactions are also the only way to run ``PERIODIC COMMIT`` newer) statements, since those Cypher clauses manage their own transactions internally. -Example: +Write example: .. code-block:: python import neo4j + async def create_person(driver, name): - async with driver.session( - default_access_mode=neo4j.WRITE_ACCESS - ) as session: + # default_access_mode defaults to WRITE_ACCESS + async with driver.session(database="neo4j") as session: query = "CREATE (a:Person { name: $name }) RETURN id(a) AS node_id" result = await session.run(query, name=name) record = await result.single() return record["node_id"] -Example: +Read example: .. code-block:: python import neo4j + async def get_numbers(driver): numbers = [] async with driver.session( + database="neo4j", default_access_mode=neo4j.READ_ACCESS ) as session: result = await session.run("UNWIND [1, 2, 3] AS x RETURN x") @@ -460,8 +473,8 @@ Example: .. _async-explicit-transactions-ref: -Explicit Async Transactions -=========================== +Explicit Transactions (Unmanaged Transactions) +============================================== Explicit transactions support multiple statements and must be created with an explicit :meth:`neo4j.AsyncSession.begin_transaction` call. This creates a new :class:`neo4j.AsyncTransaction` object that can be used to run Cypher. @@ -485,41 +498,74 @@ It also gives applications the ability to directly control ``commit`` and ``roll Closing an explicit transaction can either happen automatically at the end of a ``async with`` block, or can be explicitly controlled through the :meth:`neo4j.AsyncTransaction.commit`, :meth:`neo4j.AsyncTransaction.rollback`, :meth:`neo4j.AsyncTransaction.close` or :meth:`neo4j.AsyncTransaction.cancel` methods. -Explicit transactions are most useful for applications that need to distribute Cypher execution across multiple functions for the same transaction. +Explicit transactions are most useful for applications that need to distribute Cypher execution across multiple functions for the same transaction or that need to run multiple queries within a single transaction but without the retries provided by managed transactions. Example: .. code-block:: python + import asyncio + import neo4j - async def create_person(driver, name): + + async def transfer_to_other_bank(driver, customer_id, other_bank_id, amount): async with driver.session( + database="neo4j", + # optional, defaults to WRITE_ACCESS default_access_mode=neo4j.WRITE_ACCESS ) as session: tx = await session.begin_transaction() - node_id = await create_person_node(tx) - await set_person_name(tx, node_id, name) - await tx.commit() - - async def create_person_node(tx): - query = "CREATE (a:Person { name: $name }) RETURN id(a) AS node_id" - name = "default_name" - result = await tx.run(query, name=name) - record = await result.single() - return record["node_id"] - - async def set_person_name(tx, node_id, name): - query = "MATCH (a:Person) WHERE id(a) = $id SET a.name = $name" - result = await tx.run(query, id=node_id, name=name) - summary = await result.consume() - # use the summary for logging etc. + # or just use a `with` context instead of try/excpet/finally + try: + if not await customer_balance_check(tx, customer_id, amount): + # give up + return + await other_bank_transfer_api(customer_id, other_bank_id, amount) + # Now the money has been transferred + # => we can't retry or rollback anymore + try: + await decrease_customer_balance(tx, customer_id, amount) + await tx.commit() + except Exception as e: + request_inspection(customer_id, other_bank_id, amount, e) + raise + except asyncio.CancelledError: + tx.cancel() + raise + finally: + await tx.close() # rolls back if not yet committed + + + async def customer_balance_check(tx, customer_id, amount): + query = ("MATCH (c:Customer {id: $id}) " + "RETURN c.balance >= $amount AS sufficient") + result = await tx.run(query, id=customer_id, amount=amount) + record = await result.single(strict=True) + return record["sufficient"] + + + async def other_bank_transfer_api(customer_id, other_bank_id, amount): + ... # make some API call to other bank + + + async def decrease_customer_balance(tx, customer_id, amount): + query = ("MATCH (c:Customer {id: $id}) " + "SET c.balance = c.balance - $amount") + await tx.run(query, id=customer_id, amount=amount) + + + def request_inspection(customer_id, other_bank_id, amount, e): + # manual cleanup required; log this or similar + print("WARNING: transaction rolled back due to exception:", repr(e)) + print("customer_id:", customer_id, "other_bank_id:", other_bank_id, + "amount:", amount) .. _async-managed-transactions-ref: -Managed Async Transactions (`transaction functions`) -==================================================== +Managed Transactions (`transaction functions`) +============================================== Transaction functions are the most powerful form of transaction, providing access mode override and retry capabilities. + :meth:`neo4j.AsyncSession.execute_write` @@ -530,7 +576,7 @@ This function is called one or more times, within a configurable time limit, unt Results should be fully consumed within the function and only aggregate or status values should be returned. Returning a live result object would prevent the driver from correctly managing connections and would break retry guarantees. -This function will receive a :class:`neo4j.AsyncManagedTransaction` object as its first parameter. +This function will receive a :class:`neo4j.AsyncManagedTransaction` object as its first parameter. For more details see :meth:`neo4j.AsyncSession.execute_write` and :meth:`neo4j.AsyncSession.execute_read`. .. autoclass:: neo4j.AsyncManagedTransaction() @@ -544,8 +590,10 @@ Example: async with driver.session() as session: node_id = await session.execute_write(create_person_tx, name) + async def create_person_tx(tx, name): - query = "CREATE (a:Person { name: $name }) RETURN id(a) AS node_id" + query = ("CREATE (a:Person {name: $name, id: randomUUID()}) " + "RETURN a.id AS node_id") result = await tx.run(query, name=name) record = await result.single() return record["node_id"] diff --git a/docs/source/index.rst b/docs/source/index.rst index 1fbc82dc2..b11caecd6 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -99,23 +99,26 @@ Creating nodes and relationships. from neo4j import GraphDatabase - uri = "neo4j://localhost:7687" - driver = GraphDatabase.driver(uri, auth=("neo4j", "password")) + + URI = "neo4j://localhost:7687" + AUTH = ("neo4j", "password") + def create_person(tx, name): tx.run("CREATE (a:Person {name: $name})", name=name) + def create_friend_of(tx, name, friend): tx.run("MATCH (a:Person) WHERE a.name = $name " "CREATE (a)-[:KNOWS]->(:Person {name: $friend})", name=name, friend=friend) - with driver.session() as session: - session.execute_write(create_person, "Alice") - session.execute_write(create_friend_of, "Alice", "Bob") - session.execute_write(create_friend_of, "Alice", "Carl") - driver.close() + with GraphDatabase.driver(URI, auth=AUTH) as driver: + with driver.session() as session: + session.execute_write(create_person, "Alice") + session.execute_write(create_friend_of, "Alice", "Bob") + session.execute_write(create_friend_of, "Alice", "Carl") Finding nodes. @@ -124,8 +127,10 @@ Finding nodes. from neo4j import GraphDatabase - uri = "neo4j://localhost:7687" - driver = GraphDatabase.driver(uri, auth=("neo4j", "password")) + + URI = "neo4j://localhost:7687" + AUTH = ("neo4j", "password") + def get_friends_of(tx, name): friends = [] @@ -136,12 +141,12 @@ Finding nodes. friends.append(record["friend"]) return friends - with driver.session() as session: - friends = session.execute_read(get_friends_of, "Alice") - for friend in friends: - print(friend) - driver.close() + with GraphDatabase.driver(URI, auth=AUTH) as driver: + with driver.session() as session: + friends = session.execute_read(get_friends_of, "Alice") + for friend in friends: + print(friend) ******************* @@ -154,6 +159,7 @@ Example Application from neo4j import GraphDatabase from neo4j.exceptions import ServiceUnavailable + class App: def __init__(self, uri, user, password): @@ -218,13 +224,15 @@ Example Application scheme = "neo4j" # Connecting to Aura, use the "neo4j+s" URI scheme host_name = "example.com" port = 7687 - url = "{scheme}://{host_name}:{port}".format(scheme=scheme, host_name=host_name, port=port) + url = f"{scheme}://{host_name}:{port}" user = "" password = "" app = App(url, user, password) - app.create_friendship("Alice", "David") - app.find_person("Alice") - app.close() + try: + app.create_friendship("Alice", "David") + app.find_person("Alice") + finally: + app.close() ***************** @@ -233,17 +241,14 @@ Other Information * `Neo4j Documentation`_ * `The Neo4j Drivers Manual`_ -* `Neo4j Quick Reference Card`_ +* `Cypher Cheat Sheet`_ * `Example Project`_ * `Driver Wiki`_ (includes change logs) -* `Migration Guide - Upgrade Neo4j drivers`_ * `Neo4j Aura`_ -.. _`Python Driver 1.7`: https://neo4j.com/docs/api/python-driver/1.7/ .. _`Neo4j Documentation`: https://neo4j.com/docs/ .. _`The Neo4j Drivers Manual`: https://neo4j.com/docs/driver-manual/current/ -.. _`Neo4j Quick Reference Card`: https://neo4j.com/docs/cypher-refcard/current/ +.. _`Cypher Cheat Sheet`: https://neo4j.com/docs/cypher-cheat-sheet/current/ .. _`Example Project`: https://github.com/neo4j-examples/movies-python-bolt .. _`Driver Wiki`: https://github.com/neo4j/neo4j-python-driver/wiki -.. _`Migration Guide - Upgrade Neo4j drivers`: https://neo4j.com/docs/migration-guide/4.0/upgrade-driver/ .. _`Neo4j Aura`: https://neo4j.com/neo4j-aura/ diff --git a/neo4j/_async/driver.py b/neo4j/_async/driver.py index d838c956d..d77de374b 100644 --- a/neo4j/_async/driver.py +++ b/neo4j/_async/driver.py @@ -234,6 +234,8 @@ def bookmark_manager( import neo4j + + # omitting closing the driver for brevity driver = neo4j.AsyncGraphDatabase.driver(...) bookmark_manager = neo4j.AsyncGraphDatabase.bookmark_manager(...) @@ -591,7 +593,8 @@ async def get_server_info(self, **config) -> ServerInfo: async def supports_multi_db(self) -> bool: """ Check if the server or cluster supports multi-databases. - :return: Returns true if the server or cluster the driver connects to supports multi-databases, otherwise false. + :returns: Returns true if the server or cluster the driver connects to + supports multi-databases, otherwise false. .. note:: Feature support query, based on Bolt Protocol Version and Neo4j @@ -622,7 +625,7 @@ def open(cls, target, *, auth=None, **config): :param auth: :param config: The values that can be specified are found in :class: `neo4j.PoolConfig` and :class: `neo4j.WorkspaceConfig` - :return: + :returns: :rtype: :class: `neo4j.BoltDriver` """ from .io import AsyncBoltPool @@ -643,7 +646,7 @@ def session(self, **config) -> AsyncSession: :param config: The values that can be specified are found in :class: `neo4j.SessionConfig` - :return: + :returns: :rtype: :class: `neo4j.AsyncSession` """ session_config = SessionConfig(self._default_workspace_config, diff --git a/neo4j/_async/io/_bolt.py b/neo4j/_async/io/_bolt.py index 6215506ff..ffa2d6d2b 100644 --- a/neo4j/_async/io/_bolt.py +++ b/neo4j/_async/io/_bolt.py @@ -191,7 +191,7 @@ def protocol_handlers(cls, protocol_version=None): :param protocol_version: tuple identifying a specific protocol version (e.g. (3, 5)) or None - :return: dictionary of version tuple to handler class for all + :returns: dictionary of version tuple to handler class for all relevant and supported protocol versions :raise TypeError: if protocol version is not passed in a tuple """ @@ -256,7 +256,7 @@ def version_list(cls, versions, limit=4): def get_handshake(cls): """ Return the supported Bolt versions as bytes. The length is 16 bytes as specified in the Bolt version negotiation. - :return: bytes + :returns: bytes """ supported_versions = sorted(cls.protocol_handlers().keys(), reverse=True) offered_versions = cls.version_list(supported_versions) @@ -297,7 +297,7 @@ async def open( :param routing_context: dict containing routing context :param pool_config: - :return: connected AsyncBolt instance + :returns: connected AsyncBolt instance :raise BoltHandshakeError: raised if the Bolt Protocol can not negotiate a protocol version. @@ -523,7 +523,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, dehydration function). Dehydration functions receive the value of type understood by packstream and are free to return anything. :param handlers: handler functions passed into the returned Response object - :return: Response object + :returns: Response object """ pass @@ -631,7 +631,7 @@ async def send_all(self): async def _process_message(self, tag, fields): """ Receive at most one message from the server, if available. - :return: 2-tuple of number of detail messages and number of summary + :returns: 2-tuple of number of detail messages and number of summary messages fetched """ pass @@ -663,7 +663,7 @@ async def fetch_message(self): async def fetch_all(self): """ Fetch all outstanding messages. - :return: 2-tuple of number of detail messages and number of summary + :returns: 2-tuple of number of detail messages and number of summary messages fetched """ detail_count = summary_count = 0 diff --git a/neo4j/_async/io/_bolt3.py b/neo4j/_async/io/_bolt3.py index 418358100..2662f5267 100644 --- a/neo4j/_async/io/_bolt3.py +++ b/neo4j/_async/io/_bolt3.py @@ -333,7 +333,7 @@ def goodbye(self, dehydration_hooks=None, hydration_hooks=None): async def _process_message(self, tag, fields): """ Process at most one message from the server, if available. - :return: 2-tuple of number of detail messages and number of summary + :returns: 2-tuple of number of detail messages and number of summary messages fetched """ details = [] diff --git a/neo4j/_async/io/_bolt4.py b/neo4j/_async/io/_bolt4.py index d523659b7..1bd9457ba 100644 --- a/neo4j/_async/io/_bolt4.py +++ b/neo4j/_async/io/_bolt4.py @@ -289,7 +289,7 @@ def goodbye(self, dehydration_hooks=None, hydration_hooks=None): async def _process_message(self, tag, fields): """ Process at most one message from the server, if available. - :return: 2-tuple of number of detail messages and number of summary + :returns: 2-tuple of number of detail messages and number of summary messages fetched """ details = [] diff --git a/neo4j/_async/io/_bolt5.py b/neo4j/_async/io/_bolt5.py index 664f133ce..01d81ce9b 100644 --- a/neo4j/_async/io/_bolt5.py +++ b/neo4j/_async/io/_bolt5.py @@ -277,7 +277,7 @@ def goodbye(self, dehydration_hooks=None, hydration_hooks=None): async def _process_message(self, tag, fields): """Process at most one message from the server, if available. - :return: 2-tuple of number of detail messages and number of summary + :returns: 2-tuple of number of detail messages and number of summary messages fetched """ details = [] diff --git a/neo4j/_async/io/_common.py b/neo4j/_async/io/_common.py index 2f7c18266..fa21d3b60 100644 --- a/neo4j/_async/io/_common.py +++ b/neo4j/_async/io/_common.py @@ -277,6 +277,7 @@ def check_supported_server_product(agent): looking at the server agent string. :param agent: server agent string to check for validity + :raises UnsupportedServerProduct: if the product is not supported """ if not agent.startswith("Neo4j/"): diff --git a/neo4j/_async/io/_pool.py b/neo4j/_async/io/_pool.py index a637c2627..e578a6f8d 100644 --- a/neo4j/_async/io/_pool.py +++ b/neo4j/_async/io/_pool.py @@ -379,7 +379,7 @@ def open(cls, address, *, auth, pool_config, workspace_config): :param auth: :param pool_config: :param workspace_config: - :return: BoltPool + :returns: BoltPool """ async def opener(addr, timeout): @@ -427,7 +427,7 @@ def open(cls, *addresses, auth, pool_config, workspace_config, :param pool_config: :param workspace_config: :param routing_context: - :return: Neo4jPool + :returns: Neo4jPool """ address = addresses[0] @@ -464,7 +464,7 @@ def __init__(self, opener, pool_config, workspace_config, address): def __repr__(self): """ The representation shows the initial routing addresses. - :return: The representation + :returns: The representation :rtype: str """ return "<{} address={!r}>".format(self.__class__.__name__, @@ -493,7 +493,7 @@ async def fetch_routing_info( info should be fetched :param acquisition_timeout: connection acquisition timeout - :return: list of routing records, or None if no connection + :returns: list of routing records, or None if no connection could be established or if no readers or writers are present :raise ServiceUnavailable: if the server does not support routing, or if routing support is broken or outdated @@ -526,7 +526,7 @@ async def fetch_routing_table( :type imp_user: str or None :param bookmarks: bookmarks used when fetching routing table - :return: a new RoutingTable instance or None if the given router is + :returns: a new RoutingTable instance or None if the given router is currently unable to provide routing information """ new_routing_info = None @@ -583,7 +583,7 @@ async def _update_routing_table_from( ): """ Try to update routing tables with the given routers. - :return: True if the routing table is successfully updated, + :returns: True if the routing table is successfully updated, otherwise False """ if routers: @@ -696,7 +696,7 @@ async def ensure_routing_table_is_fresh( This method is thread-safe. - :return: `True` if an update was required, `False` otherwise. + :returns: `True` if an update was required, `False` otherwise. """ from neo4j.api import READ_ACCESS async with self.refresh_lock: diff --git a/neo4j/_async/work/result.py b/neo4j/_async/work/result.py index 0a9f2ef46..e1af24e93 100644 --- a/neo4j/_async/work/result.py +++ b/neo4j/_async/work/result.py @@ -69,9 +69,10 @@ class AsyncResult: - """A handler for the result of Cypher query execution. Instances - of this class are typically constructed and returned by - :meth:`.AyncSession.run` and :meth:`.AsyncTransaction.run`. + """Handler for the result of Cypher query execution. + + Instances of this class are typically constructed and returned by + :meth:`.AsyncSession.run` and :meth:`.AsyncTransaction.run`. """ def __init__(self, connection, fetch_size, on_closed, on_error): @@ -348,7 +349,7 @@ async def consume(self) -> ResultSummary: async def create_node_tx(tx, name): result = await tx.run( - "CREATE (n:ExampleNode { name: $name }) RETURN n", name=name + "CREATE (n:ExampleNode {name: $name}) RETURN n", name=name ) record = await result.single() value = record.value() @@ -413,8 +414,12 @@ async def single(self, strict: bool = False) -> t.Optional[Record]: Calling this method always exhausts the result. - A warning is generated if more than one record is available but - the first of these is still returned. + If ``strict`` is :const:`True`, this method will raise an exception if + there is not exactly one record left. + + If ``strict`` is :const:`False`, fewer than one record will make this + method return :const:`None`, more than one record will make this method + emit a warning and return the first record. :param strict: If :const:`True`, raise a :class:`neo4j.ResultNotSingleError` @@ -422,7 +427,10 @@ async def single(self, strict: bool = False) -> t.Optional[Record]: warning if there are more than 1 record. :const:`False` by default. - :warns: if more than one record is available + :returns: the next :class:`neo4j.Record` or :const:`None` if none remain + + :warns: if more than one record is available and + ``strict`` is :const:`False` :raises ResultNotSingleError: If ``strict=True`` and not exactly one record is available. @@ -430,8 +438,6 @@ async def single(self, strict: bool = False) -> t.Optional[Record]: was obtained has been closed or the Result has been explicitly consumed. - :returns: the next :class:`neo4j.Record` or :const:`None` if none remain - .. versionchanged:: 5.0 Added ``strict`` parameter. .. versionchanged:: 5.0 @@ -466,6 +472,9 @@ async def single(self, strict: bool = False) -> t.Optional[Record]: async def fetch(self, n: int) -> t.List[Record]: """Obtain up to n records from this result. + Fetch ``min(n, records_left)`` records from this result and return them + as a list. + :param n: the maximum number of records to fetch. :returns: list of :class:`neo4j.Record` @@ -484,6 +493,7 @@ async def fetch(self, n: int) -> t.List[Record]: async def peek(self) -> t.Optional[Record]: """Obtain the next record from this result without consuming it. + This leaves the record in the buffer for further processing. :returns: the next :class:`neo4j.Record` or :const:`None` if none @@ -502,16 +512,21 @@ async def peek(self) -> t.Optional[Record]: return None async def graph(self) -> Graph: - """Return a :class:`neo4j.graph.Graph` instance containing all the graph objects - in the result. After calling this method, the result becomes + """Turn the result into a :class:`neo4j.Graph`. + + Return a :class:`neo4j.graph.Graph` instance containing all the graph + objects in the result. This graph will also contain already consumed + records. + + After calling this method, the result becomes detached, buffering all remaining records. + :returns: a result graph + :raises ResultConsumedError: if the transaction from which this result was obtained has been closed or the Result has been explicitly consumed. - :returns: a result graph - .. versionchanged:: 5.0 Can raise :exc:`ResultConsumedError`. """ @@ -521,53 +536,58 @@ async def graph(self) -> Graph: async def value( self, key: _T_ResultKey = 0, default: t.Optional[object] = None ) -> t.List[t.Any]: - """Helper function that return the remainder of the result as a list of values. - - See :class:`neo4j.Record.value` + """Return the remainder of the result as a list of values. :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 + :raises ResultConsumedError: if the transaction from which this result was obtained has been closed or the Result has been explicitly consumed. - :returns: list of individual values - .. versionchanged:: 5.0 Can raise :exc:`ResultConsumedError`. + + .. seealso:: :meth:`.Record.value` """ return [record.value(key, default) async for record in self] async def values( self, *keys: _T_ResultKey ) -> t.List[t.List[t.Any]]: - """Helper function that return the remainder of the result as a list of values lists. - - See :class:`neo4j.Record.values` + """Return the remainder of the result as a list of values lists. :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 + :raises ResultConsumedError: if the transaction from which this result was obtained has been closed or the Result has been explicitly consumed. - :returns: list of values lists - .. versionchanged:: 5.0 Can raise :exc:`ResultConsumedError`. + + .. seealso:: :meth:`.Record.values` """ return [record.values(*keys) async for record in self] - async def data(self, *keys: _T_ResultKey) -> t.List[t.Any]: - """Helper function that return the remainder of the result as a list of dictionaries. + async def data(self, *keys: _T_ResultKey) -> t.List[t.Dict[str, t.Any]]: + """Return the remainder of the result as a list of dictionaries. + + This function provides a convenient but opinionated way to obtain the + remainder of the result as mostly JSON serializable data. It is mainly + useful for interactive sessions and rapid prototyping. - See :class:`neo4j.Record.data` + For instance, node and relationship labels are not included. You will + have to implement a custom serialzer should you need more control over + the output format. :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 :raises ResultConsumedError: if the transaction from which this result was obtained has been closed or the Result has been explicitly @@ -575,6 +595,8 @@ async def data(self, *keys: _T_ResultKey) -> t.List[t.Any]: .. versionchanged:: 5.0 Can raise :exc:`ResultConsumedError`. + + .. seealso:: :meth:`.Record.data` """ return [record.data(*keys) async for record in self] diff --git a/neo4j/_async/work/session.py b/neo4j/_async/work/session.py index cc539b645..36bfe7bef 100644 --- a/neo4j/_async/work/session.py +++ b/neo4j/_async/work/session.py @@ -65,7 +65,9 @@ class AsyncSession(AsyncWorkspace): - """A :class:`.AsyncSession` is a logical context for transactional units + """Context for executing work + + A :class:`.AsyncSession` is a logical context for transactional units of work. Connections are drawn from the :class:`.AsyncDriver` connection pool as required. @@ -77,12 +79,9 @@ class AsyncSession(AsyncWorkspace): In general, sessions will be created and destroyed within a `with` context. For example:: - async with driver.session() as session: + async with driver.session(database="neo4j") as session: result = await session.run("MATCH (n:Person) RETURN n.name AS name") - # do something with the result... - - :param pool: connection pool instance - :param config: session config instance + ... # do something with the result """ # The current connection. @@ -92,7 +91,7 @@ class AsyncSession(AsyncWorkspace): _transaction: t.Union[AsyncTransaction, AsyncManagedTransaction, None] = \ None - # The current auto-transaction result, if any. + # The current auto-commit transaction result, if any. _auto_result = None # The state this session is in. @@ -264,9 +263,9 @@ async def run( :param kwargs: additional keyword parameters. These take precedence over parameters passed as ``parameters``. - :raises SessionError: if the session has been closed. - :returns: a new :class:`neo4j.AsyncResult` object + + :raises SessionError: if the session has been closed. """ self._check_state() if not query: @@ -278,7 +277,7 @@ async def run( raise ClientError("Explicit Transaction must be handled explicitly") if self._auto_result: - # This will buffer upp all records for the previous auto-transaction + # This will buffer upp all records for the previous auto-commit tx await self._auto_result._buffer_all() if not self._connection: @@ -304,20 +303,20 @@ async def run( "This method can lead to unexpected behaviour." ) async def last_bookmark(self) -> t.Optional[str]: - """Return the bookmark received following the last completed transaction. + """Get the bookmark received following the last completed transaction. - Note: For auto-transactions (:meth:`Session.run`), this will trigger - :meth:`Result.consume` for the current result. + Note: For auto-commit transactions (:meth:`Session.run`), this will + trigger :meth:`Result.consume` for the current result. .. warning:: This method can lead to unexpected behaviour if the session has not yet successfully completed a transaction. + :returns: last bookmark + .. deprecated:: 5.0 :meth:`last_bookmark` will be removed in version 6.0. Use :meth:`last_bookmarks` instead. - - :returns: last bookmark """ # The set of bookmarks to be passed into the next transaction. @@ -353,11 +352,11 @@ async def last_bookmarks(self) -> Bookmarks: in the same session. "Most recent bookmarks" are either the bookmarks passed to the session - or creation, or the last bookmark the session received after committing + on creation, or the last bookmark the session received after committing a transaction to the server. - Note: For auto-transactions (:meth:`Session.run`), this will trigger - :meth:`Result.consume` for the current result. + Note: For auto-commit transactions (:meth:`Session.run`), this will + trigger :meth:`Result.consume` for the current result. :returns: the session's last known bookmarks """ @@ -411,11 +410,15 @@ async def begin_transaction( metadata: t.Optional[t.Dict[str, t.Any]] = None, timeout: t.Optional[float] = None ) -> AsyncTransaction: - """ Begin a new unmanaged transaction. Creates a new :class:`.AsyncTransaction` within this session. - At most one transaction may exist in a session at any point in time. - To maintain multiple concurrent transactions, use multiple concurrent sessions. + """Begin a new unmanaged transaction. - Note: For auto-transaction (AsyncSession.run) this will trigger a consume for the current result. + Creates a new :class:`.AsyncTransaction` within this session. + At most one transaction may exist in a session at any point in time. + To maintain multiple concurrent transactions, use multiple concurrent + sessions. + + Note: For auto-commit transactions (:meth:`.AsyncSession.run`), this + will trigger a :meth:`.AsyncResult.consume` for the current result. :param metadata: a dictionary with metadata. @@ -435,10 +438,10 @@ async def begin_transaction( Specified timeout overrides the default timeout configured in the database using ``dbms.transaction.timeout`` setting. Value should not represent a duration of zero or negative duration. + :returns: A new transaction instance. + :raises TransactionError: if a transaction is already open. :raises SessionError: if the session has been closed. - - :returns: A new transaction instance. """ self._check_state() # TODO: Implement TransactionConfig consumption @@ -580,14 +583,14 @@ async def get_two_tx(tx): :param transaction_function: a function that takes a transaction as an argument and does work with the transaction. - `transaction_function(tx, *args, **kwargs)` where `tx` is a + ``transaction_function(tx, *args, **kwargs)`` where ``tx`` is a :class:`.AsyncManagedTransaction`. :param args: additional arguments for the `transaction_function` :param kwargs: key word arguments for the `transaction_function` - :raises SessionError: if the session has been closed. + :returns: whatever the given `transaction_function` returns - :return: a result as returned by the given unit of work + :raises SessionError: if the session has been closed. .. versionadded:: 5.0 """ @@ -610,53 +613,16 @@ async def read_transaction( This does not necessarily imply access control, see the session configuration option :ref:`default-access-mode-ref`. - This transaction will automatically be committed when the function - returns, unless an exception is thrown during query execution or by - the user code. Note, that this function performs retries and that the - supplied `transaction_function` might get invoked more than once. - Therefore, it needs to be idempotent (i.e., have the same effect, - regardless if called once or many times). - - Example:: - - async def do_cypher_tx(tx, cypher): - result = await tx.run(cypher) - values = [record.values() async for record in result] - return values - - async with driver.session() as session: - values = await session.read_transaction(do_cypher_tx, "RETURN 1 AS x") - - Example:: - - async def get_two_tx(tx): - result = await tx.run("UNWIND [1,2,3,4] AS x RETURN x") - values = [] - async for record in result: - if len(values) >= 2: - break - values.append(record.values()) - # or shorter: values = [record.values() - # for record in await result.fetch(2)] - - # discard the remaining records if there are any - summary = await result.consume() - # use the summary for logging etc. - return values - - async with driver.session() as session: - values = await session.read_transaction(get_two_tx) - :param transaction_function: a function that takes a transaction as an argument and does work with the transaction. - `transaction_function(tx, *args, **kwargs)` where `tx` is a + ``transaction_function(tx, *args, **kwargs)`` where ``tx`` is a :class:`.AsyncManagedTransaction`. :param args: additional arguments for the `transaction_function` :param kwargs: key word arguments for the `transaction_function` - :raises SessionError: if the session has been closed. + :returns: a result as returned by the given unit of work - :return: a result as returned by the given unit of work + :raises SessionError: if the session has been closed. .. deprecated:: 5.0 Method was renamed to :meth:`.execute_read`. @@ -688,24 +654,25 @@ async def execute_write( Example:: async def create_node_tx(tx, name): - query = "CREATE (n:NodeExample { name: $name }) RETURN id(n) AS node_id" + query = ("CREATE (n:NodeExample {name: $name, id: randomUUID()}) " + "RETURN n.id AS node_id") result = await tx.run(query, name=name) record = await result.single() return record["node_id"] async with driver.session() as session: - node_id = await session.execute_write(create_node_tx, "example") + node_id = await session.execute_write(create_node_tx, "Bob") :param transaction_function: a function that takes a transaction as an argument and does work with the transaction. - `transaction_function(tx, *args, **kwargs)` where `tx` is a + ``transaction_function(tx, *args, **kwargs)`` where ``tx`` is a :class:`.AsyncManagedTransaction`. :param args: additional arguments for the `transaction_function` :param kwargs: key word arguments for the `transaction_function` - :raises SessionError: if the session has been closed. + :returns: a result as returned by the given unit of work - :return: a result as returned by the given unit of work + :raises SessionError: if the session has been closed. .. versionadded:: 5.0 """ @@ -728,34 +695,16 @@ async def write_transaction( This does not necessarily imply access control, see the session configuration option :ref:`default-access-mode-ref`. - This transaction will automatically be committed when the function - returns unless, an exception is thrown during query execution or by - the user code. Note, that this function performs retries and that the - supplied `transaction_function` might get invoked more than once. - Therefore, it needs to be idempotent (i.e., have the same effect, - regardless if called once or many times). - - Example:: - - async def create_node_tx(tx, name): - query = "CREATE (n:NodeExample { name: $name }) RETURN id(n) AS node_id" - result = await tx.run(query, name=name) - record = await result.single() - return record["node_id"] - - async with driver.session() as session: - node_id = await session.write_transaction(create_node_tx, "example") - :param transaction_function: a function that takes a transaction as an argument and does work with the transaction. - `transaction_function(tx, *args, **kwargs)` where `tx` is a + ``transaction_function(tx, *args, **kwargs)`` where ``tx`` is a :class:`.AsyncManagedTransaction`. :param args: additional arguments for the `transaction_function` :param kwargs: key word arguments for the `transaction_function` - :raises SessionError: if the session has been closed. + :returns: a result as returned by the given unit of work - :return: a result as returned by the given unit of work + :raises SessionError: if the session has been closed. .. deprecated:: 5.0 Method was renamed to :meth:`.execute_write`. diff --git a/neo4j/_async/work/transaction.py b/neo4j/_async/work/transaction.py index 49c86a662..7cc071275 100644 --- a/neo4j/_async/work/transaction.py +++ b/neo4j/_async/work/transaction.py @@ -245,7 +245,7 @@ def _cancel(self) -> None: def _closed(self): """Indicate whether the transaction has been closed or cancelled. - :return: + :returns: :const:`True` if closed or cancelled, :const:`False` otherwise. :rtype: bool """ diff --git a/neo4j/_async/work/workspace.py b/neo4j/_async/work/workspace.py index 980a8affa..a6cd808ae 100644 --- a/neo4j/_async/work/workspace.py +++ b/neo4j/_async/work/workspace.py @@ -220,7 +220,7 @@ async def close(self) -> None: def closed(self) -> bool: """Indicate whether the session has been closed. - :return: :const:`True` if closed, :const:`False` otherwise. + :returns: :const:`True` if closed, :const:`False` otherwise. """ return self._closed diff --git a/neo4j/_async_compat/network/_bolt_socket.py b/neo4j/_async_compat/network/_bolt_socket.py index fee8cedfa..64b76fb49 100644 --- a/neo4j/_async_compat/network/_bolt_socket.py +++ b/neo4j/_async_compat/network/_bolt_socket.py @@ -179,7 +179,7 @@ async def _connect_secure(cls, resolved_address, timeout, keep_alive, ssl): :param keep_alive: True or False :param ssl: SSLContext or None - :return: AsyncBoltSocket object + :returns: AsyncBoltSocket object """ loop = asyncio.get_event_loop() @@ -275,7 +275,7 @@ async def _handshake(self, resolved_address): :param s: Socket :param resolved_address: - :return: (socket, version, client_handshake, server_response_data) + :returns: (socket, version, client_handshake, server_response_data) """ local_port = self.getsockname()[1] @@ -484,7 +484,7 @@ def _connect(cls, resolved_address, timeout, keep_alive): :param resolved_address: :param timeout: seconds :param keep_alive: True or False - :return: socket object + :returns: socket object """ s = None # The socket @@ -554,7 +554,7 @@ def _handshake(cls, s, resolved_address): :param s: Socket :param resolved_address: - :return: (socket, version, client_handshake, server_response_data) + :returns: (socket, version, client_handshake, server_response_data) """ local_port = s.getsockname()[1] diff --git a/neo4j/_async_compat/network/_util.py b/neo4j/_async_compat/network/_util.py index b1ad27e95..708d3eb9b 100644 --- a/neo4j/_async_compat/network/_util.py +++ b/neo4j/_async_compat/network/_util.py @@ -38,7 +38,7 @@ async def _dns_resolver(address, family=0): :param address: :param family: - :return: + :returns: """ try: info = await AsyncNetworkUtil.get_address_info( @@ -108,7 +108,7 @@ def _dns_resolver(address, family=0): :param address: :param family: - :return: + :returns: """ try: info = NetworkUtil.get_address_info( diff --git a/neo4j/_codec/hydration/v1/spatial.py b/neo4j/_codec/hydration/v1/spatial.py index 6e2f7a6f5..0f8320682 100644 --- a/neo4j/_codec/hydration/v1/spatial.py +++ b/neo4j/_codec/hydration/v1/spatial.py @@ -46,7 +46,7 @@ def dehydrate_point(value): :param value: :type value: Point - :return: + :returns: """ dim = len(value) if dim == 2: diff --git a/neo4j/_codec/hydration/v1/temporal.py b/neo4j/_codec/hydration/v1/temporal.py index a9c511a5a..c36eda5d3 100644 --- a/neo4j/_codec/hydration/v1/temporal.py +++ b/neo4j/_codec/hydration/v1/temporal.py @@ -48,7 +48,7 @@ def hydrate_date(days): """ Hydrator for `Date` values. :param days: - :return: Date + :returns: Date """ return Date.from_ordinal(get_date_unix_epoch_ordinal() + days) @@ -58,7 +58,7 @@ def dehydrate_date(value): :param value: :type value: Date - :return: + :returns: """ return Structure(b"D", value.toordinal() - get_date_unix_epoch().toordinal()) @@ -68,7 +68,7 @@ def hydrate_time(nanoseconds, tz=None): :param nanoseconds: :param tz: - :return: Time + :returns: Time """ from pytz import FixedOffset seconds, nanoseconds = map(int, divmod(nanoseconds, 1000000000)) @@ -87,7 +87,7 @@ def dehydrate_time(value): :param value: :type value: Time - :return: + :returns: """ if isinstance(value, Time): nanoseconds = value.ticks @@ -109,7 +109,7 @@ def hydrate_datetime(seconds, nanoseconds, tz=None): :param seconds: :param nanoseconds: :param tz: - :return: datetime + :returns: datetime """ from pytz import ( FixedOffset, @@ -137,7 +137,7 @@ def dehydrate_datetime(value): :param value: :type value: datetime or DateTime - :return: + :returns: """ def seconds_and_nanoseconds(dt): @@ -178,7 +178,7 @@ def hydrate_duration(months, days, seconds, nanoseconds): :param days: :param seconds: :param nanoseconds: - :return: `duration` namedtuple + :returns: `duration` namedtuple """ return Duration(months=months, days=days, seconds=seconds, nanoseconds=nanoseconds) @@ -188,7 +188,7 @@ def dehydrate_duration(value): :param value: :type value: Duration - :return: + :returns: """ return Structure(b"E", value.months, value.days, value.seconds, value.nanoseconds) @@ -198,7 +198,7 @@ def dehydrate_timedelta(value): :param value: :type value: timedelta - :return: + :returns: """ months = 0 days = value.days diff --git a/neo4j/_codec/hydration/v2/temporal.py b/neo4j/_codec/hydration/v2/temporal.py index ad602eb52..bc3644587 100644 --- a/neo4j/_codec/hydration/v2/temporal.py +++ b/neo4j/_codec/hydration/v2/temporal.py @@ -25,7 +25,7 @@ def hydrate_datetime(seconds, nanoseconds, tz=None): # type: ignore[no-redef] :param seconds: :param nanoseconds: :param tz: - :return: datetime + :returns: datetime """ import pytz @@ -52,7 +52,7 @@ def dehydrate_datetime(value): # type: ignore[no-redef] :param value: :type value: datetime - :return: + :returns: """ import pytz diff --git a/neo4j/_conf.py b/neo4j/_conf.py index 116f69bcd..25e459446 100644 --- a/neo4j/_conf.py +++ b/neo4j/_conf.py @@ -81,6 +81,10 @@ class TrustAll(TrustStore): certificate authority. This option is primarily intended for use with the default auto-generated server certificate. + .. warning:: + This still leaves you vulnerable to man-in-the-middle attacks. It will + just prevent eavesdropping "from the side-line" (i.e., without + intercepting the connection). For example:: diff --git a/neo4j/_data.py b/neo4j/_data.py index 1fe702cd9..3d2081626 100644 --- a/neo4j/_data.py +++ b/neo4j/_data.py @@ -90,7 +90,7 @@ def __eq__(self, other: object) -> bool: for a record permit comparison with any other Sequence or Mapping. :param other: - :return: + :returns: """ compare_as_sequence = isinstance(other, Sequence) compare_as_mapping = isinstance(other, Mapping) @@ -147,7 +147,7 @@ def get(self, key: str, default: t.Optional[object] = None) -> t.Any: :param key: a key :param default: default value - :return: a value + :returns: a value """ try: index = self.__keys.index(str(key)) @@ -163,7 +163,7 @@ def index(self, key: _T_K) -> int: # type: ignore[override] :param key: a key - :return: index + :returns: index """ if isinstance(key, int): if 0 <= key < len(self.__keys): @@ -187,7 +187,7 @@ def value( :param key: an index or key :param default: default value - :return: a single value + :returns: a single value """ try: index = self.index(key) @@ -199,7 +199,7 @@ def value( def keys(self) -> t.List[str]: # type: ignore[override] """ Return the keys of the record. - :return: list of key names + :returns: list of key names """ return list(self.__keys) @@ -210,7 +210,7 @@ def values(self, *keys: _T_K) -> t.List[t.Any]: # type: ignore[override] :param keys: indexes or keys of the items to include; if none are provided, all values will be included - :return: list of values + :returns: list of values """ if keys: d: t.List[t.Any] = [] @@ -227,7 +227,7 @@ def values(self, *keys: _T_K) -> t.List[t.Any]: # type: ignore[override] def items(self, *keys): """ Return the fields of the record as a list of key and value tuples - :return: a list of value tuples + :returns: a list of value tuples """ if keys: d = [] @@ -252,9 +252,9 @@ def data(self, *keys: _T_K) -> t.Dict[str, t.Any]: :param keys: indexes or keys of the items to include; if none are provided, all values will be included - :raises: :exc:`IndexError` if an out-of-bounds index is specified + :returns: dictionary of values, keyed by field name - :return: dictionary of values, keyed by field name + :raises: :exc:`IndexError` if an out-of-bounds index is specified """ return RecordExporter().transform(dict(self.items(*keys))) @@ -269,7 +269,7 @@ def transform(self, x): """ Transform a value, or collection of values. :param x: input value - :return: output value + :returns: output value """ diff --git a/neo4j/_routing.py b/neo4j/_routing.py index 4e562635b..86d31a730 100644 --- a/neo4j/_routing.py +++ b/neo4j/_routing.py @@ -144,7 +144,7 @@ def is_fresh(self, readonly=False): def should_be_purged_from_memory(self): """ Check if the routing table is stale and not used for a long time and should be removed from memory. - :return: Returns true if it is old and not used for a while. + :returns: Returns true if it is old and not used for a while. :rtype: bool """ from neo4j._conf import RoutingConfig diff --git a/neo4j/_sync/driver.py b/neo4j/_sync/driver.py index f8018ac59..c06491f8f 100644 --- a/neo4j/_sync/driver.py +++ b/neo4j/_sync/driver.py @@ -231,6 +231,8 @@ def bookmark_manager( import neo4j + + # omitting closing the driver for brevity driver = neo4j.GraphDatabase.driver(...) bookmark_manager = neo4j.GraphDatabase.bookmark_manager(...) @@ -588,7 +590,8 @@ def get_server_info(self, **config) -> ServerInfo: def supports_multi_db(self) -> bool: """ Check if the server or cluster supports multi-databases. - :return: Returns true if the server or cluster the driver connects to supports multi-databases, otherwise false. + :returns: Returns true if the server or cluster the driver connects to + supports multi-databases, otherwise false. .. note:: Feature support query, based on Bolt Protocol Version and Neo4j @@ -619,7 +622,7 @@ def open(cls, target, *, auth=None, **config): :param auth: :param config: The values that can be specified are found in :class: `neo4j.PoolConfig` and :class: `neo4j.WorkspaceConfig` - :return: + :returns: :rtype: :class: `neo4j.BoltDriver` """ from .io import BoltPool @@ -640,7 +643,7 @@ def session(self, **config) -> Session: :param config: The values that can be specified are found in :class: `neo4j.SessionConfig` - :return: + :returns: :rtype: :class: `neo4j.Session` """ session_config = SessionConfig(self._default_workspace_config, diff --git a/neo4j/_sync/io/_bolt.py b/neo4j/_sync/io/_bolt.py index 175e63b17..57fb780d2 100644 --- a/neo4j/_sync/io/_bolt.py +++ b/neo4j/_sync/io/_bolt.py @@ -191,7 +191,7 @@ def protocol_handlers(cls, protocol_version=None): :param protocol_version: tuple identifying a specific protocol version (e.g. (3, 5)) or None - :return: dictionary of version tuple to handler class for all + :returns: dictionary of version tuple to handler class for all relevant and supported protocol versions :raise TypeError: if protocol version is not passed in a tuple """ @@ -256,7 +256,7 @@ def version_list(cls, versions, limit=4): def get_handshake(cls): """ Return the supported Bolt versions as bytes. The length is 16 bytes as specified in the Bolt version negotiation. - :return: bytes + :returns: bytes """ supported_versions = sorted(cls.protocol_handlers().keys(), reverse=True) offered_versions = cls.version_list(supported_versions) @@ -297,7 +297,7 @@ def open( :param routing_context: dict containing routing context :param pool_config: - :return: connected Bolt instance + :returns: connected Bolt instance :raise BoltHandshakeError: raised if the Bolt Protocol can not negotiate a protocol version. @@ -523,7 +523,7 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, dehydration function). Dehydration functions receive the value of type understood by packstream and are free to return anything. :param handlers: handler functions passed into the returned Response object - :return: Response object + :returns: Response object """ pass @@ -631,7 +631,7 @@ def send_all(self): def _process_message(self, tag, fields): """ Receive at most one message from the server, if available. - :return: 2-tuple of number of detail messages and number of summary + :returns: 2-tuple of number of detail messages and number of summary messages fetched """ pass @@ -663,7 +663,7 @@ def fetch_message(self): def fetch_all(self): """ Fetch all outstanding messages. - :return: 2-tuple of number of detail messages and number of summary + :returns: 2-tuple of number of detail messages and number of summary messages fetched """ detail_count = summary_count = 0 diff --git a/neo4j/_sync/io/_bolt3.py b/neo4j/_sync/io/_bolt3.py index ba24eefce..5d5c250b1 100644 --- a/neo4j/_sync/io/_bolt3.py +++ b/neo4j/_sync/io/_bolt3.py @@ -333,7 +333,7 @@ def goodbye(self, dehydration_hooks=None, hydration_hooks=None): def _process_message(self, tag, fields): """ Process at most one message from the server, if available. - :return: 2-tuple of number of detail messages and number of summary + :returns: 2-tuple of number of detail messages and number of summary messages fetched """ details = [] diff --git a/neo4j/_sync/io/_bolt4.py b/neo4j/_sync/io/_bolt4.py index 2ff95eb06..48017a6dd 100644 --- a/neo4j/_sync/io/_bolt4.py +++ b/neo4j/_sync/io/_bolt4.py @@ -289,7 +289,7 @@ def goodbye(self, dehydration_hooks=None, hydration_hooks=None): def _process_message(self, tag, fields): """ Process at most one message from the server, if available. - :return: 2-tuple of number of detail messages and number of summary + :returns: 2-tuple of number of detail messages and number of summary messages fetched """ details = [] diff --git a/neo4j/_sync/io/_bolt5.py b/neo4j/_sync/io/_bolt5.py index 25283af63..fd9e754be 100644 --- a/neo4j/_sync/io/_bolt5.py +++ b/neo4j/_sync/io/_bolt5.py @@ -277,7 +277,7 @@ def goodbye(self, dehydration_hooks=None, hydration_hooks=None): def _process_message(self, tag, fields): """Process at most one message from the server, if available. - :return: 2-tuple of number of detail messages and number of summary + :returns: 2-tuple of number of detail messages and number of summary messages fetched """ details = [] diff --git a/neo4j/_sync/io/_common.py b/neo4j/_sync/io/_common.py index 04312bfe4..89681a387 100644 --- a/neo4j/_sync/io/_common.py +++ b/neo4j/_sync/io/_common.py @@ -277,6 +277,7 @@ def check_supported_server_product(agent): looking at the server agent string. :param agent: server agent string to check for validity + :raises UnsupportedServerProduct: if the product is not supported """ if not agent.startswith("Neo4j/"): diff --git a/neo4j/_sync/io/_pool.py b/neo4j/_sync/io/_pool.py index 43fe68abe..c0c8842e1 100644 --- a/neo4j/_sync/io/_pool.py +++ b/neo4j/_sync/io/_pool.py @@ -379,7 +379,7 @@ def open(cls, address, *, auth, pool_config, workspace_config): :param auth: :param pool_config: :param workspace_config: - :return: BoltPool + :returns: BoltPool """ def opener(addr, timeout): @@ -427,7 +427,7 @@ def open(cls, *addresses, auth, pool_config, workspace_config, :param pool_config: :param workspace_config: :param routing_context: - :return: Neo4jPool + :returns: Neo4jPool """ address = addresses[0] @@ -464,7 +464,7 @@ def __init__(self, opener, pool_config, workspace_config, address): def __repr__(self): """ The representation shows the initial routing addresses. - :return: The representation + :returns: The representation :rtype: str """ return "<{} address={!r}>".format(self.__class__.__name__, @@ -493,7 +493,7 @@ def fetch_routing_info( info should be fetched :param acquisition_timeout: connection acquisition timeout - :return: list of routing records, or None if no connection + :returns: list of routing records, or None if no connection could be established or if no readers or writers are present :raise ServiceUnavailable: if the server does not support routing, or if routing support is broken or outdated @@ -526,7 +526,7 @@ def fetch_routing_table( :type imp_user: str or None :param bookmarks: bookmarks used when fetching routing table - :return: a new RoutingTable instance or None if the given router is + :returns: a new RoutingTable instance or None if the given router is currently unable to provide routing information """ new_routing_info = None @@ -583,7 +583,7 @@ def _update_routing_table_from( ): """ Try to update routing tables with the given routers. - :return: True if the routing table is successfully updated, + :returns: True if the routing table is successfully updated, otherwise False """ if routers: @@ -696,7 +696,7 @@ def ensure_routing_table_is_fresh( This method is thread-safe. - :return: `True` if an update was required, `False` otherwise. + :returns: `True` if an update was required, `False` otherwise. """ from neo4j.api import READ_ACCESS with self.refresh_lock: diff --git a/neo4j/_sync/work/result.py b/neo4j/_sync/work/result.py index 13883a0d0..2d2622cc5 100644 --- a/neo4j/_sync/work/result.py +++ b/neo4j/_sync/work/result.py @@ -69,9 +69,10 @@ class Result: - """A handler for the result of Cypher query execution. Instances - of this class are typically constructed and returned by - :meth:`.AyncSession.run` and :meth:`.Transaction.run`. + """Handler for the result of Cypher query execution. + + Instances of this class are typically constructed and returned by + :meth:`.Session.run` and :meth:`.Transaction.run`. """ def __init__(self, connection, fetch_size, on_closed, on_error): @@ -348,7 +349,7 @@ def consume(self) -> ResultSummary: def create_node_tx(tx, name): result = tx.run( - "CREATE (n:ExampleNode { name: $name }) RETURN n", name=name + "CREATE (n:ExampleNode {name: $name}) RETURN n", name=name ) record = result.single() value = record.value() @@ -413,8 +414,12 @@ def single(self, strict: bool = False) -> t.Optional[Record]: Calling this method always exhausts the result. - A warning is generated if more than one record is available but - the first of these is still returned. + If ``strict`` is :const:`True`, this method will raise an exception if + there is not exactly one record left. + + If ``strict`` is :const:`False`, fewer than one record will make this + method return :const:`None`, more than one record will make this method + emit a warning and return the first record. :param strict: If :const:`True`, raise a :class:`neo4j.ResultNotSingleError` @@ -422,7 +427,10 @@ def single(self, strict: bool = False) -> t.Optional[Record]: warning if there are more than 1 record. :const:`False` by default. - :warns: if more than one record is available + :returns: the next :class:`neo4j.Record` or :const:`None` if none remain + + :warns: if more than one record is available and + ``strict`` is :const:`False` :raises ResultNotSingleError: If ``strict=True`` and not exactly one record is available. @@ -430,8 +438,6 @@ def single(self, strict: bool = False) -> t.Optional[Record]: was obtained has been closed or the Result has been explicitly consumed. - :returns: the next :class:`neo4j.Record` or :const:`None` if none remain - .. versionchanged:: 5.0 Added ``strict`` parameter. .. versionchanged:: 5.0 @@ -466,6 +472,9 @@ def single(self, strict: bool = False) -> t.Optional[Record]: def fetch(self, n: int) -> t.List[Record]: """Obtain up to n records from this result. + Fetch ``min(n, records_left)`` records from this result and return them + as a list. + :param n: the maximum number of records to fetch. :returns: list of :class:`neo4j.Record` @@ -484,6 +493,7 @@ def fetch(self, n: int) -> t.List[Record]: def peek(self) -> t.Optional[Record]: """Obtain the next record from this result without consuming it. + This leaves the record in the buffer for further processing. :returns: the next :class:`neo4j.Record` or :const:`None` if none @@ -502,16 +512,21 @@ def peek(self) -> t.Optional[Record]: return None def graph(self) -> Graph: - """Return a :class:`neo4j.graph.Graph` instance containing all the graph objects - in the result. After calling this method, the result becomes + """Turn the result into a :class:`neo4j.Graph`. + + Return a :class:`neo4j.graph.Graph` instance containing all the graph + objects in the result. This graph will also contain already consumed + records. + + After calling this method, the result becomes detached, buffering all remaining records. + :returns: a result graph + :raises ResultConsumedError: if the transaction from which this result was obtained has been closed or the Result has been explicitly consumed. - :returns: a result graph - .. versionchanged:: 5.0 Can raise :exc:`ResultConsumedError`. """ @@ -521,53 +536,58 @@ def graph(self) -> Graph: def value( self, key: _T_ResultKey = 0, default: t.Optional[object] = None ) -> t.List[t.Any]: - """Helper function that return the remainder of the result as a list of values. - - See :class:`neo4j.Record.value` + """Return the remainder of the result as a list of values. :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 + :raises ResultConsumedError: if the transaction from which this result was obtained has been closed or the Result has been explicitly consumed. - :returns: list of individual values - .. versionchanged:: 5.0 Can raise :exc:`ResultConsumedError`. + + .. seealso:: :meth:`.Record.value` """ return [record.value(key, default) for record in self] def values( self, *keys: _T_ResultKey ) -> t.List[t.List[t.Any]]: - """Helper function that return the remainder of the result as a list of values lists. - - See :class:`neo4j.Record.values` + """Return the remainder of the result as a list of values lists. :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 + :raises ResultConsumedError: if the transaction from which this result was obtained has been closed or the Result has been explicitly consumed. - :returns: list of values lists - .. versionchanged:: 5.0 Can raise :exc:`ResultConsumedError`. + + .. seealso:: :meth:`.Record.values` """ return [record.values(*keys) for record in self] - def data(self, *keys: _T_ResultKey) -> t.List[t.Any]: - """Helper function that return the remainder of the result as a list of dictionaries. + def data(self, *keys: _T_ResultKey) -> t.List[t.Dict[str, t.Any]]: + """Return the remainder of the result as a list of dictionaries. + + This function provides a convenient but opinionated way to obtain the + remainder of the result as mostly JSON serializable data. It is mainly + useful for interactive sessions and rapid prototyping. - See :class:`neo4j.Record.data` + For instance, node and relationship labels are not included. You will + have to implement a custom serialzer should you need more control over + the output format. :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 :raises ResultConsumedError: if the transaction from which this result was obtained has been closed or the Result has been explicitly @@ -575,6 +595,8 @@ def data(self, *keys: _T_ResultKey) -> t.List[t.Any]: .. versionchanged:: 5.0 Can raise :exc:`ResultConsumedError`. + + .. seealso:: :meth:`.Record.data` """ return [record.data(*keys) for record in self] diff --git a/neo4j/_sync/work/session.py b/neo4j/_sync/work/session.py index 21dfc5264..e2106c244 100644 --- a/neo4j/_sync/work/session.py +++ b/neo4j/_sync/work/session.py @@ -65,7 +65,9 @@ class Session(Workspace): - """A :class:`.Session` is a logical context for transactional units + """Context for executing work + + A :class:`.Session` is a logical context for transactional units of work. Connections are drawn from the :class:`.Driver` connection pool as required. @@ -77,12 +79,9 @@ class Session(Workspace): In general, sessions will be created and destroyed within a `with` context. For example:: - with driver.session() as session: + with driver.session(database="neo4j") as session: result = session.run("MATCH (n:Person) RETURN n.name AS name") - # do something with the result... - - :param pool: connection pool instance - :param config: session config instance + ... # do something with the result """ # The current connection. @@ -92,7 +91,7 @@ class Session(Workspace): _transaction: t.Union[Transaction, ManagedTransaction, None] = \ None - # The current auto-transaction result, if any. + # The current auto-commit transaction result, if any. _auto_result = None # The state this session is in. @@ -264,9 +263,9 @@ def run( :param kwargs: additional keyword parameters. These take precedence over parameters passed as ``parameters``. - :raises SessionError: if the session has been closed. - :returns: a new :class:`neo4j.Result` object + + :raises SessionError: if the session has been closed. """ self._check_state() if not query: @@ -278,7 +277,7 @@ def run( raise ClientError("Explicit Transaction must be handled explicitly") if self._auto_result: - # This will buffer upp all records for the previous auto-transaction + # This will buffer upp all records for the previous auto-commit tx self._auto_result._buffer_all() if not self._connection: @@ -304,20 +303,20 @@ def run( "This method can lead to unexpected behaviour." ) def last_bookmark(self) -> t.Optional[str]: - """Return the bookmark received following the last completed transaction. + """Get the bookmark received following the last completed transaction. - Note: For auto-transactions (:meth:`Session.run`), this will trigger - :meth:`Result.consume` for the current result. + Note: For auto-commit transactions (:meth:`Session.run`), this will + trigger :meth:`Result.consume` for the current result. .. warning:: This method can lead to unexpected behaviour if the session has not yet successfully completed a transaction. + :returns: last bookmark + .. deprecated:: 5.0 :meth:`last_bookmark` will be removed in version 6.0. Use :meth:`last_bookmarks` instead. - - :returns: last bookmark """ # The set of bookmarks to be passed into the next transaction. @@ -353,11 +352,11 @@ def last_bookmarks(self) -> Bookmarks: in the same session. "Most recent bookmarks" are either the bookmarks passed to the session - or creation, or the last bookmark the session received after committing + on creation, or the last bookmark the session received after committing a transaction to the server. - Note: For auto-transactions (:meth:`Session.run`), this will trigger - :meth:`Result.consume` for the current result. + Note: For auto-commit transactions (:meth:`Session.run`), this will + trigger :meth:`Result.consume` for the current result. :returns: the session's last known bookmarks """ @@ -411,11 +410,15 @@ def begin_transaction( metadata: t.Optional[t.Dict[str, t.Any]] = None, timeout: t.Optional[float] = None ) -> Transaction: - """ Begin a new unmanaged transaction. Creates a new :class:`.Transaction` within this session. - At most one transaction may exist in a session at any point in time. - To maintain multiple concurrent transactions, use multiple concurrent sessions. + """Begin a new unmanaged transaction. - Note: For auto-transaction (Session.run) this will trigger a consume for the current result. + Creates a new :class:`.Transaction` within this session. + At most one transaction may exist in a session at any point in time. + To maintain multiple concurrent transactions, use multiple concurrent + sessions. + + Note: For auto-commit transactions (:meth:`.Session.run`), this + will trigger a :meth:`.Result.consume` for the current result. :param metadata: a dictionary with metadata. @@ -435,10 +438,10 @@ def begin_transaction( Specified timeout overrides the default timeout configured in the database using ``dbms.transaction.timeout`` setting. Value should not represent a duration of zero or negative duration. + :returns: A new transaction instance. + :raises TransactionError: if a transaction is already open. :raises SessionError: if the session has been closed. - - :returns: A new transaction instance. """ self._check_state() # TODO: Implement TransactionConfig consumption @@ -580,14 +583,14 @@ def get_two_tx(tx): :param transaction_function: a function that takes a transaction as an argument and does work with the transaction. - `transaction_function(tx, *args, **kwargs)` where `tx` is a + ``transaction_function(tx, *args, **kwargs)`` where ``tx`` is a :class:`.ManagedTransaction`. :param args: additional arguments for the `transaction_function` :param kwargs: key word arguments for the `transaction_function` - :raises SessionError: if the session has been closed. + :returns: whatever the given `transaction_function` returns - :return: a result as returned by the given unit of work + :raises SessionError: if the session has been closed. .. versionadded:: 5.0 """ @@ -610,53 +613,16 @@ def read_transaction( This does not necessarily imply access control, see the session configuration option :ref:`default-access-mode-ref`. - This transaction will automatically be committed when the function - returns, unless an exception is thrown during query execution or by - the user code. Note, that this function performs retries and that the - supplied `transaction_function` might get invoked more than once. - Therefore, it needs to be idempotent (i.e., have the same effect, - regardless if called once or many times). - - Example:: - - def do_cypher_tx(tx, cypher): - result = tx.run(cypher) - values = [record.values() for record in result] - return values - - with driver.session() as session: - values = session.read_transaction(do_cypher_tx, "RETURN 1 AS x") - - Example:: - - def get_two_tx(tx): - result = tx.run("UNWIND [1,2,3,4] AS x RETURN x") - values = [] - for record in result: - if len(values) >= 2: - break - values.append(record.values()) - # or shorter: values = [record.values() - # for record in result.fetch(2)] - - # discard the remaining records if there are any - summary = result.consume() - # use the summary for logging etc. - return values - - with driver.session() as session: - values = session.read_transaction(get_two_tx) - :param transaction_function: a function that takes a transaction as an argument and does work with the transaction. - `transaction_function(tx, *args, **kwargs)` where `tx` is a + ``transaction_function(tx, *args, **kwargs)`` where ``tx`` is a :class:`.ManagedTransaction`. :param args: additional arguments for the `transaction_function` :param kwargs: key word arguments for the `transaction_function` - :raises SessionError: if the session has been closed. + :returns: a result as returned by the given unit of work - :return: a result as returned by the given unit of work + :raises SessionError: if the session has been closed. .. deprecated:: 5.0 Method was renamed to :meth:`.execute_read`. @@ -688,24 +654,25 @@ def execute_write( Example:: def create_node_tx(tx, name): - query = "CREATE (n:NodeExample { name: $name }) RETURN id(n) AS node_id" + query = ("CREATE (n:NodeExample {name: $name, id: randomUUID()}) " + "RETURN n.id AS node_id") result = tx.run(query, name=name) record = result.single() return record["node_id"] with driver.session() as session: - node_id = session.execute_write(create_node_tx, "example") + node_id = session.execute_write(create_node_tx, "Bob") :param transaction_function: a function that takes a transaction as an argument and does work with the transaction. - `transaction_function(tx, *args, **kwargs)` where `tx` is a + ``transaction_function(tx, *args, **kwargs)`` where ``tx`` is a :class:`.ManagedTransaction`. :param args: additional arguments for the `transaction_function` :param kwargs: key word arguments for the `transaction_function` - :raises SessionError: if the session has been closed. + :returns: a result as returned by the given unit of work - :return: a result as returned by the given unit of work + :raises SessionError: if the session has been closed. .. versionadded:: 5.0 """ @@ -728,34 +695,16 @@ def write_transaction( This does not necessarily imply access control, see the session configuration option :ref:`default-access-mode-ref`. - This transaction will automatically be committed when the function - returns unless, an exception is thrown during query execution or by - the user code. Note, that this function performs retries and that the - supplied `transaction_function` might get invoked more than once. - Therefore, it needs to be idempotent (i.e., have the same effect, - regardless if called once or many times). - - Example:: - - def create_node_tx(tx, name): - query = "CREATE (n:NodeExample { name: $name }) RETURN id(n) AS node_id" - result = tx.run(query, name=name) - record = result.single() - return record["node_id"] - - with driver.session() as session: - node_id = session.write_transaction(create_node_tx, "example") - :param transaction_function: a function that takes a transaction as an argument and does work with the transaction. - `transaction_function(tx, *args, **kwargs)` where `tx` is a + ``transaction_function(tx, *args, **kwargs)`` where ``tx`` is a :class:`.ManagedTransaction`. :param args: additional arguments for the `transaction_function` :param kwargs: key word arguments for the `transaction_function` - :raises SessionError: if the session has been closed. + :returns: a result as returned by the given unit of work - :return: a result as returned by the given unit of work + :raises SessionError: if the session has been closed. .. deprecated:: 5.0 Method was renamed to :meth:`.execute_write`. diff --git a/neo4j/_sync/work/transaction.py b/neo4j/_sync/work/transaction.py index 12a551c45..23d6ee647 100644 --- a/neo4j/_sync/work/transaction.py +++ b/neo4j/_sync/work/transaction.py @@ -245,7 +245,7 @@ def _cancel(self) -> None: def _closed(self): """Indicate whether the transaction has been closed or cancelled. - :return: + :returns: :const:`True` if closed or cancelled, :const:`False` otherwise. :rtype: bool """ diff --git a/neo4j/_sync/work/workspace.py b/neo4j/_sync/work/workspace.py index df885c454..04398e065 100644 --- a/neo4j/_sync/work/workspace.py +++ b/neo4j/_sync/work/workspace.py @@ -220,7 +220,7 @@ def close(self) -> None: def closed(self) -> bool: """Indicate whether the session has been closed. - :return: :const:`True` if closed, :const:`False` otherwise. + :returns: :const:`True` if closed, :const:`False` otherwise. """ return self._closed diff --git a/neo4j/api.py b/neo4j/api.py index 7f428e084..a5e7471f8 100644 --- a/neo4j/api.py +++ b/neo4j/api.py @@ -123,7 +123,7 @@ def basic_auth( :param password: current password, this will set the credentials :param realm: specifies the authentication provider - :return: auth token for use with :meth:`GraphDatabase.driver` or + :returns: auth token for use with :meth:`GraphDatabase.driver` or :meth:`AsyncGraphDatabase.driver` """ return Auth("basic", user, password, realm) @@ -137,7 +137,7 @@ def kerberos_auth(base64_encoded_ticket: str) -> Auth: :param base64_encoded_ticket: a base64 encoded service ticket, this will set the credentials - :return: auth token for use with :meth:`GraphDatabase.driver` or + :returns: auth token for use with :meth:`GraphDatabase.driver` or :meth:`AsyncGraphDatabase.driver` """ return Auth("kerberos", "", base64_encoded_ticket) @@ -151,7 +151,7 @@ def bearer_auth(base64_encoded_token: str) -> Auth: :param base64_encoded_token: a base64 encoded authentication token generated by a Single-Sign-On provider. - :return: auth token for use with :meth:`GraphDatabase.driver` or + :returns: auth token for use with :meth:`GraphDatabase.driver` or :meth:`AsyncGraphDatabase.driver` """ return Auth("bearer", None, base64_encoded_token) @@ -173,7 +173,7 @@ def custom_auth( :param parameters: extra key word parameters passed along to the authentication provider - :return: auth token for use with :meth:`GraphDatabase.driver` or + :returns: auth token for use with :meth:`GraphDatabase.driver` or :meth:`AsyncGraphDatabase.driver` """ return Auth(scheme, principal, credentials, realm, **parameters) @@ -183,11 +183,11 @@ def custom_auth( class Bookmark: """A Bookmark object contains an immutable list of bookmark string values. + :param values: ASCII string values + .. deprecated:: 5.0 `Bookmark` will be removed in version 6.0. Use :class:`Bookmarks` instead. - - :param values: ASCII string values """ @deprecated("Use the `Bookmarks`` class instead.") @@ -207,7 +207,7 @@ def __init__(self, *values: str) -> None: def __repr__(self) -> str: """ - :return: repr string with sorted values + :returns: repr string with sorted values """ return "".format(", ".join(["'{}'".format(ix) for ix in sorted(self._values)])) @@ -217,7 +217,7 @@ def __bool__(self) -> bool: @property def values(self) -> frozenset: """ - :return: immutable list of bookmark string values + :returns: immutable list of bookmark string values """ return self._values @@ -239,7 +239,7 @@ def __init__(self): def __repr__(self) -> str: """ - :return: repr string with sorted values + :returns: repr string with sorted values """ return "".format( ", ".join(map(repr, sorted(self._raw_values))) @@ -266,7 +266,7 @@ def raw_values(self) -> t.FrozenSet[str]: You should not need to access them unless you want to serialize bookmarks. - :return: immutable list of bookmark string values + :returns: immutable list of bookmark string values :rtype: frozenset[str] """ return self._raw_values diff --git a/neo4j/debug.py b/neo4j/debug.py index 7b7fc24aa..5af57a395 100644 --- a/neo4j/debug.py +++ b/neo4j/debug.py @@ -87,9 +87,9 @@ class Watcher: enable logging for all threads. .. note:: - The exact logging format is not part of the API contract and might - change at any time without notice. It is meant for debugging purposes - and human consumption only. + The exact logging format and messages are not part of the API contract + and might change at any time without notice. They are meant for + debugging purposes and human consumption only. :param logger_names: Names of loggers to watch. :param default_level: Default minimum log level to show. @@ -204,9 +204,9 @@ def watch( # from now on, DEBUG logging to stderr is enabled in the driver .. note:: - The exact logging format is not part of the API contract and might - change at any time without notice. It is meant for debugging purposes - and human consumption only. + The exact logging format and messages are not part of the API contract + and might change at any time without notice. They are meant for + debugging purposes and human consumption only. :param logger_names: Names of loggers to watch. :param level: see ``default_level`` of :class:`.Watcher`. @@ -216,7 +216,7 @@ def watch( :param thread_info: see ``thread_info`` of :class:`.Watcher`. :param task_info: see ``task_info`` of :class:`.Watcher`. - :return: Watcher instance + :returns: Watcher instance :rtype: :class:`.Watcher` .. versionchanged:: diff --git a/neo4j/exceptions.py b/neo4j/exceptions.py index dcf250cf6..6feea3642 100644 --- a/neo4j/exceptions.py +++ b/neo4j/exceptions.py @@ -212,7 +212,7 @@ def is_retriable(self) -> bool: See :meth:`.is_retryable`. - :return: :const:`True` if the error is retryable, + :returns: :const:`True` if the error is retryable, :const:`False` otherwise. .. deprecated:: 5.0 @@ -228,7 +228,7 @@ def is_retryable(self) -> bool: retry. This method makes mostly sense when implementing a custom retry policy in conjunction with :ref:`explicit-transactions-ref`. - :return: :const:`True` if the error is retryable, + :returns: :const:`True` if the error is retryable, :const:`False` otherwise. """ return False @@ -390,7 +390,7 @@ def is_retryable(self) -> bool: retry. This method makes mostly sense when implementing a custom retry policy in conjunction with :ref:`explicit-transactions-ref`. - :return: :const:`True` if the error is retryable, + :returns: :const:`True` if the error is retryable, :const:`False` otherwise. """ return False diff --git a/neo4j/graph/__init__.py b/neo4j/graph/__init__.py index 77f08d951..3efea224b 100644 --- a/neo4j/graph/__init__.py +++ b/neo4j/graph/__init__.py @@ -44,8 +44,12 @@ class Graph: - """ Local, self-contained graph object that acts as a container for + """A graph of nodes and relationships. + + Local, self-contained graph object that acts as a container for :class:`.Node` and :class:`.Relationship` instances. + This is typically obtained via :meth:`.Result.graph` or + :meth:`.AsyncResult.graph`. """ def __init__(self) -> None: @@ -145,9 +149,10 @@ def id(self) -> int: Depending on the version of the server this entity was retrieved from, this may be empty (None). - .. Warning:: + .. warning:: This value can change for the same entity across multiple - queries. Don't rely on it for cross-query computations. + transactions. Don't rely on it for cross-transactional + computations. .. deprecated:: 5.0 Use :attr:`.element_id` instead. @@ -158,9 +163,10 @@ def id(self) -> int: def element_id(self) -> str: """The identity of this entity in its container :class:`.Graph`. - .. Warning:: + .. warning:: This value can change for the same entity across multiple - queries. Don't rely on it for cross-query computations. + transactions. Don't rely on it for cross-transactional + computations. .. versionadded:: 5.0 """ diff --git a/neo4j/spatial/__init__.py b/neo4j/spatial/__init__.py index 5a88b1474..f5ec1478e 100644 --- a/neo4j/spatial/__init__.py +++ b/neo4j/spatial/__init__.py @@ -67,7 +67,7 @@ def dehydrate_point(value): :param value: :type value: Point - :return: + :returns: """ return _hydration.dehydrate_point(value) diff --git a/neo4j/time/__init__.py b/neo4j/time/__init__.py index 3e5730f26..7ad53a40c 100644 --- a/neo4j/time/__init__.py +++ b/neo4j/time/__init__.py @@ -172,7 +172,7 @@ def _normalize_day(year, month, day): :param year: :param month: :param day: - :return: + :returns: """ if year < MIN_YEAR or year > MAX_YEAR: raise ValueError("Year out of range (%d..%d)" % (MIN_YEAR, MAX_YEAR)) @@ -1820,7 +1820,7 @@ def _utc_offset(self, dt=None): def utc_offset(self) -> t.Optional[timedelta]: """Return the UTC offset of this time. - :return: None if this is a local time (:attr:`.tzinfo` is None), else + :returns: None if this is a local time (:attr:`.tzinfo` is None), else returns `self.tzinfo.utcoffset(self)`. :raises ValueError: if `self.tzinfo.utcoffset(self)` is not None and a @@ -1834,7 +1834,7 @@ def utc_offset(self) -> t.Optional[timedelta]: def dst(self) -> t.Optional[timedelta]: """Get the daylight saving time adjustment (DST). - :return: None if this is a local time (:attr:`.tzinfo` is None), else + :returns: None if this is a local time (:attr:`.tzinfo` is None), else returns `self.tzinfo.dst(self)`. :raises ValueError: if `self.tzinfo.dst(self)` is not None and a @@ -2544,7 +2544,7 @@ def as_timezone(self, tz: _tzinfo) -> DateTime: :param tz: the new timezone - :return: the same object if ``tz`` is :const:``None``. + :returns: the same object if ``tz`` is :const:``None``. Else, a new :class:`.DateTime` that's the same point in time but in a different timezone. """ diff --git a/neo4j/time/_arithmetic.py b/neo4j/time/_arithmetic.py index 7e8cd76e7..6b93a6a6a 100644 --- a/neo4j/time/_arithmetic.py +++ b/neo4j/time/_arithmetic.py @@ -45,7 +45,7 @@ def nano_add(x, y): :param x: :param y: - :return: + :returns: """ return (int(1000000000 * x) + int(1000000000 * y)) / 1000000000 @@ -64,7 +64,7 @@ def nano_div(x, y): :param x: :param y: - :return: + :returns: """ return float(1000000000 * x) / int(1000000000 * y) @@ -79,7 +79,7 @@ def nano_divmod(x, y): :param x: :param y: - :return: + :returns: """ number = type(x) nx = int(1000000000 * x) @@ -124,7 +124,7 @@ def round_half_to_even(n): 5 :param n: - :return: + :returns: """ ten_n = 10 * n if ten_n == int(ten_n) and ten_n % 10 == 5: diff --git a/neo4j/work/query.py b/neo4j/work/query.py index a16fb5cc9..1b06687a0 100644 --- a/neo4j/work/query.py +++ b/neo4j/work/query.py @@ -26,7 +26,11 @@ class Query: - """ Create a new query. + """A query with attached extra data. + + This wrapper class for queries is used to attach extra data to queries + passed to :meth:`.Session.run` and :meth:`.AsyncSession.run`, fulfilling + a similar role as :func:`.unit_of_work` for transactions functions. :param text: The query text. :param metadata: metadata attached to the query. @@ -60,6 +64,7 @@ def unit_of_work( from neo4j import unit_of_work + @unit_of_work(timeout=100) def count_people_tx(tx): result = tx.run("MATCH (a:Person) RETURN count(a) AS persons") From 807f91e91685081f4d975e07e10e5da8af0bea2b Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Fri, 18 Nov 2022 12:38:52 +0100 Subject: [PATCH 2/3] Add cancellation to async session example --- docs/source/async_api.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/async_api.rst b/docs/source/async_api.rst index 4573a89f0..20c5746f3 100644 --- a/docs/source/async_api.rst +++ b/docs/source/async_api.rst @@ -282,6 +282,9 @@ To construct a :class:`neo4j.AsyncSession` use the :meth:`neo4j.AsyncDriver.sess try: result = await session.run("MATCH (a:Person) RETURN a.name AS name") names = [record["name"] async for record in result] + except asyncio.CancelledError: + session.cancel() + raise finally: await session.close() From 699de93119143c0bb3a15482297070691cc84af7 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 21 Nov 2022 17:05:44 +0100 Subject: [PATCH 3/3] Fix typo --- neo4j/_async/work/result.py | 2 +- neo4j/_sync/work/result.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/neo4j/_async/work/result.py b/neo4j/_async/work/result.py index e1af24e93..3f34721fd 100644 --- a/neo4j/_async/work/result.py +++ b/neo4j/_async/work/result.py @@ -582,7 +582,7 @@ async def data(self, *keys: _T_ResultKey) -> t.List[t.Dict[str, t.Any]]: useful for interactive sessions and rapid prototyping. For instance, node and relationship labels are not included. You will - have to implement a custom serialzer should you need more control over + have to implement a custom serializer should you need more control over the output format. :param keys: fields to return for each remaining record. Optionally filtering to include only certain values by index or key. diff --git a/neo4j/_sync/work/result.py b/neo4j/_sync/work/result.py index 2d2622cc5..8702b84c6 100644 --- a/neo4j/_sync/work/result.py +++ b/neo4j/_sync/work/result.py @@ -582,7 +582,7 @@ def data(self, *keys: _T_ResultKey) -> t.List[t.Dict[str, t.Any]]: useful for interactive sessions and rapid prototyping. For instance, node and relationship labels are not included. You will - have to implement a custom serialzer should you need more control over + have to implement a custom serializer should you need more control over the output format. :param keys: fields to return for each remaining record. Optionally filtering to include only certain values by index or key.