From c3221457fdbaa156281b0a1d631aefbe2a25d843 Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Tue, 29 Aug 2023 15:54:44 +0200 Subject: [PATCH 01/15] Add option to pass driver instead of database_url - WIP --- neomodel/config.py | 6 + neomodel/scripts/neomodel_install_labels.py | 10 +- neomodel/scripts/neomodel_remove_labels.py | 12 +- neomodel/util.py | 122 ++++++++++++-------- test/test_connection.py | 15 +-- test/test_database_management.py | 8 +- test/test_transactions.py | 21 ++-- 7 files changed, 115 insertions(+), 79 deletions(-) diff --git a/neomodel/config.py b/neomodel/config.py index 1f6df10b..5c882b1f 100644 --- a/neomodel/config.py +++ b/neomodel/config.py @@ -16,3 +16,9 @@ RESOLVER = None TRUSTED_CERTIFICATES = neo4j.TrustSystemCAs() USER_AGENT = f"neomodel/v{__version__}" + +DRIVER = neo4j.GraphDatabase().driver( + "bolt://localhost:7687", auth=("neo4j", "foobarbaz") +) +# TODO : Try passing a different database name +# DATABASE_NAME = "testdatabase" diff --git a/neomodel/scripts/neomodel_install_labels.py b/neomodel/scripts/neomodel_install_labels.py index 444838b2..8bd5119f 100755 --- a/neomodel/scripts/neomodel_install_labels.py +++ b/neomodel/scripts/neomodel_install_labels.py @@ -27,8 +27,8 @@ from __future__ import print_function import sys -from argparse import ArgumentParser, RawDescriptionHelpFormatter import textwrap +from argparse import ArgumentParser, RawDescriptionHelpFormatter from importlib import import_module from os import environ, path @@ -70,14 +70,16 @@ def load_python_module_or_file(name): def main(): parser = ArgumentParser( formatter_class=RawDescriptionHelpFormatter, - description=textwrap.dedent(""" + description=textwrap.dedent( + """ Setup indexes and constraints on labels in Neo4j for your neomodel schema. If a connection URL is not specified, the tool will look up the environment variable NEO4J_BOLT_URL. If that environment variable is not set, the tool will attempt to connect to the default URL bolt://neo4j:neo4j@localhost:7687 """ - )) + ), + ) parser.add_argument( "apps", @@ -107,7 +109,7 @@ def main(): # Connect after to override any code in the module that may set the connection print(f"Connecting to {bolt_url}") - db.set_connection(bolt_url) + db.set_connection(url=bolt_url) install_all_labels() diff --git a/neomodel/scripts/neomodel_remove_labels.py b/neomodel/scripts/neomodel_remove_labels.py index 58a57cdd..1ad6cc34 100755 --- a/neomodel/scripts/neomodel_remove_labels.py +++ b/neomodel/scripts/neomodel_remove_labels.py @@ -23,8 +23,8 @@ """ from __future__ import print_function -from argparse import ArgumentParser, RawDescriptionHelpFormatter import textwrap +from argparse import ArgumentParser, RawDescriptionHelpFormatter from os import environ from .. import db, remove_all_labels @@ -32,15 +32,17 @@ def main(): parser = ArgumentParser( - formatter_class=RawDescriptionHelpFormatter, - description=textwrap.dedent(""" + formatter_class=RawDescriptionHelpFormatter, + description=textwrap.dedent( + """ Drop all indexes and constraints on labels from schema in Neo4j database. If a connection URL is not specified, the tool will look up the environment variable NEO4J_BOLT_URL. If that environment variable is not set, the tool will attempt to connect to the default URL bolt://neo4j:neo4j@localhost:7687 """ - )) + ), + ) parser.add_argument( "--db", @@ -59,7 +61,7 @@ def main(): # Connect after to override any code in the module that may set the connection print(f"Connecting to {bolt_url}") - db.set_connection(bolt_url) + db.set_connection(url=bolt_url) remove_all_labels() diff --git a/neomodel/util.py b/neomodel/util.py index c03a1ce2..ccceb608 100644 --- a/neomodel/util.py +++ b/neomodel/util.py @@ -7,7 +7,7 @@ from typing import Optional, Sequence from urllib.parse import quote, unquote, urlparse -from neo4j import DEFAULT_DATABASE, GraphDatabase, basic_auth +from neo4j import DEFAULT_DATABASE, Driver, GraphDatabase, basic_auth from neo4j.api import Bookmarks from neo4j.exceptions import ClientError, ServiceUnavailable, SessionExpired from neo4j.graph import Node, Path, Relationship @@ -33,8 +33,11 @@ def wrapper(self, *args, **kwargs): else: _db = self - if not _db.url: - _db.set_connection(config.DATABASE_URL) + if not _db.driver: + if config.DRIVER: + _db.set_connection(driver=config.DRIVER) + elif config.DATABASE_URL: + _db.set_connection(url=config.DATABASE_URL) return func(self, *args, **kwargs) @@ -78,65 +81,85 @@ def __init__(self): self._database_edition = None self.impersonated_user = None - def set_connection(self, url): + def set_connection(self, url: str = None, driver: Driver = None): """ Sets the connection URL to the address a Neo4j server is set up at """ - p_start = url.replace(":", "", 1).find(":") + 2 - p_end = url.rfind("@") - password = url[p_start:p_end] - url = url.replace(password, quote(password)) - parsed_url = urlparse(url) - - valid_schemas = [ - "bolt", - "bolt+s", - "bolt+ssc", - "bolt+routing", - "neo4j", - "neo4j+s", - "neo4j+ssc", - ] - - if parsed_url.netloc.find("@") > -1 and parsed_url.scheme in valid_schemas: - credentials, hostname = parsed_url.netloc.rsplit("@", 1) - username, password = credentials.split(":") - password = unquote(password) - database_name = parsed_url.path.strip("/") - else: - raise ValueError( - f"Expecting url format: bolt://user:password@localhost:7687 got {url}" + if driver: + self.driver = driver + if hasattr(config, "DATABASE_NAME"): + self._database_name = config.DATABASE_NAME + elif url: + p_start = url.replace(":", "", 1).find(":") + 2 + p_end = url.rfind("@") + password = url[p_start:p_end] + url = url.replace(password, quote(password)) + parsed_url = urlparse(url) + + valid_schemas = [ + "bolt", + "bolt+s", + "bolt+ssc", + "bolt+routing", + "neo4j", + "neo4j+s", + "neo4j+ssc", + ] + + if parsed_url.netloc.find("@") > -1 and parsed_url.scheme in valid_schemas: + credentials, hostname = parsed_url.netloc.rsplit("@", 1) + username, password = credentials.split(":") + password = unquote(password) + database_name = parsed_url.path.strip("/") + else: + raise ValueError( + f"Expecting url format: bolt://user:password@localhost:7687 got {url}" + ) + + options = { + "auth": basic_auth(username, password), + "connection_acquisition_timeout": config.CONNECTION_ACQUISITION_TIMEOUT, + "connection_timeout": config.CONNECTION_TIMEOUT, + "keep_alive": config.KEEP_ALIVE, + "max_connection_lifetime": config.MAX_CONNECTION_LIFETIME, + "max_connection_pool_size": config.MAX_CONNECTION_POOL_SIZE, + "max_transaction_retry_time": config.MAX_TRANSACTION_RETRY_TIME, + "resolver": config.RESOLVER, + "user_agent": config.USER_AGENT, + } + + if "+s" not in parsed_url.scheme: + options["encrypted"] = config.ENCRYPTED + options["trusted_certificates"] = config.TRUSTED_CERTIFICATES + + self.driver = GraphDatabase.driver( + parsed_url.scheme + "://" + hostname, **options + ) + self.url = url + self._database_name = ( + DEFAULT_DATABASE if database_name == "" else database_name ) - options = { - "auth": basic_auth(username, password), - "connection_acquisition_timeout": config.CONNECTION_ACQUISITION_TIMEOUT, - "connection_timeout": config.CONNECTION_TIMEOUT, - "keep_alive": config.KEEP_ALIVE, - "max_connection_lifetime": config.MAX_CONNECTION_LIFETIME, - "max_connection_pool_size": config.MAX_CONNECTION_POOL_SIZE, - "max_transaction_retry_time": config.MAX_TRANSACTION_RETRY_TIME, - "resolver": config.RESOLVER, - "user_agent": config.USER_AGENT, - } - - if "+s" not in parsed_url.scheme: - options["encrypted"] = config.ENCRYPTED - options["trusted_certificates"] = config.TRUSTED_CERTIFICATES - - self.driver = GraphDatabase.driver( - parsed_url.scheme + "://" + hostname, **options - ) - self.url = url self._pid = os.getpid() self._active_transaction = None - self._database_name = DEFAULT_DATABASE if database_name == "" else database_name # Getting the information about the database version requires a connection to the database self._database_version = None self._database_edition = None self._update_database_version() + def close_connection(self): + """ + Closes the currently open driver. + The driver should always be called at the end of the application's lifecyle. + If you pass your own driver to neomodel, you can also close it yourself without this method. + """ + self._database_version = None + self._database_edition = None + self._database_name = None + self.driver.close() + self.driver = None + @property def database_version(self): if self._database_version is None: @@ -420,6 +443,7 @@ def _run_cypher_query( raise exc_info[1].with_traceback(exc_info[2]) except SessionExpired: if retry_on_session_expire: + # TODO : What about if config passes driver instead of url ? self.set_connection(self.url) return self.cypher_query( query=query, diff --git a/test/test_connection.py b/test/test_connection.py index 702a4122..7a199139 100644 --- a/test/test_connection.py +++ b/test/test_connection.py @@ -5,16 +5,14 @@ from neomodel import config, db -INITIAL_URL = db.url - @pytest.fixture(autouse=True) def setup_teardown(): yield # Teardown actions after tests have run # Reconnect to initial URL for potential subsequent tests - db.driver.close() - db.set_connection(INITIAL_URL) + db.close_connection() + db.set_connection(url=config.DATABASE_URL) @pytest.fixture(autouse=True, scope="session") @@ -27,7 +25,7 @@ def neo4j_logging(): def test_connect_to_aura(protocol): cypher_return = "hello world" default_cypher_query = f"RETURN '{cypher_return}'" - db.driver.close() + db.close_connection() _set_connection(protocol=protocol) result, _ = db.cypher_query(default_cypher_query) @@ -41,17 +39,16 @@ def _set_connection(protocol): AURA_TEST_DB_PASSWORD = os.environ["AURA_TEST_DB_PASSWORD"] AURA_TEST_DB_HOSTNAME = os.environ["AURA_TEST_DB_HOSTNAME"] - config.DATABASE_URL = f"{protocol}://{AURA_TEST_DB_USER}:{AURA_TEST_DB_PASSWORD}@{AURA_TEST_DB_HOSTNAME}" - db.set_connection(config.DATABASE_URL) + database_url = f"{protocol}://{AURA_TEST_DB_USER}:{AURA_TEST_DB_PASSWORD}@{AURA_TEST_DB_HOSTNAME}" + db.set_connection(url=database_url) @pytest.mark.parametrize( "url", ["bolt://user:password", "http://user:password@localhost:7687"] ) def test_wrong_url_format(url): - prev_url = db.url with pytest.raises( ValueError, match=rf"Expecting url format: bolt://user:password@localhost:7687 got {url}", ): - db.set_connection(url) + db.set_connection(url=url) diff --git a/test/test_database_management.py b/test/test_database_management.py index 4a09c3e3..5cc92c70 100644 --- a/test/test_database_management.py +++ b/test/test_database_management.py @@ -61,11 +61,11 @@ def test_change_password(): util.change_neo4j_password(db, "neo4j", new_password) - db.set_connection(new_url) + db.set_connection(url=new_url) with pytest.raises(AuthError): - db.set_connection(prev_url) + db.set_connection(url=prev_url) - db.set_connection(new_url) + db.set_connection(url=new_url) util.change_neo4j_password(db, "neo4j", prev_password) - db.set_connection(prev_url) + db.set_connection(url=prev_url) diff --git a/test/test_transactions.py b/test/test_transactions.py index 35e7c01f..62d13a43 100644 --- a/test/test_transactions.py +++ b/test/test_transactions.py @@ -3,7 +3,14 @@ from neo4j.exceptions import ClientError, TransactionError from pytest import raises -from neomodel import StringProperty, StructuredNode, UniqueProperty, db, install_labels +from neomodel import ( + StringProperty, + StructuredNode, + UniqueProperty, + config, + db, + install_labels, +) class APerson(StructuredNode): @@ -80,9 +87,9 @@ def test_read_transaction(): people = APerson.nodes.all() assert people - with pytest.raises(TransactionError): + with raises(TransactionError): with db.read_transaction: - with pytest.raises(ClientError) as e: + with raises(ClientError) as e: APerson(name="Gina").save() assert e.value.code == "Neo.ClientError.Statement.AccessMode" @@ -97,7 +104,7 @@ def test_write_transaction(): def double_transaction(): db.begin() - with pytest.raises(SystemError, match=r"Transaction in progress"): + with raises(SystemError, match=r"Transaction in progress"): db.begin() db.rollback() @@ -105,13 +112,11 @@ def double_transaction(): def test_set_connection_works(): assert APerson(name="New guy 1").save() - from socket import gaierror - old_url = db.url with raises(ValueError): - db.set_connection("bolt://user:password@6.6.6.6.6.6.6.6:7687") + db.set_connection(url="bolt://user:password@6.6.6.6.6.6.6.6:7687") APerson(name="New guy 2").save() - db.set_connection(old_url) + db.set_connection(url=config.DATABASE_URL) # set connection back assert APerson(name="New guy 3").save() From 45177eb7c8155d04444ffca4326ede14279c60c9 Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Tue, 5 Sep 2023 15:42:30 +0200 Subject: [PATCH 02/15] Explictly close the driver in all tests --- test/conftest.py | 5 +++++ test/test_database_management.py | 6 ++++++ test/test_transactions.py | 3 +++ 3 files changed, 14 insertions(+) diff --git a/test/conftest.py b/test/conftest.py index 1cf682df..1be37a5d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -82,6 +82,11 @@ def pytest_sessionstart(session): db.cypher_query("GRANT IMPERSONATE (troygreene) ON DBMS TO admin") +@pytest.hookimpl +def pytest_unconfigure(config): + db.close_connection() + + def version_to_dec(a_version_string): """ Converts a version string to a number to allow for quick checks on the versions of specific components. diff --git a/test/test_database_management.py b/test/test_database_management.py index 5cc92c70..2a2ece34 100644 --- a/test/test_database_management.py +++ b/test/test_database_management.py @@ -60,12 +60,18 @@ def test_change_password(): new_url = f"bolt://neo4j:{new_password}@localhost:7687" util.change_neo4j_password(db, "neo4j", new_password) + db.close_connection() db.set_connection(url=new_url) + db.close_connection() with pytest.raises(AuthError): db.set_connection(url=prev_url) + db.close_connection() + db.set_connection(url=new_url) util.change_neo4j_password(db, "neo4j", prev_password) + db.close_connection() + db.set_connection(url=prev_url) diff --git a/test/test_transactions.py b/test/test_transactions.py index 62d13a43..9cbfb8f3 100644 --- a/test/test_transactions.py +++ b/test/test_transactions.py @@ -112,10 +112,13 @@ def double_transaction(): def test_set_connection_works(): assert APerson(name="New guy 1").save() + db.close_connection() with raises(ValueError): db.set_connection(url="bolt://user:password@6.6.6.6.6.6.6.6:7687") APerson(name="New guy 2").save() + + db.close_connection() db.set_connection(url=config.DATABASE_URL) # set connection back assert APerson(name="New guy 3").save() From fc8aad27c0f9ae58a8c42473b617e3c66f116d27 Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Tue, 5 Sep 2023 16:36:59 +0200 Subject: [PATCH 03/15] Add explicit connection tests --- neomodel/config.py | 11 ++++++--- neomodel/util.py | 2 +- test/conftest.py | 10 +++++---- test/test_connection.py | 43 ++++++++++++++++++++++++++---------- test/test_multiprocessing.py | 2 +- test/test_transactions.py | 14 ------------ 6 files changed, 47 insertions(+), 35 deletions(-) diff --git a/neomodel/config.py b/neomodel/config.py index 5c882b1f..2cc5539d 100644 --- a/neomodel/config.py +++ b/neomodel/config.py @@ -3,6 +3,9 @@ from ._version import __version__ AUTO_INSTALL_LABELS = False + +# Use this to connect with automatically created driver +# The following options are the default ones that will be used as driver config DATABASE_URL = "bolt://neo4j:foobarbaz@localhost:7687" FORCE_TIMEZONE = False @@ -17,8 +20,10 @@ TRUSTED_CERTIFICATES = neo4j.TrustSystemCAs() USER_AGENT = f"neomodel/v{__version__}" -DRIVER = neo4j.GraphDatabase().driver( - "bolt://localhost:7687", auth=("neo4j", "foobarbaz") -) +# Use this to connect with your self-managed driver instead +# DRIVER = neo4j.GraphDatabase().driver( +# "bolt://localhost:7687", auth=("neo4j", "foobarbaz") +# ) + # TODO : Try passing a different database name # DATABASE_NAME = "testdatabase" diff --git a/neomodel/util.py b/neomodel/util.py index ccceb608..c32945aa 100644 --- a/neomodel/util.py +++ b/neomodel/util.py @@ -34,7 +34,7 @@ def wrapper(self, *args, **kwargs): _db = self if not _db.driver: - if config.DRIVER: + if hasattr(config, "DRIVER") and config.DRIVER: _db.set_connection(driver=config.DRIVER) elif config.DATABASE_URL: _db.set_connection(url=config.DATABASE_URL) diff --git a/test/conftest.py b/test/conftest.py index 1be37a5d..c5ef2737 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -4,10 +4,12 @@ import warnings import pytest -from neo4j.exceptions import ClientError as CypherError -from neobolt.exceptions import ClientError -from neomodel import change_neo4j_password, clear_neo4j_database, config, db +from neomodel import clear_neo4j_database, config, db + +NEO4J_URL = os.environ.get("NEO4J_URL", "bolt://localhost:7687") +NEO4J_USERNAME = os.environ.get("NEO4J_USERNAME", "neo4j") +NEO4J_PASSWORD = os.environ.get("NEO4J_PASSWORD", "foobarbaz") def pytest_addoption(parser): @@ -83,7 +85,7 @@ def pytest_sessionstart(session): @pytest.hookimpl -def pytest_unconfigure(config): +def pytest_unconfigure(): db.close_connection() diff --git a/test/test_connection.py b/test/test_connection.py index 7a199139..ccac8739 100644 --- a/test/test_connection.py +++ b/test/test_connection.py @@ -1,9 +1,12 @@ import os import pytest +from neo4j import GraphDatabase from neo4j.debug import watch -from neomodel import config, db +from neomodel import StringProperty, StructuredNode, config, db + +from .conftest import NEO4J_PASSWORD, NEO4J_URL, NEO4J_USERNAME @pytest.fixture(autouse=True) @@ -21,6 +24,33 @@ def neo4j_logging(): yield +class Pastry(StructuredNode): + name = StringProperty(unique_index=True) + + +def test_set_connection_driver_works(): + # Verify that current connection is up + assert Pastry(name="Chocolatine").save() + db.close_connection() + + # Test connection using a driver + db.set_connection( + driver=GraphDatabase().driver(NEO4J_URL, auth=(NEO4J_USERNAME, NEO4J_PASSWORD)) + ) + assert Pastry(name="Croissant").save() + + +@pytest.mark.parametrize( + "url", ["bolt://user:password", "http://user:password@localhost:7687"] +) +def test_wrong_url_format(url): + with pytest.raises( + ValueError, + match=rf"Expecting url format: bolt://user:password@localhost:7687 got {url}", + ): + db.set_connection(url=url) + + @pytest.mark.parametrize("protocol", ["neo4j+s", "neo4j+ssc", "bolt+s", "bolt+ssc"]) def test_connect_to_aura(protocol): cypher_return = "hello world" @@ -41,14 +71,3 @@ def _set_connection(protocol): database_url = f"{protocol}://{AURA_TEST_DB_USER}:{AURA_TEST_DB_PASSWORD}@{AURA_TEST_DB_HOSTNAME}" db.set_connection(url=database_url) - - -@pytest.mark.parametrize( - "url", ["bolt://user:password", "http://user:password@localhost:7687"] -) -def test_wrong_url_format(url): - with pytest.raises( - ValueError, - match=rf"Expecting url format: bolt://user:password@localhost:7687 got {url}", - ): - db.set_connection(url=url) diff --git a/test/test_multiprocessing.py b/test/test_multiprocessing.py index 9ae4340b..9e638862 100644 --- a/test/test_multiprocessing.py +++ b/test/test_multiprocessing.py @@ -1,6 +1,6 @@ from multiprocessing.pool import ThreadPool as Pool -from neomodel import StringProperty, StructuredNode, db +from neomodel import StringProperty, StructuredNode class ThingyMaBob(StructuredNode): diff --git a/test/test_transactions.py b/test/test_transactions.py index 9cbfb8f3..0481e2a7 100644 --- a/test/test_transactions.py +++ b/test/test_transactions.py @@ -110,20 +110,6 @@ def double_transaction(): db.rollback() -def test_set_connection_works(): - assert APerson(name="New guy 1").save() - db.close_connection() - - with raises(ValueError): - db.set_connection(url="bolt://user:password@6.6.6.6.6.6.6.6:7687") - APerson(name="New guy 2").save() - - db.close_connection() - db.set_connection(url=config.DATABASE_URL) - # set connection back - assert APerson(name="New guy 3").save() - - @db.transaction.with_bookmark def in_a_tx(*names): for n in names: From 88237be76d56ed8496ef68a57d5bd85ce844a8a8 Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Wed, 6 Sep 2023 09:58:41 +0200 Subject: [PATCH 04/15] Add test for passing database name --- neomodel/config.py | 3 --- neomodel/util.py | 14 ++++++++---- test/test_connection.py | 44 ++++++++++++++++++++++++++++++++++++ test/test_multiprocessing.py | 3 ++- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/neomodel/config.py b/neomodel/config.py index 2cc5539d..b54aa806 100644 --- a/neomodel/config.py +++ b/neomodel/config.py @@ -24,6 +24,3 @@ # DRIVER = neo4j.GraphDatabase().driver( # "bolt://localhost:7687", auth=("neo4j", "foobarbaz") # ) - -# TODO : Try passing a different database name -# DATABASE_NAME = "testdatabase" diff --git a/neomodel/util.py b/neomodel/util.py index c32945aa..51cdbcf3 100644 --- a/neomodel/util.py +++ b/neomodel/util.py @@ -87,7 +87,7 @@ def set_connection(self, url: str = None, driver: Driver = None): """ if driver: self.driver = driver - if hasattr(config, "DATABASE_NAME"): + if hasattr(config, "DATABASE_NAME") and config.DATABASE_NAME: self._database_name = config.DATABASE_NAME elif url: p_start = url.replace(":", "", 1).find(":") + 2 @@ -136,12 +136,18 @@ def set_connection(self, url: str = None, driver: Driver = None): parsed_url.scheme + "://" + hostname, **options ) self.url = url - self._database_name = ( - DEFAULT_DATABASE if database_name == "" else database_name - ) + # The database name can be provided through the url or the config + if database_name == "": + if hasattr(config, "DATABASE_NAME") and config.DATABASE_NAME: + self._database_name = config.DATABASE_NAME + else: + self._database_name = database_name self._pid = os.getpid() self._active_transaction = None + # Set to default database if it hasn't been set before + if self._database_name is None: + self._database_name = DEFAULT_DATABASE # Getting the information about the database version requires a connection to the database self._database_version = None diff --git a/test/test_connection.py b/test/test_connection.py index ccac8739..622ec9e6 100644 --- a/test/test_connection.py +++ b/test/test_connection.py @@ -24,6 +24,19 @@ def neo4j_logging(): yield +def get_current_database_name() -> str: + """ + Fetches the name of the currently active database from the Neo4j database. + + Returns: + - str: The name of the current database. + """ + results, meta = db.cypher_query("CALL db.info") + results_as_dict = [dict(zip(meta, row)) for row in results] + + return results_as_dict[0]["name"] + + class Pastry(StructuredNode): name = StringProperty(unique_index=True) @@ -40,6 +53,37 @@ def test_set_connection_driver_works(): assert Pastry(name="Croissant").save() +def test_connect_to_non_default_database(): + database_name = "pastries" + db.cypher_query(f"CREATE DATABASE {database_name} IF NOT EXISTS") + db.close_connection() + + # Set database name in url - for url init only + db.set_connection(url=f"{config.DATABASE_URL}/{database_name}") + assert get_current_database_name() == "pastries" + + db.close_connection() + + # Set database name in config - for both url and driver init + config.DATABASE_NAME = database_name + + # url init + db.set_connection(url=config.DATABASE_URL) + assert get_current_database_name() == "pastries" + + db.close_connection() + + # driver init + db.set_connection( + driver=GraphDatabase().driver(NEO4J_URL, auth=(NEO4J_USERNAME, NEO4J_PASSWORD)) + ) + assert get_current_database_name() == "pastries" + + # Clear config + # No need to close connection - pytest teardown will do it + config.DATABASE_NAME = None + + @pytest.mark.parametrize( "url", ["bolt://user:password", "http://user:password@localhost:7687"] ) diff --git a/test/test_multiprocessing.py b/test/test_multiprocessing.py index 9e638862..fb00675d 100644 --- a/test/test_multiprocessing.py +++ b/test/test_multiprocessing.py @@ -1,6 +1,6 @@ from multiprocessing.pool import ThreadPool as Pool -from neomodel import StringProperty, StructuredNode +from neomodel import StringProperty, StructuredNode, db class ThingyMaBob(StructuredNode): @@ -18,3 +18,4 @@ def test_concurrency(): results = p.map(thing_create, range(50)) for returned, sent in results: assert returned == sent + db.close_connection() From 21e286c011d5e472f501a18dde0a729ee6bbe9f6 Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Wed, 6 Sep 2023 11:07:22 +0200 Subject: [PATCH 05/15] Skip non default database test for community edition --- test/test_connection.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_connection.py b/test/test_connection.py index 622ec9e6..379556a2 100644 --- a/test/test_connection.py +++ b/test/test_connection.py @@ -53,6 +53,10 @@ def test_set_connection_driver_works(): assert Pastry(name="Croissant").save() +@pytest.mark.skipif( + db.database_edition != "enterprise", + reason="Skipping test for community edition - no multi database in CE", +) def test_connect_to_non_default_database(): database_name = "pastries" db.cypher_query(f"CREATE DATABASE {database_name} IF NOT EXISTS") From 4cc15693d3942563369de5c64ce54cc22f8dedb9 Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Wed, 6 Sep 2023 14:00:35 +0200 Subject: [PATCH 06/15] Update doc --- doc/source/conf.py | 2 +- doc/source/configuration.rst | 96 +++++++++++++++++++++++++++++++--- doc/source/getting_started.rst | 35 ------------- 3 files changed, 91 insertions(+), 42 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 41e50bbf..538c5190 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -57,7 +57,7 @@ # General information about the project. project = __package__ -copyright = "2019, " + __author__ +copyright = "2023, " + __author__ # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 854af8a8..93dd96ac 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -3,14 +3,22 @@ Configuration This section is covering the Neomodel 'config' module and its variables. -Database --------- +Connection +---------- -Setting the connection URL:: +There are two ways to define your connection to the database : + +1. Provide a Neo4j URL and some options - Driver will be managed by neomodel +2. Create your own Neo4j driver and pass it to neomodel + +neomodel-managed (default) +-------------------------- + +Set the connection URL:: config.DATABASE_URL = 'bolt://neo4j:neo4j@localhost:7687` -Adjust driver configuration:: +Adjust driver configuration - these options are only available for this connection method:: config.MAX_CONNECTION_POOL_SIZE = 100 # default config.CONNECTION_ACQUISITION_TIMEOUT = 60.0 # default @@ -22,12 +30,88 @@ Adjust driver configuration:: config.MAX_TRANSACTION_RETRY_TIME = 30.0 # default config.RESOLVER = None # default config.TRUST = neo4j.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES # default - config.USER_AGENT = neomodel/vNeo4j.Major.minor # default + config.USER_AGENT = neomodel/v5.1.1 # default -Setting the database name, for neo4j >= 4:: +Setting the database name, if different from the default one:: + # Using the URL only config.DATABASE_URL = 'bolt://neo4j:neo4j@localhost:7687/mydb` + # Using config option + config.DATABASE_URL = 'bolt://neo4j:neo4j@localhost:7687` + config.DATABASE_NAME = 'mydb' + +self-managed +------------ + +Create a Neo4j driver:: + + from neo4j import GraphDatabase + my_driver = GraphDatabase().driver('bolt://localhost:7687', auth=('neo4j', 'password')) + config.DRIVER = my_driver + +See the `driver documentation ` here. + +This mode allows you to use all the available driver options that neomodel doesn't implement, for example auth tokens for SSO. +Note that you have to manage the driver's lifecycle yourself. + +However, everything else is still handled by neomodel : sessions, transactions, etc... + +Change/Close the connection +--------------------------- + +Optionally, you can change the connection at any time by calling ``set_connection``:: + + from neomodel import db + # Using URL - auto-managed + db.set_connection(url='bolt://neo4j:neo4j@localhost:7687') + + # Using self-managed driver + db.set_connection(driver=my_driver) + +The new connection url will be applied to the current thread or process. + +Since Neo4j version 5, driver auto-close is deprecated. Make sure to close the connection anytime you want to replace it, +as well as at the end of your application's lifecycle by calling ``close_connection``:: + + from neomodel import db + db.close_connection() + + # If you then want a new connection + db.set_connection(url=url) + +This will close the Neo4j driver, and clean up everything that neomodel creates for its internal workings. + +Protect your credentials +------------------------ + +You should `avoid setting database access credentials in plain sight `_. Neo4J defines a number of +`environment variables `_ that are used in its tools and these can be re-used for other applications +too. + +These are: + +* ``NEO4J_USERNAME`` +* ``NEO4J_PASSWORD`` +* ``NEO4J_BOLT_URL`` + +By setting these with (for example): :: + + $ export NEO4J_USERNAME=neo4j + $ export NEO4J_PASSWORD=neo4j + $ export NEO4J_BOLT_URL="bolt://$NEO4J_USERNAME:$NEO4J_PASSWORD@localhost:7687" + +They can be accessed from a Python script via the ``environ`` dict of module ``os`` and be used to set the connection +with something like: :: + + import os + from neomodel import config + + config.DATABASE_URL = os.environ["NEO4J_BOLT_URL"] + + Enable automatic index and constraint creation ---------------------------------------------- diff --git a/doc/source/getting_started.rst b/doc/source/getting_started.rst index 3ec09ceb..19a2e38b 100644 --- a/doc/source/getting_started.rst +++ b/doc/source/getting_started.rst @@ -10,46 +10,11 @@ Before executing any neomodel code, set the connection url:: from neomodel import config config.DATABASE_URL = 'bolt://neo4j:neo4j@localhost:7687' # default - # You can specify a database name: 'bolt://neo4j:neo4j@localhost:7687/mydb' - This must be called early on in your app, if you are using Django the `settings.py` file is ideal. If you are using your neo4j server for the first time you will need to change the default password. This can be achieved by visiting the neo4j admin panel (default: ``http://localhost:7474`` ). -You can also change the connection url at any time by calling ``set_connection``:: - - from neomodel import db - db.set_connection('bolt://neo4j:neo4j@localhost:7687') - -The new connection url will be applied to the current thread or process. - -In general however, it is better to `avoid setting database access credentials in plain sight `_. Neo4J defines a number of -`environment variables `_ that are used in its tools and these can be re-used for other applications -too. - -These are: - -* ``NEO4J_USERNAME`` -* ``NEO4J_PASSWORD`` -* ``NEO4J_BOLT_URL`` - -By setting these with (for example): :: - - $ export NEO4J_USERNAME=neo4j - $ export NEO4J_PASSWORD=neo4j - $ export NEO4J_BOLT_URL="bolt://$NEO4J_USERNAME:$NEO4J_PASSWORD@localhost:7687" - -They can be accessed from a Python script via the ``environ`` dict of module ``os`` and be used to set the connection -with something like: :: - - import os - from neomodel import config - - config.DATABASE_URL = os.environ["NEO4J_BOLT_URL"] - Defining Node Entities and Relationships ======================================== From 0d355d23d17c8fade3630381f4c498efdc7943f9 Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Wed, 6 Sep 2023 14:09:46 +0200 Subject: [PATCH 07/15] Reference Connection in Getting Started --- doc/source/configuration.rst | 2 ++ doc/source/getting_started.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 93dd96ac..2ddd24d9 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -3,6 +3,8 @@ Configuration This section is covering the Neomodel 'config' module and its variables. +.. _connection_options_doc: + Connection ---------- diff --git a/doc/source/getting_started.rst b/doc/source/getting_started.rst index 19a2e38b..92e08792 100644 --- a/doc/source/getting_started.rst +++ b/doc/source/getting_started.rst @@ -12,6 +12,8 @@ Before executing any neomodel code, set the connection url:: This must be called early on in your app, if you are using Django the `settings.py` file is ideal. +See the Configuration page (:ref:`connection_options_doc`) for config options. + If you are using your neo4j server for the first time you will need to change the default password. This can be achieved by visiting the neo4j admin panel (default: ``http://localhost:7474`` ). From 77adfe9643e577eded13f11ab93e07a9d2e3880f Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Wed, 6 Sep 2023 14:26:52 +0200 Subject: [PATCH 08/15] Add test for driver set through config --- test/test_connection.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/test_connection.py b/test/test_connection.py index 379556a2..61e7e10f 100644 --- a/test/test_connection.py +++ b/test/test_connection.py @@ -53,6 +53,21 @@ def test_set_connection_driver_works(): assert Pastry(name="Croissant").save() +def test_config_driver_works(): + # Verify that current connection is up + assert Pastry(name="Chausson aux pommes").save() + db.close_connection() + + # Test connection using a driver defined in config + driver = GraphDatabase().driver(NEO4J_URL, auth=(NEO4J_USERNAME, NEO4J_PASSWORD)) + config.DRIVER = driver + assert Pastry(name="Grignette").save() + + # Clear config + # No need to close connection - pytest teardown will do it + config.DRIVER = None + + @pytest.mark.skipif( db.database_edition != "enterprise", reason="Skipping test for community edition - no multi database in CE", From c5b3a763e6719382b05a45d68adf3adee10907fa Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Wed, 6 Sep 2023 15:06:29 +0200 Subject: [PATCH 09/15] Fix SessionExpired TODO and add notice in docstring --- neomodel/config.py | 2 +- neomodel/util.py | 6 +++--- test/test_connection.py | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/neomodel/config.py b/neomodel/config.py index b54aa806..3396af5f 100644 --- a/neomodel/config.py +++ b/neomodel/config.py @@ -13,7 +13,7 @@ CONNECTION_TIMEOUT = 30.0 ENCRYPTED = False KEEP_ALIVE = True -MAX_CONNECTION_LIFETIME = 3600 +MAX_CONNECTION_LIFETIME = 30 MAX_CONNECTION_POOL_SIZE = 100 MAX_TRANSACTION_RETRY_TIME = 30.0 RESOLVER = None diff --git a/neomodel/util.py b/neomodel/util.py index 51cdbcf3..05795819 100644 --- a/neomodel/util.py +++ b/neomodel/util.py @@ -387,7 +387,8 @@ def cypher_query( :type: dict :param handle_unique: Whether or not to raise UniqueProperty exception on Cypher's ConstraintValidation errors :type: bool - :param retry_on_session_expire: Whether or not to attempt the same query again if the transaction has expired + :param retry_on_session_expire: Whether or not to attempt the same query again if the transaction has expired. + If you use neomodel with your own driver, you must catch SessionExpired exceptions yourself and retry with a new driver instance. :type: bool :param resolve_objects: Whether to attempt to resolve the returned nodes to data model objects automatically :type: bool @@ -449,8 +450,7 @@ def _run_cypher_query( raise exc_info[1].with_traceback(exc_info[2]) except SessionExpired: if retry_on_session_expire: - # TODO : What about if config passes driver instead of url ? - self.set_connection(self.url) + self.set_connection(url=self.url) return self.cypher_query( query=query, params=params, diff --git a/test/test_connection.py b/test/test_connection.py index 61e7e10f..fb3524bb 100644 --- a/test/test_connection.py +++ b/test/test_connection.py @@ -1,4 +1,5 @@ import os +import time import pytest from neo4j import GraphDatabase @@ -60,6 +61,7 @@ def test_config_driver_works(): # Test connection using a driver defined in config driver = GraphDatabase().driver(NEO4J_URL, auth=(NEO4J_USERNAME, NEO4J_PASSWORD)) + config.DRIVER = driver assert Pastry(name="Grignette").save() From 37327c66b05e81934a8e3eab7187d8876984b87a Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Wed, 6 Sep 2023 15:12:44 +0200 Subject: [PATCH 10/15] Update README, changelog and version tag --- Changelog | 5 +++++ README.rst | 14 -------------- doc/source/configuration.rst | 2 +- neomodel/_version.py | 2 +- pyproject.toml | 2 +- 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/Changelog b/Changelog index 9e0a1486..b9504967 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,8 @@ +Version 5.2.0 2023-09 +* Add an option to pass your own driver instead of relying on the automatically created one. See set_connection method. +* Add a close_connection method to explicitly close the driver to match Neo4j deprecation. +* Add a DATABASE_NAME config option, available for both auto- and self-managed driver modes. + Version 5.1.1 2023-08 * Add impersonation * Bumped neo4j-driver to 5.11.0 diff --git a/README.rst b/README.rst index 1a66076c..c4edceb0 100644 --- a/README.rst +++ b/README.rst @@ -55,20 +55,6 @@ Documentation .. _readthedocs: http://neomodel.readthedocs.org -Upcoming breaking changes notice - >=5.2 -======================================== - -As part of the current quality improvement efforts, we are planning a rework of neomodel's main Database object, which will lead to breaking changes. - -The full scope is not drawn out yet, but here are the main points : - -* Refactoring standalone methods that depend on the Database singleton into the class itself. See issue https://github.com/neo4j-contrib/neomodel/issues/739 - -* Adding an option to pass your own driver to neomodel instead of relying on the one that the library creates for you. This will not be a breaking change. - -We are aiming to release this in neomodel 5.2 - - Installation ============ diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 2ddd24d9..018de3fd 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -32,7 +32,7 @@ Adjust driver configuration - these options are only available for this connecti config.MAX_TRANSACTION_RETRY_TIME = 30.0 # default config.RESOLVER = None # default config.TRUST = neo4j.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES # default - config.USER_AGENT = neomodel/v5.1.1 # default + config.USER_AGENT = neomodel/v5.2.0 # default Setting the database name, if different from the default one:: diff --git a/neomodel/_version.py b/neomodel/_version.py index a9c316e2..6c235c59 100644 --- a/neomodel/_version.py +++ b/neomodel/_version.py @@ -1 +1 @@ -__version__ = "5.1.1" +__version__ = "5.2.0" diff --git a/pyproject.toml b/pyproject.toml index 8fbca288..e515ed74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "neobolt==1.7.17", "six==1.16.0", ] -version='5.1.1' +version='5.2.0' [project.urls] documentation = "https://neomodel.readthedocs.io/en/latest/" From 09c5fa80c12cd07f4ec10c7aefb13b56269402f1 Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Fri, 8 Sep 2023 09:07:03 +0200 Subject: [PATCH 11/15] Refactor set_connection --- neomodel/util.py | 118 ++++++++++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 52 deletions(-) diff --git a/neomodel/util.py b/neomodel/util.py index 05795819..b577f8b8 100644 --- a/neomodel/util.py +++ b/neomodel/util.py @@ -90,58 +90,7 @@ def set_connection(self, url: str = None, driver: Driver = None): if hasattr(config, "DATABASE_NAME") and config.DATABASE_NAME: self._database_name = config.DATABASE_NAME elif url: - p_start = url.replace(":", "", 1).find(":") + 2 - p_end = url.rfind("@") - password = url[p_start:p_end] - url = url.replace(password, quote(password)) - parsed_url = urlparse(url) - - valid_schemas = [ - "bolt", - "bolt+s", - "bolt+ssc", - "bolt+routing", - "neo4j", - "neo4j+s", - "neo4j+ssc", - ] - - if parsed_url.netloc.find("@") > -1 and parsed_url.scheme in valid_schemas: - credentials, hostname = parsed_url.netloc.rsplit("@", 1) - username, password = credentials.split(":") - password = unquote(password) - database_name = parsed_url.path.strip("/") - else: - raise ValueError( - f"Expecting url format: bolt://user:password@localhost:7687 got {url}" - ) - - options = { - "auth": basic_auth(username, password), - "connection_acquisition_timeout": config.CONNECTION_ACQUISITION_TIMEOUT, - "connection_timeout": config.CONNECTION_TIMEOUT, - "keep_alive": config.KEEP_ALIVE, - "max_connection_lifetime": config.MAX_CONNECTION_LIFETIME, - "max_connection_pool_size": config.MAX_CONNECTION_POOL_SIZE, - "max_transaction_retry_time": config.MAX_TRANSACTION_RETRY_TIME, - "resolver": config.RESOLVER, - "user_agent": config.USER_AGENT, - } - - if "+s" not in parsed_url.scheme: - options["encrypted"] = config.ENCRYPTED - options["trusted_certificates"] = config.TRUSTED_CERTIFICATES - - self.driver = GraphDatabase.driver( - parsed_url.scheme + "://" + hostname, **options - ) - self.url = url - # The database name can be provided through the url or the config - if database_name == "": - if hasattr(config, "DATABASE_NAME") and config.DATABASE_NAME: - self._database_name = config.DATABASE_NAME - else: - self._database_name = database_name + self._parse_driver_from_url(url=url) self._pid = os.getpid() self._active_transaction = None @@ -154,6 +103,71 @@ def set_connection(self, url: str = None, driver: Driver = None): self._database_edition = None self._update_database_version() + def _parse_driver_from_url(self, url: str) -> None: + """Parse the driver information from the given URL and initialize the driver. + + Args: + url (str): The URL to parse. + + Raises: + ValueError: If the URL format is not as expected. + + Returns: + None - Sets the driver and database_name as class properties + """ + p_start = url.replace(":", "", 1).find(":") + 2 + p_end = url.rfind("@") + password = url[p_start:p_end] + url = url.replace(password, quote(password)) + parsed_url = urlparse(url) + + valid_schemas = [ + "bolt", + "bolt+s", + "bolt+ssc", + "bolt+routing", + "neo4j", + "neo4j+s", + "neo4j+ssc", + ] + + if parsed_url.netloc.find("@") > -1 and parsed_url.scheme in valid_schemas: + credentials, hostname = parsed_url.netloc.rsplit("@", 1) + username, password = credentials.split(":") + password = unquote(password) + database_name = parsed_url.path.strip("/") + else: + raise ValueError( + f"Expecting url format: bolt://user:password@localhost:7687 got {url}" + ) + + options = { + "auth": basic_auth(username, password), + "connection_acquisition_timeout": config.CONNECTION_ACQUISITION_TIMEOUT, + "connection_timeout": config.CONNECTION_TIMEOUT, + "keep_alive": config.KEEP_ALIVE, + "max_connection_lifetime": config.MAX_CONNECTION_LIFETIME, + "max_connection_pool_size": config.MAX_CONNECTION_POOL_SIZE, + "max_transaction_retry_time": config.MAX_TRANSACTION_RETRY_TIME, + "resolver": config.RESOLVER, + "user_agent": config.USER_AGENT, + } + + if "+s" not in parsed_url.scheme: + options["encrypted"] = config.ENCRYPTED + options["trusted_certificates"] = config.TRUSTED_CERTIFICATES + + self.driver = GraphDatabase.driver( + parsed_url.scheme + "://" + hostname, **options + ) + self.url = url + # The database name can be provided through the url or the config + if database_name == "": + if hasattr(config, "DATABASE_NAME") and config.DATABASE_NAME: + self._database_name = config.DATABASE_NAME + else: + self._database_name = database_name + def close_connection(self): """ Closes the currently open driver. From 0348feddaa430d368d789bdb66b526c50519060e Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Mon, 18 Sep 2023 10:10:05 +0200 Subject: [PATCH 12/15] Fix default config --- neomodel/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neomodel/config.py b/neomodel/config.py index 3396af5f..b54aa806 100644 --- a/neomodel/config.py +++ b/neomodel/config.py @@ -13,7 +13,7 @@ CONNECTION_TIMEOUT = 30.0 ENCRYPTED = False KEEP_ALIVE = True -MAX_CONNECTION_LIFETIME = 30 +MAX_CONNECTION_LIFETIME = 3600 MAX_CONNECTION_POOL_SIZE = 100 MAX_TRANSACTION_RETRY_TIME = 30.0 RESOLVER = None From 84e3b2df3f56dd998743dbfa93366e85c441057a Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Mon, 18 Sep 2023 10:14:17 +0200 Subject: [PATCH 13/15] Improve set_connection docstring --- neomodel/util.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/neomodel/util.py b/neomodel/util.py index b577f8b8..87b6e619 100644 --- a/neomodel/util.py +++ b/neomodel/util.py @@ -83,7 +83,14 @@ def __init__(self): def set_connection(self, url: str = None, driver: Driver = None): """ - Sets the connection URL to the address a Neo4j server is set up at + Sets the connection up and relevant internal. This can be done using a Neo4j URL or a driver instance. + + Args: + url (str): Optionally, Neo4j URL in the form protocol://username:password@hostname:port/dbname. + When provided, a Neo4j driver instance will be created by neomodel. + + driver (neo4j.Driver): Optionally, a pre-created driver instance. + When provided, neomodel will not create a driver instance but use this one instead. """ if driver: self.driver = driver From bac2a0c7c013da37946a9117ca8ccbf625dac889 Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Mon, 18 Sep 2023 10:15:22 +0200 Subject: [PATCH 14/15] Update other core docstrings --- neomodel/util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/neomodel/util.py b/neomodel/util.py index 87b6e619..ad4c51b4 100644 --- a/neomodel/util.py +++ b/neomodel/util.py @@ -178,8 +178,7 @@ def _parse_driver_from_url(self, url: str) -> None: def close_connection(self): """ Closes the currently open driver. - The driver should always be called at the end of the application's lifecyle. - If you pass your own driver to neomodel, you can also close it yourself without this method. + The driver should always be closed at the end of the application's lifecyle. """ self._database_version = None self._database_edition = None From b679c52f8c51365e62bb811d9a11333c1f6262ff Mon Sep 17 00:00:00 2001 From: Marius Conjeaud Date: Thu, 26 Oct 2023 16:11:15 +0200 Subject: [PATCH 15/15] Add note that self-managed driver is synchronous only --- doc/source/configuration.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 018de3fd..d5299f54 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -59,6 +59,8 @@ Note that you have to manage the driver's lifecycle yourself. However, everything else is still handled by neomodel : sessions, transactions, etc... +NB : Only the synchronous driver will work in this way. The asynchronous driver is not supported yet. + Change/Close the connection ---------------------------