Skip to content

Commit 9b47011

Browse files
authored
Convenient handling of DBMS notifications (#1059)
This PR introduces support for a more convenient way of consuming notifications received from the DBMS server. This feature is still in **PREVIEW**. It might be changed without following the deprecation policy. ## Logging A new sub-logger `neo4j.notifications` is introduced. Every notification received will be logged through this logger. The log-level is determined by the notification's severity. If logging is not configured explicitly, warnings are logged to stderr. This means that, by default, warnings like deprecations received from the DBMS will appear on stderr. ## Warnings A new driver-level configuration `warn_notification_severity` is introduced. It can be used to configure from which notification severity level upward the driver should emit a warning (i.e., call `warnings.warn`). By default (`None`), it will be set to `OFF` (never emitting warnings on notifications), unless Python runs in development mode or the environment variable `PYTHONNEO4JDEBUG` is set, in which case the driver will emit a warning on every notification. ## Usage This functionality if mainly meant for developing and debugging. Therefore, no emphasis is put on efficiency. However, it's impact is capt to a minimum when disabled. It's assumed that in productions environments this feature is either disabled explicitly ```python neo4j.GraphDatabase.driver( ..., warn_notification_severity=neo4j.NotificationMinimumSeverity.OFF, ..., ) ``` or default behavior (see above) is used by not running Python in development mode. ## Example ```python # example.py import neo4j URL = "neo4j://localhost:7687" AUTH = ("neo4j", "pass") DB = "neo4j" with neo4j.GraphDatabase.driver( URL, auth=AUTH, warn_notification_severity=neo4j.NotificationMinimumSeverity.INFORMATION, ) as driver: driver.execute_query( "MERGE (:Test {name: 'foo'})", database_=DB, ) driver.execute_query( "MATCH (n:Test {name: 'baz'}), (m:Test1 {name: 'bar'}) " "RETURN n, m", database_=DB, routing_="r", ) ``` On an empty database, this leads to the following output: ``` $ python example.py /path/to/example.py:10: PreviewWarning: notification warnings are a preview feature. It might be changed without following the deprecation policy. See also https://github.com/neo4j/neo4j-python-driver/wiki/preview-features. with neo4j.GraphDatabase.driver( /path/to/example.py:19: Neo4jWarning: {severity: INFORMATION} {code: Neo.ClientNotification.Statement.CartesianProduct} {category: PERFORMANCE} {title: This query builds a cartesian product between disconnected patterns.} {description: If a part of a query contains multiple disconnected patterns, this will build a cartesian product between all those parts. This may produce a large amount of data and slow down query processing. While occasionally intended, it may often be possible to reformulate the query that avoids the use of this cross product, perhaps by adding a relationship between the different parts or by using OPTIONAL MATCH (identifier is: (m))} {position: line: 1, column: 1, offset: 0} for query: MATCH (n:Test {name: 'baz'}), (m:Test1 {name: 'bar'}) RETURN n, m ^ driver.execute_query( Received notification from DBMS server: {severity: WARNING} {code: Neo.ClientNotification.Statement.UnknownLabelWarning} {category: UNRECOGNIZED} {title: The provided label is not in the database.} {description: One of the labels in your query is not available in the database, make sure you didn't misspell it or that the label is available when you run this statement in your application (the missing label name is: Test1)} {position: line: 1, column: 34, offset: 33} for query: "MATCH (n:Test {name: 'baz'}), (m:Test1 {name: 'bar'}) RETURN n, m" /path/to/example.py:19: Neo4jWarning: {severity: WARNING} {code: Neo.ClientNotification.Statement.UnknownLabelWarning} {category: UNRECOGNIZED} {title: The provided label is not in the database.} {description: One of the labels in your query is not available in the database, make sure you didn't misspell it or that the label is available when you run this statement in your application (the missing label name is: Test1)} {position: line: 1, column: 34, offset: 33} for query: MATCH (n:Test {name: 'baz'}), (m:Test1 {name: 'bar'}) RETURN n, m ^ driver.execute_query( ```
1 parent 13bd804 commit 9b47011

34 files changed

+1402
-255
lines changed

docs/source/api.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ Additional configuration can be provided via the :class:`neo4j.Driver` construct
407407
+ :ref:`user-agent-ref`
408408
+ :ref:`driver-notifications-min-severity-ref`
409409
+ :ref:`driver-notifications-disabled-categories-ref`
410+
+ :ref:`driver-warn-notification-severity-ref`
410411
+ :ref:`telemetry-disabled-ref`
411412

412413

@@ -725,6 +726,28 @@ Notifications are available via :attr:`.ResultSummary.notifications` and :attr:`
725726
.. seealso:: :class:`.NotificationDisabledCategory`, session config :ref:`session-notifications-disabled-categories-ref`
726727

727728

729+
.. _driver-warn-notification-severity-ref:
730+
731+
``warn_notification_severity``
732+
------------------------------
733+
Set the minimum severity for server notifications that should cause the driver to emit a :class:`.Neo4jWarning`.
734+
735+
Setting it to :attr:`.NotificationMinimumSeverity.OFF` disables these kind of warnings.
736+
Setting it to :data:`None` will be equivalent to ``OFF``, unless Python runs in development mode
737+
(e.g., with ``python -X dev ...``) or the environment variable ``PYTHONNEO4JDEBUG`` is set, in which case the driver
738+
defaults to emitting warnings on all notification (currently equivalent to :attr:`.NotificationMinimumSeverity.INFORMATION`).
739+
740+
**This is experimental** (see :ref:`filter-warnings-ref`).
741+
It might be changed or removed any time even without prior notice.
742+
743+
:Type: :data:`None`, :class:`.NotificationMinimumSeverity`, or :class:`str`
744+
:Default: :data:`None`
745+
746+
.. versionadded:: 5.21
747+
748+
.. seealso:: :ref:`development-environment-ref`
749+
750+
728751
.. _telemetry-disabled-ref:
729752

730753
``telemetry_disabled``
@@ -2085,6 +2108,14 @@ The Python Driver uses the built-in :class:`python:ResourceWarning` class to war
20852108

20862109
.. autoclass:: neo4j.ExperimentalWarning
20872110

2111+
.. autoclass:: neo4j.warnings.Neo4jWarning
2112+
:show-inheritance:
2113+
:members:
2114+
2115+
.. autoclass:: neo4j.warnings.Neo4jDeprecationWarning
2116+
:show-inheritance:
2117+
:members:
2118+
20882119

20892120
.. _filter-warnings-ref:
20902121

@@ -2156,6 +2187,9 @@ Currently available:
21562187
* ``neo4j.pool``: Logs connection pool activity (including routing).
21572188
* ``neo4j.auth_management``: Logger for provided :class:`.AuthManager`
21582189
implementations.
2190+
* ``neo4j.notifications``: Logs notifications received from the server.
2191+
The notifications' :attr:`.SummaryNotification.severity_level` is used to
2192+
determine the log level.
21592193

21602194
There are different ways of enabling logging as listed below.
21612195

docs/source/index.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ To deactivate the current active virtual environment, use:
9999
deactivate
100100
101101
102+
.. _development-environment-ref:
103+
102104
Development Environment
103105
=======================
104106

@@ -112,9 +114,14 @@ Specifically for this driver, this will:
112114
* **This is experimental**.
113115
It might be changed or removed any time even without prior notice.
114116
* the driver will raise an exception if non-concurrency-safe methods are used concurrently.
117+
* the driver will emit warnings if the server sends back notification
118+
(see also :ref:`driver-warn-notification-severity-ref`).
115119

116120
.. versionadded:: 5.15
117121

122+
.. versionchanged:: 5.21
123+
Added functionality to automatically emit warnings on server notifications.
124+
118125
.. _development mode: https://docs.python.org/3/library/devmode.html
119126

120127

src/neo4j/_async/_debug/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@
1717
from ._concurrency_check import AsyncNonConcurrentMethodChecker
1818

1919

20-
__all__ = ["AsyncNonConcurrentMethodChecker"]
20+
__all__ = [
21+
"AsyncNonConcurrentMethodChecker",
22+
]

src/neo4j/_async/_debug/_concurrency_check.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@
1717
from __future__ import annotations
1818

1919
import inspect
20-
import os
21-
import sys
2220
import traceback
2321
import typing as t
2422
from copy import deepcopy
@@ -29,6 +27,7 @@
2927
AsyncRLock,
3028
)
3129
from ..._async_compat.util import AsyncUtil
30+
from ..._debug import ENABLED
3231
from ..._meta import copy_signature
3332

3433

@@ -37,9 +36,6 @@
3736
bound=t.Callable[..., t.AsyncIterator])
3837

3938

40-
ENABLED = sys.flags.dev_mode or bool(os.getenv("PYTHONNEO4JDEBUG"))
41-
42-
4339
class NonConcurrentMethodError(RuntimeError):
4440
pass
4541

src/neo4j/_async/driver.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
)
3131

3232
from .._api import (
33+
NotificationMinimumSeverity,
3334
RoutingControl,
3435
TelemetryAPI,
3536
)
@@ -42,6 +43,7 @@
4243
TrustStore,
4344
WorkspaceConfig,
4445
)
46+
from .._debug import ENABLED as DEBUG_ENABLED
4547
from .._meta import (
4648
deprecation_warn,
4749
experimental_warn,
@@ -157,6 +159,9 @@ def driver(
157159
notifications_disabled_categories: t.Optional[
158160
t.Iterable[T_NotificationDisabledCategory]
159161
] = ...,
162+
warn_notification_severity: t.Optional[
163+
T_NotificationMinimumSeverity
164+
] = ...,
160165
telemetry_disabled: bool = ...,
161166

162167
# undocumented/unsupported options
@@ -270,11 +275,14 @@ def driver(
270275
elif security_type == SECURITY_TYPE_SELF_SIGNED_CERTIFICATE:
271276
config["encrypted"] = True
272277
config["trusted_certificates"] = TrustAll()
273-
_normalize_notifications_config(config)
278+
if "warn_notification_severity" in config:
279+
preview_warn("notification warnings are a preview feature.",
280+
stack_level=2)
281+
_normalize_notifications_config(config, driver_level=True)
274282
liveness_check_timeout = config.get("liveness_check_timeout")
275283
if (
276-
liveness_check_timeout is not None
277-
and liveness_check_timeout < 0
284+
liveness_check_timeout is not None
285+
and liveness_check_timeout < 0
278286
):
279287
raise ConfigurationError(
280288
'The config setting "liveness_check_timeout" must be '
@@ -566,7 +574,7 @@ def session(
566574
# they may be change or removed any time without prior notice
567575
initial_retry_delay: float = ...,
568576
retry_delay_multiplier: float = ...,
569-
retry_delay_jitter_factor: float = ...
577+
retry_delay_jitter_factor: float = ...,
570578
) -> AsyncSession:
571579
...
572580

@@ -581,6 +589,10 @@ def session(self, **config) -> AsyncSession:
581589
582590
:returns: new :class:`neo4j.AsyncSession` object
583591
"""
592+
if "warn_notification_severity" in config:
593+
# Would work just fine, but we don't want to introduce yet
594+
# another undocumented/unsupported config option.
595+
del config["warn_notification_severity"]
584596
self._check_state()
585597
session_config = self._read_session_config(config)
586598
return self._session(session_config)
@@ -1312,14 +1324,33 @@ def __init__(self, pool, default_workspace_config):
13121324
AsyncDriver.__init__(self, pool, default_workspace_config)
13131325

13141326

1315-
def _normalize_notifications_config(config_kwargs):
1316-
if config_kwargs.get("notifications_disabled_categories") is not None:
1317-
config_kwargs["notifications_disabled_categories"] = [
1318-
getattr(e, "value", e)
1319-
for e in config_kwargs["notifications_disabled_categories"]
1320-
]
1321-
if config_kwargs.get("notifications_min_severity") is not None:
1322-
config_kwargs["notifications_min_severity"] = getattr(
1323-
config_kwargs["notifications_min_severity"], "value",
1324-
config_kwargs["notifications_min_severity"]
1327+
1328+
def _normalize_notifications_config(config_kwargs, *, driver_level=False):
1329+
list_config_keys = ("notifications_disabled_categories",)
1330+
for key in list_config_keys:
1331+
value = config_kwargs.get(key)
1332+
if value is not None:
1333+
config_kwargs[key] = [getattr(e, "value", e) for e in value]
1334+
single_config_keys = (
1335+
"notifications_min_severity",
1336+
"warn_notification_severity",
1337+
)
1338+
for key in single_config_keys:
1339+
value = config_kwargs.get(key)
1340+
if value is not None:
1341+
config_kwargs[key] = getattr(value, "value", value)
1342+
value = config_kwargs.get("warn_notification_severity")
1343+
if value not in (*NotificationMinimumSeverity, None):
1344+
raise ValueError(
1345+
f"Invalid value for configuration "
1346+
f"warn_notification_severity: {value}. Should be None, a "
1347+
f"NotificationMinimumSeverity, or a string representing a "
1348+
f"NotificationMinimumSeverity."
13251349
)
1350+
if driver_level:
1351+
if value is None:
1352+
if DEBUG_ENABLED:
1353+
config_kwargs["warn_notification_severity"] = \
1354+
NotificationMinimumSeverity.INFORMATION
1355+
elif value == NotificationMinimumSeverity.OFF:
1356+
config_kwargs["warn_notification_severity"] = None

0 commit comments

Comments
 (0)