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.
- langgraph/checkpoint/oceanbase/__init__.py +26 -0
- langgraph/checkpoint/oceanbase/_ainternal.py +91 -0
- langgraph/checkpoint/oceanbase/_internal.py +87 -0
- langgraph/checkpoint/oceanbase/aio.py +105 -0
- langgraph/checkpoint/oceanbase/aio_base.py +382 -0
- langgraph/checkpoint/oceanbase/asyncmy.py +99 -0
- langgraph/checkpoint/oceanbase/base.py +398 -0
- langgraph/checkpoint/oceanbase/pymysql.py +383 -0
- langgraph/checkpoint/oceanbase/shallow.py +437 -0
- langgraph/checkpoint/oceanbase/utils.py +152 -0
- langgraph/store/oceanbase/__init__.py +7 -0
- langgraph/store/oceanbase/aio.py +370 -0
- langgraph/store/oceanbase/aio_base.py +278 -0
- langgraph/store/oceanbase/asyncmy.py +72 -0
- langgraph/store/oceanbase/base.py +380 -0
- langgraph/store/oceanbase/es_backend.py +731 -0
- langgraph/store/oceanbase/pymysql.py +347 -0
- langgraph/store/oceanbase/utils.py +58 -0
- langgraph_checkpoint_oceanbase_es-3.0.0.dist-info/METADATA +147 -0
- langgraph_checkpoint_oceanbase_es-3.0.0.dist-info/RECORD +22 -0
- langgraph_checkpoint_oceanbase_es-3.0.0.dist-info/WHEEL +4 -0
- langgraph_checkpoint_oceanbase_es-3.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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"]
|