langgraph-checkpoint-dmdb 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,12 @@
1
+ """DM Database checkpoint savers for LangGraph."""
2
+
3
+ from .aio import AsyncDMDBSaver, AsyncShallowDMDBSaver
4
+ from .dmpython import DMDBSaver
5
+ from .shallow import ShallowDMDBSaver
6
+
7
+ __all__ = [
8
+ "AsyncDMDBSaver",
9
+ "AsyncShallowDMDBSaver",
10
+ "DMDBSaver",
11
+ "ShallowDMDBSaver",
12
+ ]
@@ -0,0 +1,173 @@
1
+ """Internal DB-API helpers for the synchronous DMDB saver."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ from collections.abc import Callable, Iterator, Sequence
7
+ from contextlib import contextmanager
8
+ from typing import Any, Protocol, TypeVar, Union, cast
9
+
10
+
11
+ class Cursor(Protocol):
12
+ description: Sequence[Sequence[Any]] | None
13
+ rowcount: int
14
+
15
+ def execute(
16
+ self,
17
+ operation: str,
18
+ parameters: Sequence[Any] = ...,
19
+ /,
20
+ ) -> object: ...
21
+
22
+ def executemany(
23
+ self,
24
+ operation: str,
25
+ seq_of_parameters: Sequence[Sequence[Any]],
26
+ /,
27
+ ) -> object: ...
28
+
29
+ def fetchone(self) -> Sequence[Any] | None: ...
30
+
31
+ def fetchall(self) -> Sequence[Sequence[Any]]: ...
32
+
33
+ def fetchmany(self, size: int = ...) -> Sequence[Sequence[Any]]: ...
34
+
35
+ def setinputsizes(self, *sizes: object) -> None: ...
36
+
37
+ def close(self) -> None: ...
38
+
39
+
40
+ class Connection(Protocol):
41
+ def cursor(self) -> Cursor: ...
42
+
43
+ def commit(self) -> None: ...
44
+
45
+ def rollback(self) -> None: ...
46
+
47
+ def close(self) -> None: ...
48
+
49
+
50
+ C = TypeVar("C", bound=Connection)
51
+ ConnectionFactory = Callable[[], C]
52
+ Conn = Union[C, ConnectionFactory[C]]
53
+
54
+
55
+ def is_connection(value: object) -> bool:
56
+ try:
57
+ return callable(value.cursor) # type: ignore[attr-defined]
58
+ except AttributeError:
59
+ return False
60
+
61
+
62
+ @contextmanager
63
+ def get_connection(
64
+ source: Conn[C],
65
+ *,
66
+ direct_lock: threading.RLock,
67
+ direct_thread_id: int | None,
68
+ ) -> Iterator[C]:
69
+ """Yield a borrowed direct connection or an owned factory connection."""
70
+ if is_connection(source):
71
+ if direct_thread_id != threading.get_ident():
72
+ raise RuntimeError(
73
+ "A direct dmPython connection cannot be used from another thread; "
74
+ "construct DMDBSaver with a connection factory instead."
75
+ )
76
+ with direct_lock:
77
+ yield cast(C, source)
78
+ return
79
+
80
+ if not callable(source):
81
+ raise TypeError(f"Invalid DMDB connection source: {type(source)!r}")
82
+
83
+ conn = source()
84
+ if not is_connection(conn):
85
+ raise TypeError(f"Connection factory returned an invalid value: {type(conn)!r}")
86
+ try:
87
+ yield conn
88
+ finally:
89
+ conn.close()
90
+
91
+
92
+ def column_names(cursor: Cursor) -> list[str]:
93
+ return [str(column[0]).lower() for column in cursor.description or ()]
94
+
95
+
96
+ def row_to_dict(columns: Sequence[str], row: Sequence[Any]) -> dict[str, Any]:
97
+ return dict(zip(columns, row))
98
+
99
+
100
+ def fetchone_dict(cursor: Cursor) -> dict[str, Any] | None:
101
+ columns = column_names(cursor)
102
+ row = cursor.fetchone()
103
+ return row_to_dict(columns, row) if row is not None else None
104
+
105
+
106
+ def fetchall_dict(cursor: Cursor) -> list[dict[str, Any]]:
107
+ columns = column_names(cursor)
108
+ return [row_to_dict(columns, row) for row in cursor.fetchall()]
109
+
110
+
111
+ def fetchmany_dict(cursor: Cursor, size: int) -> list[dict[str, Any]]:
112
+ columns = column_names(cursor)
113
+ return [row_to_dict(columns, row) for row in cursor.fetchmany(size)]
114
+
115
+
116
+ def execute_batch(
117
+ cursor: Cursor,
118
+ operation: str,
119
+ parameters: Sequence[Sequence[Any]],
120
+ ) -> None:
121
+ """Execute DM MERGE rows individually within the caller's transaction.
122
+
123
+ dmPython 2.5.32 may size executemany buffers from an earlier row and raise
124
+ ``-70005 String truncated`` when later serialized BLOB values are larger.
125
+ """
126
+ for row in parameters:
127
+ cursor.execute(operation, row)
128
+
129
+
130
+ def configure_input_sizes(cursor: Cursor, *sizes: int | str) -> None:
131
+ """Configure dmPython bind sizes without importing the driver in test fakes."""
132
+ try:
133
+ import dmPython # type: ignore[import-not-found]
134
+ except ImportError:
135
+ return
136
+ try:
137
+ resolved = [
138
+ getattr(dmPython, size.upper()) if isinstance(size, str) else size
139
+ for size in sizes
140
+ ]
141
+ except AttributeError:
142
+ return
143
+ cursor.setinputsizes(*resolved)
144
+
145
+
146
+ def _read_lob(value: object) -> object:
147
+ read = getattr(value, "read", None)
148
+ return _read_lob(read()) if callable(read) else value
149
+
150
+
151
+ def read_clob(value: object) -> str:
152
+ value = _read_lob(value)
153
+ if isinstance(value, str):
154
+ return value
155
+ if isinstance(value, (bytes, bytearray, memoryview)):
156
+ return bytes(value).decode("utf-8")
157
+ raise TypeError(f"Unsupported CLOB value: {type(value)!r}")
158
+
159
+
160
+ def read_blob(value: object) -> bytes:
161
+ value = _read_lob(value)
162
+ if isinstance(value, str):
163
+ return value.encode("utf-8")
164
+ if isinstance(value, (bytes, bytearray, memoryview)):
165
+ return bytes(value)
166
+ raise TypeError(f"Unsupported BLOB value: {type(value)!r}")
167
+
168
+
169
+ def rollback_quietly(conn: Connection) -> None:
170
+ try:
171
+ conn.rollback()
172
+ except Exception:
173
+ pass
@@ -0,0 +1,182 @@
1
+ """Async facades for dmPython-backed checkpoint savers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from collections.abc import AsyncIterator, Callable, Iterator, Sequence
7
+ from typing import Any
8
+
9
+ from langchain_core.runnables import RunnableConfig
10
+ from langgraph.checkpoint.base import (
11
+ ChannelVersions,
12
+ Checkpoint,
13
+ CheckpointMetadata,
14
+ CheckpointTuple,
15
+ )
16
+ from langgraph.checkpoint.serde.base import SerializerProtocol
17
+ from typing_extensions import Self
18
+
19
+ from . import _internal
20
+ from .base import BaseDMDBSaver
21
+ from .dmpython import (
22
+ DMDBSaver,
23
+ _connection_factory,
24
+ _validate_conn_params,
25
+ )
26
+ from .shallow import ShallowDMDBSaver
27
+
28
+
29
+ class AsyncDMDBSaver(BaseDMDBSaver):
30
+ """Async CheckpointSaver using one factory-created connection per call."""
31
+
32
+ sync_saver_class: type[DMDBSaver] = DMDBSaver
33
+
34
+ def __init__(
35
+ self,
36
+ conn: Callable[[], _internal.Connection],
37
+ *,
38
+ serde: SerializerProtocol | None = None,
39
+ schema: str | None = None,
40
+ ) -> None:
41
+ if _internal.is_connection(conn):
42
+ raise TypeError(
43
+ "AsyncDMDBSaver requires a connection factory; a dmPython connection "
44
+ "cannot be shared with worker threads."
45
+ )
46
+ if not callable(conn):
47
+ raise TypeError("AsyncDMDBSaver requires a connection factory")
48
+ super().__init__(serde=serde)
49
+ self._sync = self.sync_saver_class(conn, serde=self.serde, schema=schema)
50
+
51
+ @classmethod
52
+ def from_conn_string(
53
+ cls,
54
+ conn_string: str,
55
+ *,
56
+ serde: SerializerProtocol | None = None,
57
+ ) -> Self:
58
+ params = DMDBSaver.parse_conn_string(conn_string)
59
+ return cls(_connection_factory(params), serde=serde, schema=params.schema)
60
+
61
+ @classmethod
62
+ def from_conn_params(
63
+ cls,
64
+ *,
65
+ host: str,
66
+ user: str,
67
+ password: str,
68
+ port: int = 5236,
69
+ schema: str | None = None,
70
+ serde: SerializerProtocol | None = None,
71
+ ) -> Self:
72
+ params = _validate_conn_params(
73
+ host=host,
74
+ user=user,
75
+ password=password,
76
+ port=port,
77
+ schema=schema,
78
+ )
79
+ return cls(_connection_factory(params), serde=serde, schema=params.schema)
80
+
81
+ async def __aenter__(self) -> Self:
82
+ self._sync._ensure_open()
83
+ return self
84
+
85
+ async def __aexit__(self, *exc_info: object) -> None:
86
+ await self.aclose()
87
+
88
+ def close(self) -> None:
89
+ self._sync.close()
90
+
91
+ async def aclose(self) -> None:
92
+ self.close()
93
+
94
+ async def setup(self) -> None:
95
+ await asyncio.to_thread(self._sync.setup)
96
+
97
+ def list(
98
+ self,
99
+ config: RunnableConfig | None,
100
+ *,
101
+ filter: dict[str, Any] | None = None,
102
+ before: RunnableConfig | None = None,
103
+ limit: int | None = None,
104
+ ) -> Iterator[CheckpointTuple]:
105
+ return self._sync.list(config, filter=filter, before=before, limit=limit)
106
+
107
+ async def alist(
108
+ self,
109
+ config: RunnableConfig | None,
110
+ *,
111
+ filter: dict[str, Any] | None = None,
112
+ before: RunnableConfig | None = None,
113
+ limit: int | None = None,
114
+ ) -> AsyncIterator[CheckpointTuple]:
115
+ values = await asyncio.to_thread(
116
+ lambda: list(
117
+ self._sync.list(config, filter=filter, before=before, limit=limit)
118
+ )
119
+ )
120
+ for value in values:
121
+ yield value
122
+
123
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
124
+ return self._sync.get_tuple(config)
125
+
126
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
127
+ return await asyncio.to_thread(self._sync.get_tuple, config)
128
+
129
+ def put(
130
+ self,
131
+ config: RunnableConfig,
132
+ checkpoint: Checkpoint,
133
+ metadata: CheckpointMetadata,
134
+ new_versions: ChannelVersions,
135
+ ) -> RunnableConfig:
136
+ return self._sync.put(config, checkpoint, metadata, new_versions)
137
+
138
+ async def aput(
139
+ self,
140
+ config: RunnableConfig,
141
+ checkpoint: Checkpoint,
142
+ metadata: CheckpointMetadata,
143
+ new_versions: ChannelVersions,
144
+ ) -> RunnableConfig:
145
+ return await asyncio.to_thread(
146
+ self._sync.put, config, checkpoint, metadata, new_versions
147
+ )
148
+
149
+ def put_writes(
150
+ self,
151
+ config: RunnableConfig,
152
+ writes: Sequence[tuple[str, Any]],
153
+ task_id: str,
154
+ task_path: str = "",
155
+ ) -> None:
156
+ self._sync.put_writes(config, writes, task_id, task_path)
157
+
158
+ async def aput_writes(
159
+ self,
160
+ config: RunnableConfig,
161
+ writes: Sequence[tuple[str, Any]],
162
+ task_id: str,
163
+ task_path: str = "",
164
+ ) -> None:
165
+ await asyncio.to_thread(
166
+ self._sync.put_writes, config, writes, task_id, task_path
167
+ )
168
+
169
+ def delete_thread(self, thread_id: str) -> None:
170
+ self._sync.delete_thread(thread_id)
171
+
172
+ async def adelete_thread(self, thread_id: str) -> None:
173
+ await asyncio.to_thread(self._sync.delete_thread, thread_id)
174
+
175
+
176
+ class AsyncShallowDMDBSaver(AsyncDMDBSaver):
177
+ """Async facade for :class:`ShallowDMDBSaver`."""
178
+
179
+ sync_saver_class = ShallowDMDBSaver
180
+
181
+
182
+ __all__ = ["AsyncDMDBSaver", "AsyncShallowDMDBSaver"]