djraphdb 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
djraphdb/__init__.py ADDED
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ if TYPE_CHECKING:
6
+ from contextlib import AbstractContextManager
7
+
8
+ import neo4j
9
+
10
+ __version__ = "0.1.0"
11
+
12
+ from djraphdb.conf import ConnectionConfig, DjraphdbSettings, get_settings
13
+ from djraphdb.decorators import graph_read, graph_transaction, graph_write
14
+ from djraphdb.exceptions import (
15
+ ConfigurationError,
16
+ DjraphdbError,
17
+ QueryError,
18
+ RetryExhaustedError,
19
+ TransactionError,
20
+ )
21
+ from djraphdb.exceptions import (
22
+ ConnectionError as GraphConnectionError,
23
+ )
24
+ from djraphdb.health import (
25
+ Neo4jHealthView,
26
+ check_neo4j_connection,
27
+ get_all_health,
28
+ get_neo4j_health,
29
+ )
30
+ from djraphdb.middleware import (
31
+ GraphSessionMiddleware,
32
+ GraphTransactionMiddleware,
33
+ get_graph_session,
34
+ get_graph_tx,
35
+ )
36
+ from djraphdb.nodes import DoesNotExist, GraphNode, MultipleObjectsReturned
37
+ from djraphdb.query import CypherQuery
38
+ from djraphdb.retry import RetryConfig
39
+ from djraphdb.service import GraphService
40
+ from djraphdb.signals import (
41
+ neo4j_connected,
42
+ neo4j_disconnected,
43
+ neo4j_query_executed,
44
+ neo4j_slow_query,
45
+ )
46
+ from djraphdb.types import NodeResult, RelationshipResult, ResultMapper
47
+
48
+ __all__ = [
49
+ "__version__",
50
+ # Settings
51
+ "DjraphdbSettings",
52
+ "ConnectionConfig",
53
+ "get_settings",
54
+ # Exceptions
55
+ "DjraphdbError",
56
+ "GraphConnectionError",
57
+ "QueryError",
58
+ "TransactionError",
59
+ "ConfigurationError",
60
+ "RetryExhaustedError",
61
+ # Retry
62
+ "RetryConfig",
63
+ # Types
64
+ "NodeResult",
65
+ "RelationshipResult",
66
+ "ResultMapper",
67
+ # Nodes
68
+ "GraphNode",
69
+ "DoesNotExist",
70
+ "MultipleObjectsReturned",
71
+ # Service
72
+ "GraphService",
73
+ # Query builder
74
+ "CypherQuery",
75
+ # Connection helpers
76
+ "get_driver",
77
+ "get_session",
78
+ # Decorators
79
+ "graph_read",
80
+ "graph_write",
81
+ "graph_transaction",
82
+ # Middleware
83
+ "GraphSessionMiddleware",
84
+ "GraphTransactionMiddleware",
85
+ "get_graph_session",
86
+ "get_graph_tx",
87
+ # Health
88
+ "check_neo4j_connection",
89
+ "get_neo4j_health",
90
+ "get_all_health",
91
+ "Neo4jHealthView",
92
+ # Signals
93
+ "neo4j_connected",
94
+ "neo4j_disconnected",
95
+ "neo4j_query_executed",
96
+ "neo4j_slow_query",
97
+ ]
98
+
99
+
100
+ def get_driver(using: str = "default") -> "neo4j.Driver": # type: ignore[name-defined]
101
+ """Return the ``neo4j.Driver`` for the named connection.
102
+
103
+ Convenience wrapper around
104
+ :meth:`~djraphdb.connection.ConnectionManager.get_driver`.
105
+
106
+ Args:
107
+ using: Connection name. Defaults to ``"default"``.
108
+
109
+ Returns:
110
+ The ``neo4j.Driver`` for the named connection.
111
+
112
+ Raises:
113
+ :exc:`GraphConnectionError`: If the manager is not initialised or
114
+ ``using`` is not a known connection name.
115
+ """
116
+ from djraphdb.connection import connection_manager
117
+
118
+ return connection_manager.get_driver(using)
119
+
120
+
121
+ def get_session(
122
+ using: str = "default", **kwargs: Any
123
+ ) -> "AbstractContextManager[neo4j.Session]":
124
+ """Return a session context manager for the named connection.
125
+
126
+ Convenience wrapper around
127
+ :meth:`~djraphdb.connection.ConnectionManager.session`.
128
+
129
+ Args:
130
+ using: Connection name. Defaults to ``"default"``.
131
+ **kwargs: Forwarded to ``neo4j.Driver.session()``.
132
+
133
+ Returns:
134
+ A context manager that yields a ``neo4j.Session``.
135
+ """
136
+ from djraphdb.connection import connection_manager
137
+
138
+ return connection_manager.session(using, **kwargs)
djraphdb/apps.py ADDED
@@ -0,0 +1,58 @@
1
+ """AppConfig for the djraphdb Django application."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import atexit
6
+ import logging
7
+
8
+ from django.apps import AppConfig
9
+
10
+ logger = logging.getLogger("djraphdb")
11
+
12
+
13
+ class DjraphdbConfig(AppConfig):
14
+ """AppConfig for djraphdb.
15
+
16
+ Registers the djraphdb application and initialises the Neo4j connection
17
+ manager when Django starts up.
18
+ """
19
+
20
+ name = "djraphdb"
21
+ verbose_name = "djraphdb"
22
+ default_auto_field = "django.db.models.BigAutoField"
23
+
24
+ def ready(self) -> None:
25
+ """Initialise the Neo4j connection manager when Django starts.
26
+
27
+ Reads settings from ``DJRAPHDB``, creates drivers for each configured
28
+ connection, then verifies connectivity. Connectivity failures are
29
+ logged as warnings rather than raised, so a temporarily-unreachable
30
+ Neo4j server will not prevent Django from starting.
31
+
32
+ An ``atexit`` handler is registered to close all drivers cleanly
33
+ when the process exits.
34
+ """
35
+ from djraphdb.conf import DjraphdbSettings
36
+ from djraphdb.connection import connection_manager
37
+
38
+ try:
39
+ settings = DjraphdbSettings.from_django_settings()
40
+ except Exception as exc:
41
+ logger.warning(
42
+ "djraphdb: could not load settings, skipping initialisation: %s",
43
+ exc,
44
+ )
45
+ return
46
+
47
+ connection_manager.initialize(settings)
48
+ atexit.register(connection_manager.close)
49
+
50
+ # Verify all connections; log warnings but do not crash.
51
+ results = connection_manager.verify_all()
52
+ for name, reachable in results.items():
53
+ if not reachable:
54
+ logger.warning(
55
+ "djraphdb: connection %r is not reachable at startup", name
56
+ )
57
+
58
+ from djraphdb import health as _health_module # noqa: F401 — registers system checks
djraphdb/conf.py ADDED
@@ -0,0 +1,420 @@
1
+ """
2
+ Settings and configuration module for djraphdb.
3
+
4
+ Reads configuration from ``django.conf.settings.DJRAPHDB`` and exposes it
5
+ through ``DjraphdbSettings`` and ``ConnectionConfig``.
6
+
7
+ Two configuration formats are supported:
8
+
9
+ **Flat / single-connection format** — ``"URI"`` appears as a top-level key.
10
+ The entire dict is automatically wrapped as the ``"default"`` connection::
11
+
12
+ DJRAPHDB = {
13
+ "URI": "bolt://localhost:7687",
14
+ "AUTH": ("neo4j", "password"),
15
+ "DATABASE": "neo4j",
16
+ }
17
+
18
+ **Multi-database format** — every top-level value is itself a dict (and
19
+ ``"URI"`` is *not* a top-level key). Each key names a connection::
20
+
21
+ DJRAPHDB = {
22
+ "default": {
23
+ "URI": "bolt://localhost:7687",
24
+ "AUTH": ("neo4j", "password"),
25
+ },
26
+ "analytics": {
27
+ "URI": "bolt://analytics:7687",
28
+ "AUTH": ("neo4j", "password"),
29
+ "DATABASE": "analytics",
30
+ },
31
+ "LOG_QUERIES": True,
32
+ }
33
+
34
+ Global settings (``LOG_QUERIES``, ``LOG_QUERY_PARAMETERS``,
35
+ ``SLOW_QUERY_THRESHOLD_MS``) are extracted from the top-level dict in both
36
+ formats.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ from dataclasses import dataclass, field
42
+
43
+ __all__ = [
44
+ "ConnectionConfig",
45
+ "DjraphdbSettings",
46
+ "get_settings",
47
+ ]
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Internal helpers
51
+ # ---------------------------------------------------------------------------
52
+
53
+ #: Keys that are treated as global settings rather than per-connection config.
54
+ _GLOBAL_SETTING_KEYS: frozenset[str] = frozenset(
55
+ {"LOG_QUERIES", "LOG_QUERY_PARAMETERS", "SLOW_QUERY_THRESHOLD_MS"}
56
+ )
57
+
58
+ #: Default retry configuration shared by every new ``ConnectionConfig``.
59
+ _DEFAULT_RETRY: dict = {
60
+ "enabled": True,
61
+ "max_attempts": 3,
62
+ "initial_delay": 1.0,
63
+ "multiplier": 2.0,
64
+ "max_delay": 30.0,
65
+ }
66
+
67
+
68
+ @dataclass
69
+ class ConnectionConfig:
70
+ """Per-connection configuration for a single Neo4j driver instance.
71
+
72
+ All fields except ``uri`` have sensible defaults so most deployments only
73
+ need to supply ``URI`` and optionally ``AUTH``.
74
+
75
+ Attributes:
76
+ uri: The Bolt/Neo4j URI, e.g. ``"bolt://localhost:7687"``.
77
+ auth: A ``(username, password)`` tuple or ``None`` for unauthenticated
78
+ connections.
79
+ database: The Neo4j database name. ``None`` lets Neo4j use its
80
+ configured default database.
81
+ max_connection_pool_size: Maximum number of connections kept open by
82
+ the driver's internal pool.
83
+ connection_timeout: Seconds to wait when establishing a new
84
+ connection.
85
+ max_transaction_retry_time: Maximum seconds spent retrying a failed
86
+ transaction.
87
+ connection_acquisition_timeout: Seconds to wait to acquire a
88
+ connection from the pool before raising an error.
89
+ encrypted: Whether to use TLS. Defaults to ``False``.
90
+ trusted_certificates: A ``neo4j.TrustAll``, ``neo4j.TrustCustomCAs``
91
+ or similar object. ``None`` uses the driver default.
92
+ retry: Retry/backoff configuration dict with keys ``enabled``,
93
+ ``max_attempts``, ``initial_delay``, ``multiplier``, ``max_delay``.
94
+ options: Additional keyword arguments forwarded verbatim to
95
+ ``neo4j.GraphDatabase.driver()``.
96
+ """
97
+
98
+ uri: str
99
+ auth: tuple[str, str] | None = field(default=None, repr=False)
100
+ database: str | None = None
101
+ max_connection_pool_size: int = 50
102
+ connection_timeout: float = 30.0
103
+ max_transaction_retry_time: float = 30.0
104
+ connection_acquisition_timeout: float = 60.0
105
+ encrypted: bool = False
106
+ trusted_certificates: object | None = None
107
+ retry: dict = field(default_factory=lambda: dict(_DEFAULT_RETRY))
108
+ options: dict = field(default_factory=dict)
109
+
110
+ def __repr__(self) -> str:
111
+ """Return a repr that redacts the password from the auth tuple.
112
+
113
+ The username is preserved to aid debugging (e.g. confirming which
114
+ Neo4j user is being used), but the password is replaced with ``***``
115
+ so that credentials are never exposed in logs, Sentry reports, or
116
+ Django debug pages.
117
+ """
118
+ auth_display = (self.auth[0], "***") if self.auth else None
119
+ return (
120
+ f"ConnectionConfig(uri={self.uri!r}, auth={auth_display!r}, "
121
+ f"database={self.database!r})"
122
+ )
123
+
124
+
125
+ @dataclass
126
+ class DjraphdbSettings:
127
+ """Top-level djraphdb configuration, loaded from ``settings.DJRAPHDB``.
128
+
129
+ Attributes:
130
+ connections: Mapping from connection name to ``ConnectionConfig``.
131
+ There must be at least one entry named ``"default"``.
132
+ log_queries: When ``True``, every Cypher query is logged at DEBUG
133
+ level.
134
+ log_query_parameters: When ``True``, query parameters are included in
135
+ DEBUG logs. Only meaningful when ``log_queries`` is also
136
+ ``True``.
137
+
138
+ Warning: enabling this will write all query parameter values (which
139
+ may include passwords, PII, or sensitive data) to the DEBUG log.
140
+ slow_query_threshold_ms: Queries that take longer than this many
141
+ milliseconds are logged at WARNING level.
142
+ """
143
+
144
+ connections: dict[str, ConnectionConfig] = field(default_factory=dict)
145
+ log_queries: bool = False
146
+ log_query_parameters: bool = False
147
+ slow_query_threshold_ms: int = 1000
148
+
149
+ # ------------------------------------------------------------------
150
+ # Public factory methods
151
+ # ------------------------------------------------------------------
152
+
153
+ @classmethod
154
+ def from_django_settings(cls) -> "DjraphdbSettings":
155
+ """Load configuration from ``django.conf.settings.DJRAPHDB``.
156
+
157
+ Returns:
158
+ A fully populated ``DjraphdbSettings`` instance.
159
+
160
+ Raises:
161
+ django.core.exceptions.ImproperlyConfigured: If ``DJRAPHDB`` is
162
+ not present in Django settings, if any connection is missing
163
+ a ``URI``, or if ``AUTH`` is not a 2-element sequence of
164
+ strings.
165
+ """
166
+ from django.conf import settings
167
+ from django.core.exceptions import ImproperlyConfigured
168
+
169
+ if not hasattr(settings, "DJRAPHDB"):
170
+ raise ImproperlyConfigured(
171
+ "djraphdb requires a DJRAPHDB dict in your Django settings. "
172
+ "See the djraphdb documentation for configuration examples."
173
+ )
174
+
175
+ raw: dict = settings.DJRAPHDB
176
+ if not isinstance(raw, dict):
177
+ raise ImproperlyConfigured(
178
+ "DJRAPHDB must be a dict, got %s." % type(raw).__name__
179
+ )
180
+
181
+ return cls.from_dict(raw)
182
+
183
+ @classmethod
184
+ def from_dict(cls, config: dict) -> "DjraphdbSettings":
185
+ """Build a ``DjraphdbSettings`` instance from a plain dict.
186
+
187
+ This method implements the same parsing logic as
188
+ ``from_django_settings`` and is useful for testing without a live
189
+ Django project.
190
+
191
+ The dict may use either the flat single-connection format (where
192
+ ``"URI"`` is a top-level key) or the multi-database format (where
193
+ every top-level value is itself a dict).
194
+
195
+ Args:
196
+ config: A plain Python dict following the ``DJRAPHDB`` structure
197
+ documented in this module.
198
+
199
+ Returns:
200
+ A fully populated ``DjraphdbSettings`` instance.
201
+
202
+ Raises:
203
+ django.core.exceptions.ImproperlyConfigured: If any connection is
204
+ missing a ``URI``, or if ``AUTH`` is malformed.
205
+ """
206
+ from django.core.exceptions import ImproperlyConfigured
207
+
208
+ if not isinstance(config, dict):
209
+ raise ImproperlyConfigured(
210
+ "DJRAPHDB configuration must be a dict, got %s." % type(config).__name__
211
+ )
212
+
213
+ # ----------------------------------------------------------------
214
+ # Determine format: flat vs. multi-database
215
+ # ----------------------------------------------------------------
216
+ is_flat = "URI" in config
217
+
218
+ if is_flat:
219
+ # Treat the whole dict as the "default" connection. Global
220
+ # settings are pulled from the same top-level namespace.
221
+ connection_dicts: dict[str, dict] = {"default": config}
222
+ else:
223
+ # Multi-database format. Separate connection sub-dicts from
224
+ # global-setting keys.
225
+ connection_dicts = {}
226
+ for key, value in config.items():
227
+ if key in _GLOBAL_SETTING_KEYS:
228
+ continue
229
+ if not isinstance(value, dict):
230
+ raise ImproperlyConfigured(
231
+ "DJRAPHDB: expected a connection dict for key %r, got %s. "
232
+ "If you meant to configure a single connection, include "
233
+ "a top-level 'URI' key." % (key, type(value).__name__)
234
+ )
235
+ connection_dicts[key] = value
236
+
237
+ # ----------------------------------------------------------------
238
+ # Extract global settings
239
+ # ----------------------------------------------------------------
240
+ log_queries: bool = bool(config.get("LOG_QUERIES", False))
241
+ log_query_parameters: bool = bool(config.get("LOG_QUERY_PARAMETERS", False))
242
+ _raw_threshold = config.get("SLOW_QUERY_THRESHOLD_MS", 1000)
243
+ try:
244
+ slow_query_threshold_ms: int = int(_raw_threshold)
245
+ except (TypeError, ValueError) as exc:
246
+ raise ImproperlyConfigured(
247
+ "DJRAPHDB: 'SLOW_QUERY_THRESHOLD_MS' must be an integer, got %r."
248
+ % (_raw_threshold,)
249
+ ) from exc
250
+
251
+ # ----------------------------------------------------------------
252
+ # Parse each named connection
253
+ # ----------------------------------------------------------------
254
+ connections: dict[str, ConnectionConfig] = {}
255
+ for name, conn_dict in connection_dicts.items():
256
+ connections[name] = _parse_connection_config(
257
+ name=name,
258
+ raw=conn_dict,
259
+ ImproperlyConfigured=ImproperlyConfigured,
260
+ )
261
+
262
+ if not connections:
263
+ raise ImproperlyConfigured(
264
+ "DJRAPHDB must define at least one connection. "
265
+ "Provide a 'URI' key (flat format) or a dict of named connections."
266
+ )
267
+
268
+ return cls(
269
+ connections=connections,
270
+ log_queries=log_queries,
271
+ log_query_parameters=log_query_parameters,
272
+ slow_query_threshold_ms=slow_query_threshold_ms,
273
+ )
274
+
275
+
276
+ # ---------------------------------------------------------------------------
277
+ # Module-level convenience
278
+ # ---------------------------------------------------------------------------
279
+
280
+
281
+ def get_settings() -> DjraphdbSettings:
282
+ """Return the current djraphdb settings, loaded from Django settings.
283
+
284
+ This is a thin convenience wrapper around
285
+ ``DjraphdbSettings.from_django_settings()``.
286
+
287
+ Returns:
288
+ A ``DjraphdbSettings`` instance populated from
289
+ ``django.conf.settings.DJRAPHDB``.
290
+
291
+ Raises:
292
+ django.core.exceptions.ImproperlyConfigured: If ``DJRAPHDB`` is
293
+ absent or invalid.
294
+ """
295
+ return DjraphdbSettings.from_django_settings()
296
+
297
+
298
+ # ---------------------------------------------------------------------------
299
+ # Private parsing helpers
300
+ # ---------------------------------------------------------------------------
301
+
302
+
303
+ def _parse_connection_config(
304
+ *,
305
+ name: str,
306
+ raw: dict,
307
+ ImproperlyConfigured: type[Exception],
308
+ ) -> ConnectionConfig:
309
+ """Parse a single connection configuration dict into a ``ConnectionConfig``.
310
+
311
+ Args:
312
+ name: The connection name (used in error messages).
313
+ raw: The raw dict for this connection (uppercase keys).
314
+ ImproperlyConfigured: The exception class to raise on validation
315
+ failures (passed in to avoid a circular import with Django).
316
+
317
+ Returns:
318
+ A ``ConnectionConfig`` instance.
319
+
320
+ Raises:
321
+ ImproperlyConfigured: If ``URI`` is missing or ``AUTH`` is malformed.
322
+ """
323
+ # --- URI (required) ---------------------------------------------------
324
+ uri: str | None = raw.get("URI")
325
+ if not uri:
326
+ raise ImproperlyConfigured(
327
+ "DJRAPHDB connection %r is missing the required 'URI' key." % name
328
+ )
329
+
330
+ # --- AUTH (optional) --------------------------------------------------
331
+ auth_raw = raw.get("AUTH", None)
332
+ auth: tuple[str, str] | None = None
333
+ if auth_raw is not None:
334
+ if (
335
+ not isinstance(auth_raw, (list, tuple))
336
+ or len(auth_raw) != 2
337
+ or not all(isinstance(part, str) for part in auth_raw)
338
+ ):
339
+ raise ImproperlyConfigured(
340
+ "DJRAPHDB connection %r: 'AUTH' must be a 2-element tuple of "
341
+ "strings (username, password), got %s with length %d."
342
+ % (
343
+ name,
344
+ type(auth_raw).__name__,
345
+ len(auth_raw) if hasattr(auth_raw, "__len__") else 0,
346
+ )
347
+ )
348
+ auth = (auth_raw[0], auth_raw[1])
349
+
350
+ # --- Retry config (merge user values over defaults) -------------------
351
+ retry_raw: dict = raw.get("RETRY", {})
352
+ if not isinstance(retry_raw, dict):
353
+ raise ImproperlyConfigured(
354
+ "DJRAPHDB connection %r: 'RETRY' must be a dict, got %s."
355
+ % (name, type(retry_raw).__name__)
356
+ )
357
+ retry: dict = {**_DEFAULT_RETRY, **retry_raw}
358
+
359
+ # --- MAX_CONNECTION_POOL_SIZE (optional, integer) ----------------------
360
+ _raw_pool_size = raw.get("MAX_CONNECTION_POOL_SIZE", 50)
361
+ try:
362
+ max_connection_pool_size: int = int(_raw_pool_size)
363
+ except (TypeError, ValueError) as exc:
364
+ raise ImproperlyConfigured(
365
+ "DJRAPHDB connection %r: 'MAX_CONNECTION_POOL_SIZE' must be an "
366
+ "integer, got %r." % (name, _raw_pool_size)
367
+ ) from exc
368
+
369
+ # --- CONNECTION_TIMEOUT (optional, float) ------------------------------
370
+ _raw_conn_timeout = raw.get("CONNECTION_TIMEOUT", 30.0)
371
+ try:
372
+ connection_timeout: float = float(_raw_conn_timeout)
373
+ except (TypeError, ValueError) as exc:
374
+ raise ImproperlyConfigured(
375
+ "DJRAPHDB connection %r: 'CONNECTION_TIMEOUT' must be a number, "
376
+ "got %r." % (name, _raw_conn_timeout)
377
+ ) from exc
378
+
379
+ # --- MAX_TRANSACTION_RETRY_TIME (optional, float) ----------------------
380
+ _raw_retry_time = raw.get("MAX_TRANSACTION_RETRY_TIME", 30.0)
381
+ try:
382
+ max_transaction_retry_time: float = float(_raw_retry_time)
383
+ except (TypeError, ValueError) as exc:
384
+ raise ImproperlyConfigured(
385
+ "DJRAPHDB connection %r: 'MAX_TRANSACTION_RETRY_TIME' must be a "
386
+ "number, got %r." % (name, _raw_retry_time)
387
+ ) from exc
388
+
389
+ # --- CONNECTION_ACQUISITION_TIMEOUT (optional, float) ------------------
390
+ _raw_acq_timeout = raw.get("CONNECTION_ACQUISITION_TIMEOUT", 60.0)
391
+ try:
392
+ connection_acquisition_timeout: float = float(_raw_acq_timeout)
393
+ except (TypeError, ValueError) as exc:
394
+ raise ImproperlyConfigured(
395
+ "DJRAPHDB connection %r: 'CONNECTION_ACQUISITION_TIMEOUT' must be "
396
+ "a number, got %r." % (name, _raw_acq_timeout)
397
+ ) from exc
398
+
399
+ # --- OPTIONS (optional, dict) ------------------------------------------
400
+ options_raw = raw.get("OPTIONS", {})
401
+ if not isinstance(options_raw, dict):
402
+ raise ImproperlyConfigured(
403
+ "DJRAPHDB connection %r: 'OPTIONS' must be a dict, got %s."
404
+ % (name, type(options_raw).__name__)
405
+ )
406
+ options: dict = dict(options_raw)
407
+
408
+ return ConnectionConfig(
409
+ uri=uri,
410
+ auth=auth,
411
+ database=raw.get("DATABASE"),
412
+ max_connection_pool_size=max_connection_pool_size,
413
+ connection_timeout=connection_timeout,
414
+ max_transaction_retry_time=max_transaction_retry_time,
415
+ connection_acquisition_timeout=connection_acquisition_timeout,
416
+ encrypted=bool(raw.get("ENCRYPTED", False)),
417
+ trusted_certificates=raw.get("TRUSTED_CERTIFICATES", None),
418
+ retry=retry,
419
+ options=options,
420
+ )