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 +138 -0
- djraphdb/apps.py +58 -0
- djraphdb/conf.py +420 -0
- djraphdb/connection.py +279 -0
- djraphdb/decorators.py +295 -0
- djraphdb/exceptions.py +108 -0
- djraphdb/fields.py +292 -0
- djraphdb/health.py +250 -0
- djraphdb/management/__init__.py +0 -0
- djraphdb/management/commands/__init__.py +0 -0
- djraphdb/management/commands/graph_clear.py +94 -0
- djraphdb/management/commands/graph_info.py +220 -0
- djraphdb/management/commands/graph_seed.py +95 -0
- djraphdb/middleware.py +205 -0
- djraphdb/nodes.py +592 -0
- djraphdb/py.typed +0 -0
- djraphdb/query.py +492 -0
- djraphdb/retry.py +158 -0
- djraphdb/service.py +413 -0
- djraphdb/signals.py +62 -0
- djraphdb/test/__init__.py +33 -0
- djraphdb/test/fixtures.py +133 -0
- djraphdb/test/mock.py +242 -0
- djraphdb/test/testcases.py +247 -0
- djraphdb/types.py +328 -0
- djraphdb-0.1.0.dist-info/METADATA +167 -0
- djraphdb-0.1.0.dist-info/RECORD +29 -0
- djraphdb-0.1.0.dist-info/WHEEL +4 -0
- djraphdb-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|
+
)
|