Skip to content

Commit 972f386

Browse files
authored
ADR 013: bolt_agent, user_agent update, Bolt 5.3 (#910)
Added support for Bolt 5.3 with the new `bolt_agent` dictionary
1 parent f267a2f commit 972f386

32 files changed

+1847
-45
lines changed

src/neo4j/_async/io/_bolt.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
BoltError,
3535
BoltHandshakeError,
3636
)
37-
from ..._meta import get_user_agent
37+
from ..._meta import USER_AGENT
3838
from ...addressing import ResolvedAddress
3939
from ...api import (
4040
ServerInfo,
@@ -154,7 +154,7 @@ def __init__(self, unresolved_address, sock, max_connection_lifetime, *,
154154
if user_agent:
155155
self.user_agent = user_agent
156156
else:
157-
self.user_agent = get_user_agent()
157+
self.user_agent = USER_AGENT
158158

159159
self.auth = auth
160160
self.auth_dict = self._to_auth_dict(auth)
@@ -263,6 +263,7 @@ def protocol_handlers(cls, protocol_version=None):
263263
AsyncBolt5x0,
264264
AsyncBolt5x1,
265265
AsyncBolt5x2,
266+
AsyncBolt5x3,
266267
)
267268

268269
handlers = {
@@ -275,6 +276,7 @@ def protocol_handlers(cls, protocol_version=None):
275276
AsyncBolt5x0.PROTOCOL_VERSION: AsyncBolt5x0,
276277
AsyncBolt5x1.PROTOCOL_VERSION: AsyncBolt5x1,
277278
AsyncBolt5x2.PROTOCOL_VERSION: AsyncBolt5x2,
279+
AsyncBolt5x3.PROTOCOL_VERSION: AsyncBolt5x3,
278280
}
279281

280282
if protocol_version is None:
@@ -389,7 +391,10 @@ async def open(
389391

390392
# Carry out Bolt subclass imports locally to avoid circular dependency
391393
# issues.
392-
if protocol_version == (5, 2):
394+
if protocol_version == (5, 3):
395+
from ._bolt5 import AsyncBolt5x3
396+
bolt_cls = AsyncBolt5x3
397+
elif protocol_version == (5, 2):
393398
from ._bolt5 import AsyncBolt5x2
394399
bolt_cls = AsyncBolt5x2
395400
elif protocol_version == (5, 1):

src/neo4j/_async/io/_bolt5.py

+11
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from ..._codec.hydration import v2 as hydration_v2
2525
from ..._exceptions import BoltProtocolError
26+
from ..._meta import BOLT_AGENT_DICT
2627
from ...api import (
2728
READ_ACCESS,
2829
Version,
@@ -618,3 +619,13 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
618619
self._append(b"\x11", (extra,),
619620
Response(self, "begin", hydration_hooks, **handlers),
620621
dehydration_hooks=dehydration_hooks)
622+
623+
624+
class AsyncBolt5x3(AsyncBolt5x2):
625+
626+
PROTOCOL_VERSION = Version(5, 3)
627+
628+
def get_base_headers(self):
629+
headers = super().get_base_headers()
630+
headers["bolt_agent"] = BOLT_AGENT_DICT
631+
return headers

src/neo4j/_conf.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
deprecation_warn,
2727
experimental_warn,
2828
ExperimentalWarning,
29-
get_user_agent,
3029
)
3130
from .api import (
3231
DEFAULT_DATABASE,
@@ -400,7 +399,7 @@ class PoolConfig(Config):
400399
# The use of this option is strongly discouraged.
401400

402401
#: User Agent (Python Driver Specific)
403-
user_agent = get_user_agent()
402+
user_agent = None
404403
# Specify the client agent name.
405404

406405
#: Socket Keep Alive (Python and .NET Driver Specific)

src/neo4j/_meta.py

+35-7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
from __future__ import annotations
2020

2121
import asyncio
22+
import platform
23+
import sys
2224
import tracemalloc
2325
import typing as t
2426
from functools import wraps
@@ -35,17 +37,43 @@
3537
deprecated_package = False
3638

3739

40+
def _compute_bolt_agent() -> t.Dict[str, str]:
41+
def format_version_info(version_info):
42+
return "{}.{}.{}-{}-{}".format(*version_info)
43+
44+
return {
45+
"product": f"neo4j-python/{version}",
46+
"platform":
47+
f"{platform.system() or 'Unknown'} "
48+
f"{platform.release() or 'unknown'}; "
49+
f"{platform.machine() or 'unknown'}",
50+
"language": f"Python/{format_version_info(sys.version_info)}",
51+
"language_details":
52+
f"{platform.python_implementation()}; "
53+
f"{format_version_info(sys.implementation.version)} "
54+
f"({', '.join(platform.python_build())}) "
55+
f"[{platform.python_compiler()}]"
56+
}
57+
58+
59+
BOLT_AGENT_DICT = _compute_bolt_agent()
60+
61+
62+
def _compute_user_agent() -> str:
63+
template = "neo4j-python/{} Python/{}.{}.{}-{}-{} ({})"
64+
fields = (version,) + tuple(sys.version_info) + (sys.platform,)
65+
return template.format(*fields)
66+
67+
68+
USER_AGENT = _compute_user_agent()
69+
70+
71+
# TODO: 6.0 - remove this function
3872
def get_user_agent():
3973
""" Obtain the default user agent string sent to the server after
4074
a successful handshake.
4175
"""
42-
from sys import (
43-
platform,
44-
version_info,
45-
)
46-
template = "neo4j-python/{} Python/{}.{}.{}-{}-{} ({})"
47-
fields = (version,) + tuple(version_info) + (platform,)
48-
return template.format(*fields)
76+
return USER_AGENT
4977

5078

5179
def _id(x):

src/neo4j/_sync/io/_bolt.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
BoltError,
3535
BoltHandshakeError,
3636
)
37-
from ..._meta import get_user_agent
37+
from ..._meta import USER_AGENT
3838
from ...addressing import ResolvedAddress
3939
from ...api import (
4040
ServerInfo,
@@ -154,7 +154,7 @@ def __init__(self, unresolved_address, sock, max_connection_lifetime, *,
154154
if user_agent:
155155
self.user_agent = user_agent
156156
else:
157-
self.user_agent = get_user_agent()
157+
self.user_agent = USER_AGENT
158158

159159
self.auth = auth
160160
self.auth_dict = self._to_auth_dict(auth)
@@ -263,6 +263,7 @@ def protocol_handlers(cls, protocol_version=None):
263263
Bolt5x0,
264264
Bolt5x1,
265265
Bolt5x2,
266+
Bolt5x3,
266267
)
267268

268269
handlers = {
@@ -275,6 +276,7 @@ def protocol_handlers(cls, protocol_version=None):
275276
Bolt5x0.PROTOCOL_VERSION: Bolt5x0,
276277
Bolt5x1.PROTOCOL_VERSION: Bolt5x1,
277278
Bolt5x2.PROTOCOL_VERSION: Bolt5x2,
279+
Bolt5x3.PROTOCOL_VERSION: Bolt5x3,
278280
}
279281

280282
if protocol_version is None:
@@ -389,7 +391,10 @@ def open(
389391

390392
# Carry out Bolt subclass imports locally to avoid circular dependency
391393
# issues.
392-
if protocol_version == (5, 2):
394+
if protocol_version == (5, 3):
395+
from ._bolt5 import Bolt5x3
396+
bolt_cls = Bolt5x3
397+
elif protocol_version == (5, 2):
393398
from ._bolt5 import Bolt5x2
394399
bolt_cls = Bolt5x2
395400
elif protocol_version == (5, 1):

src/neo4j/_sync/io/_bolt5.py

+11
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
from ..._codec.hydration import v2 as hydration_v2
2525
from ..._exceptions import BoltProtocolError
26+
from ..._meta import BOLT_AGENT_DICT
2627
from ...api import (
2728
READ_ACCESS,
2829
Version,
@@ -618,3 +619,13 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
618619
self._append(b"\x11", (extra,),
619620
Response(self, "begin", hydration_hooks, **handlers),
620621
dehydration_hooks=dehydration_hooks)
622+
623+
624+
class Bolt5x3(Bolt5x2):
625+
626+
PROTOCOL_VERSION = Version(5, 3)
627+
628+
def get_base_headers(self):
629+
headers = super().get_base_headers()
630+
headers["bolt_agent"] = BOLT_AGENT_DICT
631+
return headers

testkitbackend/test_config.json

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"Feature:Bolt:5.0": true,
4949
"Feature:Bolt:5.1": true,
5050
"Feature:Bolt:5.2": true,
51+
"Feature:Bolt:5.3": true,
5152
"Feature:Bolt:Patch:UTC": true,
5253
"Feature:Impersonation": true,
5354
"Feature:TLS:1.1": "Driver blocks TLS 1.1 for security reasons.",

tests/unit/async_/io/test_class_bolt.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def test_class_method_protocol_handlers():
3939
expected_handlers = {
4040
(3, 0),
4141
(4, 1), (4, 2), (4, 3), (4, 4),
42-
(5, 0), (5, 1), (5, 2),
42+
(5, 0), (5, 1), (5, 2), (5, 3),
4343
}
4444

4545
protocol_handlers = AsyncBolt.protocol_handlers()
@@ -64,7 +64,8 @@ def test_class_method_protocol_handlers():
6464
((5, 0), 1),
6565
((5, 1), 1),
6666
((5, 2), 1),
67-
((5, 3), 0),
67+
((5, 3), 1),
68+
((5, 4), 0),
6869
((6, 0), 0),
6970
]
7071
)
@@ -84,7 +85,7 @@ def test_class_method_protocol_handlers_with_invalid_protocol_version():
8485
# [bolt-version-bump] search tag when changing bolt version support
8586
def test_class_method_get_handshake():
8687
handshake = AsyncBolt.get_handshake()
87-
assert (b"\x00\x02\x02\x05\x00\x02\x04\x04\x00\x00\x01\x04\x00\x00\x00\x03"
88+
assert (b"\x00\x03\x03\x05\x00\x02\x04\x04\x00\x00\x01\x04\x00\x00\x00\x03"
8889
== handshake)
8990

9091

@@ -130,6 +131,7 @@ async def test_cancel_hello_in_open(mocker, none_auth):
130131
((5, 0), "neo4j._async.io._bolt5.AsyncBolt5x0"),
131132
((5, 1), "neo4j._async.io._bolt5.AsyncBolt5x1"),
132133
((5, 2), "neo4j._async.io._bolt5.AsyncBolt5x2"),
134+
((5, 3), "neo4j._async.io._bolt5.AsyncBolt5x3"),
133135
),
134136
)
135137
@mark_async_test
@@ -162,13 +164,13 @@ async def test_version_negotiation(
162164
(2, 0),
163165
(4, 0),
164166
(3, 1),
165-
(5, 3),
167+
(5, 4),
166168
(6, 0),
167169
))
168170
@mark_async_test
169171
async def test_failing_version_negotiation(mocker, bolt_version, none_auth):
170172
supported_protocols = \
171-
"('3.0', '4.1', '4.2', '4.3', '4.4', '5.0', '5.1', '5.2')"
173+
"('3.0', '4.1', '4.2', '4.3', '4.4', '5.0', '5.1', '5.2', '5.3')"
172174

173175
address = ("localhost", 7687)
174176
socket_mock = mocker.AsyncMock(spec=AsyncBoltSocket)

tests/unit/async_/io/test_class_bolt3.py

+48
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import neo4j
2525
from neo4j._async.io._bolt3 import AsyncBolt3
2626
from neo4j._conf import PoolConfig
27+
from neo4j._meta import USER_AGENT
2728
from neo4j.exceptions import ConfigurationError
2829

2930
from ...._async_compat import mark_async_test
@@ -249,3 +250,50 @@ async def test_hello_does_not_support_notification_filters(
249250
)
250251
with pytest.raises(ConfigurationError, match="Notification filtering"):
251252
await connection.hello()
253+
254+
255+
@mark_async_test
256+
@pytest.mark.parametrize(
257+
"user_agent", (None, "test user agent", "", USER_AGENT)
258+
)
259+
async def test_user_agent(fake_socket_pair, user_agent):
260+
address = neo4j.Address(("127.0.0.1", 7687))
261+
sockets = fake_socket_pair(address,
262+
packer_cls=AsyncBolt3.PACKER_CLS,
263+
unpacker_cls=AsyncBolt3.UNPACKER_CLS)
264+
await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"})
265+
await sockets.server.send_message(b"\x70", {})
266+
max_connection_lifetime = 0
267+
connection = AsyncBolt3(
268+
address, sockets.client, max_connection_lifetime, user_agent=user_agent
269+
)
270+
await connection.hello()
271+
272+
tag, fields = await sockets.server.pop_message()
273+
extra = fields[0]
274+
if not user_agent:
275+
assert extra["user_agent"] == USER_AGENT
276+
else:
277+
assert extra["user_agent"] == user_agent
278+
279+
280+
@mark_async_test
281+
@pytest.mark.parametrize(
282+
"user_agent", (None, "test user agent", "", USER_AGENT)
283+
)
284+
async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent):
285+
address = neo4j.Address(("127.0.0.1", 7687))
286+
sockets = fake_socket_pair(address,
287+
packer_cls=AsyncBolt3.PACKER_CLS,
288+
unpacker_cls=AsyncBolt3.UNPACKER_CLS)
289+
await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"})
290+
await sockets.server.send_message(b"\x70", {})
291+
max_connection_lifetime = 0
292+
connection = AsyncBolt3(
293+
address, sockets.client, max_connection_lifetime, user_agent=user_agent
294+
)
295+
await connection.hello()
296+
297+
tag, fields = await sockets.server.pop_message()
298+
extra = fields[0]
299+
assert "bolt_agent" not in extra

