Skip to content

Commit 52d3a94

Browse files
authored
General API docs improvements (#858)
Including * more details * better examples * code formatting * more consistent docstring format * demonstrate how to handle async cancellation
1 parent 87b8f6d commit 52d3a94

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+570
-499
lines changed

docs/source/api.rst

Lines changed: 137 additions & 87 deletions
Large diffs are not rendered by default.

docs/source/async_api.rst

Lines changed: 94 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The :class:`neo4j.AsyncDriver` construction is done via a ``classmethod`` on the
3333
3434
from neo4j import AsyncGraphDatabase
3535
36+
3637
async def main():
3738
uri = "neo4j://example.com:7687"
3839
driver = AsyncGraphDatabase.driver(uri, auth=("neo4j", "password"))
@@ -59,12 +60,12 @@ The :class:`neo4j.AsyncDriver` construction is done via a ``classmethod`` on the
5960
6061
from neo4j import AsyncGraphDatabase
6162
63+
6264
async def main():
6365
uri = "neo4j://example.com:7687"
6466
auth = ("neo4j", "password")
6567
async with AsyncGraphDatabase.driver(uri, auth=auth) as driver:
66-
# use the driver
67-
...
68+
... # use the driver
6869
6970
asyncio.run(main())
7071
@@ -164,6 +165,7 @@ For example:
164165
165166
from neo4j import AsyncGraphDatabase
166167
168+
167169
async def custom_resolver(socket_address):
168170
if socket_address == ("example.com", 9999):
169171
yield "::1", 7687
@@ -172,16 +174,18 @@ For example:
172174
from socket import gaierror
173175
raise gaierror("Unexpected socket address %r" % socket_address)
174176
177+
175178
# alternatively
176179
def custom_resolver(socket_address):
177180
...
178181
182+
179183
driver = AsyncGraphDatabase.driver("neo4j://example.com:9999",
180184
auth=("neo4j", "password"),
181185
resolver=custom_resolver)
182186
183187
184-
:Default: ``None``
188+
:Default: :const:`None`
185189

186190

187191

@@ -196,6 +200,7 @@ For example:
196200
197201
from neo4j import AsyncGraphDatabase
198202
203+
199204
class Application:
200205
201206
def __init__(self, uri, user, password)
@@ -206,7 +211,7 @@ For example:
206211
207212
Connection details held by the :class:`neo4j.AsyncDriver` are immutable.
208213
Therefore if, for example, a password is changed, a replacement :class:`neo4j.AsyncDriver` object must be created.
209-
More than one :class:`.AsyncDriver` may be required if connections to multiple databases, or connections as multiple users, are required,
214+
More than one :class:`.AsyncDriver` may be required if connections to multiple remotes, or connections as multiple users, are required,
210215
unless when using impersonation (:ref:`impersonated-user-ref`).
211216

212217
:class:`neo4j.AsyncDriver` objects are safe to be used in concurrent coroutines.
@@ -270,13 +275,18 @@ To construct a :class:`neo4j.AsyncSession` use the :meth:`neo4j.AsyncDriver.sess
270275
271276
from neo4j import AsyncGraphDatabase
272277
278+
273279
async def main():
274-
driver = AsyncGraphDatabase(uri, auth=(user, password))
275-
session = driver.session()
276-
result = await session.run("MATCH (a:Person) RETURN a.name AS name")
277-
names = [record["name"] async for record in result]
278-
await session.close()
279-
await driver.close()
280+
async with AsyncGraphDatabase(uri, auth=(user, password)) as driver:
281+
session = driver.session()
282+
try:
283+
result = await session.run("MATCH (a:Person) RETURN a.name AS name")
284+
names = [record["name"] async for record in result]
285+
except asyncio.CancelledError:
286+
session.cancel()
287+
raise
288+
finally:
289+
await session.close()
280290
281291
asyncio.run(main())
282292
@@ -289,7 +299,7 @@ properly even when an exception is raised.
289299
290300
async with driver.session() as session:
291301
result = await session.run("MATCH (a:Person) RETURN a.name AS name")
292-
# do something with the result...
302+
... # do something with the result
293303
294304
295305
Sessions will often be created with some configuration settings, see :ref:`async-session-configuration-ref`.
@@ -299,7 +309,7 @@ Sessions will often be created with some configuration settings, see :ref:`async
299309
async with driver.session(database="example_database",
300310
fetch_size=100) as session:
301311
result = await session.run("MATCH (a:Person) RETURN a.name AS name")
302-
# do something with the result...
312+
... # do something with the result
303313
304314
305315
************
@@ -315,7 +325,9 @@ AsyncSession
315325
This introduces concurrency and can lead to undefined behavior as
316326
:class:`AsyncSession` is not concurrency-safe.
317327

318-
Consider this **wrong** example::
328+
Consider this **wrong** example
329+
330+
.. code-block:: python
319331
320332
async def dont_do_this(driver):
321333
async with driver.session() as session:
@@ -330,7 +342,9 @@ AsyncSession
330342

331343
In this particular example, the problem could be solved by shielding
332344
the whole coroutine ``dont_do_this`` instead of only the
333-
``session.run``. Like so::
345+
``session.run``. Like so
346+
347+
.. code-block:: python
334348
335349
async def thats_better(driver):
336350
async def inner()
@@ -426,30 +440,32 @@ Auto-commit transactions are also the only way to run ``PERIODIC COMMIT``
426440
newer) statements, since those Cypher clauses manage their own transactions
427441
internally.
428442

