Skip to content

Commit 6da0a46

Browse files
authored
Revamp UnsupportedServerProduct exception (#1164)
* Revamp `UnsupportedServerProduct` exception * Make it a subclass of `ServiceUnavailable` * Raise it (instead of internal `neo4j._exceptions.BoltHandshakeError`) when no common Bolt version could be negotiated. * Drop public Version for internal BoltProtocolVersion
1 parent 381cde2 commit 6da0a46

File tree

29 files changed

+313
-343
lines changed

29 files changed

+313
-343
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,21 @@ See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog.
2222
- Remove deprecated exports from `neo4j`:
2323
- `log`, `Config`, `PoolConfig`, `SessionConfig`, `WorkspaceConfig` (internal - no replacement)
2424
- `SummaryNotificationPosition` (use `SummaryInputPosition` instead)
25+
- `api.Version` has been removed as it's unused now.
26+
`ServerInfo.protocol_version` now is a `tuple[int, int]` insteadof a `api.Version`.
27+
This should be drop-in replacement is most cases:
28+
- `Version` was a sup-type of `tuple[int, int]`
29+
- `ServerInfo.protocol_version` was already documented and typed as `tuple[int, int]`
30+
- `Version`'s additional methods were undocumented and shouldn't have been used
2531
- Changed errors raised under certain circumstances
2632
- `ConfigurationError` if the passed `auth` parameters is not valid (instead of `AuthError`)
2733
- This improves the differentiation between `DriverError` for client-side errors and `Neo4jError` for server-side errors.
2834
- `access_mode` configuration option
2935
- `ValueError` on invalid value (instead of `ClientError`)
3036
- Consistently check the value (also for non-routing drivers)
37+
- `neo4j.exceptions.UnsupportedServerProduct` if no common bolt protocol version could be negotiated with the server
38+
(instead of internal `neo4j._exceptions.BoltHandshakeError`).
39+
`UnsupportedServerProduct` is now a subclass of `ServiceUnavailable` (instead of `Exception` directly).
3140

3241

3342
## Version 5.28

docs/source/api.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2109,6 +2109,8 @@ Client-side errors
21092109

21102110
* :class:`neo4j.exceptions.ReadServiceUnavailable`
21112111

2112+
* :class:`neo4j.exceptions.UnsupportedServerProduct`
2113+
21122114
* :class:`neo4j.exceptions.IncompleteCommit`
21132115

21142116
* :class:`neo4j.exceptions.ConfigurationError`
@@ -2164,6 +2166,9 @@ Client-side errors
21642166
.. autoexception:: neo4j.exceptions.ReadServiceUnavailable()
21652167
:show-inheritance:
21662168

2169+
.. autoexception:: neo4j.exceptions.UnsupportedServerProduct()
2170+
:show-inheritance:
2171+
21672172
.. autoexception:: neo4j.exceptions.IncompleteCommit()
21682173
:show-inheritance:
21692174

src/neo4j/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@
103103
SYSTEM_DATABASE,
104104
TRUST_ALL_CERTIFICATES,
105105
TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
106-
Version,
107106
WRITE_ACCESS,
108107
)
109108

@@ -159,7 +158,6 @@
159158
"TrustAll",
160159
"TrustCustomCAs",
161160
"TrustSystemCAs",
162-
"Version",
163161
"__version__",
164162
"basic_auth",
165163
"bearer_auth",

src/neo4j/_async/io/_bolt.py

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,18 @@
3535
BoltError,
3636
BoltHandshakeError,
3737
)
38+
from ..._io import BoltProtocolVersion
3839
from ..._meta import USER_AGENT
3940
from ..._sync.config import PoolConfig
4041
from ...addressing import ResolvedAddress
41-
from ...api import (
42-
ServerInfo,
43-
Version,
44-
)
42+
from ...api import ServerInfo
4543
from ...exceptions import (
4644
ConfigurationError,
4745
DriverError,
4846
IncompleteCommit,
4947
ServiceUnavailable,
5048
SessionExpired,
49+
UnsupportedServerProduct,
5150
)
5251
from ..config import AsyncPoolConfig
5352
from ._bolt_socket import AsyncBoltSocket
@@ -109,7 +108,7 @@ class AsyncBolt:
109108

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

