langgraph-checkpoint-postgres 2.0.4__tar.gz → 2.0.5__tar.gz
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_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/PKG-INFO +2 -2
- {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/__init__.py +12 -7
- {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/_ainternal.py +2 -1
- {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/_internal.py +2 -1
- {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/aio.py +17 -11
- {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/base.py +3 -2
- {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/store/postgres/aio.py +203 -128
- langgraph_checkpoint_postgres-2.0.5/langgraph/store/postgres/base.py +974 -0
- {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/pyproject.toml +2 -2
- langgraph_checkpoint_postgres-2.0.4/langgraph/store/postgres/base.py +0 -546
- {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/README.md +0 -0
- {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/checkpoint/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/store/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-2.0.4 → langgraph_checkpoint_postgres-2.0.5}/langgraph/store/postgres/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: langgraph-checkpoint-postgres
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.5
|
|
4
4
|
Summary: Library with a Postgres implementation of LangGraph checkpoint saver.
|
|
5
5
|
Home-page: https://www.github.com/langchain-ai/langgraph
|
|
6
6
|
License: MIT
|
|
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
-
Requires-Dist: langgraph-checkpoint (>=2.0.
|
|
14
|
+
Requires-Dist: langgraph-checkpoint (>=2.0.7,<3.0.0)
|
|
15
15
|
Requires-Dist: orjson (>=3.10.1)
|
|
16
16
|
Requires-Dist: psycopg (>=3.0.0,<4.0.0)
|
|
17
17
|
Requires-Dist: psycopg-pool (>=3.0.0,<4.0.0)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import threading
|
|
2
|
+
from collections.abc import Iterator, Sequence
|
|
2
3
|
from contextlib import contextmanager
|
|
3
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Optional
|
|
4
5
|
|
|
5
6
|
from langchain_core.runnables import RunnableConfig
|
|
6
7
|
from psycopg import Capabilities, Connection, Cursor, Pipeline
|
|
@@ -378,15 +379,19 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
378
379
|
# a connection not in pipeline mode can only be used by one
|
|
379
380
|
# thread/coroutine at a time, so we acquire a lock
|
|
380
381
|
if self.supports_pipeline:
|
|
381
|
-
with
|
|
382
|
-
|
|
383
|
-
|
|
382
|
+
with (
|
|
383
|
+
self.lock,
|
|
384
|
+
conn.pipeline(),
|
|
385
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
386
|
+
):
|
|
384
387
|
yield cur
|
|
385
388
|
else:
|
|
386
389
|
# Use connection's transaction context manager when pipeline mode not supported
|
|
387
|
-
with
|
|
388
|
-
|
|
389
|
-
|
|
390
|
+
with (
|
|
391
|
+
self.lock,
|
|
392
|
+
conn.transaction(),
|
|
393
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
394
|
+
):
|
|
390
395
|
yield cur
|
|
391
396
|
else:
|
|
392
397
|
with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Shared async utility functions for the Postgres checkpoint & storage classes."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
3
4
|
from contextlib import asynccontextmanager
|
|
4
|
-
from typing import
|
|
5
|
+
from typing import Union
|
|
5
6
|
|
|
6
7
|
from psycopg import AsyncConnection
|
|
7
8
|
from psycopg.rows import DictRow
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Shared utility functions for the Postgres checkpoint & storage classes."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Iterator
|
|
3
4
|
from contextlib import contextmanager
|
|
4
|
-
from typing import
|
|
5
|
+
from typing import Union
|
|
5
6
|
|
|
6
7
|
from psycopg import Connection
|
|
7
8
|
from psycopg.rows import DictRow
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from collections.abc import AsyncIterator, Iterator, Sequence
|
|
2
3
|
from contextlib import asynccontextmanager
|
|
3
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Optional
|
|
4
5
|
|
|
5
6
|
from langchain_core.runnables import RunnableConfig
|
|
6
7
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
|
@@ -338,20 +339,25 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
338
339
|
# a connection not in pipeline mode can only be used by one
|
|
339
340
|
# thread/coroutine at a time, so we acquire a lock
|
|
340
341
|
if self.supports_pipeline:
|
|
341
|
-
async with
|
|
342
|
-
|
|
343
|
-
|
|
342
|
+
async with (
|
|
343
|
+
self.lock,
|
|
344
|
+
conn.pipeline(),
|
|
345
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
346
|
+
):
|
|
344
347
|
yield cur
|
|
345
348
|
else:
|
|
346
349
|
# Use connection's transaction context manager when pipeline mode not supported
|
|
347
|
-
async with
|
|
348
|
-
|
|
349
|
-
|
|
350
|
+
async with (
|
|
351
|
+
self.lock,
|
|
352
|
+
conn.transaction(),
|
|
353
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
354
|
+
):
|
|
350
355
|
yield cur
|
|
351
356
|
else:
|
|
352
|
-
async with
|
|
353
|
-
|
|
354
|
-
|
|
357
|
+
async with (
|
|
358
|
+
self.lock,
|
|
359
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
360
|
+
):
|
|
355
361
|
yield cur
|
|
356
362
|
|
|
357
363
|
def list(
|
|
@@ -380,7 +386,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
380
386
|
while True:
|
|
381
387
|
try:
|
|
382
388
|
yield asyncio.run_coroutine_threadsafe(
|
|
383
|
-
anext(aiter_),
|
|
389
|
+
anext(aiter_), # noqa: F821
|
|
384
390
|
self.loop,
|
|
385
391
|
).result()
|
|
386
392
|
except StopAsyncIteration:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import random
|
|
2
|
-
from
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import Any, Optional, cast
|
|
3
4
|
|
|
4
5
|
from langchain_core.runnables import RunnableConfig
|
|
5
6
|
from psycopg.types.json import Jsonb
|
|
@@ -249,7 +250,7 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|
|
249
250
|
config: Optional[RunnableConfig],
|
|
250
251
|
filter: MetadataInput,
|
|
251
252
|
before: Optional[RunnableConfig] = None,
|
|
252
|
-
) ->
|
|
253
|
+
) -> tuple[str, list[Any]]:
|
|
253
254
|
"""Return WHERE clause predicates for alist() given config, filter, before.
|
|
254
255
|
|
|
255
256
|
This method returns a tuple of a string and a tuple of values. The string
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
|
+
from collections.abc import AsyncIterator, Iterable, Sequence
|
|
3
4
|
from contextlib import asynccontextmanager
|
|
4
|
-
from typing import
|
|
5
|
-
Any,
|
|
6
|
-
AsyncIterator,
|
|
7
|
-
Callable,
|
|
8
|
-
Iterable,
|
|
9
|
-
Optional,
|
|
10
|
-
Sequence,
|
|
11
|
-
Union,
|
|
12
|
-
cast,
|
|
13
|
-
)
|
|
5
|
+
from typing import Any, Callable, Optional, Union, cast
|
|
14
6
|
|
|
15
7
|
import orjson
|
|
16
8
|
from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline, Capabilities
|
|
@@ -19,22 +11,40 @@ from psycopg.rows import DictRow, dict_row
|
|
|
19
11
|
from psycopg_pool import AsyncConnectionPool
|
|
20
12
|
|
|
21
13
|
from langgraph.checkpoint.postgres import _ainternal
|
|
22
|
-
from langgraph.store.base import
|
|
14
|
+
from langgraph.store.base import (
|
|
15
|
+
GetOp,
|
|
16
|
+
ListNamespacesOp,
|
|
17
|
+
Op,
|
|
18
|
+
PutOp,
|
|
19
|
+
Result,
|
|
20
|
+
SearchOp,
|
|
21
|
+
)
|
|
23
22
|
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
|
24
23
|
from langgraph.store.postgres.base import (
|
|
24
|
+
_PLACEHOLDER,
|
|
25
25
|
BasePostgresStore,
|
|
26
26
|
PoolConfig,
|
|
27
|
+
PostgresIndexConfig,
|
|
27
28
|
Row,
|
|
28
29
|
_decode_ns_bytes,
|
|
30
|
+
_ensure_index_config,
|
|
29
31
|
_group_ops,
|
|
30
32
|
_row_to_item,
|
|
33
|
+
_row_to_search_item,
|
|
31
34
|
)
|
|
32
35
|
|
|
33
36
|
logger = logging.getLogger(__name__)
|
|
34
37
|
|
|
35
38
|
|
|
36
39
|
class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Conn]):
|
|
37
|
-
__slots__ = (
|
|
40
|
+
__slots__ = (
|
|
41
|
+
"_deserializer",
|
|
42
|
+
"pipe",
|
|
43
|
+
"lock",
|
|
44
|
+
"supports_pipeline",
|
|
45
|
+
"index_config",
|
|
46
|
+
"embeddings",
|
|
47
|
+
)
|
|
38
48
|
|
|
39
49
|
def __init__(
|
|
40
50
|
self,
|
|
@@ -44,6 +54,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|
|
44
54
|
deserializer: Optional[
|
|
45
55
|
Callable[[Union[bytes, orjson.Fragment]], dict[str, Any]]
|
|
46
56
|
] = None,
|
|
57
|
+
index: Optional[PostgresIndexConfig] = None,
|
|
47
58
|
) -> None:
|
|
48
59
|
if isinstance(conn, AsyncConnectionPool) and pipe is not None:
|
|
49
60
|
raise ValueError(
|
|
@@ -56,6 +67,12 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|
|
56
67
|
self.lock = asyncio.Lock()
|
|
57
68
|
self.loop = asyncio.get_running_loop()
|
|
58
69
|
self.supports_pipeline = Capabilities().has_pipeline()
|
|
70
|
+
self.index_config = index
|
|
71
|
+
if self.index_config:
|
|
72
|
+
self.embeddings, self.index_config = _ensure_index_config(self.index_config)
|
|
73
|
+
|
|
74
|
+
else:
|
|
75
|
+
self.embeddings = None
|
|
59
76
|
|
|
60
77
|
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
|
|
61
78
|
grouped_ops, num_ops = _group_ops(ops)
|
|
@@ -70,13 +87,117 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|
|
70
87
|
|
|
71
88
|
return results
|
|
72
89
|
|
|
90
|
+
def batch(self, ops: Iterable[Op]) -> list[Result]:
|
|
91
|
+
return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
@asynccontextmanager
|
|
95
|
+
async def from_conn_string(
|
|
96
|
+
cls,
|
|
97
|
+
conn_string: str,
|
|
98
|
+
*,
|
|
99
|
+
pipeline: bool = False,
|
|
100
|
+
pool_config: Optional[PoolConfig] = None,
|
|
101
|
+
index: Optional[PostgresIndexConfig] = None,
|
|
102
|
+
) -> AsyncIterator["AsyncPostgresStore"]:
|
|
103
|
+
"""Create a new AsyncPostgresStore instance from a connection string.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
conn_string (str): The Postgres connection info string.
|
|
107
|
+
pipeline (bool): Whether to use AsyncPipeline (only for single connections)
|
|
108
|
+
pool_config (Optional[PoolConfig]): Configuration for the connection pool.
|
|
109
|
+
If provided, will create a connection pool and use it instead of a single connection.
|
|
110
|
+
This overrides the `pipeline` argument.
|
|
111
|
+
index (Optional[PostgresIndexConfig]): The embedding config.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
AsyncPostgresStore: A new AsyncPostgresStore instance.
|
|
115
|
+
"""
|
|
116
|
+
if pool_config is not None:
|
|
117
|
+
pc = pool_config.copy()
|
|
118
|
+
async with cast(
|
|
119
|
+
AsyncConnectionPool[AsyncConnection[DictRow]],
|
|
120
|
+
AsyncConnectionPool(
|
|
121
|
+
conn_string,
|
|
122
|
+
min_size=pc.pop("min_size", 1),
|
|
123
|
+
max_size=pc.pop("max_size", None),
|
|
124
|
+
kwargs={
|
|
125
|
+
"autocommit": True,
|
|
126
|
+
"prepare_threshold": 0,
|
|
127
|
+
"row_factory": dict_row,
|
|
128
|
+
**(pc.pop("kwargs", None) or {}),
|
|
129
|
+
},
|
|
130
|
+
**cast(dict, pc),
|
|
131
|
+
),
|
|
132
|
+
) as pool:
|
|
133
|
+
yield cls(conn=pool, index=index)
|
|
134
|
+
else:
|
|
135
|
+
async with await AsyncConnection.connect(
|
|
136
|
+
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
|
|
137
|
+
) as conn:
|
|
138
|
+
if pipeline:
|
|
139
|
+
async with conn.pipeline() as pipe:
|
|
140
|
+
yield cls(conn=conn, pipe=pipe, index=index)
|
|
141
|
+
else:
|
|
142
|
+
yield cls(conn=conn, index=index)
|
|
143
|
+
|
|
144
|
+
async def setup(self) -> None:
|
|
145
|
+
"""Set up the store database asynchronously.
|
|
146
|
+
|
|
147
|
+
This method creates the necessary tables in the Postgres database if they don't
|
|
148
|
+
already exist and runs database migrations. It MUST be called directly by the user
|
|
149
|
+
the first time the store is used.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
async def _get_version(cur: AsyncCursor[DictRow], table: str) -> int:
|
|
153
|
+
try:
|
|
154
|
+
await cur.execute(f"SELECT v FROM {table} ORDER BY v DESC LIMIT 1")
|
|
155
|
+
row = await cur.fetchone()
|
|
156
|
+
if row is None:
|
|
157
|
+
version = -1
|
|
158
|
+
else:
|
|
159
|
+
version = row["v"]
|
|
160
|
+
except UndefinedTable:
|
|
161
|
+
version = -1
|
|
162
|
+
await cur.execute(
|
|
163
|
+
f"""
|
|
164
|
+
CREATE TABLE IF NOT EXISTS {table} (
|
|
165
|
+
v INTEGER PRIMARY KEY
|
|
166
|
+
)
|
|
167
|
+
"""
|
|
168
|
+
)
|
|
169
|
+
return version
|
|
170
|
+
|
|
171
|
+
async with self._cursor() as cur:
|
|
172
|
+
version = await _get_version(cur, table="store_migrations")
|
|
173
|
+
for v, sql in enumerate(self.MIGRATIONS[version + 1 :], start=version + 1):
|
|
174
|
+
await cur.execute(sql)
|
|
175
|
+
await cur.execute("INSERT INTO store_migrations (v) VALUES (%s)", (v,))
|
|
176
|
+
|
|
177
|
+
if self.index_config:
|
|
178
|
+
version = await _get_version(cur, table="vector_migrations")
|
|
179
|
+
for v, migration in enumerate(
|
|
180
|
+
self.VECTOR_MIGRATIONS[version + 1 :], start=version + 1
|
|
181
|
+
):
|
|
182
|
+
sql = migration.sql
|
|
183
|
+
if migration.params:
|
|
184
|
+
params = {
|
|
185
|
+
k: v(self) if v is not None and callable(v) else v
|
|
186
|
+
for k, v in migration.params.items()
|
|
187
|
+
}
|
|
188
|
+
sql = sql % params
|
|
189
|
+
await cur.execute(sql)
|
|
190
|
+
await cur.execute(
|
|
191
|
+
"INSERT INTO vector_migrations (v) VALUES (%s)", (v,)
|
|
192
|
+
)
|
|
193
|
+
|
|
73
194
|
async def _execute_batch(
|
|
74
195
|
self,
|
|
75
196
|
grouped_ops: dict,
|
|
76
197
|
results: list[Result],
|
|
77
198
|
conn: AsyncConnection[DictRow],
|
|
78
199
|
) -> None:
|
|
79
|
-
async with self._cursor(
|
|
200
|
+
async with self._cursor(pipeline=True) as cur:
|
|
80
201
|
if GetOp in grouped_ops:
|
|
81
202
|
await self._batch_get_ops(
|
|
82
203
|
cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]),
|
|
@@ -131,7 +252,31 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|
|
131
252
|
put_ops: Sequence[tuple[int, PutOp]],
|
|
132
253
|
cur: AsyncCursor[DictRow],
|
|
133
254
|
) -> None:
|
|
134
|
-
queries = self.
|
|
255
|
+
queries, embedding_request = self._prepare_batch_PUT_queries(put_ops)
|
|
256
|
+
if embedding_request:
|
|
257
|
+
if self.embeddings is None:
|
|
258
|
+
# Should not get here since the embedding config is required
|
|
259
|
+
# to return an embedding_request above
|
|
260
|
+
raise ValueError(
|
|
261
|
+
"Embedding configuration is required for vector operations "
|
|
262
|
+
f"(for semantic search). "
|
|
263
|
+
f"Please provide an EmbeddingConfig when initializing the {self.__class__.__name__}."
|
|
264
|
+
)
|
|
265
|
+
query, txt_params = embedding_request
|
|
266
|
+
vectors = await self.embeddings.aembed_documents(
|
|
267
|
+
[param[-1] for param in txt_params]
|
|
268
|
+
)
|
|
269
|
+
queries.append(
|
|
270
|
+
(
|
|
271
|
+
query,
|
|
272
|
+
[
|
|
273
|
+
p
|
|
274
|
+
for (ns, k, pathname, _), vector in zip(txt_params, vectors)
|
|
275
|
+
for p in (ns, k, pathname, vector)
|
|
276
|
+
],
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
135
280
|
for query, params in queries:
|
|
136
281
|
await cur.execute(query, params)
|
|
137
282
|
|
|
@@ -141,12 +286,23 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|
|
141
286
|
results: list[Result],
|
|
142
287
|
cur: AsyncCursor[DictRow],
|
|
143
288
|
) -> None:
|
|
144
|
-
queries = self.
|
|
145
|
-
|
|
289
|
+
queries, embedding_requests = self._prepare_batch_search_queries(search_ops)
|
|
290
|
+
|
|
291
|
+
if embedding_requests and self.embeddings:
|
|
292
|
+
vectors = await self.embeddings.aembed_documents(
|
|
293
|
+
[query for _, query in embedding_requests]
|
|
294
|
+
)
|
|
295
|
+
for (idx, _), vector in zip(embedding_requests, vectors):
|
|
296
|
+
_paramslist = queries[idx][1]
|
|
297
|
+
for i in range(len(_paramslist)):
|
|
298
|
+
if _paramslist[i] is _PLACEHOLDER:
|
|
299
|
+
_paramslist[i] = vector
|
|
300
|
+
|
|
301
|
+
for (idx, _), (query, params) in zip(search_ops, queries):
|
|
146
302
|
await cur.execute(query, params)
|
|
147
303
|
rows = cast(list[Row], await cur.fetchall())
|
|
148
304
|
items = [
|
|
149
|
-
|
|
305
|
+
_row_to_search_item(
|
|
150
306
|
_decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
|
|
151
307
|
)
|
|
152
308
|
for row in rows
|
|
@@ -168,127 +324,46 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|
|
168
324
|
|
|
169
325
|
@asynccontextmanager
|
|
170
326
|
async def _cursor(
|
|
171
|
-
self,
|
|
172
|
-
) -> AsyncIterator[AsyncCursor[
|
|
327
|
+
self, *, pipeline: bool = False
|
|
328
|
+
) -> AsyncIterator[AsyncCursor[DictRow]]:
|
|
173
329
|
"""Create a database cursor as a context manager.
|
|
174
330
|
|
|
175
331
|
Args:
|
|
176
|
-
conn: The database connection to use
|
|
177
332
|
pipeline: whether to use pipeline for the DB operations inside the context manager.
|
|
178
333
|
Will be applied regardless of whether the PostgresStore instance was initialized with a pipeline.
|
|
179
334
|
If pipeline mode is not supported, will fall back to using transaction context manager.
|
|
180
335
|
"""
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
336
|
+
async with _ainternal.get_connection(self.conn) as conn:
|
|
337
|
+
if self.pipe:
|
|
338
|
+
# a connection in pipeline mode can be used concurrently
|
|
339
|
+
# in multiple threads/coroutines, but only one cursor can be
|
|
340
|
+
# used at a time
|
|
186
341
|
try:
|
|
187
|
-
|
|
342
|
+
async with conn.cursor(binary=True, row_factory=dict_row) as cur:
|
|
343
|
+
yield cur
|
|
188
344
|
finally:
|
|
189
345
|
if pipeline:
|
|
190
346
|
await self.pipe.sync()
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
yield cur
|
|
202
|
-
else:
|
|
203
|
-
async with conn.cursor(binary=True) as cur:
|
|
204
|
-
yield cur
|
|
205
|
-
|
|
206
|
-
def batch(self, ops: Iterable[Op]) -> list[Result]:
|
|
207
|
-
return asyncio.run_coroutine_threadsafe(self.abatch(ops), self.loop).result()
|
|
208
|
-
|
|
209
|
-
@classmethod
|
|
210
|
-
@asynccontextmanager
|
|
211
|
-
async def from_conn_string(
|
|
212
|
-
cls,
|
|
213
|
-
conn_string: str,
|
|
214
|
-
*,
|
|
215
|
-
pipeline: bool = False,
|
|
216
|
-
pool_config: Optional[PoolConfig] = None,
|
|
217
|
-
) -> AsyncIterator["AsyncPostgresStore"]:
|
|
218
|
-
"""Create a new AsyncPostgresStore instance from a connection string.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
conn_string (str): The Postgres connection info string.
|
|
222
|
-
pipeline (bool): Whether to use AsyncPipeline (only for single connections)
|
|
223
|
-
pool_config (Optional[PoolConfig]): Configuration for the connection pool.
|
|
224
|
-
If provided, will create a connection pool and use it instead of a single connection.
|
|
225
|
-
This overrides the `pipeline` argument.
|
|
226
|
-
|
|
227
|
-
Returns:
|
|
228
|
-
AsyncPostgresStore: A new AsyncPostgresStore instance.
|
|
229
|
-
"""
|
|
230
|
-
if pool_config is not None:
|
|
231
|
-
pc = pool_config.copy()
|
|
232
|
-
async with cast(
|
|
233
|
-
AsyncConnectionPool[AsyncConnection[DictRow]],
|
|
234
|
-
AsyncConnectionPool(
|
|
235
|
-
conn_string,
|
|
236
|
-
min_size=pc.pop("min_size", 1),
|
|
237
|
-
max_size=pc.pop("max_size", None),
|
|
238
|
-
kwargs={
|
|
239
|
-
"autocommit": True,
|
|
240
|
-
"prepare_threshold": 0,
|
|
241
|
-
"row_factory": dict_row,
|
|
242
|
-
**(pc.pop("kwargs", None) or {}),
|
|
243
|
-
},
|
|
244
|
-
**cast(dict, pc),
|
|
245
|
-
),
|
|
246
|
-
) as pool:
|
|
247
|
-
yield cls(conn=pool)
|
|
248
|
-
else:
|
|
249
|
-
async with await AsyncConnection.connect(
|
|
250
|
-
conn_string, autocommit=True, prepare_threshold=0, row_factory=dict_row
|
|
251
|
-
) as conn:
|
|
252
|
-
if pipeline:
|
|
253
|
-
async with conn.pipeline() as pipe:
|
|
254
|
-
yield cls(conn=conn, pipe=pipe)
|
|
347
|
+
elif pipeline:
|
|
348
|
+
# a connection not in pipeline mode can only be used by one
|
|
349
|
+
# thread/coroutine at a time, so we acquire a lock
|
|
350
|
+
if self.supports_pipeline:
|
|
351
|
+
async with (
|
|
352
|
+
self.lock,
|
|
353
|
+
conn.pipeline(),
|
|
354
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
355
|
+
):
|
|
356
|
+
yield cur
|
|
255
357
|
else:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
async with conn.cursor() as cur:
|
|
267
|
-
try:
|
|
268
|
-
await cur.execute(
|
|
269
|
-
"SELECT v FROM store_migrations ORDER BY v DESC LIMIT 1"
|
|
270
|
-
)
|
|
271
|
-
row = cast(dict, await cur.fetchone())
|
|
272
|
-
if row is None:
|
|
273
|
-
version = -1
|
|
274
|
-
else:
|
|
275
|
-
version = row["v"]
|
|
276
|
-
except UndefinedTable:
|
|
277
|
-
version = -1
|
|
278
|
-
# Create store_migrations table if it doesn't exist
|
|
279
|
-
await cur.execute(
|
|
280
|
-
"""
|
|
281
|
-
CREATE TABLE IF NOT EXISTS store_migrations (
|
|
282
|
-
v INTEGER PRIMARY KEY
|
|
283
|
-
)
|
|
284
|
-
"""
|
|
285
|
-
)
|
|
286
|
-
for v, migration in enumerate(
|
|
287
|
-
self.MIGRATIONS[version + 1 :], start=version + 1
|
|
358
|
+
async with (
|
|
359
|
+
self.lock,
|
|
360
|
+
conn.transaction(),
|
|
361
|
+
conn.cursor(binary=True, row_factory=dict_row) as cur,
|
|
362
|
+
):
|
|
363
|
+
yield cur
|
|
364
|
+
else:
|
|
365
|
+
async with (
|
|
366
|
+
self.lock,
|
|
367
|
+
conn.cursor(binary=True) as cur,
|
|
288
368
|
):
|
|
289
|
-
|
|
290
|
-
await cur.execute(
|
|
291
|
-
"INSERT INTO store_migrations (v) VALUES (%s)", (v,)
|
|
292
|
-
)
|
|
293
|
-
if self.pipe:
|
|
294
|
-
await self.pipe.sync()
|
|
369
|
+
yield cur
|