Skip to content

Revamp UnsupportedServerProduct exception #1164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: 6.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog.

## NEXT RELEASE
- Python 3.7, 3.8, and 3.9 support has been dropped.
- Changed errors raised under certain circumstances
- `neo4j.exceptions.UnsupportedServerProduct` if no common bolt protocol version could be negotiated with the server
(instead of internal `neo4j._exceptions.BoltHandshakeError`).
`UnsupportedServerProduct` is now a subclass of `ServiceUnavailable` (instead of `Exception` directly).


## Version 5.28
Expand Down
5 changes: 5 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2109,6 +2109,8 @@ Client-side errors

* :class:`neo4j.exceptions.ReadServiceUnavailable`

* :class:`neo4j.exceptions.UnsupportedServerProduct`

* :class:`neo4j.exceptions.IncompleteCommit`

* :class:`neo4j.exceptions.ConfigurationError`
Expand Down Expand Up @@ -2164,6 +2166,9 @@ Client-side errors
.. autoexception:: neo4j.exceptions.ReadServiceUnavailable()
:show-inheritance:

.. autoexception:: neo4j.exceptions.UnsupportedServerProduct()
:show-inheritance:

.. autoexception:: neo4j.exceptions.IncompleteCommit()
:show-inheritance:

Expand Down
24 changes: 9 additions & 15 deletions src/neo4j/_async/io/_bolt.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,14 @@
from ..._meta import USER_AGENT
from ..._sync.config import PoolConfig
from ...addressing import ResolvedAddress
from ...api import (
ServerInfo,
Version,
)
from ...api import ServerInfo
from ...exceptions import (
ConfigurationError,
DriverError,
IncompleteCommit,
ServiceUnavailable,
SessionExpired,
UnsupportedServerProduct,
)
from ..config import AsyncPoolConfig
from ._bolt_socket import AsyncBoltSocket
Expand Down Expand Up @@ -109,7 +107,7 @@ class AsyncBolt:

MAGIC_PREAMBLE = b"\x60\x60\xb0\x17"

PROTOCOL_VERSION: Version = None # type: ignore[assignment]
PROTOCOL_VERSION: tuple[int, int] = None # type: ignore[assignment]

# flag if connection needs RESET to go back to READY state
is_reset = False
Expand Down Expand Up @@ -261,7 +259,7 @@ def assert_notification_filtering_support(self):
f"{self.server_info.agent!r}"
)

protocol_handlers: t.ClassVar[dict[Version, type[AsyncBolt]]] = {}
protocol_handlers: t.ClassVar[dict[tuple[int, int], type[AsyncBolt]]] = {}

def __init_subclass__(cls: type[te.Self], **kwargs: t.Any) -> None:
if cls.SKIP_REGISTRATION:
Expand All @@ -273,7 +271,7 @@ def __init_subclass__(cls: type[te.Self], **kwargs: t.Any) -> None:
"AsyncBolt subclasses must define PROTOCOL_VERSION"
)
if not (
isinstance(protocol_version, Version)
isinstance(protocol_version, tuple)
and len(protocol_version) == 2
and all(isinstance(i, int) for i in protocol_version)
):
Expand Down Expand Up @@ -336,16 +334,15 @@ async def ping(cls, address, *, deadline=None, pool_config=None):
await AsyncBoltSocket.close_socket(s)
return protocol_version

@classmethod
@staticmethod
async def open(
cls,
address,
*,
auth_manager=None,
deadline=None,
routing_context=None,
pool_config=None,
):
) -> AsyncBolt:
"""
Open a new Bolt connection to a given server address.

Expand All @@ -366,7 +363,7 @@ async def open(
if deadline is None:
deadline = Deadline(None)

s, protocol_version, handshake, data = await AsyncBoltSocket.connect(
s, protocol_version = await AsyncBoltSocket.connect(
address,
tcp_timeout=pool_config.connection_timeout,
deadline=deadline,
Expand All @@ -383,13 +380,10 @@ async def open(
await AsyncBoltSocket.close_socket(s)

# TODO: 6.0 - raise public DriverError subclass instead
raise BoltHandshakeError(
raise UnsupportedServerProduct(
"The neo4j server does not support communication with this "
"driver. This driver has support for Bolt protocols "
f"{tuple(map(str, AsyncBolt.protocol_handlers.keys()))}.",
address=address,
request_data=handshake,
response_data=data,
)

try:
Expand Down
7 changes: 2 additions & 5 deletions src/neo4j/_async/io/_bolt3.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,7 @@
from ssl import SSLSocket

from ..._exceptions import BoltProtocolError
from ...api import (
READ_ACCESS,
Version,
)
from ...api import READ_ACCESS
from ...exceptions import (
ConfigurationError,
DatabaseUnavailable,
Expand Down Expand Up @@ -146,7 +143,7 @@ class AsyncBolt3(AsyncBolt):
This is supported by Neo4j versions 3.5, 4.0, 4.1, 4.2, 4.3, and 4.4.
"""