429-
Example:
443+
Write example:
430444

431445
.. code-block:: python
432446
433447
import neo4j
434448
449+
435450
async def create_person(driver, name):
436-
async with driver.session(
437-
default_access_mode=neo4j.WRITE_ACCESS
438-
) as session:
451+
# default_access_mode defaults to WRITE_ACCESS
452+
async with driver.session(database="neo4j") as session:
439453
query = "CREATE (a:Person { name: $name }) RETURN id(a) AS node_id"
440454
result = await session.run(query, name=name)
441455
record = await result.single()
442456
return record["node_id"]
443457
444-
Example:
458+
Read example:
445459

446460
.. code-block:: python
447461
448462
import neo4j
449463
464+
450465
async def get_numbers(driver):
451466
numbers = []
452467
async with driver.session(
468+
database="neo4j",
453469
default_access_mode=neo4j.READ_ACCESS
454470
) as session:
455471
result = await session.run("UNWIND [1, 2, 3] AS x RETURN x")
@@ -460,8 +476,8 @@ Example:
460476
461477
.. _async-explicit-transactions-ref:
462478

463-
Explicit Async Transactions
464-
===========================
479+
Explicit Transactions (Unmanaged Transactions)
480+
==============================================
465481
Explicit transactions support multiple statements and must be created with an explicit :meth:`neo4j.AsyncSession.begin_transaction` call.
466482

467483
This creates a new :class:`neo4j.AsyncTransaction` object that can be used to run Cypher.
@@ -485,41 +501,74 @@ It also gives applications the ability to directly control ``commit`` and ``roll
485501
Closing an explicit transaction can either happen automatically at the end of a ``async with`` block,
486502
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.
487503

488-
Explicit transactions are most useful for applications that need to distribute Cypher execution across multiple functions for the same transaction.
504+
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.
489505

490506
Example:
491507

