diff --git a/README.rst b/README.rst index 5f89113b..2a8ea716 100644 --- a/README.rst +++ b/README.rst @@ -47,99 +47,56 @@ Quick Example .. code-block:: python - from neo4j import GraphDatabase + from neo4j import GraphDatabase, RoutingControl - driver = GraphDatabase.driver("neo4j://localhost:7687", - auth=("neo4j", "password")) - def add_friend(tx, name, friend_name): - tx.run("MERGE (a:Person {name: $name}) " - "MERGE (a)-[:KNOWS]->(friend:Person {name: $friend_name})", - name=name, friend_name=friend_name) + URI = "neo4j://localhost:7687" + AUTH = ("neo4j", "password") - def print_friends(tx, name): - query = ("MATCH (a:Person)-[:KNOWS]->(friend) WHERE a.name = $name " - "RETURN friend.name ORDER BY friend.name") - for record in tx.run(query, name=name): - print(record["friend.name"]) - - with driver.session(database="neo4j") as session: - session.execute_write(add_friend, "Arthur", "Guinevere") - session.execute_write(add_friend, "Arthur", "Lancelot") - session.execute_write(add_friend, "Arthur", "Merlin") - session.execute_read(print_friends, "Arthur") - - driver.close() - - -Connection Settings Breaking Change (4.x) -========================================= - -+ The driver’s default configuration for encrypted is now false - (meaning that driver will only attempt plain text connections by default). - -+ Connections to encrypted services (such as Neo4j Aura) should now explicitly - be set to encrypted. - -+ When encryption is explicitly enabled, the default trust mode is to trust the - CAs that are trusted by operating system and use hostname verification. - -+ This means that encrypted connections to servers holding self-signed - certificates will now fail on certificate verification by default. - -+ Using the new ``neo4j+ssc`` scheme will allow to connect to servers holding self-signed certificates and not use hostname verification. - -+ The ``neo4j://`` scheme replaces ``bolt+routing://`` and can be used for both clustered and single-instance configurations with Neo4j 4.0. - - - -See, https://neo4j.com/docs/migration-guide/4.0/upgrade-driver/#upgrade-driver-breakingchanges + def add_friend(driver, name, friend_name): + driver.execute_query( + "MERGE (a:Person {name: $name}) " + "MERGE (friend:Person {name: $friend_name}) " + "MERGE (a)-[:KNOWS]->(friend)", + name=name, friend_name=friend_name, database_="neo4j", + ) -See, https://neo4j.com/docs/driver-manual/current/client-applications/#driver-connection-uris for changes in default security settings between 3.x and 4.x + def print_friends(driver, name): + records, _, _ = driver.execute_query( + "MATCH (a:Person)-[:KNOWS]->(friend) WHERE a.name = $name " + "RETURN friend.name ORDER BY friend.name", + name=name, database_="neo4j", routing_=RoutingControl.READ, + ) + for record in records: + print(record["friend.name"]) -Connecting with Python Driver 4.x to Neo4j 3.5 (EOL) ----------------------------------------------------- - -Using the Python Driver 4.x and connecting to Neo4j 3.5 with default connection settings for Neo4j 3.5. - -.. code-block:: python - - # the preferred form - - driver = GraphDatabase.driver("neo4j+ssc://localhost:7687", auth=("neo4j", "password")) - - # is equivalent to - - driver = GraphDatabase.driver("neo4j://localhost:7687", auth=("neo4j", "password"), encrypted=True, trust=False) - - -Connecting with Python Driver 1.7 (EOL) to Neo4j 4.x ----------------------------------------------------- - -Using the Python Driver 1.7 and connecting to Neo4j 4.x with default connection settings for Neo4j 4.x. - -.. code-block:: python - driver = GraphDatabase.driver("neo4j://localhost:7687", auth=("neo4j", "password"), encrypted=False) + with GraphDatabase.driver(URI, auth=AUTH) as driver: + add_friend(driver, "Arthur", "Guinevere") + add_friend(driver, "Arthur", "Lancelot") + add_friend(driver, "Arthur", "Merlin") + print_friends(driver, "Arthur") -Other Information -================= +Further Information +=================== -* `The Neo4j Operations Manual`_ -* `The Neo4j Drivers Manual`_ -* `Python Driver API Documentation`_ -* `Neo4j Cypher Refcard`_ -* `Example Project`_ +* `The Neo4j Operations Manual`_ (docs on how to run a Neo4j server) +* `The Neo4j Python Driver Manual`_ (good introduction to this driver) +* `Python Driver API Documentation`_ (full API documentation for this driver) +* `Neo4j Cypher Cheat Sheet`_ (summary of Cypher syntax - Neo4j's graph query language) +* `Example Project`_ (small web application using this driver) +* `GraphAcademy`_ (interactive, free online trainings for Neo4j) * `Driver Wiki`_ (includes change logs) -* `Neo4j 4.0 Migration Guide`_ +* `Neo4j Migration Guide`_ .. _`The Neo4j Operations Manual`: https://neo4j.com/docs/operations-manual/current/ -.. _`The Neo4j Drivers Manual`: https://neo4j.com/docs/driver-manual/current/ +.. _`The Neo4j Python Driver Manual`: https://neo4j.com/docs/python-manual/current/ .. _`Python Driver API Documentation`: https://neo4j.com/docs/api/python-driver/current/ -.. _`Neo4j Cypher Refcard`: https://neo4j.com/docs/cypher-refcard/current/ +.. _`Neo4j Cypher Cheat Sheet`: https://neo4j.com/docs/cypher-cheat-sheet/ .. _`Example Project`: https://github.com/neo4j-examples/movies-python-bolt +.. _`GraphAcademy`: https://graphacademy.neo4j.com/categories/python/ .. _`Driver Wiki`: https://github.com/neo4j/neo4j-python-driver/wiki -.. _`Neo4j 4.0 Migration Guide`: https://neo4j.com/docs/migration-guide/4.0/ +.. _`Neo4j Migration Guide`: https://neo4j.com/docs/migration-guide/current/ diff --git a/docs/source/api.rst b/docs/source/api.rst index 970d08d2..c9fd9d59 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1030,7 +1030,7 @@ See :class:`.BookmarkManager` for more information. ``auth`` -------- Optional :class:`neo4j.Auth` or ``(user, password)``-tuple. Use this overwrite the -authentication information for the session. +authentication information for the session (user-switching). This requires the server to support re-authentication on the protocol level. You can check this by calling :meth:`.Driver.supports_session_auth` / :meth:`.AsyncDriver.supports_session_auth`. diff --git a/docs/source/index.rst b/docs/source/index.rst index 3e334931..79e53a16 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -69,6 +69,14 @@ To install the latest pre-release, use: python -m pip install --pre neo4j +.. TODO: 7.0 - remove this note + +.. note:: + + ``neo4j-driver`` is the old name for this package. It is now deprecated and + and will receive no further updates starting with 6.0.0. Make sure to + install ``neo4j`` as shown above. + .. note:: It is always recommended to install python packages for user space in a virtual environment. @@ -100,60 +108,40 @@ To deactivate the current active virtual environment, use: Quick Example ************* -Creating nodes and relationships. - .. code-block:: python - from neo4j import GraphDatabase + from neo4j import GraphDatabase, RoutingControl URI = "neo4j://localhost:7687" AUTH = ("neo4j", "password") - def create_person(tx, name): - tx.run("CREATE (a:Person {name: $name})", name=name) + def add_friend(driver, name, friend_name): + driver.execute_query( + "MERGE (a:Person {name: $name}) " + "MERGE (friend:Person {name: $friend_name}) " + "MERGE (a)-[:KNOWS]->(friend)", + name=name, friend_name=friend_name, database_="neo4j", + ) - 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) + def print_friends(driver, name): + records, _, _ = driver.execute_query( + "MATCH (a:Person)-[:KNOWS]->(friend) WHERE a.name = $name " + "RETURN friend.name ORDER BY friend.name", + name=name, database_="neo4j", routing_=RoutingControl.READ, + ) + for record in records: + print(record["friend.name"]) 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") - + add_friend(driver, "Arthur", "Guinevere") + add_friend(driver, "Arthur", "Lancelot") + add_friend(driver, "Arthur", "Merlin") + print_friends(driver, "Arthur") -Finding nodes. - -.. code-block:: python - - from neo4j import GraphDatabase - - - URI = "neo4j://localhost:7687" - AUTH = ("neo4j", "password") - - - def get_friends_of(tx, name): - friends = [] - result = tx.run("MATCH (a:Person)-[:KNOWS]->(f) " - "WHERE a.name = $name " - "RETURN f.name AS friend", name=name) - for record in result: - friends.append(record["friend"]) - return friends - - - 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) ******************* @@ -163,78 +151,87 @@ Example Application .. code-block:: python import logging - from neo4j import GraphDatabase - from neo4j.exceptions import ServiceUnavailable + + from neo4j import GraphDatabase, RoutingControl + from neo4j.exceptions import DriverError, Neo4jError class App: - def __init__(self, uri, user, password): + def __init__(self, uri, user, password, database=None): self.driver = GraphDatabase.driver(uri, auth=(user, password)) + self.database = database def close(self): - # Don't forget to close the driver connection when you are finished with it + # Don't forget to close the driver connection when you are finished + # with it self.driver.close() def create_friendship(self, person1_name, person2_name): with self.driver.session() as session: - # Write transactions allow the driver to handle retries and transient errors - result = session.execute_write( - self._create_and_return_friendship, person1_name, person2_name) - for record in result: - print("Created friendship between: {p1}, {p2}".format( - p1=record['p1'], p2=record['p2'])) + # Write transactions allow the driver to handle retries and + # transient errors + result = self._create_and_return_friendship( + person1_name, person2_name + ) + print("Created friendship between: " + f"{result['p1']}, {result['p2']}") - @staticmethod - def _create_and_return_friendship(tx, person1_name, person2_name): + def _create_and_return_friendship(self, person1_name, person2_name): # To learn more about the Cypher syntax, # see https://neo4j.com/docs/cypher-manual/current/ - # The Reference Card is also a good resource for keywords, - # see https://neo4j.com/docs/cypher-refcard/current/ + # The Cheat Sheet is also a good resource for keywords, + # see https://neo4j.com/docs/cypher-cheat-sheet/ query = ( "CREATE (p1:Person { name: $person1_name }) " "CREATE (p2:Person { name: $person2_name }) " "CREATE (p1)-[:KNOWS]->(p2) " - "RETURN p1, p2" + "RETURN p1.name, p2.name" ) - result = tx.run(query, person1_name=person1_name, person2_name=person2_name) try: - return [{"p1": record["p1"]["name"], "p2": record["p2"]["name"]} - for record in result] + record = self.driver.execute_query( + query, person1_name=person1_name, person2_name=person2_name, + database_=self.database, + result_transformer_=lambda r: r.single(strict=True) + ) + return {"p1": record["p1.name"], "p2": record["p2.name"]} # Capture any errors along with the query and data for traceability - except ServiceUnavailable as exception: - logging.error("{query} raised an error: \n {exception}".format( - query=query, exception=exception)) + except (DriverError, Neo4jError) as exception: + logging.error("%s raised an error: \n%s", query, exception) raise def find_person(self, person_name): - with self.driver.session() as session: - result = session.execute_read(self._find_and_return_person, person_name) - for record in result: - print("Found person: {record}".format(record=record)) + names = self._find_and_return_person(person_name) + for name in names: + print(f"Found person: {name}") - @staticmethod - def _find_and_return_person(tx, person_name): + def _find_and_return_person(self, person_name): query = ( "MATCH (p:Person) " "WHERE p.name = $person_name " "RETURN p.name AS name" ) - result = tx.run(query, person_name=person_name) - return [record["name"] for record in result] + names = self.driver.execute_query( + query, person_name=person_name, + database_=self.database, routing_=RoutingControl.READ, + result_transformer_=lambda r: r.value("name") + ) + return names if __name__ == "__main__": - # See https://neo4j.com/developer/aura-connect-driver/ for Aura specific connection URL. + # For Aura specific connection URI, + # see https://neo4j.com/developer/aura-connect-driver/ . scheme = "neo4j" # Connecting to Aura, use the "neo4j+s" URI scheme host_name = "example.com" port = 7687 - url = f"{scheme}://{host_name}:{port}" + uri = f"{scheme}://{host_name}:{port}" user = "" password = "" - app = App(url, user, password) + database = "neo4j" + app = App(uri, user, password, database) try: app.create_friendship("Alice", "David") app.find_person("Alice") @@ -242,20 +239,24 @@ Example Application app.close() -***************** -Other Information -***************** +******************* +Further Information +******************* -* `Neo4j Documentation`_ -* `The Neo4j Drivers Manual`_ -* `Cypher Cheat Sheet`_ -* `Example Project`_ +* `The Neo4j Operations Manual`_ (docs on how to run a Neo4j server) +* `The Neo4j Python Driver Manual`_ (good introduction to this driver) +* `Python Driver API Documentation`_ (full API documentation for this driver) +* `Neo4j Cypher Cheat Sheet`_ (summary of Cypher syntax - Neo4j's graph query language) +* `Example Project`_ (small web application using this driver) +* `GraphAcademy`_ (interactive, free online trainings for Neo4j) * `Driver Wiki`_ (includes change logs) -* `Neo4j Aura`_ +* `Neo4j Migration Guide`_ -.. _`Neo4j Documentation`: https://neo4j.com/docs/ -.. _`The Neo4j Drivers Manual`: https://neo4j.com/docs/driver-manual/current/ -.. _`Cypher Cheat Sheet`: https://neo4j.com/docs/cypher-cheat-sheet/current/ +.. _`The Neo4j Operations Manual`: https://neo4j.com/docs/operations-manual/current/ +.. _`The Neo4j Python Driver Manual`: https://neo4j.com/docs/python-manual/current/ +.. _`Python Driver API Documentation`: https://neo4j.com/docs/api/python-driver/current/ +.. _`Neo4j Cypher Cheat Sheet`: https://neo4j.com/docs/cypher-cheat-sheet/ .. _`Example Project`: https://github.com/neo4j-examples/movies-python-bolt +.. _`GraphAcademy`: https://graphacademy.neo4j.com/categories/python/ .. _`Driver Wiki`: https://github.com/neo4j/neo4j-python-driver/wiki -.. _`Neo4j Aura`: https://neo4j.com/neo4j-aura/ +.. _`Neo4j Migration Guide`: https://neo4j.com/docs/migration-guide/current/ diff --git a/src/neo4j/_async/auth_management.py b/src/neo4j/_async/auth_management.py index e423ddac..6a32d7b4 100644 --- a/src/neo4j/_async/auth_management.py +++ b/src/neo4j/_async/auth_management.py @@ -149,9 +149,11 @@ def expiration_based( The provider function **must not** interact with the driver in any way as this can cause deadlocks and undefined behaviour. - The provider function only ever return auth information belonging - to the same identity. + The provider function must only ever return auth information + belonging to the same identity. Switching identities is undefined behavior. + You may use session-level authentication for such use-cases + :ref:`session-auth-ref`. Example:: diff --git a/src/neo4j/_auth_management.py b/src/neo4j/_auth_management.py index c4f7b259..77a00461 100644 --- a/src/neo4j/_auth_management.py +++ b/src/neo4j/_auth_management.py @@ -110,6 +110,8 @@ class AuthManager(metaclass=abc.ABCMeta): The token returned must always belong to the same identity. Switching identities using the `AuthManager` is undefined behavior. + You may use session-level authentication for such use-cases + :ref:`session-auth-ref`. **This is a preview** (see :ref:`filter-warnings-ref`). It might be changed without following the deprecation policy. @@ -132,6 +134,8 @@ def get_auth(self) -> _TAuth: The method must only ever return auth information belonging to the same identity. Switching identities using the `AuthManager` is undefined behavior. + You may use session-level authentication for such use-cases + :ref:`session-auth-ref`. """ ... diff --git a/src/neo4j/_sync/auth_management.py b/src/neo4j/_sync/auth_management.py index f54c7078..d57108a6 100644 --- a/src/neo4j/_sync/auth_management.py +++ b/src/neo4j/_sync/auth_management.py @@ -149,9 +149,11 @@ def expiration_based( The provider function **must not** interact with the driver in any way as this can cause deadlocks and undefined behaviour. - The provider function only ever return auth information belonging - to the same identity. + The provider function must only ever return auth information + belonging to the same identity. Switching identities is undefined behavior. + You may use session-level authentication for such use-cases + :ref:`session-auth-ref`. Example:: diff --git a/tests/integration/test_readme.py b/tests/integration/test_readme.py index 7bebde9a..30b2bc41 100644 --- a/tests/integration/test_readme.py +++ b/tests/integration/test_readme.py @@ -16,55 +16,57 @@ # limitations under the License. +import re from pathlib import Path # python -m pytest tests/integration/test_readme.py -s -v -def _work(tx, query, **params): - tx.run(query, **params).consume() - - def test_should_run_readme(uri, auth): names = set() print = names.add # === START: README === - from neo4j import GraphDatabase + from neo4j import GraphDatabase, RoutingControl # isort:skip - driver = GraphDatabase.driver("neo4j://localhost:7687", - auth=("neo4j", "password")) - # === END: README === - driver.close() - driver = GraphDatabase.driver(uri, auth=auth) - # === START: README === - def add_friend(tx, name, friend_name): - tx.run("MERGE (a:Person {name: $name}) " - "MERGE (a)-[:KNOWS]->(friend:Person {name: $friend_name})", - name=name, friend_name=friend_name) + URI = "neo4j://localhost:7687" + AUTH = ("neo4j", "password") + - def print_friends(tx, name): - query = ("MATCH (a:Person)-[:KNOWS]->(friend) WHERE a.name = $name " - "RETURN friend.name ORDER BY friend.name") - for record in tx.run(query, name=name): + def add_friend(driver, name, friend_name): + driver.execute_query( + "MERGE (a:Person {name: $name}) " + "MERGE (friend:Person {name: $friend_name}) " + "MERGE (a)-[:KNOWS]->(friend)", + name=name, friend_name=friend_name, database_="neo4j", + ) + + + def print_friends(driver, name): + records, _, _ = driver.execute_query( + "MATCH (a:Person)-[:KNOWS]->(friend) WHERE a.name = $name " + "RETURN friend.name ORDER BY friend.name", + name=name, database_="neo4j", routing_=RoutingControl.READ, + ) + for record in records: print(record["friend.name"]) - with driver.session(database="neo4j") as session: + + with GraphDatabase.driver(URI, auth=AUTH) as driver: # === END: README === - session.execute_write(_work, "MATCH (a) DETACH DELETE a") + pass + with GraphDatabase.driver(uri, auth=auth) as driver: + driver.execute_query("MATCH (a) DETACH DELETE a") # === START: README === - session.execute_write(add_friend, "Arthur", "Guinevere") - session.execute_write(add_friend, "Arthur", "Lancelot") - session.execute_write(add_friend, "Arthur", "Merlin") - session.execute_read(print_friends, "Arthur") + add_friend(driver, "Arthur", "Guinevere") + add_friend(driver, "Arthur", "Lancelot") + add_friend(driver, "Arthur", "Merlin") + print_friends(driver, "Arthur") # === END: README === - session.execute_write(_work, "MATCH (a) DETACH DELETE a") - # === START: README === + driver.execute_query("MATCH (a) DETACH DELETE a") - driver.close() - # === END: README === assert names == {"Guinevere", "Lancelot", "Merlin"} @@ -88,6 +90,7 @@ def test_readme_contains_example(): adding = False continue if adding: + line = re.sub(r"\s+# isort:skip\s+$", "\n", line) stripped_test_content += line assert stripped_test_content in readme_content