Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ See also https://github.com/neo4j/neo4j-python-driver/wiki for a full changelog.
- `neo4j.graph.Node`, `neo4j.graph.Relationship`, `neo4j.graph.Path`
- `neo4j.time.Date`, `neo4j.time.Time`, `neo4j.time.DateTime`
- `neo4j.spatial.Point` (and subclasses)
- Configuring the driver with a URL that cannot be DNS resolved will raise a (retryable) `ServiceUnavailable` error
instead of a `ValueError`.
- Separate out log entries that are session-related (including transaction retries)
form sub-logger `neo4j.pool` to a new sub-logger `neo4j.session`.
- Notifications:
Expand Down
2 changes: 1 addition & 1 deletion src/neo4j/_addressing.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def parse_list(

>>> Address.parse_list("localhost:7687", "[::1]:7687")
[IPv4Address(('localhost', 7687)), IPv6Address(('::1', 7687, 0, 0))]
>>> Address.parse_list("localhost:7687 [::1]:7687")
>>> Address.parse_list("localhost:7687", "[::1]:7687")
[IPv4Address(('localhost', 7687)), IPv6Address(('::1', 7687, 0, 0))]

:param s: The string(s) to parse.
Expand Down
31 changes: 29 additions & 2 deletions src/neo4j/_async_compat/network/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
Address,
ResolvedAddress,
)
from ...exceptions import ServiceUnavailable
from ..util import AsyncUtil


Expand All @@ -40,6 +41,14 @@ def _resolved_addresses_from_info(info, host_name):
yield ResolvedAddress(addr, host_name=host_name)


_RETRYABLE_DNS_ERRNOS = {
socket.EAI_ADDRFAMILY,
socket.EAI_AGAIN,
socket.EAI_MEMORY,
socket.EAI_NODATA,
}


class AsyncNetworkUtil:
@staticmethod
async def get_address_info(
Expand Down Expand Up @@ -69,7 +78,16 @@ async def _dns_resolver(address, family=0):
type=socket.SOCK_STREAM,
)
except OSError as e:
raise ValueError(f"Cannot resolve address {address}") from e
if e.errno in _RETRYABLE_DNS_ERRNOS or (
e.errno == socket.EAI_NONAME
and (address.host is not None or address.port is not None)
):
raise ServiceUnavailable(
f"Failed to DNS resolve address {address}: {e}"
) from e
raise ValueError(
f"Failed to DNS resolve address {address}: {e}"
) from e
return list(_resolved_addresses_from_info(info, address._host_name))

@staticmethod
Expand Down Expand Up @@ -151,7 +169,16 @@ def _dns_resolver(address, family=0):
type=socket.SOCK_STREAM,
)
except OSError as e:
raise ValueError(f"Cannot resolve address {address}") from e
if e.errno in _RETRYABLE_DNS_ERRNOS or (
e.errno == socket.EAI_NONAME
and (address.host is not None or address.port is not None)
):
raise ServiceUnavailable(
f"Failed to DNS resolve address {address}: {e}"
) from e
raise ValueError(
f"Failed to DNS resolve address {address}: {e}"
) from e
return _resolved_addresses_from_info(info, address._host_name)

@staticmethod
Expand Down
14 changes: 14 additions & 0 deletions tests/integration/async_/async_compat/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (c) "Neo4j"
# Neo4j Sweden AB [https://neo4j.com]
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
14 changes: 14 additions & 0 deletions tests/integration/async_/async_compat/network/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright (c) "Neo4j"
# Neo4j Sweden AB [https://neo4j.com]
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
64 changes: 64 additions & 0 deletions tests/integration/async_/async_compat/network/test_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright (c) "Neo4j"
# Neo4j Sweden AB [https://neo4j.com]
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from __future__ import annotations

import socket

import pytest

from neo4j._addressing import (
ResolvedAddress,
ResolvedIPv4Address,
ResolvedIPv6Address,
)
from neo4j._async_compat.network import AsyncNetworkUtil
from neo4j.addressing import Address
from neo4j.exceptions import ServiceUnavailable

from ....._async_compat import mark_async_test


@mark_async_test
async def test_resolve_address():
resolved = [
addr
async for addr in AsyncNetworkUtil.resolve_address(
Address(("localhost", 1234)),
)
]
assert all(isinstance(addr, ResolvedAddress) for addr in resolved)
for addr in resolved:
if isinstance(addr, ResolvedIPv4Address):
assert len(addr) == 2
assert addr[0].startswith("127.0.0.")
assert addr[1] == 1234
elif isinstance(addr, ResolvedIPv6Address):
assert len(addr) == 4
assert addr[:2] == ("::1", 1234)


@mark_async_test
async def test_resolve_invalid_address():
with pytest.raises(ServiceUnavailable) as exc:
await anext(
AsyncNetworkUtil.resolve_address(
Address(("example.invalid", 1234)),
)
)
cause = exc.value.__cause__
assert isinstance(cause, socket.gaierror)
assert cause.errno, socket.EAI_NONAME
14 changes: 14 additions & 0 deletions tests/integration/sync/async_compat/__init__.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions tests/integration/sync/async_compat/network/__init__.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions tests/integration/sync/async_compat/network/test_util.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions tests/unit/async_/test_addressing.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
)
from neo4j._async_compat.network import AsyncNetworkUtil
from neo4j._async_compat.util import AsyncUtil
from neo4j.exceptions import ServiceUnavailable

from ..._async_compat import mark_async_test

Expand Down Expand Up @@ -53,14 +54,19 @@ async def test_address_resolve_with_custom_resolver_none() -> None:
@pytest.mark.parametrize(
("test_input", "expected"),
[
(Address(("example.invalid", "7687")), ServiceUnavailable),
(Address(("example.invalid", 7687)), ServiceUnavailable),
(Address(("127.0.0.1", "abcd")), ValueError),
(Address((None, None)), ValueError),
(Address((1234, "7687")), TypeError),
],
)
@mark_async_test
async def test_address_resolve_with_unresolvable_address(
test_input, expected
) -> None:
# import contextlib
# with contextlib.suppress(Exception):
with pytest.raises(expected):
await AsyncUtil.list(
AsyncNetworkUtil.resolve_address(test_input, resolver=None)
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/sync/test_addressing.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.