langgraph-checkpoint-oceanbase-es 3.0.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.

Potentially problematic release.


This version of langgraph-checkpoint-oceanbase-es might be problematic. Click here for more details.

@@ -0,0 +1,26 @@
1
+ """OceanBase checkpoint saver - MySQL compatible with OceanBase naming."""
2
+ from __future__ import annotations
3
+
4
+ from langgraph.checkpoint.oceanbase.pymysql import (
5
+ Conn,
6
+ OceanBaseSaver,
7
+ ShallowOceanBaseSaver,
8
+ )
9
+ from langgraph.checkpoint.oceanbase.aio import (
10
+ AsyncOceanBaseSaver,
11
+ ShallowAsyncOceanBaseSaver,
12
+ )
13
+ from langgraph.checkpoint.oceanbase.asyncmy import (
14
+ AsyncMyOceanBaseSaver,
15
+ ShallowAsyncMyOceanBaseSaver,
16
+ )
17
+
18
+ __all__ = [
19
+ "OceanBaseSaver",
20
+ "AsyncOceanBaseSaver",
21
+ "AsyncMyOceanBaseSaver",
22
+ "ShallowOceanBaseSaver",
23
+ "ShallowAsyncOceanBaseSaver",
24
+ "ShallowAsyncMyOceanBaseSaver",
25
+ "Conn",
26
+ ]
@@ -0,0 +1,91 @@
1
+ """Shared utility functions for asynchronous OceanBase connections."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import AsyncIterator, Mapping, Sequence
5
+ from contextlib import AbstractAsyncContextManager, asynccontextmanager
6
+ from typing import (
7
+ Any,
8
+ Generic,
9
+ Protocol,
10
+ TypeVar,
11
+ Union,
12
+ cast,
13
+ )
14
+
15
+
16
+ class AsyncDictCursor(AbstractAsyncContextManager, Protocol):
17
+ """Protocol that an asynchronous OceanBase/MySQL cursor should implement."""
18
+
19
+ async def execute(
20
+ self,
21
+ operation: str,
22
+ parameters: Sequence[Any] | Mapping[str, Any] = ...,
23
+ /,
24
+ ) -> object: ...
25
+ async def executemany(
26
+ self, operation: str, seq_of_parameters: Sequence[Sequence[Any]], /
27
+ ) -> object: ...
28
+ async def fetchone(self) -> dict[str, Any] | None: ...
29
+ async def fetchall(self) -> Sequence[dict[str, Any]]: ...
30
+
31
+ def __aiter__(self) -> AsyncIterator[dict[str, Any]]: ...
32
+
33
+
34
+ R = TypeVar("R", bound=AsyncDictCursor) # cursor type
35
+
36
+
37
+ class AsyncConnection(AbstractAsyncContextManager, Protocol):
38
+ """Protocol that an asynchronous OceanBase/MySQL connection should implement."""
39
+
40
+ async def begin(self) -> None:
41
+ """Begin transaction."""
42
+ ...
43
+
44
+ async def commit(self) -> None:
45
+ """Commit changes to stable storage."""
46
+ ...
47
+
48
+ async def rollback(self) -> None:
49
+ """Roll back the current transaction."""
50
+ ...
51
+
52
+ async def set_charset(self, charset: str) -> None:
53
+ """Sets the character set for the current connection"""
54
+ ...
55
+
56
+
57
+ C = TypeVar("C", bound=AsyncConnection) # connection type
58
+ COut = TypeVar("COut", bound=AsyncConnection, covariant=True) # connection type
59
+
60
+
61
+ class AsyncPool(Protocol, Generic[COut]):
62
+ def acquire(self) -> COut:
63
+ """Gets a connection from the connection pool."""
64
+ ...
65
+
66
+
67
+ Conn = Union[C, AsyncPool[C]]
68
+
69
+
70
+ @asynccontextmanager
71
+ async def get_connection(
72
+ conn: Conn[C],
73
+ ) -> AsyncIterator[C]:
74
+ """Get a connection from a connection object or pool.
75
+
76
+ Args:
77
+ conn: Either a connection object or an async pool
78
+
79
+ Yields:
80
+ The connection object
81
+ """
82
+ if hasattr(conn, "cursor"):
83
+ yield cast(C, conn)
84
+ elif hasattr(conn, "acquire"):
85
+ async with cast(AsyncPool[C], conn).acquire() as _conn:
86
+ yield _conn
87
+ else:
88
+ raise TypeError(f"Invalid connection type: {type(conn)}")
89
+
90
+
91
+ __all__ = ["get_connection", "Conn", "AsyncConnection", "AsyncDictCursor", "AsyncPool"]
@@ -0,0 +1,87 @@
1
+ """Shared utility functions for synchronous OceanBase connections."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Callable, Iterator, Mapping, Sequence
5
+ from contextlib import AbstractContextManager, closing, contextmanager
6
+ from typing import (
7
+ Any,
8
+ Protocol,
9
+ TypeVar,
10
+ Union,
11
+ cast,
12
+ )
13
+
14
+
15
+ class DictCursor(AbstractContextManager, Protocol):
16
+ """Protocol that a synchronous OceanBase/MySQL cursor should implement."""
17
+
18
+ def execute(
19
+ self,
20
+ operation: str,
21
+ parameters: Sequence[Any] | Mapping[str, Any] = ...,
22
+ /,
23
+ ) -> object: ...
24
+ def executemany(
25
+ self, operation: str, seq_of_parameters: Sequence[Sequence[Any]], /
26
+ ) -> object: ...
27
+ def fetchone(self) -> dict[str, Any] | None: ...
28
+ def fetchall(self) -> Sequence[dict[str, Any]]: ...
29
+
30
+
31
+ R = TypeVar("R", bound=DictCursor) # cursor type
32
+
33
+
34
+ class Connection(AbstractContextManager, Protocol):
35
+ """Protocol that a synchronous OceanBase/MySQL connection should implement."""
36
+
37
+ def begin(self) -> None:
38
+ """Begin transaction."""
39
+ ...
40
+
41
+ def commit(self) -> None:
42
+ """Commit changes to stable storage."""
43
+ ...
44
+
45
+ def rollback(self) -> None:
46
+ """Roll back the current transaction."""
47
+ ...
48
+
49
+
50
+ C = TypeVar("C", bound=Connection) # connection type
51
+
52
+
53
+ ConnectionFactory = Callable[[], Any]
54
+ Conn = Union[C, ConnectionFactory]
55
+
56
+
57
+ @contextmanager
58
+ def get_connection(conn: Conn[C]) -> Iterator[C]:
59
+ """Get a connection from a connection object or factory.
60
+
61
+ Args:
62
+ conn: Either a connection object, a callable that returns a connection,
63
+ or an object with a connect() method (like SQLAlchemy pool)
64
+
65
+ Yields:
66
+ The connection object
67
+ """
68
+ if hasattr(conn, "cursor"):
69
+ yield cast(C, conn)
70
+ elif callable(conn):
71
+ _conn = conn()
72
+ if isinstance(_conn, AbstractContextManager):
73
+ yield cast(C, _conn)
74
+ else:
75
+ with closing(_conn) as __conn:
76
+ yield __conn
77
+ # This is kept for backwards compatibility
78
+ elif hasattr(conn, "connect"):
79
+ # sqlalchemy pool
80
+ factory: ConnectionFactory = getattr(conn, "connect") # noqa: B009
81
+ with get_connection(factory) as _conn:
82
+ yield _conn
83
+ else:
84
+ raise TypeError(f"Invalid connection or pool type: {type(conn)}")
85
+
86
+
87
+ __all__ = ["get_connection", "Conn", "Connection", "DictCursor"]
@@ -0,0 +1,105 @@
1
+ """OceanBase checkpoint saver using aiomysql (asynchronous)."""
2
+ from __future__ import annotations
3
+
4
+ import warnings
5
+ from collections.abc import AsyncIterator
6
+ from contextlib import asynccontextmanager
7
+ from typing import Any, cast
8
+
9
+ import aiomysql # type: ignore
10
+ from typing_extensions import Self, override
11
+
12
+ from langgraph.checkpoint.oceanbase.aio_base import BaseAsyncOceanBaseSaver
13
+ from langgraph.checkpoint.oceanbase.shallow import BaseShallowSyncOceanBaseSaver
14
+ from langgraph.checkpoint.oceanbase.utils import parse_conn_string_async
15
+ from langgraph.checkpoint.serde.base import SerializerProtocol
16
+
17
+ # Type alias for backward compatibility
18
+ Conn = aiomysql.Connection
19
+
20
+
21
+ class AsyncOceanBaseSaver(BaseAsyncOceanBaseSaver[aiomysql.Connection, aiomysql.DictCursor]):
22
+ """OceanBase checkpoint saver using aiomysql (async).
23
+
24
+ OceanBase is MySQL-compatible, so this inherits from BaseAsyncOceanBaseSaver.
25
+ Default port is 2881 (OceanBase) instead of 3306 (MySQL).
26
+ """
27
+
28
+ @staticmethod
29
+ def parse_conn_string(conn_string: str) -> dict[str, Any]:
30
+ """Parse connection string with OceanBase default port (2881)."""
31
+ return parse_conn_string_async(conn_string, default_port=2881)
32
+
33
+ @classmethod
34
+ @asynccontextmanager
35
+ async def from_conn_string(
36
+ cls,
37
+ conn_string: str,
38
+ *,
39
+ serde: SerializerProtocol | None = None,
40
+ ) -> AsyncIterator[Self]:
41
+ """Create a new AsyncOceanBaseSaver instance from a connection string.
42
+
43
+ Args:
44
+ conn_string: The OceanBase/MySQL connection info string.
45
+ serde: Optional serializer protocol.
46
+
47
+ Returns:
48
+ AsyncOceanBaseSaver: A new AsyncOceanBaseSaver instance.
49
+
50
+ Example:
51
+ conn_string=mysql+aiomysql://user:***@localhost:2881/db?unix_socket=/path/to/socket
52
+ """
53
+ async with aiomysql.connect(
54
+ **cls.parse_conn_string(conn_string),
55
+ autocommit=True,
56
+ ) as conn:
57
+ yield cls(conn=conn, serde=serde)
58
+
59
+ @override
60
+ @staticmethod
61
+ def _get_cursor_from_connection(conn: aiomysql.Connection) -> aiomysql.DictCursor:
62
+ return cast(aiomysql.DictCursor, conn.cursor(aiomysql.DictCursor))
63
+
64
+
65
+ class ShallowAsyncOceanBaseSaver(BaseShallowSyncOceanBaseSaver):
66
+ """Deprecated shallow async OceanBase saver.
67
+
68
+ Use AsyncOceanBaseSaver instead with `await graph.ainvoke(..., durability='exit')`.
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ conn: aiomysql.Connection,
74
+ serde: SerializerProtocol | None = None,
75
+ ) -> None:
76
+ warnings.warn(
77
+ "ShallowAsyncOceanBaseSaver is deprecated as of version 2.0.15 and will be removed in 3.0.0. "
78
+ "Use AsyncOceanBaseSaver instead, and invoke the graph with `await graph.ainvoke(..., durability='exit')`.",
79
+ DeprecationWarning,
80
+ stacklevel=2,
81
+ )
82
+ super().__init__(conn, serde=serde)
83
+
84
+ @classmethod
85
+ @asynccontextmanager
86
+ async def from_conn_string(
87
+ cls,
88
+ conn_string: str,
89
+ *,
90
+ serde: SerializerProtocol | None = None,
91
+ ) -> AsyncIterator[Self]:
92
+ """Create a new ShallowAsyncOceanBaseSaver instance from a connection string."""
93
+ async with aiomysql.connect(
94
+ **AsyncOceanBaseSaver.parse_conn_string(conn_string),
95
+ autocommit=True,
96
+ ) as conn:
97
+ yield cls(conn)
98
+
99
+ @override
100
+ @staticmethod
101
+ def _get_cursor_from_connection(conn: aiomysql.Connection) -> aiomysql.DictCursor:
102
+ return cast(aiomysql.DictCursor, conn.cursor(aiomysql.DictCursor))
103
+
104
+
105
+ __all__ = ["AsyncOceanBaseSaver", "ShallowAsyncOceanBaseSaver", "Conn"]
@@ -0,0 +1,382 @@
1
+ """OceanBase async checkpoint saver - async base class."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ from collections import defaultdict
7
+ from collections.abc import AsyncIterator, Iterator, Sequence
8
+ from contextlib import asynccontextmanager
9
+ from typing import Any, Generic
10
+
11
+ from langchain_core.runnables import RunnableConfig
12
+
13
+ from langgraph.checkpoint.base import (
14
+ WRITES_IDX_MAP,
15
+ ChannelVersions,
16
+ Checkpoint,
17
+ CheckpointMetadata,
18
+ CheckpointTuple,
19
+ get_checkpoint_id,
20
+ get_serializable_checkpoint_metadata,
21
+ )
22
+ from langgraph.checkpoint.oceanbase import _ainternal
23
+ from langgraph.checkpoint.oceanbase.base import BaseOceanBaseSaver
24
+ from langgraph.checkpoint.oceanbase.utils import (
25
+ deserialize_channel_values,
26
+ deserialize_pending_sends,
27
+ deserialize_pending_writes,
28
+ )
29
+ from langgraph.checkpoint.serde.base import SerializerProtocol
30
+
31
+
32
+ class BaseAsyncOceanBaseSaver(BaseOceanBaseSaver, Generic[_ainternal.C, _ainternal.R]):
33
+ """Base async OceanBase checkpoint saver."""
34
+
35
+ lock: asyncio.Lock
36
+
37
+ def __init__(
38
+ self,
39
+ conn: _ainternal.Conn[_ainternal.C],
40
+ serde: SerializerProtocol | None = None,
41
+ ) -> None:
42
+ super().__init__(serde=serde)
43
+ self.conn = conn
44
+ self.lock = asyncio.Lock()
45
+ self.loop = asyncio.get_running_loop()
46
+
47
+ @staticmethod
48
+ def _get_cursor_from_connection(conn: _ainternal.C) -> _ainternal.R:
49
+ raise NotImplementedError
50
+
51
+ async def setup(self) -> None:
52
+ """Set up the checkpoint database asynchronously."""
53
+ async with self._cursor() as cur:
54
+ await cur.execute(self.MIGRATIONS[0])
55
+ await cur.execute(
56
+ "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
57
+ )
58
+ row = await cur.fetchone()
59
+ if row is None:
60
+ version = -1
61
+ else:
62
+ version = row["v"]
63
+ for v, migration in zip(
64
+ range(version + 1, len(self.MIGRATIONS)),
65
+ self.MIGRATIONS[version + 1 :],
66
+ ):
67
+ await cur.execute(migration)
68
+ await cur.execute(
69
+ "INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
70
+ )
71
+
72
+ async def alist(
73
+ self,
74
+ config: RunnableConfig | None,
75
+ *,
76
+ filter: dict[str, Any] | None = None,
77
+ before: RunnableConfig | None = None,
78
+ limit: int | None = None,
79
+ ) -> AsyncIterator[CheckpointTuple]:
80
+ """List checkpoints from the database asynchronously."""
81
+ where, args = self._search_where(config, filter, before)
82
+ query = self._select_sql(where) + " ORDER BY checkpoint_id DESC"
83
+ if limit is not None:
84
+ query += " LIMIT %(limit)s"
85
+ args = {**args, "limit": int(limit)}
86
+
87
+ async with self._cursor() as cur:
88
+ await cur.execute(query, args)
89
+ values = await cur.fetchall()
90
+ if not values:
91
+ return
92
+ for value in values:
93
+ value["checkpoint"] = json.loads(value["checkpoint"])
94
+ value["channel_values"] = deserialize_channel_values(
95
+ value["channel_values"]
96
+ )
97
+ if to_migrate := [
98
+ v
99
+ for v in values
100
+ if v["checkpoint"]["v"] < 4 and v["parent_checkpoint_id"]
101
+ ]:
102
+ await cur.execute(
103
+ self._select_pending_sends_sql(len(to_migrate)),
104
+ (
105
+ values[0]["thread_id"],
106
+ *[v["parent_checkpoint_id"] for v in to_migrate],
107
+ ),
108
+ )
109
+ grouped_by_parent = defaultdict(list)
110
+ for value in to_migrate:
111
+ grouped_by_parent[value["parent_checkpoint_id"]].append(value)
112
+ async for sends in cur:
113
+ for value in grouped_by_parent[sends["checkpoint_id"]]:
114
+ if value["channel_values"] is None:
115
+ value["channel_values"] = []
116
+ self._migrate_pending_sends(
117
+ deserialize_pending_sends(sends["sends"]),
118
+ value["checkpoint"],
119
+ value["channel_values"],
120
+ )
121
+ for value in values:
122
+ yield await self._load_checkpoint_tuple(value)
123
+
124
+ async def aget_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
125
+ """Get a checkpoint tuple from the database asynchronously."""
126
+ thread_id = config["configurable"]["thread_id"]
127
+ checkpoint_id = get_checkpoint_id(config)
128
+ checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
129
+ if checkpoint_id:
130
+ args = {
131
+ "thread_id": thread_id,
132
+ "checkpoint_ns": checkpoint_ns,
133
+ "checkpoint_id": checkpoint_id,
134
+ }
135
+ where = "WHERE thread_id = %(thread_id)s AND checkpoint_ns_hash = UNHEX(MD5(%(checkpoint_ns)s)) AND checkpoint_id = %(checkpoint_id)s"
136
+ else:
137
+ args = {
138
+ "thread_id": thread_id,
139
+ "checkpoint_ns": checkpoint_ns,
140
+ }
141
+ where = "WHERE thread_id = %(thread_id)s AND checkpoint_ns_hash = UNHEX(MD5(%(checkpoint_ns)s))"
142
+
143
+ query = self._select_sql(where)
144
+ if not checkpoint_id:
145
+ query += " ORDER BY checkpoint_id DESC LIMIT 1"
146
+ async with self._cursor() as cur:
147
+ await cur.execute(query, args)
148
+ value = await cur.fetchone()
149
+ if value is None:
150
+ return None
151
+
152
+ value["checkpoint"] = json.loads(value["checkpoint"])
153
+ value["channel_values"] = deserialize_channel_values(
154
+ value["channel_values"]
155
+ )
156
+
157
+ if value["checkpoint"]["v"] < 4 and value["parent_checkpoint_id"]:
158
+ await cur.execute(
159
+ self._select_pending_sends_sql(1),
160
+ (thread_id, value["parent_checkpoint_id"]),
161
+ )
162
+ if sends := await cur.fetchone():
163
+ if value["channel_values"] is None:
164
+ value["channel_values"] = []
165
+ self._migrate_pending_sends(
166
+ deserialize_pending_sends(sends["sends"]),
167
+ value["checkpoint"],
168
+ value["channel_values"],
169
+ )
170
+
171
+ return await self._load_checkpoint_tuple(value)
172
+
173
+ async def aput(
174
+ self,
175
+ config: RunnableConfig,
176
+ checkpoint: Checkpoint,
177
+ metadata: CheckpointMetadata,
178
+ new_versions: ChannelVersions,
179
+ ) -> RunnableConfig:
180
+ """Save a checkpoint to the database asynchronously."""
181
+ configurable = config["configurable"].copy()
182
+ thread_id = configurable.pop("thread_id")
183
+ checkpoint_ns = configurable.pop("checkpoint_ns")
184
+ checkpoint_id = configurable.pop("checkpoint_id", None)
185
+
186
+ copy = checkpoint.copy()
187
+ copy["channel_values"] = copy["channel_values"].copy()
188
+ next_config = {
189
+ "configurable": {
190
+ "thread_id": thread_id,
191
+ "checkpoint_ns": checkpoint_ns,
192
+ "checkpoint_id": checkpoint["id"],
193
+ }
194
+ }
195
+
196
+ blob_values = {}
197
+ for k, v in checkpoint["channel_values"].items():
198
+ if v is None or isinstance(v, (str, int, float, bool)):
199
+ pass
200
+ else:
201
+ blob_values[k] = copy["channel_values"].pop(k)
202
+
203
+ async with self._cursor(pipeline=True) as cur:
204
+ if blob_versions := {
205
+ k: v for k, v in new_versions.items() if k in blob_values
206
+ }:
207
+ await cur.executemany(
208
+ self.UPSERT_CHECKPOINT_BLOBS_SQL,
209
+ await asyncio.to_thread(
210
+ self._dump_blobs,
211
+ thread_id,
212
+ checkpoint_ns,
213
+ blob_values,
214
+ blob_versions,
215
+ ),
216
+ )
217
+ await cur.execute(
218
+ self.UPSERT_CHECKPOINTS_SQL,
219
+ (
220
+ thread_id,
221
+ checkpoint_ns,
222
+ checkpoint_ns,
223
+ checkpoint["id"],
224
+ checkpoint_id,
225
+ json.dumps(copy),
226
+ json.dumps(get_serializable_checkpoint_metadata(config, metadata)),
227
+ ),
228
+ )
229
+ return next_config
230
+
231
+ async def aput_writes(
232
+ self,
233
+ config: RunnableConfig,
234
+ writes: Sequence[tuple[str, Any]],
235
+ task_id: str,
236
+ task_path: str = "",
237
+ ) -> None:
238
+ """Store intermediate writes linked to a checkpoint asynchronously."""
239
+ query = (
240
+ self.UPSERT_CHECKPOINT_WRITES_SQL
241
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
242
+ else self.INSERT_CHECKPOINT_WRITES_SQL
243
+ )
244
+ params = await asyncio.to_thread(
245
+ self._dump_writes,
246
+ config["configurable"]["thread_id"],
247
+ config["configurable"]["checkpoint_ns"],
248
+ config["configurable"]["checkpoint_id"],
249
+ task_id,
250
+ task_path,
251
+ writes,
252
+ )
253
+ async with self._cursor(pipeline=True) as cur:
254
+ await cur.executemany(query, params)
255
+
256
+ async def adelete_thread(self, thread_id: str) -> None:
257
+ """Delete all checkpoints and writes associated with a thread ID."""
258
+ async with self._cursor(pipeline=True) as cur:
259
+ await cur.execute(
260
+ "DELETE FROM checkpoints WHERE thread_id = %s",
261
+ (str(thread_id),),
262
+ )
263
+ await cur.execute(
264
+ "DELETE FROM checkpoint_blobs WHERE thread_id = %s",
265
+ (str(thread_id),),
266
+ )
267
+ await cur.execute(
268
+ "DELETE FROM checkpoint_writes WHERE thread_id = %s",
269
+ (str(thread_id),),
270
+ )
271
+
272
+ @asynccontextmanager
273
+ async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[_ainternal.R]:
274
+ """Create a database cursor as a context manager."""
275
+ async with _ainternal.get_connection(self.conn) as conn:
276
+ if pipeline:
277
+ async with self.lock:
278
+ await conn.begin()
279
+ try:
280
+ async with self._get_cursor_from_connection(conn) as cur:
281
+ yield cur
282
+ await conn.commit()
283
+ except:
284
+ await conn.rollback()
285
+ raise
286
+ else:
287
+ async with (
288
+ self.lock,
289
+ self._get_cursor_from_connection(conn) as cur,
290
+ ):
291
+ yield cur
292
+
293
+ async def _load_checkpoint_tuple(self, value: dict[str, Any]) -> CheckpointTuple:
294
+ """Convert a database row into a CheckpointTuple object."""
295
+ return CheckpointTuple(
296
+ {
297
+ "configurable": {
298
+ "thread_id": value["thread_id"],
299
+ "checkpoint_ns": value["checkpoint_ns"],
300
+ "checkpoint_id": value["checkpoint_id"],
301
+ }
302
+ },
303
+ {
304
+ **value["checkpoint"],
305
+ "channel_values": {
306
+ **value["checkpoint"].get("channel_values"),
307
+ **self._load_blobs(value["channel_values"]),
308
+ },
309
+ },
310
+ json.loads(value["metadata"]),
311
+ (
312
+ {
313
+ "configurable": {
314
+ "thread_id": value["thread_id"],
315
+ "checkpoint_ns": value["checkpoint_ns"],
316
+ "checkpoint_id": value["parent_checkpoint_id"],
317
+ }
318
+ }
319
+ if value["parent_checkpoint_id"]
320
+ else None
321
+ ),
322
+ await asyncio.to_thread(
323
+ self._load_writes,
324
+ deserialize_pending_writes(value["pending_writes"]),
325
+ ),
326
+ )
327
+
328
+ def list(
329
+ self,
330
+ config: RunnableConfig | None,
331
+ *,
332
+ filter: dict[str, Any] | None = None,
333
+ before: RunnableConfig | None = None,
334
+ limit: int | None = None,
335
+ ) -> Iterator[CheckpointTuple]:
336
+ """List checkpoints from the database (sync wrapper)."""
337
+ try:
338
+ if asyncio.get_running_loop() is self.loop:
339
+ raise asyncio.InvalidStateError(
340
+ "Synchronous calls to AsyncOceanBaseSaver are only allowed from a "
341
+ "different thread. From the main thread, use the async interface."
342
+ )
343
+ except RuntimeError:
344
+ pass
345
+ aiter_ = self.alist(config, filter=filter, before=before, limit=limit)
346
+ while True:
347
+ try:
348
+ yield asyncio.run_coroutine_threadsafe(
349
+ anext(aiter_),
350
+ self.loop,
351
+ ).result()
352
+ except StopAsyncIteration:
353
+ break
354
+
355
+ def get_tuple(self, config: RunnableConfig) -> CheckpointTuple | None:
356
+ """Get a checkpoint tuple from the database (sync wrapper)."""
357
+ try:
358
+ if asyncio.get_running_loop() is self.loop:
359
+ raise asyncio.InvalidStateError(
360
+ "Synchronous calls to AsyncOceanBaseSaver are only allowed from a "
361
+ "different thread. From the main thread, use the async interface."
362
+ )
363
+ except RuntimeError:
364
+ pass
365
+ return asyncio.run_coroutine_threadsafe(
366
+ self.aget_tuple(config), self.loop
367
+ ).result()
368
+
369
+ def put(
370
+ self,
371
+ config: RunnableConfig,
372
+ checkpoint: Checkpoint,
373
+ metadata: CheckpointMetadata,
374
+ new_versions: ChannelVersions,
375
+ ) -> RunnableConfig:
376
+ """Save a checkpoint to the database (sync wrapper)."""
377
+ return asyncio.run_coroutine_threadsafe(
378
+ self.aput(config, checkpoint, metadata, new_versions), self.loop
379
+ ).result()
380
+
381
+
382
+ __all__ = ["BaseAsyncOceanBaseSaver"]