Skip to content

ADR 013: bolt_agent, user_agent update, Bolt 5.3 #910

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

Merged
merged 9 commits into from
May 25, 2023
Merged
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
11 changes: 8 additions & 3 deletions src/neo4j/_async/io/_bolt.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
BoltError,
BoltHandshakeError,
)
from ..._meta import get_user_agent
from ..._meta import USER_AGENT
from ...addressing import ResolvedAddress
from ...api import (
ServerInfo,
Expand Down Expand Up @@ -154,7 +154,7 @@ def __init__(self, unresolved_address, sock, max_connection_lifetime, *,
if user_agent:
self.user_agent = user_agent
else:
self.user_agent = get_user_agent()
self.user_agent = USER_AGENT

self.auth = auth
self.auth_dict = self._to_auth_dict(auth)
Expand Down Expand Up @@ -263,6 +263,7 @@ def protocol_handlers(cls, protocol_version=None):
AsyncBolt5x0,
AsyncBolt5x1,
AsyncBolt5x2,
AsyncBolt5x3,
)

handlers = {
Expand All @@ -275,6 +276,7 @@ def protocol_handlers(cls, protocol_version=None):
AsyncBolt5x0.PROTOCOL_VERSION: AsyncBolt5x0,
AsyncBolt5x1.PROTOCOL_VERSION: AsyncBolt5x1,
AsyncBolt5x2.PROTOCOL_VERSION: AsyncBolt5x2,
AsyncBolt5x3.PROTOCOL_VERSION: AsyncBolt5x3,
}

if protocol_version is None:
Expand Down Expand Up @@ -389,7 +391,10 @@ async def open(

# Carry out Bolt subclass imports locally to avoid circular dependency
# issues.
if protocol_version == (5, 2):
if protocol_version == (5, 3):
from ._bolt5 import AsyncBolt5x3
bolt_cls = AsyncBolt5x3
elif protocol_version == (5, 2):
from ._bolt5 import AsyncBolt5x2
bolt_cls = AsyncBolt5x2
elif protocol_version == (5, 1):
Expand Down
11 changes: 11 additions & 0 deletions src/neo4j/_async/io/_bolt5.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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,
Expand Down Expand Up @@ -618,3 +619,13 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
self._append(b"\x11", (extra,),
Response(self, "begin", hydration_hooks, **handlers),
dehydration_hooks=dehydration_hooks)


class AsyncBolt5x3(AsyncBolt5x2):

PROTOCOL_VERSION = Version(5, 3)

def get_base_headers(self):
headers = super().get_base_headers()
headers["bolt_agent"] = BOLT_AGENT_DICT
return headers
3 changes: 1 addition & 2 deletions src/neo4j/_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
deprecation_warn,
experimental_warn,
ExperimentalWarning,
get_user_agent,
)
from .api import (
DEFAULT_DATABASE,
Expand Down Expand Up @@ -400,7 +399,7 @@ class PoolConfig(Config):
# The use of this option is strongly discouraged.

#: User Agent (Python Driver Specific)
user_agent = get_user_agent()
user_agent = None
# Specify the client agent name.

#: Socket Keep Alive (Python and .NET Driver Specific)
Expand Down
42 changes: 35 additions & 7 deletions src/neo4j/_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
from __future__ import annotations

import asyncio
import platform
import sys
import tracemalloc
import typing as t
from functools import wraps
Expand All @@ -35,17 +37,43 @@
deprecated_package = False


def _compute_bolt_agent() -> t.Dict[str, str]:
def format_version_info(version_info):
return "{}.{}.{}-{}-{}".format(*version_info)

return {
"product": f"neo4j-python/{version}",
"platform":
f"{platform.system() or 'Unknown'} "
f"{platform.release() or 'unknown'}; "
f"{platform.machine() or 'unknown'}",
"language": f"Python/{format_version_info(sys.version_info)}",
"language_details":
f"{platform.python_implementation()}; "
f"{format_version_info(sys.implementation.version)} "
f"({', '.join(platform.python_build())}) "
f"[{platform.python_compiler()}]"
}


BOLT_AGENT_DICT = _compute_bolt_agent()


def _compute_user_agent() -> str:
template = "neo4j-python/{} Python/{}.{}.{}-{}-{} ({})"
fields = (version,) + tuple(sys.version_info) + (sys.platform,)
return template.format(*fields)


USER_AGENT = _compute_user_agent()


# TODO: 6.0 - remove this function
def get_user_agent():
""" Obtain the default user agent string sent to the server after
a successful handshake.
"""
from sys import (
platform,
version_info,
)
template = "neo4j-python/{} Python/{}.{}.{}-{}-{} ({})"
fields = (version,) + tuple(version_info) + (platform,)
return template.format(*fields)
return USER_AGENT


def _id(x):
Expand Down
11 changes: 8 additions & 3 deletions src/neo4j/_sync/io/_bolt.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
BoltError,
BoltHandshakeError,
)
from ..._meta import get_user_agent
from ..._meta import USER_AGENT
from ...addressing import ResolvedAddress
from ...api import (
ServerInfo,
Expand Down Expand Up @@ -154,7 +154,7 @@ def __init__(self, unresolved_address, sock, max_connection_lifetime, *,
if user_agent:
self.user_agent = user_agent
else:
self.user_agent = get_user_agent()
self.user_agent = USER_AGENT

self.auth = auth
self.auth_dict = self._to_auth_dict(auth)
Expand Down Expand Up @@ -263,6 +263,7 @@ def protocol_handlers(cls, protocol_version=None):
Bolt5x0,
Bolt5x1,
Bolt5x2,
Bolt5x3,
)

handlers = {
Expand All @@ -275,6 +276,7 @@ def protocol_handlers(cls, protocol_version=None):
Bolt5x0.PROTOCOL_VERSION: Bolt5x0,
Bolt5x1.PROTOCOL_VERSION: Bolt5x1,
Bolt5x2.PROTOCOL_VERSION: Bolt5x2,
Bolt5x3.PROTOCOL_VERSION: Bolt5x3,
}

if protocol_version is None:
Expand Down Expand Up @@ -389,7 +391,10 @@ def open(

# Carry out Bolt subclass imports locally to avoid circular dependency
# issues.
if protocol_version == (5, 2):
if protocol_version == (5, 3):
from ._bolt5 import Bolt5x3
bolt_cls = Bolt5x3
elif protocol_version == (5, 2):
from ._bolt5 import Bolt5x2
bolt_cls = Bolt5x2
elif protocol_version == (5, 1):
Expand Down
11 changes: 11 additions & 0 deletions src/neo4j/_sync/io/_bolt5.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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,
Expand Down Expand Up @@ -618,3 +619,13 @@ def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
self._append(b"\x11", (extra,),
Response(self, "begin", hydration_hooks, **handlers),
dehydration_hooks=dehydration_hooks)


class Bolt5x3(Bolt5x2):

PROTOCOL_VERSION = Version(5, 3)

def get_base_headers(self):
headers = super().get_base_headers()
headers["bolt_agent"] = BOLT_AGENT_DICT
return headers
1 change: 1 addition & 0 deletions testkitbackend/test_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"Feature:Bolt:5.0": true,
"Feature:Bolt:5.1": true,
"Feature:Bolt:5.2": true,
"Feature:Bolt:5.3": true,
"Feature:Bolt:Patch:UTC": true,
"Feature:Impersonation": true,
"Feature:TLS:1.1": "Driver blocks TLS 1.1 for security reasons.",
Expand Down
12 changes: 7 additions & 5 deletions tests/unit/async_/io/test_class_bolt.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_class_method_protocol_handlers():
expected_handlers = {
(3, 0),
(4, 1), (4, 2), (4, 3), (4, 4),
(5, 0), (5, 1), (5, 2),
(5, 0), (5, 1), (5, 2), (5, 3),
}

protocol_handlers = AsyncBolt.protocol_handlers()
Expand All @@ -64,7 +64,8 @@ def test_class_method_protocol_handlers():
((5, 0), 1),
((5, 1), 1),
((5, 2), 1),
((5, 3), 0),
((5, 3), 1),
((5, 4), 0),
((6, 0), 0),
]
)
Expand All @@ -84,7 +85,7 @@ def test_class_method_protocol_handlers_with_invalid_protocol_version():
# [bolt-version-bump] search tag when changing bolt version support
def test_class_method_get_handshake():
handshake = AsyncBolt.get_handshake()
assert (b"\x00\x02\x02\x05\x00\x02\x04\x04\x00\x00\x01\x04\x00\x00\x00\x03"
assert (b"\x00\x03\x03\x05\x00\x02\x04\x04\x00\x00\x01\x04\x00\x00\x00\x03"
== handshake)


Expand Down Expand Up @@ -130,6 +131,7 @@ async def test_cancel_hello_in_open(mocker, none_auth):
((5, 0), "neo4j._async.io._bolt5.AsyncBolt5x0"),
((5, 1), "neo4j._async.io._bolt5.AsyncBolt5x1"),
((5, 2), "neo4j._async.io._bolt5.AsyncBolt5x2"),
((5, 3), "neo4j._async.io._bolt5.AsyncBolt5x3"),
),
)
@mark_async_test
Expand Down Expand Up @@ -162,13 +164,13 @@ async def test_version_negotiation(
(2, 0),
(4, 0),
(3, 1),
(5, 3),
(5, 4),
(6, 0),
))
@mark_async_test
async def test_failing_version_negotiation(mocker, bolt_version, none_auth):
supported_protocols = \
"('3.0', '4.1', '4.2', '4.3', '4.4', '5.0', '5.1', '5.2')"
"('3.0', '4.1', '4.2', '4.3', '4.4', '5.0', '5.1', '5.2', '5.3')"

