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.
- langgraph/checkpoint/dmdb/__init__.py +12 -0
- langgraph/checkpoint/dmdb/_internal.py +173 -0
- langgraph/checkpoint/dmdb/aio.py +182 -0
- langgraph/checkpoint/dmdb/base.py +461 -0
- langgraph/checkpoint/dmdb/dmpython.py +796 -0
- langgraph/checkpoint/dmdb/py.typed +0 -0
- langgraph/checkpoint/dmdb/shallow.py +171 -0
- langgraph/store/dmdb/__init__.py +6 -0
- langgraph/store/dmdb/aio.py +103 -0
- langgraph/store/dmdb/base.py +246 -0
- langgraph/store/dmdb/dmpython.py +380 -0
- langgraph/store/dmdb/py.typed +0 -0
- langgraph_checkpoint_dmdb-0.1.0.dist-info/METADATA +192 -0
- langgraph_checkpoint_dmdb-0.1.0.dist-info/RECORD +16 -0
- langgraph_checkpoint_dmdb-0.1.0.dist-info/WHEEL +4 -0
- langgraph_checkpoint_dmdb-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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"]
|