sonnet-graph 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.
@@ -0,0 +1,51 @@
1
+ """sonnet-graph -- Graph database integration for sonnet-server applications.
2
+
3
+ Public API:
4
+
5
+ Extension (register in app.py):
6
+ GraphDBExtension -- pool lifecycle, readiness checks
7
+
8
+ Pool context:
9
+ borrow_graphdb -- context manager: acquire connection + set ContextVar
10
+ get_current_graphdb -- read ambient CypherGraphDB from ContextVar
11
+ init_graphdb_pool -- lazily create the pool singleton
12
+ dispose_graphdb_pool -- close and discard the pool
13
+ is_pool_initialized -- check if pool exists
14
+ graphdb_configured -- check if CGDB_* settings are present
15
+
16
+ FastAPI dependencies:
17
+ get_graphdb_session -- async generator for router-level ambient session
18
+ get_graphdb -- sync generator yielding CypherGraphDB explicitly
19
+
20
+ Readiness pipeline (auto-registered by GraphDBExtension):
21
+ GraphdbStage -- pipeline stage marker (order=40)
22
+ """
23
+
24
+ from sonnet_graph.checks.stages import GraphdbStage
25
+ from sonnet_graph.context import (
26
+ borrow_graphdb,
27
+ dispose_graphdb_pool,
28
+ get_current_graphdb,
29
+ graphdb_configured,
30
+ init_graphdb_pool,
31
+ is_pool_initialized,
32
+ )
33
+ from sonnet_graph.dependencies import get_graphdb, get_graphdb_session
34
+ from sonnet_graph.extension import GraphDBExtension
35
+
36
+ __all__ = [
37
+ # Extension
38
+ "GraphDBExtension",
39
+ # Pool context
40
+ "borrow_graphdb",
41
+ "dispose_graphdb_pool",
42
+ "get_current_graphdb",
43
+ "graphdb_configured",
44
+ "init_graphdb_pool",
45
+ "is_pool_initialized",
46
+ # FastAPI dependencies
47
+ "get_graphdb",
48
+ "get_graphdb_session",
49
+ # Readiness pipeline
50
+ "GraphdbStage",
51
+ ]
@@ -0,0 +1,11 @@
1
+ """Graph DB readiness checks for the sonnet-graph package.
2
+
3
+ Importing this package registers ``GraphdbStage`` in the global stage
4
+ registry (via the ``@readiness_stage`` decorator in ``stages.py``).
5
+
6
+ Check classes are registered lazily -- they are imported by
7
+ ``GraphDBExtension.on_startup()`` so they only appear in the pipeline
8
+ when the extension is active.
9
+ """
10
+
11
+ from sonnet_graph.checks.stages import GraphdbStage # noqa: F401 -- registers stage
@@ -0,0 +1,35 @@
1
+ """GraphDB connection health check for readiness pipeline."""
2
+
3
+ from loguru import logger
4
+
5
+ from sonnet_graph.checks.stages import GraphdbStage
6
+ from sonnet_graph.context import borrow_graphdb
7
+ from sonnet_server.readiness_pipeline import ReadinessCheck, ReadinessCheckResult, readiness_check
8
+
9
+
10
+ @readiness_check(stage=GraphdbStage, is_critical=True, run_once=False, order=1)
11
+ class GraphDBHealthCheck(ReadinessCheck):
12
+ """Verifies a live connection to the graph database."""
13
+
14
+ def __init__(self) -> None:
15
+ super().__init__("graphdb_health_check")
16
+
17
+ def _execute(self) -> ReadinessCheckResult:
18
+ logger.info("Checking GraphDB connection health")
19
+ try:
20
+ with borrow_graphdb() as cdb:
21
+ if not cdb.check_connection():
22
+ return self.failed(
23
+ "CypherGraphDB connection check failed",
24
+ {"graph": getattr(cdb, "graph_name", None)},
25
+ )
26
+ return self.success(
27
+ "CypherGraphDB connection is healthy",
28
+ {
29
+ "id": getattr(cdb, "id", None),
30
+ "graph": getattr(cdb, "graph_name", None),
31
+ },
32
+ )
33
+ except (OSError, RuntimeError, ConnectionError, TimeoutError) as e:
34
+ logger.error("GraphDB health check failed: {}", e)
35
+ return self.failed(f"GraphDB health check failed: {e}", {"error": str(e)})
@@ -0,0 +1,28 @@
1
+ """GraphDB pool initialization check for readiness pipeline."""
2
+
3
+ from loguru import logger
4
+
5
+ from sonnet_graph.checks.stages import GraphdbStage
6
+ from sonnet_graph.context import init_graphdb_pool, is_pool_initialized
7
+ from sonnet_server.readiness_pipeline import ReadinessCheck, ReadinessCheckResult, readiness_check
8
+
9
+
10
+ @readiness_check(stage=GraphdbStage, is_critical=True, run_once=True, order=0)
11
+ class GraphDBInitializationCheck(ReadinessCheck):
12
+ """Ensures the CypherGraphDB connection pool is initialized."""
13
+
14
+ def __init__(self) -> None:
15
+ super().__init__("graphdb_initialization")
16
+
17
+ def _execute(self) -> ReadinessCheckResult:
18
+ logger.info("Checking GraphDB pool initialization")
19
+ try:
20
+ if not is_pool_initialized():
21
+ init_graphdb_pool()
22
+ return self.success("CypherGraphDB pool initialized", {"initialized": True})
23
+ except (OSError, RuntimeError, ConnectionError, TimeoutError) as e:
24
+ logger.error("CypherGraphDB initialization failed: {}", e)
25
+ return self.failed(
26
+ f"CypherGraphDB initialization failed: {e}",
27
+ {"error": str(e), "type": type(e).__name__, "initialized": False},
28
+ )
@@ -0,0 +1,13 @@
1
+ """Graph database readiness pipeline stage.
2
+
3
+ Registered at import time via the @readiness_stage decorator.
4
+ Order 40 places it after database (1), messaging (2), cache (3)
5
+ but before application-specific stages (50+).
6
+ """
7
+
8
+ from sonnet_server.readiness_pipeline import readiness_stage
9
+
10
+
11
+ @readiness_stage(order=40, description="Graph database checks", is_critical=False, fail_fast=True)
12
+ class GraphdbStage:
13
+ """CypherGraphDB connection pool initialization and health."""
@@ -0,0 +1,151 @@
1
+ """Graph DB connection pool lifecycle and ambient session management.
2
+
3
+ Provides:
4
+ - ``init_graphdb_pool()`` -- lazily initialise the connection pool
5
+ - ``dispose_graphdb_pool()`` -- close and discard the pool
6
+ - ``borrow_graphdb()`` -- context manager: acquire connection, set ContextVar
7
+ - ``get_current_graphdb()`` -- read the ambient CypherGraphDB from ContextVar
8
+ - ``is_pool_initialized()`` -- check if the pool has been created
9
+ - ``graphdb_configured()`` -- check if CGDB_* settings are present
10
+
11
+ Configuration comes from cypher-graphdb's own settings
12
+ (CGDB_BACKEND, CGDB_CINFO, CGDB_GRAPH) -- not duplicated here.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Iterator
18
+ from contextlib import contextmanager, suppress
19
+ from contextvars import ContextVar
20
+
21
+ from cypher_graphdb import CypherGraphDB
22
+ from cypher_graphdb.dbpool import CypherGraphDBPool
23
+ from cypher_graphdb.settings import Settings as CypherSettings
24
+ from cypher_graphdb.settings import get_settings as get_cypher_settings
25
+ from loguru import logger
26
+
27
+ _current_graphdb: ContextVar[CypherGraphDB | None] = ContextVar("_current_graphdb", default=None)
28
+
29
+ # Held as a module-level variable so dispose_graphdb_pool() can close it
30
+ # without risk of accidentally recreating it.
31
+ _pool_instance: CypherGraphDBPool | None = None
32
+
33
+
34
+ def graphdb_configured(settings: CypherSettings | None = None) -> bool:
35
+ """Return True if the required CGDB_* environment variables are set."""
36
+ s = settings or get_cypher_settings()
37
+ return bool(s.backend and s.cinfo and s.graph)
38
+
39
+
40
+ def get_current_graphdb() -> CypherGraphDB:
41
+ """Return the ambient CypherGraphDB session.
42
+
43
+ Must be called within a ``borrow_graphdb()`` context.
44
+
45
+ Raises:
46
+ RuntimeError: If no active session exists.
47
+ """
48
+ cdb = _current_graphdb.get()
49
+ if cdb is None:
50
+ raise RuntimeError("No active CypherGraphDB session -- call within borrow_graphdb() context")
51
+ return cdb
52
+
53
+
54
+ @contextmanager
55
+ def borrow_graphdb(block: bool = True, timeout: float = 10.0) -> Iterator[CypherGraphDB]:
56
+ """Acquire a CypherGraphDB from the pool and set the ambient ContextVar.
57
+
58
+ The connection is returned to the pool when the context exits.
59
+
60
+ Args:
61
+ block: Whether to block if no connections are available.
62
+ timeout: Maximum seconds to wait for a connection.
63
+ """
64
+ pool = _get_or_create_pool()
65
+ with pool.acquire(block=block, timeout=timeout) as cdb:
66
+ token = _current_graphdb.set(cdb)
67
+ try:
68
+ yield cdb
69
+ finally:
70
+ _current_graphdb.reset(token)
71
+
72
+
73
+ def init_graphdb_pool(*, pool_size: int = 3) -> bool:
74
+ """Initialize the graph DB connection pool.
75
+
76
+ Must be called before the first ``borrow_graphdb()`` to configure
77
+ pool_size. Safe to call multiple times -- idempotent once created.
78
+
79
+ Returns True if the pool was created, False if CGDB_* settings are missing.
80
+ """
81
+ global _pool_instance
82
+ if not graphdb_configured():
83
+ return False
84
+ if _pool_instance is None:
85
+ _pool_instance = _create_pool(pool_size)
86
+ logger.info("GraphDB pool initialized")
87
+ return True
88
+
89
+
90
+ def dispose_graphdb_pool() -> None:
91
+ """Shut down the pool and release all connections."""
92
+ global _pool_instance
93
+ if _pool_instance is not None:
94
+ with suppress(RuntimeError, ConnectionError, OSError):
95
+ _pool_instance.close()
96
+ _pool_instance = None
97
+ logger.info("GraphDB pool disposed")
98
+
99
+
100
+ def is_pool_initialized() -> bool:
101
+ """Return True if the pool has been created."""
102
+ return _pool_instance is not None
103
+
104
+
105
+ # -- internal ----------------------------------------------------------------
106
+
107
+
108
+ def _get_or_create_pool() -> CypherGraphDBPool:
109
+ """Return the existing pool or create one with default pool_size=3.
110
+
111
+ Called by ``borrow_graphdb()`` when the pool was not pre-initialized
112
+ via ``init_graphdb_pool()``. Falls back to pool_size=3.
113
+ """
114
+ global _pool_instance
115
+ if _pool_instance is None:
116
+ _pool_instance = _create_pool(pool_size=3)
117
+ return _pool_instance
118
+
119
+
120
+ def _create_pool(pool_size: int) -> CypherGraphDBPool:
121
+ """Create and return a new CypherGraphDBPool."""
122
+ settings = get_cypher_settings()
123
+ if not settings.backend:
124
+ raise RuntimeError("Graph backend not configured. Set CGDB_BACKEND.")
125
+ if not settings.cinfo:
126
+ raise RuntimeError("Graph connection not configured. Set CGDB_CINFO.")
127
+ if not settings.graph:
128
+ raise RuntimeError("Graph name not configured. Set CGDB_GRAPH.")
129
+
130
+ connect_params = {
131
+ "cinfo": settings.cinfo,
132
+ "graph_name": settings.graph,
133
+ "read_only": settings.read_only,
134
+ "create_graph": settings.create_graph,
135
+ }
136
+ # 0 means "no timeout" -- don't pass it (leave backend at its own default).
137
+ if settings.query_timeout_s:
138
+ connect_params["query_timeout_s"] = settings.query_timeout_s
139
+ pool = CypherGraphDBPool(
140
+ backend=settings.backend,
141
+ connect_params=connect_params,
142
+ pool_size=pool_size,
143
+ auto_connect=True,
144
+ )
145
+ logger.debug(
146
+ "GraphDB pool created: backend={}, graph={}, pool_size={}",
147
+ settings.backend,
148
+ settings.graph,
149
+ pool_size,
150
+ )
151
+ return pool
@@ -0,0 +1,53 @@
1
+ """FastAPI dependencies for graph database access.
2
+
3
+ Provides two dependency patterns:
4
+
5
+ 1. **Ambient session** (recommended) -- use ``graphdb_router()`` from
6
+ sonnet-server's ``api.dependencies`` module, or add
7
+ ``get_graphdb_session`` as a router-level dependency. Services then
8
+ call ``get_current_graphdb()`` without parameters.
9
+
10
+ 2. **Explicit parameter** -- use ``Depends(get_graphdb)`` on individual
11
+ endpoints to receive a ``CypherGraphDB`` instance directly.
12
+
13
+ Example::
14
+
15
+ from fastapi import APIRouter, Depends
16
+ from sonnet_graph.dependencies import get_graphdb_session
17
+ from sonnet_graph.context import get_current_graphdb
18
+
19
+ router = APIRouter(dependencies=[Depends(get_graphdb_session)])
20
+
21
+ @router.get("/nodes")
22
+ def list_nodes():
23
+ db = get_current_graphdb()
24
+ return db.execute("MATCH (n) RETURN n LIMIT 10")
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from collections.abc import AsyncIterator, Iterator
30
+
31
+ from cypher_graphdb import CypherGraphDB
32
+
33
+ from sonnet_graph.context import borrow_graphdb
34
+
35
+
36
+ async def get_graphdb_session() -> AsyncIterator[None]:
37
+ """Router-level dependency establishing an ambient graphdb session.
38
+
39
+ Sets the ContextVar so services can call ``get_current_graphdb()``
40
+ without receiving CypherGraphDB as a parameter.
41
+ """
42
+ with borrow_graphdb():
43
+ yield
44
+
45
+
46
+ def get_graphdb() -> Iterator[CypherGraphDB]:
47
+ """Endpoint-level dependency yielding a CypherGraphDB instance explicitly.
48
+
49
+ Prefer the ambient session pattern (``get_graphdb_session`` + ``get_current_graphdb``)
50
+ for new code.
51
+ """
52
+ with borrow_graphdb() as db:
53
+ yield db
@@ -0,0 +1,72 @@
1
+ """GraphDB infrastructure extension for sonnet-server applications.
2
+
3
+ Manages the CypherGraphDB connection pool lifecycle (startup/shutdown)
4
+ and optionally registers readiness checks.
5
+
6
+ Usage in app.py::
7
+
8
+ from sonnet_graph import GraphDBExtension
9
+
10
+ _extension_registry = create_extension_registry(
11
+ GraphDBExtension(), # required -- raises if CGDB_* missing
12
+ GraphDBExtension(optional=True), # optional -- skips if unconfigured
13
+ )
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from loguru import logger
19
+
20
+ from sonnet_server.extensions import Extension
21
+ from sonnet_server.settings import Settings
22
+
23
+
24
+ class GraphDBExtension(Extension):
25
+ """Infrastructure extension for the CypherGraphDB connection pool.
26
+
27
+ Starts unconditionally (no profile). Initialises the pool on startup
28
+ and disposes it on shutdown.
29
+
30
+ Args:
31
+ optional: If True, silently skip when CGDB_* settings are absent.
32
+ If False (default), raise RuntimeError on missing settings.
33
+ pool_size: Maximum number of pooled connections (default: 3).
34
+ """
35
+
36
+ def __init__(self, *, optional: bool = False, pool_size: int = 3) -> None:
37
+ self._optional = optional
38
+ self._pool_size = pool_size
39
+ self._active = False
40
+
41
+ @property
42
+ def name(self) -> str:
43
+ return "graphdb"
44
+
45
+ async def on_startup(self, settings: Settings) -> None:
46
+ from sonnet_graph.context import graphdb_configured, init_graphdb_pool
47
+
48
+ if not graphdb_configured():
49
+ if self._optional:
50
+ logger.info("GraphDBExtension: CGDB_* settings not configured, skipping (optional)")
51
+ return
52
+ raise RuntimeError(
53
+ "GraphDBExtension: graph database not configured. "
54
+ "Set CGDB_BACKEND, CGDB_CINFO, and CGDB_GRAPH environment variables."
55
+ )
56
+
57
+ init_graphdb_pool(pool_size=self._pool_size)
58
+ self._active = True
59
+
60
+ # Import check modules to trigger @readiness_check decorator registration.
61
+ import sonnet_graph.checks.graphdb_health_check # noqa: F401
62
+ import sonnet_graph.checks.graphdb_initialization # noqa: F401
63
+
64
+ logger.info("GraphDBExtension: pool initialized, readiness checks registered")
65
+
66
+ async def on_shutdown(self) -> None:
67
+ if not self._active:
68
+ return
69
+ from sonnet_graph.context import dispose_graphdb_pool
70
+
71
+ dispose_graphdb_pool()
72
+ self._active = False
@@ -0,0 +1,75 @@
1
+ Metadata-Version: 2.4
2
+ Name: sonnet-graph
3
+ Version: 0.1.0
4
+ Summary: Graph database integration (cypher-graphdb) for sonnet-server applications
5
+ Author-email: Wolfgang Miller <wolfgang.miller@petrarca-labs.com>
6
+ License-Expression: Apache-2.0
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Requires-Python: <4.0,>=3.14
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: sonnet-server>=0.1.11
14
+ Requires-Dist: cypher-graphdb>=0.2.7
15
+ Requires-Dist: loguru>=0.7.3
16
+ Provides-Extra: dev
17
+ Requires-Dist: ruff>=0.3.0; extra == "dev"
18
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
19
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
20
+
21
+ # sonnet-graph
22
+
23
+ Graph database integration (cypher-graphdb) for sonnet-server applications.
24
+
25
+ Provides reusable infrastructure for sonnet-server apps that need a
26
+ cypher-graphdb connection pool:
27
+
28
+ - **`GraphDBExtension`** -- sonnet-server extension managing pool lifecycle
29
+ (startup/shutdown), with optional mode for apps where graph is not required.
30
+ - **Pool context** -- `borrow_graphdb()` context manager and `get_current_graphdb()`
31
+ ambient session accessor via `ContextVar`.
32
+ - **Readiness checks** -- `GraphdbStage` with initialization and health checks
33
+ for the sonnet-server readiness pipeline.
34
+ - **FastAPI dependencies** -- `get_graphdb_session()` async generator for
35
+ router-level ambient sessions.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install sonnet-graph
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```python
46
+ from sonnet_graph import GraphDBExtension, borrow_graphdb, get_current_graphdb
47
+
48
+ # In app.py -- register the extension
49
+ _extension_registry = create_extension_registry(
50
+ DatabaseExtension(),
51
+ GraphDBExtension(), # required (raises on missing CGDB_* settings)
52
+ GraphDBExtension(optional=True), # or optional (skips if unconfigured)
53
+ ...
54
+ )
55
+
56
+ # In service code -- borrow a connection
57
+ with borrow_graphdb() as db:
58
+ db.execute("MATCH (n) RETURN count(n)")
59
+
60
+ # In service code -- ambient session (within borrow_graphdb scope)
61
+ db = get_current_graphdb()
62
+ ```
63
+
64
+ ## Configuration
65
+
66
+ Graph connection is configured via cypher-graphdb's own environment variables
67
+ (not duplicated in consumer settings):
68
+
69
+ | Variable | Description |
70
+ |---|---|
71
+ | `CGDB_BACKEND` | Backend type: `age` or `memgraph` |
72
+ | `CGDB_CINFO` | Connection string / DSN |
73
+ | `CGDB_GRAPH` | Graph name |
74
+ | `CGDB_READ_ONLY` | Read-only mode (default: `false`) |
75
+ | `CGDB_CREATE_GRAPH_IF_NOT_EXISTS` | Auto-create graph, AGE only (default: `false`) |
@@ -0,0 +1,12 @@
1
+ sonnet_graph/__init__.py,sha256=fFQOGmLtKebF3gcrw3NmAk_KvCWXNWWcjb5HmgmrIdE,1634
2
+ sonnet_graph/context.py,sha256=bpohxZZeU-Hw_crxty-GeFiC6uNXFkcKHOXe5d9F9lo,5263
3
+ sonnet_graph/dependencies.py,sha256=9Cbse8q9Wwcy06sHpgCgM_GBQG3hpsAwMTjoy5NuNVE,1639
4
+ sonnet_graph/extension.py,sha256=zxONLgP2Dg0h1RqtM0HWgzic9vL2ph6cRIk8dO21g90,2466
5
+ sonnet_graph/checks/__init__.py,sha256=YjS-WiUifxxmznb6hjclpT3uVoelUhgYwH8zLMngIQ4,449
6
+ sonnet_graph/checks/graphdb_health_check.py,sha256=4O1QPpOkEmvZtWzdea-4nEF1MeQwWyJUwSN6rueLyI4,1468
7
+ sonnet_graph/checks/graphdb_initialization.py,sha256=45-sX3cMBqgWfbwU0l1rPtirxjFD6_CA6NALhDReWs8,1224
8
+ sonnet_graph/checks/stages.py,sha256=pZbMcGKr-0vwSQQcK_F_pdPCD5Mo3y4aJC8rF_Gtl4I,471
9
+ sonnet_graph-0.1.0.dist-info/METADATA,sha256=nsyTdsVRsimmuw1bT1y0dfxqVfJxU3UWbnfPF_52AAM,2561
10
+ sonnet_graph-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ sonnet_graph-0.1.0.dist-info/top_level.txt,sha256=HCrJGvnLfVHPBz2E9t6WM-QHMQTDOeE1QjH1k-XZtiU,13
12
+ sonnet_graph-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sonnet_graph