PROTOCOL_VERSION = Version(3, 0)
PROTOCOL_VERSION = (3, 0)

ssr_enabled = False

Expand Down
11 changes: 5 additions & 6 deletions src/neo4j/_async/io/_bolt4.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from ...api import (
READ_ACCESS,
SYSTEM_DATABASE,
Version,
)
from ...exceptions import (
ConfigurationError,
Expand Down Expand Up @@ -62,7 +61,7 @@ class AsyncBolt4x0(AsyncBolt):
This is supported by Neo4j versions 4.0-4.4.
"""

PROTOCOL_VERSION = Version(4, 0)
PROTOCOL_VERSION = (4, 0)

ssr_enabled = False

Expand Down Expand Up @@ -529,7 +528,7 @@ class AsyncBolt4x1(AsyncBolt4x0):
This is supported by Neo4j versions 4.1 - 4.4.
"""

PROTOCOL_VERSION = Version(4, 1)
PROTOCOL_VERSION = (4, 1)

def get_base_headers(self):
# Bolt 4.1 passes the routing context, originally taken from
Expand All @@ -551,7 +550,7 @@ class AsyncBolt4x2(AsyncBolt4x1):
This is supported by Neo4j version 4.2 - 4.4.
"""

PROTOCOL_VERSION = Version(4, 2)
PROTOCOL_VERSION = (4, 2)

SKIP_REGISTRATION = False

Expand All @@ -563,7 +562,7 @@ class AsyncBolt4x3(AsyncBolt4x2):
This is supported by Neo4j version 4.3 - 4.4.
"""

PROTOCOL_VERSION = Version(4, 3)
PROTOCOL_VERSION = (4, 3)

def get_base_headers(self):
headers = super().get_base_headers()
Expand Down Expand Up @@ -668,7 +667,7 @@ class AsyncBolt4x4(AsyncBolt4x3):
This is supported by Neo4j version 4.4.
"""

PROTOCOL_VERSION = Version(4, 4)
PROTOCOL_VERSION = (4, 4)

async def route(
self,
Expand Down
23 changes: 10 additions & 13 deletions src/neo4j/_async/io/_bolt5.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,7 @@
from ..._codec.hydration import v2 as hydration_v2
from ..._exceptions import BoltProtocolError
from ..._meta import BOLT_AGENT_DICT
from ...api import (
READ_ACCESS,
Version,
)
from ...api import READ_ACCESS
from ...exceptions import (
DatabaseUnavailable,
ForbiddenOnReadOnlyDatabase,
Expand Down Expand Up @@ -62,7 +59,7 @@
class AsyncBolt5x0(AsyncBolt):
"""Protocol handler for Bolt 5.0."""

PROTOCOL_VERSION = Version(5, 0)
PROTOCOL_VERSION = (5, 0)

HYDRATION_HANDLER_CLS = hydration_v2.HydrationHandler

Expand Down Expand Up @@ -593,7 +590,7 @@ class ClientStateManager5x1(ClientStateManager):
class AsyncBolt5x1(AsyncBolt5x0):
"""Protocol handler for Bolt 5.1."""

PROTOCOL_VERSION = Version(5, 1)
PROTOCOL_VERSION = (5, 1)

supports_re_auth = True

Expand Down Expand Up @@ -684,7 +681,7 @@ def logoff(self, dehydration_hooks=None, hydration_hooks=None):


class AsyncBolt5x2(AsyncBolt5x1):
PROTOCOL_VERSION = Version(5, 2)
PROTOCOL_VERSION = (5, 2)

supports_notification_filtering = True

Expand Down Expand Up @@ -869,7 +866,7 @@ def begin(


class AsyncBolt5x3(AsyncBolt5x2):
PROTOCOL_VERSION = Version(5, 3)
PROTOCOL_VERSION = (5, 3)

def get_base_headers(self):
headers = super().get_base_headers()
Expand All @@ -878,7 +875,7 @@ def get_base_headers(self):


class AsyncBolt5x4(AsyncBolt5x3):
PROTOCOL_VERSION = Version(5, 4)
PROTOCOL_VERSION = (5, 4)

def telemetry(
self,
Expand Down Expand Up @@ -907,7 +904,7 @@ def telemetry(


class AsyncBolt5x5(AsyncBolt5x4):
PROTOCOL_VERSION = Version(5, 5)
PROTOCOL_VERSION = (5, 5)

def get_base_headers(self):
headers = super().get_base_headers()
Expand Down Expand Up @@ -1107,7 +1104,7 @@ def pull(


class AsyncBolt5x6(AsyncBolt5x5):
PROTOCOL_VERSION = Version(5, 6)
PROTOCOL_VERSION = (5, 6)

def _make_enrich_statuses_handler(self, wrapped_handler=None):
async def handler(metadata):
Expand Down Expand Up @@ -1139,7 +1136,7 @@ def enrich(metadata_):


class AsyncBolt5x7(AsyncBolt5x6):
PROTOCOL_VERSION = Version(5, 7)
PROTOCOL_VERSION = (5, 7)

DEFAULT_ERROR_DIAGNOSTIC_RECORD = (
AsyncBolt5x5.DEFAULT_STATUS_DIAGNOSTIC_RECORD
Expand Down Expand Up @@ -1232,7 +1229,7 @@ async def _process_message(self, tag, fields):


class AsyncBolt5x8(AsyncBolt5x7):
PROTOCOL_VERSION = Version(5, 8)
PROTOCOL_VERSION = (5, 8)

@property
def ssr_enabled(self) -> bool:
Expand Down
13 changes: 6 additions & 7 deletions src/neo4j/_async/io/_bolt_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ async def _handshake(
self,
resolved_address: ResolvedAddress,
deadline: Deadline,
) -> tuple[tuple[int, int], bytes, bytes]:
) -> tuple[int, int]:
"""
Perform BOLT handshake.

Expand Down Expand Up @@ -288,7 +288,7 @@ async def _handshake(
response,
)

return agreed_version, handshake, response
return agreed_version

@classmethod
async def connect(
Expand All @@ -300,7 +300,7 @@ async def connect(
custom_resolver: t.Callable | None,
ssl_context: SSLContext | None,
keep_alive: bool,
) -> tuple[te.Self, tuple[int, int], bytes, bytes]:
) -> tuple[te.Self, tuple[int, int]]:
"""
Connect and perform a handshake.

Expand Down Expand Up @@ -328,10 +328,8 @@ async def connect(
s = await cls._connect_secure(
resolved_address, tcp_timeout, keep_alive, ssl_context
)
agreed_version, handshake, response = await s._handshake(
resolved_address, deadline
)
return s, agreed_version, handshake, response
agreed_version = await s._handshake(resolved_address, deadline)
return s, agreed_version
except (BoltError, DriverError, OSError) as error:
local_port = 0
if isinstance(s, cls):
Expand Down Expand Up @@ -367,6 +365,7 @@ async def connect(
await cls.close_socket(s)
raise
address_strs = tuple(map(str, failed_addresses))
# TODO: 7.0 - when Python 3.11+ is the minimum, use exception groups
if not errors:
raise ServiceUnavailable(
f"Couldn't connect to {address} (resolved to {address_strs})"
Expand Down
2 changes: 2 additions & 0 deletions src/neo4j/_async/io/_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,8 @@ async def fetch_routing_table(
"[#0000] _: <POOL> failed to fetch routing info from %r",
address,
)
# TODO: 7.0 - when Python 3.11+ is the minimum,
# use exception groups instead of swallowing discovery errors
return None
else:
servers = new_routing_info[0]["servers"]
Expand Down
2 changes: 2 additions & 0 deletions src/neo4j/_async/work/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,8 @@ def api_success_cb(meta):
raise

if errors:
# TODO: 7.0 - when Python 3.11+ is the minimum,
# use exception groups
raise errors[-1]
else:
raise ServiceUnavailable("Transaction failed")
Expand Down
Loading