2323from logging import getLogger
2424from time import monotonic
2525
26- from ..._async_compat .network import AsyncBoltSocket
2726from ..._async_compat .util import AsyncUtil
2827from ..._auth_management import to_auth_dict
2928from ..._codec .hydration import (
5150 SessionExpired ,
5251)
5352from ..config import AsyncPoolConfig
53+ from ._bolt_socket import AsyncBoltSocket
5454from ._common import (
5555 AsyncInbox ,
5656 AsyncOutbox ,
5959
6060
6161if t .TYPE_CHECKING :
62+ import typing_extensions as te
63+
6264 from ..._api import TelemetryAPI
6365
6466
@@ -134,6 +136,8 @@ class AsyncBolt:
134136 # results for it.
135137 most_recent_qid = None
136138
139+ SKIP_REGISTRATION = False
140+
137141 def __init__ (
138142 self ,
139143 unresolved_address ,
@@ -257,101 +261,36 @@ def assert_notification_filtering_support(self):
257261 f"{ self .server_info .agent !r} "
258262 )
259263
260- # [bolt-version-bump] search tag when changing bolt version support
261- @classmethod
262- def protocol_handlers (cls , protocol_version = None ):
263- """
264- Return a dictionary of available Bolt protocol handlers.
265-
266- The handlers are keyed by version tuple. If an explicit protocol
267- version is provided, the dictionary will contain either zero or one
268- items, depending on whether that version is supported. If no protocol
269- version is provided, all available versions will be returned.
270-
271- :param protocol_version: tuple identifying a specific protocol
272- version (e.g. (3, 5)) or None
273- :returns: dictionary of version tuple to handler class for all
274- relevant and supported protocol versions
275- :raise TypeError: if protocol version is not passed in a tuple
276- """
277- # Carry out Bolt subclass imports locally to avoid circular dependency
278- # issues.
279- from ._bolt3 import AsyncBolt3
280- from ._bolt4 import (
281- AsyncBolt4x1 ,
282- AsyncBolt4x2 ,
283- AsyncBolt4x3 ,
284- AsyncBolt4x4 ,
285- )
286- from ._bolt5 import (
287- AsyncBolt5x0 ,
288- AsyncBolt5x1 ,
289- AsyncBolt5x2 ,
290- AsyncBolt5x3 ,
291- AsyncBolt5x4 ,
292- AsyncBolt5x5 ,
293- AsyncBolt5x6 ,
294- AsyncBolt5x7 ,
295- AsyncBolt5x8 ,
296- )
297-
298- handlers = {
299- AsyncBolt3 .PROTOCOL_VERSION : AsyncBolt3 ,
300- # 4.0 unsupported because no space left in the handshake
301- AsyncBolt4x1 .PROTOCOL_VERSION : AsyncBolt4x1 ,
302- AsyncBolt4x2 .PROTOCOL_VERSION : AsyncBolt4x2 ,
303- AsyncBolt4x3 .PROTOCOL_VERSION : AsyncBolt4x3 ,
304- AsyncBolt4x4 .PROTOCOL_VERSION : AsyncBolt4x4 ,
305- AsyncBolt5x0 .PROTOCOL_VERSION : AsyncBolt5x0 ,
306- AsyncBolt5x1 .PROTOCOL_VERSION : AsyncBolt5x1 ,
307- AsyncBolt5x2 .PROTOCOL_VERSION : AsyncBolt5x2 ,
308- AsyncBolt5x3 .PROTOCOL_VERSION : AsyncBolt5x3 ,
309- AsyncBolt5x4 .PROTOCOL_VERSION : AsyncBolt5x4 ,
310- AsyncBolt5x5 .PROTOCOL_VERSION : AsyncBolt5x5 ,
311- AsyncBolt5x6 .PROTOCOL_VERSION : AsyncBolt5x6 ,
312- AsyncBolt5x7 .PROTOCOL_VERSION : AsyncBolt5x7 ,
313- AsyncBolt5x8 .PROTOCOL_VERSION : AsyncBolt5x8 ,
314- }
264+ protocol_handlers : t .ClassVar [dict [Version , type [AsyncBolt ]]] = {}
315265
266+ def __init_subclass__ (cls : type [te .Self ], ** kwargs : t .Any ) -> None :
267+ if cls .SKIP_REGISTRATION :
268+ super ().__init_subclass__ (** kwargs )
269+ return
270+ protocol_version = cls .PROTOCOL_VERSION
316271 if protocol_version is None :
317- return handlers
318-
319- if not isinstance (protocol_version , tuple ):
320- raise TypeError ("Protocol version must be specified as a tuple" )
321-
322- if protocol_version in handlers :
323- return {protocol_version : handlers [protocol_version ]}
324-
325- return {}
326-
327- @classmethod
328- def version_list (cls , versions ):
329- """
330- Return a list of supported protocol versions in order of preference.
331-
332- The number of protocol versions (or ranges) returned is limited to 4.
333- """
334- # In fact, 4.3 is the fist version to support ranges. However, the
335- # range support got backported to 4.2. But even if the server is too
336- # old to have the backport, negotiating BOLT 4.1 is no problem as it's
337- # equivalent to 4.2
338- first_with_range_support = Version (4 , 2 )
339- result = []
340- for version in versions :
341- if (
342- result
343- and version >= first_with_range_support
344- and result [- 1 ][0 ] == version [0 ]
345- and result [- 1 ][1 ][1 ] == version [1 ] + 1
346- ):
347- # can use range to encompass this version
348- result [- 1 ][1 ][1 ] = version [1 ]
349- continue
350- result .append (Version (version [0 ], [version [1 ], version [1 ]]))
351- if len (result ) == 4 :
352- break
353- return result
272+ raise ValueError (
273+ "AsyncBolt subclasses must define PROTOCOL_VERSION"
274+ )
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+ ):
280+ raise TypeError (
281+ "PROTOCOL_VERSION must be a 2-tuple of integers, not "
282+ f"{ protocol_version !r} "
283+ )
284+ if protocol_version in AsyncBolt .protocol_handlers :
285+ cls_conflict = AsyncBolt .protocol_handlers [protocol_version ]
286+ raise TypeError (
287+ f"Multiple classes for the same protocol version "
288+ f"{ protocol_version } : { cls } , { cls_conflict } "
289+ )
290+ cls .protocol_handlers [protocol_version ] = cls
291+ super ().__init_subclass__ (** kwargs )
354292
293+ # [bolt-version-bump] search tag when changing bolt version support
355294 @classmethod
356295 def get_handshake (cls ):
357296 """
@@ -360,12 +299,9 @@ def get_handshake(cls):
360299 The length is 16 bytes as specified in the Bolt version negotiation.
361300 :returns: bytes
362301 """
363- supported_versions = sorted (
364- cls . protocol_handlers (). keys (), reverse = True
302+ return (
303+ b" \x00 \x00 \x01 \xff \x00 \x08 \x08 \x05 \x00 \x02 \x04 \x04 \x00 \x00 \x00 \x03 "
365304 )
366- offered_versions = cls .version_list (supported_versions )
367- versions_bytes = (v .to_bytes () for v in offered_versions )
368- return b"" .join (versions_bytes ).ljust (16 , b"\x00 " )
369305
370306 @classmethod
371307 async def ping (cls , address , * , deadline = None , pool_config = None ):
@@ -400,7 +336,6 @@ async def ping(cls, address, *, deadline=None, pool_config=None):
400336 await AsyncBoltSocket .close_socket (s )
401337 return protocol_version
402338
403- # [bolt-version-bump] search tag when changing bolt version support
404339 @classmethod
405340 async def open (
406341 cls ,
@@ -441,71 +376,17 @@ async def open(
441376 )
442377
443378 pool_config .protocol_version = protocol_version
444-
445- # Carry out Bolt subclass imports locally to avoid circular dependency
446- # issues.
447-
448- # avoid new lines after imports for better readability and conciseness
449- # fmt: off
450- if protocol_version == (5 , 8 ):
451- from ._bolt5 import AsyncBolt5x8
452- bolt_cls = AsyncBolt5x8
453- elif protocol_version == (5 , 7 ):
454- from ._bolt5 import AsyncBolt5x7
455- bolt_cls = AsyncBolt5x7
456- elif protocol_version == (5 , 6 ):
457- from ._bolt5 import AsyncBolt5x6
458- bolt_cls = AsyncBolt5x6
459- elif protocol_version == (5 , 5 ):
460- from ._bolt5 import AsyncBolt5x5
461- bolt_cls = AsyncBolt5x5
462- elif protocol_version == (5 , 4 ):
463- from ._bolt5 import AsyncBolt5x4
464- bolt_cls = AsyncBolt5x4
465- elif protocol_version == (5 , 3 ):
466- from ._bolt5 import AsyncBolt5x3
467- bolt_cls = AsyncBolt5x3
468- elif protocol_version == (5 , 2 ):
469- from ._bolt5 import AsyncBolt5x2
470- bolt_cls = AsyncBolt5x2
471- elif protocol_version == (5 , 1 ):
472- from ._bolt5 import AsyncBolt5x1
473- bolt_cls = AsyncBolt5x1
474- elif protocol_version == (5 , 0 ):
475- from ._bolt5 import AsyncBolt5x0
476- bolt_cls = AsyncBolt5x0
477- elif protocol_version == (4 , 4 ):
478- from ._bolt4 import AsyncBolt4x4
479- bolt_cls = AsyncBolt4x4
480- elif protocol_version == (4 , 3 ):
481- from ._bolt4 import AsyncBolt4x3
482- bolt_cls = AsyncBolt4x3
483- elif protocol_version == (4 , 2 ):
484- from ._bolt4 import AsyncBolt4x2
485- bolt_cls = AsyncBolt4x2
486- elif protocol_version == (4 , 1 ):
487- from ._bolt4 import AsyncBolt4x1
488- bolt_cls = AsyncBolt4x1
489- # Implementation for 4.0 exists, but there was no space left in the
490- # handshake to offer this version to the server. Hence, the server
491- # should never request us to speak bolt 4.0.
492- # elif protocol_version == (4, 0):
493- # from ._bolt4 import AsyncBolt4x0
494- # bolt_cls = AsyncBolt4x0
495- elif protocol_version == (3 , 0 ):
496- from ._bolt3 import AsyncBolt3
497- bolt_cls = AsyncBolt3
498- # fmt: on
499- else :
379+ protocol_handlers = AsyncBolt .protocol_handlers
380+ bolt_cls = protocol_handlers .get (protocol_version )
381+ if bolt_cls is None :
500382 log .debug ("[#%04X] C: <CLOSE>" , s .getsockname ()[1 ])
501383 await AsyncBoltSocket .close_socket (s )
502384
503- supported_versions = cls .protocol_handlers ().keys ()
504385 # TODO: 6.0 - raise public DriverError subclass instead
505386 raise BoltHandshakeError (
506387 "The neo4j server does not support communication with this "
507388 "driver. This driver has support for Bolt protocols "
508- f"{ tuple (map (str , supported_versions ))} ." ,
389+ f"{ tuple (map (str , AsyncBolt . protocol_handlers . keys () ))} ." ,
509390 address = address ,
510391 request_data = handshake ,
511392 response_data = data ,
0 commit comments