112-
PROTOCOL_VERSION: Version = None # type: ignore[assignment]
111+
PROTOCOL_VERSION: BoltProtocolVersion = None # type: ignore[assignment]
113112

114113
# flag if connection needs RESET to go back to READY state
115114
is_reset = False
@@ -159,7 +158,7 @@ def __init__(
159158
ResolvedAddress(
160159
sock.getpeername(), host_name=unresolved_address.host
161160
),
162-
self.PROTOCOL_VERSION,
161+
self.PROTOCOL_VERSION.version,
163162
)
164163
self.connection_hints = {}
165164
self.patch = {}
@@ -244,7 +243,7 @@ def assert_re_auth_support(self):
244243
if not self.supports_re_auth:
245244
raise ConfigurationError(
246245
"User switching is not supported for Bolt "
247-
f"Protocol {self.PROTOCOL_VERSION!r}. Server Agent "
246+
f"Protocol {self.PROTOCOL_VERSION}. Server Agent "
248247
f"{self.server_info.agent!r}"
249248
)
250249

@@ -257,11 +256,13 @@ def assert_notification_filtering_support(self):
257256
if not self.supports_notification_filtering:
258257
raise ConfigurationError(
259258
"Notification filtering is not supported for the Bolt "
260-
f"Protocol {self.PROTOCOL_VERSION!r}. Server Agent "
259+
f"Protocol {self.PROTOCOL_VERSION}. Server Agent "
261260
f"{self.server_info.agent!r}"
262261
)
263262

264-
protocol_handlers: t.ClassVar[dict[Version, type[AsyncBolt]]] = {}
263+
protocol_handlers: t.ClassVar[
264+
dict[BoltProtocolVersion, type[AsyncBolt]]
265+
] = {}
265266

266267
def __init_subclass__(cls: type[te.Self], **kwargs: t.Any) -> None:
267268
if cls.SKIP_REGISTRATION:
@@ -272,14 +273,10 @@ def __init_subclass__(cls: type[te.Self], **kwargs: t.Any) -> None:
272273
raise ValueError(
273274
"AsyncBolt subclasses must define PROTOCOL_VERSION"
274275
)
275-
if not (
276-
isinstance(protocol_version, Version)
277-
and len(protocol_version) == 2
278-
and all(isinstance(i, int) for i in protocol_version)
279-
):
276+
if not isinstance(protocol_version, BoltProtocolVersion):
280277
raise TypeError(
281-
"PROTOCOL_VERSION must be a 2-tuple of integers, not "
282-
f"{protocol_version!r}"
278+
"PROTOCOL_VERSION must be a BoltProtocolVersion, found "
279+
f"{type(protocol_version)} for {cls.__name__}"
283280
)
284281
if protocol_version in AsyncBolt.protocol_handlers:
285282
cls_conflict = AsyncBolt.protocol_handlers[protocol_version]
@@ -336,16 +333,15 @@ async def ping(cls, address, *, deadline=None, pool_config=None):
336333
await AsyncBoltSocket.close_socket(s)
337334
return protocol_version
338335