address = ("localhost", 7687)
socket_mock = mocker.AsyncMock(spec=AsyncBoltSocket)
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/async_/io/test_class_bolt3.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import neo4j
from neo4j._async.io._bolt3 import AsyncBolt3
from neo4j._conf import PoolConfig
from neo4j._meta import USER_AGENT
from neo4j.exceptions import ConfigurationError

from ...._async_compat import mark_async_test
Expand Down Expand Up @@ -249,3 +250,50 @@ async def test_hello_does_not_support_notification_filters(
)
with pytest.raises(ConfigurationError, match="Notification filtering"):
await connection.hello()


@mark_async_test
@pytest.mark.parametrize(
"user_agent", (None, "test user agent", "", USER_AGENT)
)
async def test_user_agent(fake_socket_pair, user_agent):
address = neo4j.Address(("127.0.0.1", 7687))
sockets = fake_socket_pair(address,
packer_cls=AsyncBolt3.PACKER_CLS,
unpacker_cls=AsyncBolt3.UNPACKER_CLS)
await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"})
await sockets.server.send_message(b"\x70", {})
max_connection_lifetime = 0
connection = AsyncBolt3(
address, sockets.client, max_connection_lifetime, user_agent=user_agent
)
await connection.hello()

tag, fields = await sockets.server.pop_message()
extra = fields[0]
if not user_agent:
assert extra["user_agent"] == USER_AGENT
else:
assert extra["user_agent"] == user_agent


