langgraph-checkpoint-postgres 3.2.0__tar.gz → 3.2.1__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-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/PKG-INFO +1 -1
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/aio.py +12 -3
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/store/postgres/aio.py +8 -1
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/store/postgres/base.py +7 -2
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/pyproject.toml +1 -1
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_async.py +31 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_store.py +25 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/uv.lock +5 -5
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/.gitignore +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/LICENSE +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/Makefile +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/README.md +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/_internal.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/base.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/checkpoint/postgres/shallow.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/store/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/langgraph/store/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/compose-postgres.yml +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/conftest.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/embed_test_utils.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_async_store.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_conformance_delta.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_delta_pagination.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_delta_plain_value_seed.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_prune.py +0 -0
- {langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: langgraph-checkpoint-postgres
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.1
|
|
4
4
|
Summary: Library with a Postgres implementation of LangGraph checkpoint saver.
|
|
5
5
|
Project-URL: Source, https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres
|
|
6
6
|
Project-URL: Twitter, https://x.com/langchain_oss
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from collections import defaultdict
|
|
5
5
|
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
|
|
6
|
-
from contextlib import asynccontextmanager
|
|
6
|
+
from contextlib import asynccontextmanager, nullcontext
|
|
7
7
|
from typing import Any, cast
|
|
8
8
|
|
|
9
9
|
from langchain_core.runnables import RunnableConfig
|
|
@@ -425,7 +425,12 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
425
425
|
Will be applied regardless of whether the AsyncPostgresSaver instance was initialized with a pipeline.
|
|
426
426
|
If pipeline mode is not supported, will fall back to using transaction context manager.
|
|
427
427
|
"""
|
|
428
|
-
|
|
428
|
+
# A pool hands each call its own connection, so only a shared one needs
|
|
429
|
+
# the lock.
|
|
430
|
+
lock = (
|
|
431
|
+
nullcontext() if isinstance(self.conn, AsyncConnectionPool) else self.lock
|
|
432
|
+
)
|
|
433
|
+
async with lock, _ainternal.get_connection(self.conn) as conn:
|
|
429
434
|
if self.pipe:
|
|
430
435
|
# a connection in pipeline mode can be used concurrently
|
|
431
436
|
# in multiple threads/coroutines, but only one cursor can be
|
|
@@ -594,7 +599,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
594
599
|
if value["parent_checkpoint_id"]
|
|
595
600
|
else None
|
|
596
601
|
),
|
|
597
|
-
|
|
602
|
+
(
|
|
603
|
+
await asyncio.to_thread(self._load_writes, value["pending_writes"])
|
|
604
|
+
if value["pending_writes"]
|
|
605
|
+
else []
|
|
606
|
+
),
|
|
598
607
|
)
|
|
599
608
|
|
|
600
609
|
def list(
|
|
@@ -413,7 +413,14 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
|
|
413
413
|
) -> None:
|
|
414
414
|
# Keep `conn` for compatibility with subclasses overriding this private hook.
|
|
415
415
|
# All database I/O goes through `_cursor()`, which owns connection acquisition.
|
|
416
|
-
|
|
416
|
+
# A lone read is one statement: outside a pipeline it costs one round
|
|
417
|
+
# trip instead of two (its fetch, then the Sync on leaving the pipeline).
|
|
418
|
+
lone_read = (
|
|
419
|
+
not self.pipe
|
|
420
|
+
and PutOp not in grouped_ops
|
|
421
|
+
and sum(map(len, grouped_ops.values())) == 1
|
|
422
|
+
)
|
|
423
|
+
async with self._cursor(pipeline=not lone_read) as cur:
|
|
417
424
|
if GetOp in grouped_ops:
|
|
418
425
|
await self._batch_get_ops(
|
|
419
426
|
cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]),
|
|
@@ -977,14 +977,19 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
|
|
977
977
|
):
|
|
978
978
|
yield cur
|
|
979
979
|
else:
|
|
980
|
-
|
|
980
|
+
# Lone reads land here, so take the lock like the pipeline branch:
|
|
981
|
+
# another thread may be holding this connection in a pipeline.
|
|
982
|
+
with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
|
|
981
983
|
yield cur
|
|
982
984
|
|
|
983
985
|
def batch(self, ops: Iterable[Op]) -> list[Result]:
|
|
984
986
|
grouped_ops, num_ops = _group_ops(ops)
|
|
985
987
|
results: list[Result] = [None] * num_ops
|
|
986
988
|
|
|
987
|
-
|
|
989
|
+
# A lone read is one statement: outside a pipeline it costs one round
|
|
990
|
+
# trip instead of two (its fetch, then the Sync on leaving the pipeline).
|
|
991
|
+
lone_read = num_ops == 1 and PutOp not in grouped_ops and not self.pipe
|
|
992
|
+
with self._cursor(pipeline=not lone_read) as cur:
|
|
988
993
|
if GetOp in grouped_ops:
|
|
989
994
|
self._batch_get_ops(
|
|
990
995
|
cast(Sequence[tuple[int, GetOp]], grouped_ops[GetOp]), results, cur
|
{langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_async.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# type: ignore
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
from contextlib import asynccontextmanager
|
|
4
5
|
from typing import Any
|
|
5
6
|
from uuid import uuid4
|
|
@@ -417,3 +418,33 @@ async def test_delta_channel_chain_reconstruction(saver_name: str) -> None:
|
|
|
417
418
|
assert msgs[1].content == "reply-1"
|
|
418
419
|
assert msgs[2].content == "there"
|
|
419
420
|
assert msgs[3].content == "reply-3"
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
async def test_pool_saver_reads_while_a_list_is_open(test_data) -> None:
|
|
424
|
+
async with _pool_saver() as saver:
|
|
425
|
+
config = await saver.aput(
|
|
426
|
+
test_data["configs"][0],
|
|
427
|
+
test_data["checkpoints"][0],
|
|
428
|
+
test_data["metadata"][0],
|
|
429
|
+
{},
|
|
430
|
+
)
|
|
431
|
+
listing = saver.alist(None)
|
|
432
|
+
try:
|
|
433
|
+
assert await anext(listing) is not None
|
|
434
|
+
assert await asyncio.wait_for(saver.aget_tuple(config), timeout=5)
|
|
435
|
+
finally:
|
|
436
|
+
await listing.aclose()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
async def test_base_saver_serializes_concurrent_calls(test_data) -> None:
|
|
440
|
+
async with _base_saver() as saver:
|
|
441
|
+
config = await saver.aput(
|
|
442
|
+
test_data["configs"][0],
|
|
443
|
+
test_data["checkpoints"][0],
|
|
444
|
+
test_data["metadata"][0],
|
|
445
|
+
{},
|
|
446
|
+
)
|
|
447
|
+
await asyncio.gather(
|
|
448
|
+
*(saver.aput_writes(config, [("x", i)], str(uuid4())) for i in range(20)),
|
|
449
|
+
*(saver.aget_tuple(config) for _ in range(20)),
|
|
450
|
+
)
|
{langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_store.py
RENAMED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import re
|
|
5
|
+
import threading
|
|
5
6
|
import time
|
|
6
7
|
from contextlib import contextmanager
|
|
7
8
|
from typing import Any
|
|
@@ -289,6 +290,30 @@ def test_basic_store_ops(store) -> None:
|
|
|
289
290
|
assert deleted_item is None
|
|
290
291
|
|
|
291
292
|
|
|
293
|
+
def test_lone_reads_and_puts_share_a_connection(store: PostgresStore) -> None:
|
|
294
|
+
errors: list[BaseException] = []
|
|
295
|
+
|
|
296
|
+
def run(op: Any) -> None:
|
|
297
|
+
try:
|
|
298
|
+
for i in range(100):
|
|
299
|
+
op(i)
|
|
300
|
+
except BaseException as e:
|
|
301
|
+
errors.append(e)
|
|
302
|
+
|
|
303
|
+
def read(i: int) -> None:
|
|
304
|
+
store.get(("race",), f"k{i % 10}")
|
|
305
|
+
|
|
306
|
+
def write(i: int) -> None:
|
|
307
|
+
store.put(("race",), f"k{i % 10}", {"i": i})
|
|
308
|
+
|
|
309
|
+
threads = [threading.Thread(target=run, args=(op,)) for op in [read, write] * 4]
|
|
310
|
+
for t in threads:
|
|
311
|
+
t.start()
|
|
312
|
+
for t in threads:
|
|
313
|
+
t.join()
|
|
314
|
+
assert errors == []
|
|
315
|
+
|
|
316
|
+
|
|
292
317
|
def test_list_namespaces(store) -> None:
|
|
293
318
|
# Create test data with various namespaces
|
|
294
319
|
test_namespaces = [
|
|
@@ -40,11 +40,11 @@ wheels = [
|
|
|
40
40
|
|
|
41
41
|
[[package]]
|
|
42
42
|
name = "certifi"
|
|
43
|
-
version = "2026.
|
|
43
|
+
version = "2026.10.19"
|
|
44
44
|
source = { registry = "https://pypi.org/simple" }
|
|
45
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
45
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" }
|
|
46
46
|
wheels = [
|
|
47
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
47
|
+
{ url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" },
|
|
48
48
|
]
|
|
49
49
|
|
|
50
50
|
[[package]]
|
|
@@ -278,7 +278,7 @@ wheels = [
|
|
|
278
278
|
|
|
279
279
|
[[package]]
|
|
280
280
|
name = "langgraph-checkpoint"
|
|
281
|
-
version = "4.3.
|
|
281
|
+
version = "4.3.2"
|
|
282
282
|
source = { editable = "../checkpoint" }
|
|
283
283
|
dependencies = [
|
|
284
284
|
{ name = "langchain-core" },
|
|
@@ -353,7 +353,7 @@ test = [
|
|
|
353
353
|
|
|
354
354
|
[[package]]
|
|
355
355
|
name = "langgraph-checkpoint-postgres"
|
|
356
|
-
version = "3.2.
|
|
356
|
+
version = "3.2.1"
|
|
357
357
|
source = { editable = "." }
|
|
358
358
|
dependencies = [
|
|
359
359
|
{ name = "langgraph-checkpoint" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/conftest.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_prune.py
RENAMED
|
File without changes
|
{langgraph_checkpoint_postgres-3.2.0 → langgraph_checkpoint_postgres-3.2.1}/tests/test_sync.py
RENAMED
|
File without changes
|