492508
.. code-block:: python
493509
510+
import asyncio
511+
494512
import neo4j
495513
496-
async def create_person(driver, name):
514+
515+
async def transfer_to_other_bank(driver, customer_id, other_bank_id, amount):
497516
async with driver.session(
517+
database="neo4j",
518+
# optional, defaults to WRITE_ACCESS
498519
default_access_mode=neo4j.WRITE_ACCESS
499520
) as session:
500521
tx = await session.begin_transaction()
501-
node_id = await create_person_node(tx)
502-
await set_person_name(tx, node_id, name)
503-
await tx.commit()
504-
505-
async def create_person_node(tx):
506-
query = "CREATE (a:Person { name: $name }) RETURN id(a) AS node_id"
507-
name = "default_name"
508-
result = await tx.run(query, name=name)
509-
record = await result.single()
510-
return record["node_id"]
511-
512-
async def set_person_name(tx, node_id, name):
513-
query = "MATCH (a:Person) WHERE id(a) = $id SET a.name = $name"
514-
result = await tx.run(query, id=node_id, name=name)
515-
summary = await result.consume()
516-
# use the summary for logging etc.
522+
# or just use a `with` context instead of try/excpet/finally
523+
try:
524+
if not await customer_balance_check(tx, customer_id, amount):
525+
# give up
526+
return
527+
await other_bank_transfer_api(customer_id, other_bank_id, amount)
528+
# Now the money has been transferred
529+
# => we can't retry or rollback anymore
530+
try:
531+
await decrease_customer_balance(tx, customer_id, amount)
532+
await tx.commit()
533+
except Exception as e:
534+
request_inspection(customer_id, other_bank_id, amount, e)
535+
raise
536+
except asyncio.CancelledError:
537+
tx.cancel()
538+
raise
539+
finally:
540+
await tx.close() # rolls back if not yet committed
541+
542+
543+
async def customer_balance_check(tx, customer_id, amount):
544+
query = ("MATCH (c:Customer {id: $id}) "
545+
"RETURN c.balance >= $amount AS sufficient")
546+
result = await tx.run(query, id=customer_id, amount=amount)
547+
record = await result.single(strict=True)
548+
return record["sufficient"]
549+
550+
551+
async def other_bank_transfer_api(customer_id, other_bank_id, amount):
552+
... # make some API call to other bank
553+
554+
555+
async def decrease_customer_balance(tx, customer_id, amount):
556+
query = ("MATCH (c:Customer {id: $id}) "
557+
"SET c.balance = c.balance - $amount")
558+
await tx.run(query, id=customer_id, amount=amount)
559+
560+
561+
def request_inspection(customer_id, other_bank_id, amount, e):
562+
# manual cleanup required; log this or similar
563+
print("WARNING: transaction rolled back due to exception:", repr(e))
564+
print("customer_id:", customer_id, "other_bank_id:", other_bank_id,
565+
"amount:", amount)
517566
518567
.. _async-managed-transactions-ref:
519568

520569

521-
Managed Async Transactions (`transaction functions`)
522-
====================================================
570+
Managed Transactions (`transaction functions`)
571+
==============================================
523572
Transaction functions are the most powerful form of transaction, providing access mode override and retry capabilities.
524573

525574
+ :meth:`neo4j.AsyncSession.execute_write`
@@ -530,7 +579,7 @@ This function is called one or more times, within a configurable time limit, unt
530579
Results should be fully consumed within the function and only aggregate or status values should be returned.
531580
Returning a live result object would prevent the driver from correctly managing connections and would break retry guarantees.
532581

533-
This function will receive a :class:`neo4j.AsyncManagedTransaction` object as its first parameter.
582+
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`.
534583

535584
.. autoclass:: neo4j.AsyncManagedTransaction()
536585

@@ -544,8 +593,10 @@ Example:
544593
async with driver.session() as session:
545594
node_id = await session.execute_write(create_person_tx, name)
546595
596+
547597
async def create_person_tx(tx, name):
548-
query = "CREATE (a:Person { name: $name }) RETURN id(a) AS node_id"
598+
query = ("CREATE (a:Person {name: $name, id: randomUUID()}) "
599+
"RETURN a.id AS node_id")
549600
result = await tx.run(query, name=name)
550601
record = await result.single()
551602
return record["node_id"]

0 commit comments

Comments
 (0)