@mark_async_test
@pytest.mark.parametrize(
"user_agent", (None, "test user agent", "", USER_AGENT)
)
async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent):
address = neo4j.Address(("127.0.0.1", 7687))
sockets = fake_socket_pair(address,
packer_cls=AsyncBolt3.PACKER_CLS,
unpacker_cls=AsyncBolt3.UNPACKER_CLS)
await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"})
await sockets.server.send_message(b"\x70", {})
max_connection_lifetime = 0
connection = AsyncBolt3(
address, sockets.client, max_connection_lifetime, user_agent=user_agent
)
await connection.hello()

tag, fields = await sockets.server.pop_message()
extra = fields[0]
assert "bolt_agent" not in extra
48 changes: 48 additions & 0 deletions tests/unit/async_/io/test_class_bolt4x0.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import neo4j
from neo4j._async.io._bolt4 import AsyncBolt4x0
from neo4j._conf import PoolConfig
from neo4j._meta import USER_AGENT
from neo4j.exceptions import ConfigurationError

from ...._async_compat import mark_async_test
Expand Down Expand Up @@ -345,3 +346,50 @@ async def test_hello_does_not_support_notification_filters(
)
with pytest.raises(ConfigurationError, match="Notification filtering"):
await connection.hello()


@mark_async_test
@pytest.mark.parametrize(
"user_agent", (None, "test user agent", "", USER_AGENT)
)
async def test_user_agent(fake_socket_pair, user_agent):
address = neo4j.Address(("127.0.0.1", 7687))
sockets = fake_socket_pair(address,
packer_cls=AsyncBolt4x0.PACKER_CLS,
unpacker_cls=AsyncBolt4x0.UNPACKER_CLS)
await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"})
await sockets.server.send_message(b"\x70", {})
max_connection_lifetime = 0
connection = AsyncBolt4x0(
address, sockets.client, max_connection_lifetime, user_agent=user_agent
)
await connection.hello()

tag, fields = await sockets.server.pop_message()
extra = fields[0]
if not user_agent:
assert extra["user_agent"] == USER_AGENT
else:
assert extra["user_agent"] == user_agent


@mark_async_test
@pytest.mark.parametrize(
"user_agent", (None, "test user agent", "", USER_AGENT)
)
async def test_does_not_send_bolt_agent(fake_socket_pair, user_agent):
address = neo4j.Address(("127.0.0.1", 7687))
sockets = fake_socket_pair(address,
packer_cls=AsyncBolt4x0.PACKER_CLS,
unpacker_cls=AsyncBolt4x0.UNPACKER_CLS)
await sockets.server.send_message(b"\x70", {"server": "Neo4j/1.2.3"})
await sockets.server.send_message(b"\x70", {})
max_connection_lifetime = 0
connection = AsyncBolt4x0(
address, sockets.client, max_connection_lifetime, user_agent=user_agent
)
await connection.hello()

tag, fields = await sockets.server.pop_message()
extra = fields[0]
assert "bolt_agent" not in extra
Loading