tests/unit/async_/io/test_class_bolt4x0.py

+48
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import neo4j
2525
from neo4j._async.io._bolt4 import AsyncBolt4x0
2626
from neo4j._conf import PoolConfig
27+
from neo4j._meta import USER_AGENT
2728
from neo4j.exceptions import ConfigurationError
2829

2930
from ...._async_compat import mark_async_test
@@ -345,3 +346,50 @@ async def test_hello_does_not_support_notification_filters(
345346
)
346347
with pytest.raises(ConfigurationError, match="Notification filtering"):
347348
await connection.hello()
349+
350+
351+
@mark_async_test
352+
@pytest.mark.parametrize(
353+
"user_agent", (None, "test user agent", "", USER_AGENT)
354+
)
355+
async def test_user_agent(fake_socket_pair, user_agent):
356+
address = neo4j.Address(("127.0.0.1", 7687))
357+
sockets = fake_socket_pair(address,
358+
packer_cls=AsyncBolt4x0.PACKER_CLS,
359+
unpacker_cls=AsyncBolt4x0.UNPACKER_CLS)
360+
await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"})
361+
await sockets.server.send_message(b"\x70", {})
362+
max_connection_lifetime = 0
363+
connection = AsyncBolt4x0(
364+
address, sockets.client, max_connection_lifetime, user_agent=user_agent
365+
)
366+
await connection.hello()
367+
368+
tag, fields = await sockets.server.pop_message()
369+
extra = fields[0]
370+
if not user_agent:
371+
assert extra["user_agent"] == USER_AGENT
372+
else:
373+
assert extra["user_agent"] == user_agent
374+
375+
376+
@mark_async_test
377+
@pytest.mark.parametrize(
378+
"user_agent", (None, "test user agent", "", USER_AGENT)
379+
)
380+
async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent):
381+
address = neo4j.Address(("127.0.0.1", 7687))
382+
sockets = fake_socket_pair(address,
383+
packer_cls=AsyncBolt4x0.PACKER_CLS,
384+
unpacker_cls=AsyncBolt4x0.UNPACKER_CLS)
385+
await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"})
386+
await sockets.server.send_message(b"\x70", {})
387+
max_connection_lifetime = 0
388+
connection = AsyncBolt4x0(
389+
address, sockets.client, max_connection_lifetime, user_agent=user_agent
390+
)
391+
await connection.hello()
392+
393+
tag, fields = await sockets.server.pop_message()
394+
extra = fields[0]
395+
assert "bolt_agent" not in extra

0 commit comments

Comments
 (0)