339-
@classmethod
336+
@staticmethod
340337
async def open(
341-
cls,
342338
address,
343339
*,
344340
auth_manager=None,
345341
deadline=None,
346342
routing_context=None,
347343
pool_config=None,
348-
):
344+
) -> AsyncBolt:
349345
"""
350346
Open a new Bolt connection to a given server address.
351347
@@ -366,7 +362,7 @@ async def open(
366362
if deadline is None:
367363
deadline = Deadline(None)
368364

369-
s, protocol_version, handshake, data = await AsyncBoltSocket.connect(
365+
s, protocol_version = await AsyncBoltSocket.connect(
370366
address,
371367
tcp_timeout=pool_config.connection_timeout,
372368
deadline=deadline,
@@ -381,15 +377,10 @@ async def open(
381377
if bolt_cls is None:
382378
log.debug("[#%04X] C: <CLOSE>", s.getsockname()[1])
383379
await AsyncBoltSocket.close_socket(s)
384-
385-
# TODO: 6.0 - raise public DriverError subclass instead
386-
raise BoltHandshakeError(
380+
raise UnsupportedServerProduct(
387381
"The neo4j server does not support communication with this "
388382
"driver. This driver has support for Bolt protocols "
389-
f"{tuple(map(str, AsyncBolt.protocol_handlers.keys()))}.",
390-
address=address,
391-
request_data=handshake,
392-
response_data=data,
383+
f"{tuple(map(str, AsyncBolt.protocol_handlers))}.",
393384
)
394385

395386
try:

src/neo4j/_async/io/_bolt3.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@
2222
from ssl import SSLSocket
2323

2424
from ..._exceptions import BoltProtocolError
25-
from ...api import (
26-
READ_ACCESS,
27-
Version,
28-
)
25+
from ..._io import BoltProtocolVersion
26+
from ...api import READ_ACCESS
2927
from ...exceptions import (
3028
ConfigurationError,
3129
DatabaseUnavailable,
@@ -146,7 +144,7 @@ class AsyncBolt3(AsyncBolt):
146144
This is supported by Neo4j versions 3.5, 4.0, 4.1, 4.2, 4.3, and 4.4.
147145
"""
148146

149-
PROTOCOL_VERSION = Version(3, 0)
147+
PROTOCOL_VERSION = BoltProtocolVersion(3, 0)
150148

151149
ssr_enabled = False
152150

@@ -270,14 +268,14 @@ async def route(
270268
if database is not None:
271269
raise ConfigurationError(
272270
"Database name parameter for selecting database is not "
273-
f"supported in Bolt Protocol {self.PROTOCOL_VERSION!r}. "
271+
f"supported in Bolt Protocol {self.PROTOCOL_VERSION}. "
274272
f"Database name {database!r}. "
275273
f"Server Agent {self.server_info.agent!r}"
276274
)
277275
if imp_user is not None:
278276
raise ConfigurationError(
279277
"Impersonation is not supported in Bolt Protocol "
280-
f"{self.PROTOCOL_VERSION!r}. Trying to impersonate "
278+
f"{self.PROTOCOL_VERSION}. Trying to impersonate "
281279
f"{imp_user!r}."
282280
)
283281
dehydration_hooks, hydration_hooks = self._default_hydration_hooks(
@@ -332,13 +330,13 @@ def run(
332330
if db is not None:
333331
raise ConfigurationError(
334332
"Database name parameter for selecting database is not "
335-
f"supported in Bolt Protocol {self.PROTOCOL_VERSION!r}. "
333+
f"supported in Bolt Protocol {self.PROTOCOL_VERSION}. "
336334
f"Database name {db!r}."
337335
)
338336
if imp_user is not None:
339337
raise ConfigurationError(
340338
"Impersonation is not supported in Bolt Protocol "
341-
f"{self.PROTOCOL_VERSION!r}. Trying to impersonate "
339+
f"{self.PROTOCOL_VERSION}. Trying to impersonate "
342340
f"{imp_user!r}."
343341
)
344342
if (
@@ -439,13 +437,13 @@ def begin(
439437
if db is not None:
440438
raise ConfigurationError(
441439
"Database name parameter for selecting database is not "
442-
f"supported in Bolt Protocol {self.PROTOCOL_VERSION!r}. "
440+
f"supported in Bolt Protocol {self.PROTOCOL_VERSION}. "
443441
f"Database name {db!r}."
444442
)
445443
if imp_user is not None:
446444
raise ConfigurationError(
447445
"Impersonation is not supported in Bolt Protocol "
448-
f"{self.PROTOCOL_VERSION!r}. Trying to impersonate "
446+
f"{self.PROTOCOL_VERSION}. Trying to impersonate "
449447
f"{imp_user!r}."
450448
)
451449
if (

src/neo4j/_async/io/_bolt4.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919

2020
from ..._api import TelemetryAPI
2121
from ..._exceptions import BoltProtocolError
22+
from ..._io import BoltProtocolVersion
2223
from ...api import (
2324
READ_ACCESS,
2425
SYSTEM_DATABASE,
25-
Version,
2626
)
2727
from ...exceptions import (
2828
ConfigurationError,
@@ -62,7 +62,7 @@ class AsyncBolt4x0(AsyncBolt):
6262
This is supported by Neo4j versions 4.0-4.4.
6363
"""
6464

65-
PROTOCOL_VERSION = Version(4, 0)
65+
PROTOCOL_VERSION = BoltProtocolVersion(4, 0)
6666

6767
ssr_enabled = False
6868

@@ -188,7 +188,7 @@ async def route(
188188
if imp_user is not None:
189189
raise ConfigurationError(
190190
"Impersonation is not supported in Bolt Protocol "
191-
f"{self.PROTOCOL_VERSION!r}. Trying to impersonate "
191+
f"{self.PROTOCOL_VERSION}. Trying to impersonate "
192192
f"{imp_user!r}."
193193
)
194194
dehydration_hooks, hydration_hooks = self._default_hydration_hooks(
@@ -247,7 +247,7 @@ def run(
247247
if imp_user is not None:
248248
raise ConfigurationError(
249249
"Impersonation is not supported in Bolt Protocol "
250-
f"{self.PROTOCOL_VERSION!r}. Trying to impersonate "
250+
f"{self.PROTOCOL_VERSION}. Trying to impersonate "
251251
f"{imp_user!r}."
252252
)
253253
if (
@@ -359,7 +359,7 @@ def begin(
359359
if imp_user is not None:
360360
raise ConfigurationError(
361361
"Impersonation is not supported in Bolt Protocol "
362-
f"{self.PROTOCOL_VERSION!r}. Trying to impersonate "
362+
f"{self.PROTOCOL_VERSION}. Trying to impersonate "
363363
f"{imp_user!r}."
364364
)
365365
if (
@@ -529,7 +529,7 @@ class AsyncBolt4x1(AsyncBolt4x0):
529529
This is supported by Neo4j versions 4.1 - 4.4.
530530
"""
531531

532-
PROTOCOL_VERSION = Version(4, 1)
532+
PROTOCOL_VERSION = BoltProtocolVersion(4, 1)
533533

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

554-
PROTOCOL_VERSION = Version(4, 2)
554+
PROTOCOL_VERSION = BoltProtocolVersion(4, 2)
555555

556556
SKIP_REGISTRATION = False
557557

@@ -563,7 +563,7 @@ class AsyncBolt4x3(AsyncBolt4x2):
563563
This is supported by Neo4j version 4.3 - 4.4.
564564
"""
565565

566-
PROTOCOL_VERSION = Version(4, 3)
566+
PROTOCOL_VERSION = BoltProtocolVersion(4, 3)
567567

568568
def get_base_headers(self):
569569
headers = super().get_base_headers()
@@ -581,7 +581,7 @@ async def route(
581581
if imp_user is not None:
582582
raise ConfigurationError(
583583
"Impersonation is not supported in Bolt Protocol "
584-
f"{self.PROTOCOL_VERSION!r}. Trying to impersonate "
584+
f"{self.PROTOCOL_VERSION}. Trying to impersonate "
585585
f"{imp_user!r}."
586586
)
587587
dehydration_hooks, hydration_hooks = self._default_hydration_hooks(
@@ -668,7 +668,7 @@ class AsyncBolt4x4(AsyncBolt4x3):
668668
This is supported by Neo4j version 4.4.
669669
"""
670670

671-
PROTOCOL_VERSION = Version(4, 4)
671+
PROTOCOL_VERSION = BoltProtocolVersion(4, 4)
672672

673673
async def route(
674674
self,

0 commit comments

Comments
 (0)