langgraph-checkpoint-postgres 3.1.1__tar.gz → 3.1.2__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.1.1 → langgraph_checkpoint_postgres-3.1.2}/PKG-INFO +9 -33
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/README.md +8 -32
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/__init__.py +12 -5
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/aio.py +12 -5
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/base.py +90 -20
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/shallow.py +2 -2
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/pyproject.toml +5 -1
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/test_async.py +9 -7
- langgraph_checkpoint_postgres-3.1.2/tests/test_conformance_delta.py +30 -0
- langgraph_checkpoint_postgres-3.1.2/tests/test_delta_plain_value_seed.py +204 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/uv.lock +81 -50
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/.gitignore +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/LICENSE +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/Makefile +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/_internal.py +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/store/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/store/postgres/aio.py +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/store/postgres/base.py +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/store/postgres/py.typed +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/__init__.py +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/compose-postgres.yml +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/conftest.py +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/embed_test_utils.py +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/test_async_store.py +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/test_store.py +0 -0
- {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/test_sync.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-checkpoint-postgres
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.2
|
|
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
|
|
@@ -84,24 +84,12 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
|
|
84
84
|
"v": 4,
|
|
85
85
|
"ts": "2024-07-31T20:14:19.804150+00:00",
|
|
86
86
|
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
|
87
|
-
"channel_values": {
|
|
88
|
-
|
|
89
|
-
"node": "node"
|
|
90
|
-
},
|
|
91
|
-
"channel_versions": {
|
|
92
|
-
"__start__": 2,
|
|
93
|
-
"my_key": 3,
|
|
94
|
-
"start:node": 3,
|
|
95
|
-
"node": 3
|
|
96
|
-
},
|
|
87
|
+
"channel_values": {"my_key": "meow", "node": "node"},
|
|
88
|
+
"channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
|
|
97
89
|
"versions_seen": {
|
|
98
90
|
"__input__": {},
|
|
99
|
-
"__start__": {
|
|
100
|
-
"
|
|
101
|
-
},
|
|
102
|
-
"node": {
|
|
103
|
-
"start:node": 2
|
|
104
|
-
}
|
|
91
|
+
"__start__": {"__start__": 1},
|
|
92
|
+
"node": {"start:node": 2},
|
|
105
93
|
},
|
|
106
94
|
}
|
|
107
95
|
|
|
@@ -125,24 +113,12 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
|
|
125
113
|
"v": 4,
|
|
126
114
|
"ts": "2024-07-31T20:14:19.804150+00:00",
|
|
127
115
|
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
|
128
|
-
"channel_values": {
|
|
129
|
-
|
|
130
|
-
"node": "node"
|
|
131
|
-
},
|
|
132
|
-
"channel_versions": {
|
|
133
|
-
"__start__": 2,
|
|
134
|
-
"my_key": 3,
|
|
135
|
-
"start:node": 3,
|
|
136
|
-
"node": 3
|
|
137
|
-
},
|
|
116
|
+
"channel_values": {"my_key": "meow", "node": "node"},
|
|
117
|
+
"channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
|
|
138
118
|
"versions_seen": {
|
|
139
119
|
"__input__": {},
|
|
140
|
-
"__start__": {
|
|
141
|
-
"
|
|
142
|
-
},
|
|
143
|
-
"node": {
|
|
144
|
-
"start:node": 2
|
|
145
|
-
}
|
|
120
|
+
"__start__": {"__start__": 1},
|
|
121
|
+
"node": {"start:node": 2},
|
|
146
122
|
},
|
|
147
123
|
}
|
|
148
124
|
|
|
@@ -67,24 +67,12 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
|
|
67
67
|
"v": 4,
|
|
68
68
|
"ts": "2024-07-31T20:14:19.804150+00:00",
|
|
69
69
|
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
|
70
|
-
"channel_values": {
|
|
71
|
-
|
|
72
|
-
"node": "node"
|
|
73
|
-
},
|
|
74
|
-
"channel_versions": {
|
|
75
|
-
"__start__": 2,
|
|
76
|
-
"my_key": 3,
|
|
77
|
-
"start:node": 3,
|
|
78
|
-
"node": 3
|
|
79
|
-
},
|
|
70
|
+
"channel_values": {"my_key": "meow", "node": "node"},
|
|
71
|
+
"channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
|
|
80
72
|
"versions_seen": {
|
|
81
73
|
"__input__": {},
|
|
82
|
-
"__start__": {
|
|
83
|
-
"
|
|
84
|
-
},
|
|
85
|
-
"node": {
|
|
86
|
-
"start:node": 2
|
|
87
|
-
}
|
|
74
|
+
"__start__": {"__start__": 1},
|
|
75
|
+
"node": {"start:node": 2},
|
|
88
76
|
},
|
|
89
77
|
}
|
|
90
78
|
|
|
@@ -108,24 +96,12 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
|
|
|
108
96
|
"v": 4,
|
|
109
97
|
"ts": "2024-07-31T20:14:19.804150+00:00",
|
|
110
98
|
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
|
|
111
|
-
"channel_values": {
|
|
112
|
-
|
|
113
|
-
"node": "node"
|
|
114
|
-
},
|
|
115
|
-
"channel_versions": {
|
|
116
|
-
"__start__": 2,
|
|
117
|
-
"my_key": 3,
|
|
118
|
-
"start:node": 3,
|
|
119
|
-
"node": 3
|
|
120
|
-
},
|
|
99
|
+
"channel_values": {"my_key": "meow", "node": "node"},
|
|
100
|
+
"channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
|
|
121
101
|
"versions_seen": {
|
|
122
102
|
"__input__": {},
|
|
123
|
-
"__start__": {
|
|
124
|
-
"
|
|
125
|
-
},
|
|
126
|
-
"node": {
|
|
127
|
-
"start:node": 2
|
|
128
|
-
}
|
|
103
|
+
"__start__": {"__start__": 1},
|
|
104
|
+
"node": {"start:node": 2},
|
|
129
105
|
},
|
|
130
106
|
}
|
|
131
107
|
|
|
@@ -223,7 +223,7 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
223
223
|
>>> checkpoint_tuple = memory.get_tuple(config)
|
|
224
224
|
>>> print(checkpoint_tuple)
|
|
225
225
|
CheckpointTuple(...)
|
|
226
|
-
"""
|
|
226
|
+
"""
|
|
227
227
|
thread_id = config["configurable"]["thread_id"]
|
|
228
228
|
checkpoint_id = get_checkpoint_id(config)
|
|
229
229
|
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
@@ -478,9 +478,11 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
478
478
|
stage1_sql = _build_delta_stage1_sql(channels, paged=True)
|
|
479
479
|
parent_of: dict[str, str | None] = {}
|
|
480
480
|
ver_by_i_by_cid: list[dict[str, str | None]] = [{} for _ in channels]
|
|
481
|
-
|
|
481
|
+
hb_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
|
|
482
|
+
inline_by_i_by_cid: list[dict[str, Any]] = [{} for _ in channels]
|
|
482
483
|
chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
|
|
483
484
|
seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
|
|
485
|
+
seed_inline_by_ch: dict[str, Any] = {}
|
|
484
486
|
walk_cursor_by_ch: dict[str, str | None] = {}
|
|
485
487
|
seeded: set[str] = set()
|
|
486
488
|
cursor: str | None = None
|
|
@@ -489,7 +491,8 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
489
491
|
while True:
|
|
490
492
|
stage1_params: list[Any] = []
|
|
491
493
|
for ch in channels:
|
|
492
|
-
|
|
494
|
+
# ver_i, blob channel, blob version, inline_i
|
|
495
|
+
stage1_params.extend([ch, ch, ch, ch])
|
|
493
496
|
stage1_params.extend(
|
|
494
497
|
[thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
|
|
495
498
|
)
|
|
@@ -502,16 +505,19 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
502
505
|
channels,
|
|
503
506
|
parent_of,
|
|
504
507
|
ver_by_i_by_cid,
|
|
505
|
-
|
|
508
|
+
hb_by_i_by_cid,
|
|
509
|
+
inline_by_i_by_cid,
|
|
506
510
|
)
|
|
507
511
|
self._try_advance_walks(
|
|
508
512
|
checkpoint_id,
|
|
509
513
|
channels,
|
|
510
514
|
parent_of,
|
|
511
515
|
ver_by_i_by_cid,
|
|
512
|
-
|
|
516
|
+
hb_by_i_by_cid,
|
|
517
|
+
inline_by_i_by_cid,
|
|
513
518
|
chain_by_ch,
|
|
514
519
|
seed_ver_by_ch,
|
|
520
|
+
seed_inline_by_ch,
|
|
515
521
|
walk_cursor_by_ch,
|
|
516
522
|
seeded,
|
|
517
523
|
)
|
|
@@ -546,6 +552,7 @@ class PostgresSaver(BasePostgresSaver):
|
|
|
546
552
|
channels=channels,
|
|
547
553
|
chain_by_ch=chain_by_ch,
|
|
548
554
|
seed_ver_by_ch=seed_ver_by_ch,
|
|
555
|
+
seed_inline_by_ch=seed_inline_by_ch,
|
|
549
556
|
stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
|
|
550
557
|
)
|
|
551
558
|
|
|
@@ -426,9 +426,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
426
426
|
stage1_sql = _build_delta_stage1_sql(channels, paged=True)
|
|
427
427
|
parent_of: dict[str, str | None] = {}
|
|
428
428
|
ver_by_i_by_cid: list[dict[str, str | None]] = [{} for _ in channels]
|
|
429
|
-
|
|
429
|
+
hb_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
|
|
430
|
+
inline_by_i_by_cid: list[dict[str, Any]] = [{} for _ in channels]
|
|
430
431
|
chain_by_ch: dict[str, list[str]] = {ch: [] for ch in channels}
|
|
431
432
|
seed_ver_by_ch: dict[str, str | None] = {ch: None for ch in channels}
|
|
433
|
+
seed_inline_by_ch: dict[str, Any] = {}
|
|
432
434
|
walk_cursor_by_ch: dict[str, str | None] = {}
|
|
433
435
|
seeded: set[str] = set()
|
|
434
436
|
cursor: str | None = None
|
|
@@ -437,7 +439,8 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
437
439
|
while True:
|
|
438
440
|
stage1_params: list[Any] = []
|
|
439
441
|
for ch in channels:
|
|
440
|
-
|
|
442
|
+
# ver_i, blob channel, blob version, inline_i
|
|
443
|
+
stage1_params.extend([ch, ch, ch, ch])
|
|
441
444
|
stage1_params.extend(
|
|
442
445
|
[thread_id, checkpoint_ns, cursor, cursor, _DELTA_PAGE_SIZE]
|
|
443
446
|
)
|
|
@@ -450,16 +453,19 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
450
453
|
channels,
|
|
451
454
|
parent_of,
|
|
452
455
|
ver_by_i_by_cid,
|
|
453
|
-
|
|
456
|
+
hb_by_i_by_cid,
|
|
457
|
+
inline_by_i_by_cid,
|
|
454
458
|
)
|
|
455
459
|
self._try_advance_walks(
|
|
456
460
|
checkpoint_id,
|
|
457
461
|
channels,
|
|
458
462
|
parent_of,
|
|
459
463
|
ver_by_i_by_cid,
|
|
460
|
-
|
|
464
|
+
hb_by_i_by_cid,
|
|
465
|
+
inline_by_i_by_cid,
|
|
461
466
|
chain_by_ch,
|
|
462
467
|
seed_ver_by_ch,
|
|
468
|
+
seed_inline_by_ch,
|
|
463
469
|
walk_cursor_by_ch,
|
|
464
470
|
seeded,
|
|
465
471
|
)
|
|
@@ -490,6 +496,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
490
496
|
channels=channels,
|
|
491
497
|
chain_by_ch=chain_by_ch,
|
|
492
498
|
seed_ver_by_ch=seed_ver_by_ch,
|
|
499
|
+
seed_inline_by_ch=seed_inline_by_ch,
|
|
493
500
|
stage2_rows=cast("list[_DeltaStage2Row]", stage2_rows),
|
|
494
501
|
)
|
|
495
502
|
|
|
@@ -573,7 +580,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
|
|
573
580
|
while True:
|
|
574
581
|
try:
|
|
575
582
|
yield asyncio.run_coroutine_threadsafe(
|
|
576
|
-
anext(aiter_), # type: ignore[arg-type]
|
|
583
|
+
anext(aiter_), # type: ignore[arg-type]
|
|
577
584
|
self.loop,
|
|
578
585
|
).result()
|
|
579
586
|
except StopAsyncIteration:
|
|
@@ -199,27 +199,68 @@ class _DeltaStage2Row(TypedDict, total=False):
|
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def _build_delta_stage1_sql(channels: Sequence[str], *, paged: bool) -> str:
|
|
202
|
-
"""Build stage 1 SQL with
|
|
202
|
+
"""Build stage 1 SQL with K parallel version lookups + seed probes.
|
|
203
203
|
|
|
204
204
|
For channels=["messages", "files"] (with `paged=True`) the result is::
|
|
205
205
|
|
|
206
206
|
SELECT checkpoint_id, parent_checkpoint_id,
|
|
207
207
|
checkpoint -> 'channel_versions' ->> %s AS ver_0,
|
|
208
|
-
(
|
|
208
|
+
EXISTS (SELECT 1 FROM checkpoint_blobs b0
|
|
209
|
+
WHERE b0.thread_id = checkpoints.thread_id
|
|
210
|
+
AND b0.checkpoint_ns = checkpoints.checkpoint_ns
|
|
211
|
+
AND b0.channel = %s
|
|
212
|
+
AND b0.version = checkpoint -> 'channel_versions' ->> %s
|
|
213
|
+
AND b0.type <> 'empty') AS hb_0,
|
|
214
|
+
checkpoint -> 'channel_values' -> %s AS inline_0,
|
|
209
215
|
checkpoint -> 'channel_versions' ->> %s AS ver_1,
|
|
210
|
-
(
|
|
216
|
+
EXISTS (...) AS hb_1,
|
|
217
|
+
checkpoint -> 'channel_values' -> %s AS inline_1
|
|
211
218
|
FROM checkpoints
|
|
212
219
|
WHERE thread_id = %s AND checkpoint_ns = %s
|
|
213
220
|
AND (%s::text IS NULL OR checkpoint_id < %s)
|
|
214
221
|
ORDER BY checkpoint_id DESC
|
|
215
222
|
LIMIT %s
|
|
216
223
|
|
|
224
|
+
A stored value for a channel lives in one of two places, because `put`
|
|
225
|
+
splits them:
|
|
226
|
+
|
|
227
|
+
* **blob** — non-primitive values (and `_DeltaSnapshot`) are moved to
|
|
228
|
+
`checkpoint_blobs`. `hb_i` ("has blob") probes for one. The probe hits
|
|
229
|
+
that table's primary key `(thread_id, checkpoint_ns, channel, version)`
|
|
230
|
+
exactly, so it is an index lookup per row per channel.
|
|
231
|
+
* **inline** — `None`, `str`, `int`, `float` and `bool` stay in the
|
|
232
|
+
checkpoint's own `channel_values` and get no blob row at all. `inline_i`
|
|
233
|
+
returns that value.
|
|
234
|
+
|
|
235
|
+
Testing only for a key in `channel_values` (the previous approach) missed
|
|
236
|
+
blob-stored plain values, since `put` leaves an inline marker there for
|
|
237
|
+
`_DeltaSnapshot` but not for a plain value — which is what a thread
|
|
238
|
+
migrated from a pre-delta channel type leaves behind. Probing only the
|
|
239
|
+
blobs table would conversely miss inline primitives. Both are needed, and
|
|
240
|
+
the caller treats "either present" as the seed.
|
|
241
|
+
|
|
242
|
+
`hb_i` also disambiguates the two: for a `_DeltaSnapshot`, `inline_i` is the
|
|
243
|
+
literal `true` marker rather than the value, so a blob must win over an
|
|
244
|
+
inline reading whenever one exists. That ordering is what makes a genuine
|
|
245
|
+
inline `true` (a bool channel) distinguishable from the marker.
|
|
246
|
+
|
|
247
|
+
The `type <> 'empty'` predicate mirrors the check stage 2 already applies
|
|
248
|
+
when resolving the seed blob. `put` does not currently produce `empty` rows
|
|
249
|
+
on this path — `blob_versions` is filtered to keys present in
|
|
250
|
+
`channel_values`, so `_dump_blobs`' empty branch is unreachable from it —
|
|
251
|
+
but without the predicate the two stages could disagree: stage 1 would
|
|
252
|
+
terminate the walk on a row stage 2 then discards, yielding no seed *and* a
|
|
253
|
+
truncated write chain, which is the failure this function exists to avoid.
|
|
254
|
+
|
|
217
255
|
Channel names are passed as `%s` parameters (safe from SQL injection).
|
|
218
|
-
Only the column aliases `ver_i` / `
|
|
219
|
-
SQL string (i is bounded by len(channels)
|
|
256
|
+
Only the column aliases `ver_i` / `hb_i` / `inline_i` and the subquery alias
|
|
257
|
+
`b{i}` are interpolated into the SQL string (i is bounded by len(channels)
|
|
258
|
+
and uses safe identifiers).
|
|
220
259
|
|
|
221
|
-
Caller must extend params with `[ch_0
|
|
222
|
-
|
|
260
|
+
Caller must extend params with `[ch_0 x4, ch_1 x4, ..., thread_id, ns,
|
|
261
|
+
cursor, cursor, page_size]` when `paged=True` — four per channel: the
|
|
262
|
+
version lookup, the blob's channel, the version the blob must match, and the
|
|
263
|
+
inline lookup.
|
|
223
264
|
|
|
224
265
|
When `paged=False`, the WHERE has no cursor predicate and there's no
|
|
225
266
|
LIMIT/ORDER BY — kept as a non-public helper for tests/diagnostics.
|
|
@@ -228,7 +269,13 @@ def _build_delta_stage1_sql(channels: Sequence[str], *, paged: bool) -> str:
|
|
|
228
269
|
for i in range(len(channels)):
|
|
229
270
|
cols.append(
|
|
230
271
|
f"checkpoint -> 'channel_versions' ->> %s AS ver_{i}, "
|
|
231
|
-
f"(
|
|
272
|
+
f"EXISTS (SELECT 1 FROM checkpoint_blobs b{i} "
|
|
273
|
+
f"WHERE b{i}.thread_id = checkpoints.thread_id "
|
|
274
|
+
f"AND b{i}.checkpoint_ns = checkpoints.checkpoint_ns "
|
|
275
|
+
f"AND b{i}.channel = %s "
|
|
276
|
+
f"AND b{i}.version = checkpoint -> 'channel_versions' ->> %s "
|
|
277
|
+
f"AND b{i}.type <> 'empty') AS hb_{i}, "
|
|
278
|
+
f"checkpoint -> 'channel_values' -> %s AS inline_{i}"
|
|
232
279
|
)
|
|
233
280
|
sql = (
|
|
234
281
|
"SELECT checkpoint_id, parent_checkpoint_id, "
|
|
@@ -342,7 +389,8 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|
|
342
389
|
channels: Sequence[str],
|
|
343
390
|
parent_of: dict[str, str | None],
|
|
344
391
|
ver_by_i_by_cid: list[dict[str, str | None]],
|
|
345
|
-
|
|
392
|
+
hb_by_i_by_cid: list[dict[str, bool]],
|
|
393
|
+
inline_by_i_by_cid: list[dict[str, Any]],
|
|
346
394
|
) -> str | None:
|
|
347
395
|
"""Fold one stage-1 page into the running walk-state mappings.
|
|
348
396
|
|
|
@@ -356,7 +404,8 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|
|
356
404
|
parent_of[cid] = cast("str | None", r["parent_checkpoint_id"])
|
|
357
405
|
for i in range(len(channels)):
|
|
358
406
|
ver_by_i_by_cid[i][cid] = cast("str | None", r.get(f"ver_{i}"))
|
|
359
|
-
|
|
407
|
+
hb_by_i_by_cid[i][cid] = bool(r.get(f"hb_{i}"))
|
|
408
|
+
inline_by_i_by_cid[i][cid] = r.get(f"inline_{i}")
|
|
360
409
|
# Rows are DESC; the last one is the smallest cid in the page.
|
|
361
410
|
oldest = cid
|
|
362
411
|
return oldest
|
|
@@ -367,9 +416,11 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|
|
367
416
|
channels: Sequence[str],
|
|
368
417
|
parent_of: Mapping[str, str | None],
|
|
369
418
|
ver_by_i_by_cid: Sequence[Mapping[str, str | None]],
|
|
370
|
-
|
|
419
|
+
hb_by_i_by_cid: Sequence[Mapping[str, bool]],
|
|
420
|
+
inline_by_i_by_cid: Sequence[Mapping[str, Any]],
|
|
371
421
|
chain_by_ch: dict[str, list[str]],
|
|
372
422
|
seed_ver_by_ch: dict[str, str | None],
|
|
423
|
+
seed_inline_by_ch: dict[str, Any],
|
|
373
424
|
walk_cursor_by_ch: dict[str, str | None],
|
|
374
425
|
seeded: set[str],
|
|
375
426
|
) -> None:
|
|
@@ -377,14 +428,15 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|
|
377
428
|
|
|
378
429
|
Uses the partial `parent_of` map accumulated so far. A walk stops
|
|
379
430
|
either because:
|
|
380
|
-
(a) it found a
|
|
431
|
+
(a) it found a stored value for its channel — a blob or an inline
|
|
432
|
+
primitive (channel becomes seeded),
|
|
381
433
|
(b) it reached a real root (parent_of[cid] is None — fully
|
|
382
434
|
materialized at this point), or
|
|
383
435
|
(c) the next ancestor cid isn't in `parent_of` yet (waiting for
|
|
384
436
|
a later page; the cursor stays put).
|
|
385
437
|
|
|
386
|
-
Mutates `chain_by_ch`, `seed_ver_by_ch`, `
|
|
387
|
-
`seeded` in place.
|
|
438
|
+
Mutates `chain_by_ch`, `seed_ver_by_ch`, `seed_inline_by_ch`,
|
|
439
|
+
`walk_cursor_by_ch`, and `seeded` in place.
|
|
388
440
|
"""
|
|
389
441
|
for i, ch in enumerate(channels):
|
|
390
442
|
if ch in seeded:
|
|
@@ -394,15 +446,22 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|
|
394
446
|
walk_cursor_by_ch[ch] = parent_of.get(target_id)
|
|
395
447
|
cur_cid = walk_cursor_by_ch[ch]
|
|
396
448
|
ch_chain = chain_by_ch[ch]
|
|
397
|
-
|
|
449
|
+
hb_i = hb_by_i_by_cid[i]
|
|
450
|
+
inline_i = inline_by_i_by_cid[i]
|
|
398
451
|
ver_i = ver_by_i_by_cid[i]
|
|
399
452
|
while cur_cid is not None:
|
|
400
453
|
if cur_cid not in parent_of:
|
|
401
454
|
# Need more pages to continue this walk.
|
|
402
455
|
break
|
|
403
456
|
ch_chain.append(cur_cid)
|
|
404
|
-
|
|
457
|
+
has_blob = hb_i.get(cur_cid, False)
|
|
458
|
+
inline = inline_i.get(cur_cid)
|
|
459
|
+
if has_blob or inline is not None:
|
|
460
|
+
# A blob wins: for a `_DeltaSnapshot` the inline reading is
|
|
461
|
+
# the `true` marker, not the value.
|
|
405
462
|
seed_ver_by_ch[ch] = ver_i.get(cur_cid)
|
|
463
|
+
if not has_blob:
|
|
464
|
+
seed_inline_by_ch[ch] = inline
|
|
406
465
|
seeded.add(ch)
|
|
407
466
|
cur_cid = None
|
|
408
467
|
break
|
|
@@ -415,16 +474,23 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|
|
415
474
|
channels: Sequence[str],
|
|
416
475
|
chain_by_ch: Mapping[str, list[str]],
|
|
417
476
|
seed_ver_by_ch: Mapping[str, str | None],
|
|
477
|
+
seed_inline_by_ch: Mapping[str, Any],
|
|
418
478
|
stage2_rows: Sequence[_DeltaStage2Row],
|
|
419
479
|
) -> dict[str, DeltaChannelHistory]:
|
|
420
480
|
"""Demux stage 2 rows per channel; produce per-channel histories.
|
|
421
481
|
|
|
422
482
|
stage2_rows carry `channel` on every row. We build per-channel
|
|
423
483
|
`writes_by_cid` and per-channel `seed_blob` dicts, then assemble
|
|
424
|
-
a `DeltaChannelHistory` per requested channel.
|
|
425
|
-
|
|
426
|
-
seed
|
|
427
|
-
|
|
484
|
+
a `DeltaChannelHistory` per requested channel.
|
|
485
|
+
|
|
486
|
+
A seed comes from the blobs table when the walk found one there, and
|
|
487
|
+
otherwise from `seed_inline_by_ch` — `put` keeps `None`, `str`, `int`,
|
|
488
|
+
`float` and `bool` values in the checkpoint's own `channel_values` with
|
|
489
|
+
no blob row, so those never appear in `stage2_rows`.
|
|
490
|
+
|
|
491
|
+
The `seed` key is omitted when the walk reached root without finding a
|
|
492
|
+
stored value, or when the seed blob is sentinel "empty" — in both cases
|
|
493
|
+
the consumer treats absence as "start empty".
|
|
428
494
|
"""
|
|
429
495
|
# writes_by_ch_by_cid[channel][cid] = list of (type, blob, task_id, idx)
|
|
430
496
|
writes_by_ch_by_cid: dict[str, dict[str, list[tuple[str, bytes, str, int]]]] = {
|
|
@@ -473,6 +539,10 @@ class BasePostgresSaver(BaseCheckpointSaver[str]):
|
|
|
473
539
|
blob = seed_blob_by_ver.get((ch, seed_version))
|
|
474
540
|
if blob is not None and blob[0] != "empty":
|
|
475
541
|
entry["seed"] = self.serde.loads_typed(blob)
|
|
542
|
+
elif ch in seed_inline_by_ch:
|
|
543
|
+
# Inline primitive: stored in the checkpoint, not the blobs
|
|
544
|
+
# table, so stage 2 never returned a row for it.
|
|
545
|
+
entry["seed"] = seed_inline_by_ch[ch]
|
|
476
546
|
result[ch] = entry
|
|
477
547
|
return result
|
|
478
548
|
|
|
@@ -334,7 +334,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
|
|
334
334
|
>>> checkpoint_tuple = memory.get_tuple(config)
|
|
335
335
|
>>> print(checkpoint_tuple)
|
|
336
336
|
CheckpointTuple(...)
|
|
337
|
-
"""
|
|
337
|
+
"""
|
|
338
338
|
thread_id = config["configurable"]["thread_id"]
|
|
339
339
|
checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
|
|
340
340
|
args = (thread_id, checkpoint_ns)
|
|
@@ -885,7 +885,7 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
|
|
885
885
|
while True:
|
|
886
886
|
try:
|
|
887
887
|
yield asyncio.run_coroutine_threadsafe(
|
|
888
|
-
anext(aiter_), # type: ignore[arg-type]
|
|
888
|
+
anext(aiter_), # type: ignore[arg-type]
|
|
889
889
|
self.loop,
|
|
890
890
|
).result()
|
|
891
891
|
except StopAsyncIteration:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langgraph-checkpoint-postgres"
|
|
7
|
-
version = "3.1.
|
|
7
|
+
version = "3.1.2"
|
|
8
8
|
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
|
9
9
|
authors = []
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -32,6 +32,7 @@ test = [
|
|
|
32
32
|
"pytest-mock",
|
|
33
33
|
"psycopg[binary]",
|
|
34
34
|
"langgraph-checkpoint",
|
|
35
|
+
"langgraph-checkpoint-conformance",
|
|
35
36
|
"pytest-watcher",
|
|
36
37
|
]
|
|
37
38
|
lint = [
|
|
@@ -49,6 +50,7 @@ default-groups = ['dev']
|
|
|
49
50
|
|
|
50
51
|
[tool.uv.sources]
|
|
51
52
|
langgraph-checkpoint = { path = "../checkpoint", editable = true }
|
|
53
|
+
langgraph-checkpoint-conformance = { path = "../checkpoint-conformance", editable = true }
|
|
52
54
|
|
|
53
55
|
[tool.hatch.build.targets.wheel]
|
|
54
56
|
include = ["langgraph"]
|
|
@@ -64,6 +66,8 @@ lint.select = [
|
|
|
64
66
|
"UP", # pyupgrade
|
|
65
67
|
"B", # flake8-bugbear
|
|
66
68
|
"I", # isort
|
|
69
|
+
"PLC0415", # import-outside-top-level
|
|
70
|
+
"RUF100", # unused noqa directive
|
|
67
71
|
"UP", # pyupgrade
|
|
68
72
|
]
|
|
69
73
|
lint.ignore = ["E501", "B008"]
|
{langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/test_async.py
RENAMED
|
@@ -380,13 +380,15 @@ async def test_delta_channel_chain_reconstruction(saver_name: str) -> None:
|
|
|
380
380
|
"langgraph.channels.delta", reason="langgraph core not installed"
|
|
381
381
|
)
|
|
382
382
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
from
|
|
386
|
-
|
|
387
|
-
from
|
|
388
|
-
from langgraph.
|
|
389
|
-
from
|
|
383
|
+
# Deferred on purpose: langgraph core is not a test dependency of this
|
|
384
|
+
# package, so these must stay behind the importorskip above.
|
|
385
|
+
from typing import Annotated # noqa: PLC0415
|
|
386
|
+
|
|
387
|
+
from langchain_core.messages import AIMessage, HumanMessage # noqa: PLC0415
|
|
388
|
+
from langgraph.channels.delta import DeltaChannel # noqa: PLC0415
|
|
389
|
+
from langgraph.graph import START, StateGraph # noqa: PLC0415
|
|
390
|
+
from langgraph.graph.message import _messages_delta_reducer # noqa: PLC0415
|
|
391
|
+
from typing_extensions import TypedDict # noqa: PLC0415
|
|
390
392
|
|
|
391
393
|
class State(TypedDict):
|
|
392
394
|
messages: Annotated[list, DeltaChannel(_messages_delta_reducer)]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Run delta-channel conformance capabilities against AsyncPostgresSaver."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from langgraph.checkpoint.conformance import validate
|
|
7
|
+
from langgraph.checkpoint.conformance.initializer import checkpointer_test
|
|
8
|
+
|
|
9
|
+
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
|
10
|
+
from tests.conftest import DEFAULT_URI
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.asyncio
|
|
14
|
+
async def test_delta_channel_conformance():
|
|
15
|
+
@checkpointer_test(name="AsyncPostgresSaver")
|
|
16
|
+
async def postgres_saver():
|
|
17
|
+
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
|
|
18
|
+
await saver.setup()
|
|
19
|
+
yield saver
|
|
20
|
+
|
|
21
|
+
report = await validate(
|
|
22
|
+
postgres_saver,
|
|
23
|
+
capabilities={
|
|
24
|
+
"delta_channel_history",
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
for cap, result in report.results.items():
|
|
28
|
+
if result.passed is False:
|
|
29
|
+
details = "\n".join(result.failures or [])
|
|
30
|
+
pytest.fail(f"Capability {cap} failed:\n{details}")
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Seed detection for `DeltaChannel` histories on Postgres.
|
|
2
|
+
|
|
3
|
+
`put` splits stored values in two: primitives stay inline in the checkpoint's
|
|
4
|
+
`channel_values`, everything else moves to `checkpoint_blobs`. Only
|
|
5
|
+
`_DeltaSnapshot` leaves an inline marker behind when it moves, so the stage-1
|
|
6
|
+
walk has to check both places — a blob probe alone misses inline primitives, and
|
|
7
|
+
an inline-key check alone missed blob-stored plain values, which is what a thread
|
|
8
|
+
migrated from a pre-delta channel type leaves behind. See #8534.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Any
|
|
14
|
+
from uuid import uuid4
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
from langgraph.checkpoint.base import Checkpoint, empty_checkpoint
|
|
18
|
+
from langgraph.checkpoint.base.id import uuid6
|
|
19
|
+
from langgraph.checkpoint.serde.types import _DeltaSnapshot
|
|
20
|
+
|
|
21
|
+
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
|
22
|
+
from tests.conftest import DEFAULT_URI
|
|
23
|
+
|
|
24
|
+
CHANNEL = "items"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def _build_chain(saver: AsyncPostgresSaver, seed_value: Any) -> tuple[str, dict]:
|
|
28
|
+
"""Store `seed_value` at step 1, then two steps that store nothing.
|
|
29
|
+
|
|
30
|
+
Every step carries a write so the walk has something to collect.
|
|
31
|
+
Returns `(thread_id, head_config)`.
|
|
32
|
+
"""
|
|
33
|
+
thread_id = str(uuid4())
|
|
34
|
+
parent: dict | None = None
|
|
35
|
+
for step in range(4):
|
|
36
|
+
config: dict = {"configurable": {"thread_id": thread_id, "checkpoint_ns": ""}}
|
|
37
|
+
if parent is not None:
|
|
38
|
+
config["configurable"]["checkpoint_id"] = parent["configurable"][
|
|
39
|
+
"checkpoint_id"
|
|
40
|
+
]
|
|
41
|
+
cp: Checkpoint = empty_checkpoint()
|
|
42
|
+
cp["id"] = str(uuid6(clock_seq=step))
|
|
43
|
+
new_versions: dict[str, Any] = {}
|
|
44
|
+
if step == 1:
|
|
45
|
+
cp["channel_values"][CHANNEL] = seed_value
|
|
46
|
+
cp["channel_versions"][CHANNEL] = "v1"
|
|
47
|
+
new_versions[CHANNEL] = "v1"
|
|
48
|
+
else:
|
|
49
|
+
cp["channel_versions"][CHANNEL] = f"v{step}"
|
|
50
|
+
parent = await saver.aput(
|
|
51
|
+
config, cp, {"source": "loop", "step": step, "parents": {}}, new_versions
|
|
52
|
+
)
|
|
53
|
+
await saver.aput_writes(parent, [(CHANNEL, f"w{step}")], str(uuid4()))
|
|
54
|
+
assert parent is not None
|
|
55
|
+
return thread_id, parent
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_plain_value_seed_is_found() -> None:
|
|
60
|
+
"""A pre-delta plain value must be located as the seed.
|
|
61
|
+
|
|
62
|
+
Before #8534 the walk ran to the root and returned no seed, which happens
|
|
63
|
+
to reconstruct correctly for additive reducers while costing an
|
|
64
|
+
O(thread length) replay on every read.
|
|
65
|
+
"""
|
|
66
|
+
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
|
|
67
|
+
await saver.setup()
|
|
68
|
+
_, head = await _build_chain(saver, [10, 20])
|
|
69
|
+
|
|
70
|
+
result = await saver.aget_delta_channel_history(config=head, channels=[CHANNEL])
|
|
71
|
+
entry = result[CHANNEL]
|
|
72
|
+
|
|
73
|
+
assert entry.get("seed") == [10, 20], (
|
|
74
|
+
f"expected the plain value as seed, got {entry.get('seed', '<missing>')}"
|
|
75
|
+
)
|
|
76
|
+
# Only the writes between the seed and the head's parent replay: step 1
|
|
77
|
+
# (the seed's own) and step 2. Step 0 is older than the seed, step 3 is
|
|
78
|
+
# pending at the head.
|
|
79
|
+
assert [w[2] for w in entry["writes"]] == ["w1", "w2"]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
async def test_delta_snapshot_seed_is_found() -> None:
|
|
84
|
+
"""The `_DeltaSnapshot` path keeps working, so both seed kinds agree."""
|
|
85
|
+
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
|
|
86
|
+
await saver.setup()
|
|
87
|
+
_, head = await _build_chain(saver, _DeltaSnapshot([10, 20]))
|
|
88
|
+
|
|
89
|
+
result = await saver.aget_delta_channel_history(config=head, channels=[CHANNEL])
|
|
90
|
+
entry = result[CHANNEL]
|
|
91
|
+
|
|
92
|
+
seed = entry.get("seed")
|
|
93
|
+
assert isinstance(seed, _DeltaSnapshot), f"expected a snapshot, got {seed!r}"
|
|
94
|
+
assert seed.value == [10, 20]
|
|
95
|
+
assert [w[2] for w in entry["writes"]] == ["w1", "w2"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.asyncio
|
|
99
|
+
async def test_version_bump_without_a_value_does_not_hide_an_older_seed() -> None:
|
|
100
|
+
"""A delta-era step bumps `channel_versions` without storing a value, so no
|
|
101
|
+
blob exists for that version. The probe must report no seed there and keep
|
|
102
|
+
walking rather than stopping at a version it cannot resolve.
|
|
103
|
+
|
|
104
|
+
Step 0 holds the real value; step 1 bumps the version with nothing stored.
|
|
105
|
+
Walking back from the head has to pass step 1 to reach step 0.
|
|
106
|
+
"""
|
|
107
|
+
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
|
|
108
|
+
await saver.setup()
|
|
109
|
+
thread_id = str(uuid4())
|
|
110
|
+
parent: dict | None = None
|
|
111
|
+
for step in range(4):
|
|
112
|
+
config: dict = {
|
|
113
|
+
"configurable": {"thread_id": thread_id, "checkpoint_ns": ""}
|
|
114
|
+
}
|
|
115
|
+
if parent is not None:
|
|
116
|
+
config["configurable"]["checkpoint_id"] = parent["configurable"][
|
|
117
|
+
"checkpoint_id"
|
|
118
|
+
]
|
|
119
|
+
cp: Checkpoint = empty_checkpoint()
|
|
120
|
+
cp["id"] = str(uuid6(clock_seq=step))
|
|
121
|
+
new_versions: dict[str, Any] = {}
|
|
122
|
+
if step == 0:
|
|
123
|
+
cp["channel_values"][CHANNEL] = [10, 20]
|
|
124
|
+
cp["channel_versions"][CHANNEL] = "v0"
|
|
125
|
+
new_versions[CHANNEL] = "v0"
|
|
126
|
+
elif step == 1:
|
|
127
|
+
# Version bumped, value absent -> no blob row written.
|
|
128
|
+
cp["channel_versions"][CHANNEL] = "v1"
|
|
129
|
+
new_versions[CHANNEL] = "v1"
|
|
130
|
+
else:
|
|
131
|
+
cp["channel_versions"][CHANNEL] = "v1"
|
|
132
|
+
parent = await saver.aput(
|
|
133
|
+
config,
|
|
134
|
+
cp,
|
|
135
|
+
{"source": "loop", "step": step, "parents": {}},
|
|
136
|
+
new_versions,
|
|
137
|
+
)
|
|
138
|
+
await saver.aput_writes(parent, [(CHANNEL, f"w{step}")], str(uuid4()))
|
|
139
|
+
assert parent is not None
|
|
140
|
+
|
|
141
|
+
result = await saver.aget_delta_channel_history(
|
|
142
|
+
config=parent, channels=[CHANNEL]
|
|
143
|
+
)
|
|
144
|
+
entry = result[CHANNEL]
|
|
145
|
+
|
|
146
|
+
assert entry.get("seed") == [10, 20], (
|
|
147
|
+
"the walk stopped at the empty blob instead of reaching the real "
|
|
148
|
+
f"value at step 0; got {entry.get('seed', '<missing>')}"
|
|
149
|
+
)
|
|
150
|
+
assert [w[2] for w in entry["writes"]] == ["w0", "w1", "w2"]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@pytest.mark.asyncio
|
|
154
|
+
async def test_inline_primitive_seed_is_found() -> None:
|
|
155
|
+
"""`put` keeps `None`, `str`, `int`, `float` and `bool` in the checkpoint's
|
|
156
|
+
own `channel_values` with no blob row, so a blob probe alone cannot see
|
|
157
|
+
them. Stage 1 reads the inline value too and uses it when there is no blob.
|
|
158
|
+
"""
|
|
159
|
+
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
|
|
160
|
+
await saver.setup()
|
|
161
|
+
for seed_value in (42, "x", 3.5, None):
|
|
162
|
+
_, head = await _build_chain(saver, seed_value)
|
|
163
|
+
entry = (
|
|
164
|
+
await saver.aget_delta_channel_history(config=head, channels=[CHANNEL])
|
|
165
|
+
)[CHANNEL]
|
|
166
|
+
if seed_value is None:
|
|
167
|
+
# A JSON null is indistinguishable from "no value stored", so
|
|
168
|
+
# the walk keeps going; replay from empty is the correct result.
|
|
169
|
+
assert "seed" not in entry
|
|
170
|
+
else:
|
|
171
|
+
assert entry.get("seed") == seed_value, (
|
|
172
|
+
f"inline {type(seed_value).__name__} seed not found: "
|
|
173
|
+
f"{entry.get('seed', '<missing>')!r}"
|
|
174
|
+
)
|
|
175
|
+
assert [w[2] for w in entry["writes"]] == ["w1", "w2"]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@pytest.mark.asyncio
|
|
179
|
+
async def test_inline_true_is_not_read_as_a_snapshot_marker() -> None:
|
|
180
|
+
"""`put` inlines a literal `true` in `channel_values` as the marker for a
|
|
181
|
+
`_DeltaSnapshot`, which is also what a genuine `bool` channel holding
|
|
182
|
+
`True` looks like. A blob exists only in the snapshot case, so preferring
|
|
183
|
+
the blob keeps the two apart.
|
|
184
|
+
"""
|
|
185
|
+
async with AsyncPostgresSaver.from_conn_string(DEFAULT_URI) as saver:
|
|
186
|
+
await saver.setup()
|
|
187
|
+
|
|
188
|
+
_, head = await _build_chain(saver, True)
|
|
189
|
+
entry = (
|
|
190
|
+
await saver.aget_delta_channel_history(config=head, channels=[CHANNEL])
|
|
191
|
+
)[CHANNEL]
|
|
192
|
+
assert entry.get("seed") is True, (
|
|
193
|
+
f"a real inline True must survive, got {entry.get('seed', '<missing>')!r}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
_, snap_head = await _build_chain(saver, _DeltaSnapshot(True))
|
|
197
|
+
snap_entry = (
|
|
198
|
+
await saver.aget_delta_channel_history(config=snap_head, channels=[CHANNEL])
|
|
199
|
+
)[CHANNEL]
|
|
200
|
+
seed = snap_entry.get("seed")
|
|
201
|
+
assert isinstance(seed, _DeltaSnapshot), (
|
|
202
|
+
f"the marker must resolve to the blob, not inline true; got {seed!r}"
|
|
203
|
+
)
|
|
204
|
+
assert seed.value is True
|
|
@@ -17,16 +17,16 @@ wheels = [
|
|
|
17
17
|
|
|
18
18
|
[[package]]
|
|
19
19
|
name = "anyio"
|
|
20
|
-
version = "4.14.
|
|
20
|
+
version = "4.14.2"
|
|
21
21
|
source = { registry = "https://pypi.org/simple" }
|
|
22
22
|
dependencies = [
|
|
23
23
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
24
24
|
{ name = "idna" },
|
|
25
25
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
26
26
|
]
|
|
27
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
27
|
+
sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" }
|
|
28
28
|
wheels = [
|
|
29
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
29
|
+
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[[package]]
|
|
@@ -138,11 +138,11 @@ wheels = [
|
|
|
138
138
|
|
|
139
139
|
[[package]]
|
|
140
140
|
name = "codespell"
|
|
141
|
-
version = "2.4.
|
|
141
|
+
version = "2.4.3"
|
|
142
142
|
source = { registry = "https://pypi.org/simple" }
|
|
143
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
143
|
+
sdist = { url = "https://files.pythonhosted.org/packages/80/19/45e941380f69c042b43423513d201e6592346f992394347f5e7174c31407/codespell-2.4.3.tar.gz", hash = "sha256:cbe085e331227b37bb86ef8bddd08dc768c704ee9a07ca869852c093fa2793e2", size = 352773, upload-time = "2026-07-15T11:51:54.159Z" }
|
|
144
144
|
wheels = [
|
|
145
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
145
|
+
{ url = "https://files.pythonhosted.org/packages/8b/bf/bdb951d34eb169140b546f44be9ec4525d1acefb9eb5572071f5492b19fc/codespell-2.4.3-py3-none-any.whl", hash = "sha256:af2505b335e8573dbd2d384d1c4ef498f4006f4ba2d6fceca01e55b91f52628a", size = 340736, upload-time = "2026-07-15T11:51:52.925Z" },
|
|
146
146
|
]
|
|
147
147
|
|
|
148
148
|
[[package]]
|
|
@@ -159,7 +159,7 @@ name = "exceptiongroup"
|
|
|
159
159
|
version = "1.3.1"
|
|
160
160
|
source = { registry = "https://pypi.org/simple" }
|
|
161
161
|
dependencies = [
|
|
162
|
-
{ name = "typing-extensions"
|
|
162
|
+
{ name = "typing-extensions" },
|
|
163
163
|
]
|
|
164
164
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
|
165
165
|
wheels = [
|
|
@@ -244,7 +244,7 @@ wheels = [
|
|
|
244
244
|
|
|
245
245
|
[[package]]
|
|
246
246
|
name = "langchain-core"
|
|
247
|
-
version = "1.
|
|
247
|
+
version = "1.5.2"
|
|
248
248
|
source = { registry = "https://pypi.org/simple" }
|
|
249
249
|
dependencies = [
|
|
250
250
|
{ name = "jsonpatch" },
|
|
@@ -257,9 +257,9 @@ dependencies = [
|
|
|
257
257
|
{ name = "typing-extensions" },
|
|
258
258
|
{ name = "uuid-utils" },
|
|
259
259
|
]
|
|
260
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
260
|
+
sdist = { url = "https://files.pythonhosted.org/packages/6e/58/3ad53096eee1e07728e8219ded30ae308b0f2b8b7b26b8ebb6371917c0f5/langchain_core-1.5.2.tar.gz", hash = "sha256:2d13ab35b42eec63d4669a483776b8cdd778ee764107149369fb369d84c08c41", size = 972322, upload-time = "2026-07-28T16:38:37.977Z" }
|
|
261
261
|
wheels = [
|
|
262
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
262
|
+
{ url = "https://files.pythonhosted.org/packages/ca/e4/024e402a65f5ee2eb6ae9667b5ffc5f08776cb4e712f5a11ee1997d56083/langchain_core-1.5.2-py3-none-any.whl", hash = "sha256:a687dd7c3b22c6c1294e1c1eeb61fb6f3a308e6015a1d75b576b3836ad5b5aed", size = 561643, upload-time = "2026-07-28T16:38:36.38Z" },
|
|
263
263
|
]
|
|
264
264
|
|
|
265
265
|
[[package]]
|
|
@@ -276,7 +276,7 @@ wheels = [
|
|
|
276
276
|
|
|
277
277
|
[[package]]
|
|
278
278
|
name = "langgraph-checkpoint"
|
|
279
|
-
version = "4.
|
|
279
|
+
version = "4.2.0"
|
|
280
280
|
source = { editable = "../checkpoint" }
|
|
281
281
|
dependencies = [
|
|
282
282
|
{ name = "langchain-core" },
|
|
@@ -322,9 +322,36 @@ test = [
|
|
|
322
322
|
{ name = "redis" },
|
|
323
323
|
]
|
|
324
324
|
|
|
325
|
+
[[package]]
|
|
326
|
+
name = "langgraph-checkpoint-conformance"
|
|
327
|
+
version = "0.0.2"
|
|
328
|
+
source = { editable = "../checkpoint-conformance" }
|
|
329
|
+
dependencies = [
|
|
330
|
+
{ name = "langgraph-checkpoint" },
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
[package.metadata]
|
|
334
|
+
requires-dist = [{ name = "langgraph-checkpoint", editable = "../checkpoint" }]
|
|
335
|
+
|
|
336
|
+
[package.metadata.requires-dev]
|
|
337
|
+
dev = [
|
|
338
|
+
{ name = "pytest" },
|
|
339
|
+
{ name = "pytest-asyncio" },
|
|
340
|
+
{ name = "ruff" },
|
|
341
|
+
{ name = "ty" },
|
|
342
|
+
]
|
|
343
|
+
lint = [
|
|
344
|
+
{ name = "ruff" },
|
|
345
|
+
{ name = "ty" },
|
|
346
|
+
]
|
|
347
|
+
test = [
|
|
348
|
+
{ name = "pytest" },
|
|
349
|
+
{ name = "pytest-asyncio" },
|
|
350
|
+
]
|
|
351
|
+
|
|
325
352
|
[[package]]
|
|
326
353
|
name = "langgraph-checkpoint-postgres"
|
|
327
|
-
version = "3.1.
|
|
354
|
+
version = "3.1.2"
|
|
328
355
|
source = { editable = "." }
|
|
329
356
|
dependencies = [
|
|
330
357
|
{ name = "langgraph-checkpoint" },
|
|
@@ -338,6 +365,7 @@ dev = [
|
|
|
338
365
|
{ name = "anyio" },
|
|
339
366
|
{ name = "codespell" },
|
|
340
367
|
{ name = "langgraph-checkpoint" },
|
|
368
|
+
{ name = "langgraph-checkpoint-conformance" },
|
|
341
369
|
{ name = "psycopg", extra = ["binary"] },
|
|
342
370
|
{ name = "pytest" },
|
|
343
371
|
{ name = "pytest-asyncio" },
|
|
@@ -354,6 +382,7 @@ lint = [
|
|
|
354
382
|
test = [
|
|
355
383
|
{ name = "anyio" },
|
|
356
384
|
{ name = "langgraph-checkpoint" },
|
|
385
|
+
{ name = "langgraph-checkpoint-conformance" },
|
|
357
386
|
{ name = "psycopg", extra = ["binary"] },
|
|
358
387
|
{ name = "pytest" },
|
|
359
388
|
{ name = "pytest-asyncio" },
|
|
@@ -374,6 +403,7 @@ dev = [
|
|
|
374
403
|
{ name = "anyio" },
|
|
375
404
|
{ name = "codespell" },
|
|
376
405
|
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
|
406
|
+
{ name = "langgraph-checkpoint-conformance", editable = "../checkpoint-conformance" },
|
|
377
407
|
{ name = "psycopg", extras = ["binary"] },
|
|
378
408
|
{ name = "pytest" },
|
|
379
409
|
{ name = "pytest-asyncio" },
|
|
@@ -390,6 +420,7 @@ lint = [
|
|
|
390
420
|
test = [
|
|
391
421
|
{ name = "anyio" },
|
|
392
422
|
{ name = "langgraph-checkpoint", editable = "../checkpoint" },
|
|
423
|
+
{ name = "langgraph-checkpoint-conformance", editable = "../checkpoint-conformance" },
|
|
393
424
|
{ name = "psycopg", extras = ["binary"] },
|
|
394
425
|
{ name = "pytest" },
|
|
395
426
|
{ name = "pytest-asyncio" },
|
|
@@ -957,27 +988,27 @@ wheels = [
|
|
|
957
988
|
|
|
958
989
|
[[package]]
|
|
959
990
|
name = "ruff"
|
|
960
|
-
version = "0.
|
|
991
|
+
version = "0.16.0"
|
|
961
992
|
source = { registry = "https://pypi.org/simple" }
|
|
962
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
993
|
+
sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" }
|
|
963
994
|
wheels = [
|
|
964
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
965
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
966
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
967
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
968
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
969
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
970
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
971
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
972
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
973
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
974
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
975
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
976
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
977
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
978
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
979
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
980
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
995
|
+
{ url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" },
|
|
996
|
+
{ url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" },
|
|
997
|
+
{ url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" },
|
|
998
|
+
{ url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" },
|
|
999
|
+
{ url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" },
|
|
1000
|
+
{ url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" },
|
|
1001
|
+
{ url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" },
|
|
1002
|
+
{ url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" },
|
|
1003
|
+
{ url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" },
|
|
1004
|
+
{ url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" },
|
|
1005
|
+
{ url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" },
|
|
1006
|
+
{ url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" },
|
|
1007
|
+
{ url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" },
|
|
1008
|
+
{ url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" },
|
|
1009
|
+
{ url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" },
|
|
1010
|
+
{ url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" },
|
|
1011
|
+
{ url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" },
|
|
981
1012
|
]
|
|
982
1013
|
|
|
983
1014
|
[[package]]
|
|
@@ -1045,27 +1076,27 @@ wheels = [
|
|
|
1045
1076
|
|
|
1046
1077
|
[[package]]
|
|
1047
1078
|
name = "ty"
|
|
1048
|
-
version = "0.0.
|
|
1079
|
+
version = "0.0.64"
|
|
1049
1080
|
source = { registry = "https://pypi.org/simple" }
|
|
1050
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1081
|
+
sdist = { url = "https://files.pythonhosted.org/packages/8e/aa/14c9965d3b173105692473897cc89c34cd91241368b2044e43167e1c17ff/ty-0.0.64.tar.gz", hash = "sha256:d12ddbb05f15158bb518af619378b385486450def95fb06f8ab98037febe9f2c", size = 6350966, upload-time = "2026-07-27T18:32:45.403Z" }
|
|
1051
1082
|
wheels = [
|
|
1052
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1053
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1054
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1055
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1056
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1057
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1058
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1059
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1060
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1061
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1062
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1063
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1064
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1065
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1066
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1067
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1068
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1083
|
+
{ url = "https://files.pythonhosted.org/packages/ae/4c/c54937e4ff3fa7b34a99ea3387ec766bf0ad98dc8df8d792e89b388e658e/ty-0.0.64-py3-none-linux_armv6l.whl", hash = "sha256:3830a6675ab43635ced1c4c557f380ac4a49e9414e03975e4e4e8db644c64944", size = 12118357, upload-time = "2026-07-27T18:32:08.702Z" },
|
|
1084
|
+
{ url = "https://files.pythonhosted.org/packages/ce/aa/a839ee2bc78e943d079e6abe199a97b4eeffb7e5c9a57326d69de452186a/ty-0.0.64-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff07d7bc32a2135f58afe57393789a32ea2fed1a66216129a8e535e76043903", size = 11790882, upload-time = "2026-07-27T18:32:10.997Z" },
|
|
1085
|
+
{ url = "https://files.pythonhosted.org/packages/4c/de/19f14357888a7198438926303753cf749428e3d62e8980ff1e9a72a78402/ty-0.0.64-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f6d1c7f897cca05d12bacbf1435150d5ffa496099515aa6ed303c8b29e1d0bb", size = 11317394, upload-time = "2026-07-27T18:32:13.162Z" },
|
|
1086
|
+
{ url = "https://files.pythonhosted.org/packages/08/2f/f54462300535ab99b551eda733177be2eef5dbc2997d3fdb357c4ddd760a/ty-0.0.64-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:138d6c37ad4bf8583aa7a9b29d90954151d7856f9910a03eae3eb34b34c57215", size = 11863042, upload-time = "2026-07-27T18:32:15.307Z" },
|
|
1087
|
+
{ url = "https://files.pythonhosted.org/packages/5e/95/dbecf745520ebe8bd7b02fc55eee6441c9be312ebf6addce605eb52740dd/ty-0.0.64-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ed9719d1b7b66fb8efe073d860208a44c40af1f6cd5c2364aa9b323a1e579b4", size = 11910730, upload-time = "2026-07-27T18:32:17.467Z" },
|
|
1088
|
+
{ url = "https://files.pythonhosted.org/packages/3b/26/12cfd40028e51ceed7b3cb645281c61c02eb64ff9fb0c09231d65c30ff25/ty-0.0.64-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68b23e5169e2137b5f1de7169ab0cebecab6c8eda374c34c1f6394308f58242", size = 12631936, upload-time = "2026-07-27T18:32:19.533Z" },
|
|
1089
|
+
{ url = "https://files.pythonhosted.org/packages/a0/d0/65ffc2b0a686347193c6f98e9421a7fc2a96fc3cd0b1001cf7cf284baff9/ty-0.0.64-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f41cb07d89d32626fcaf3ed4d262778fcb28b2628b6ed1e172cd7b18820668d8", size = 13171049, upload-time = "2026-07-27T18:32:22.026Z" },
|
|
1090
|
+
{ url = "https://files.pythonhosted.org/packages/f6/a4/975a5961842dcd6fa60f0770a102c0bba7509da909ab652919bfcdcd4fc7/ty-0.0.64-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f24e1504ab9e212f92356b82fe088fdeb3a39f9a2f4ff25e505d2e1d0db9056", size = 12826438, upload-time = "2026-07-27T18:32:24.178Z" },
|
|
1091
|
+
{ url = "https://files.pythonhosted.org/packages/af/ef/dfb9b7f9bcc032d3b540b0d1f55f532a336e2fb41b1bd539c05ae81a151a/ty-0.0.64-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86db830cb914bb33bb8247b66ccea58de4496c2391bd42658aba744b439f3290", size = 12440880, upload-time = "2026-07-27T18:32:26.341Z" },
|
|
1092
|
+
{ url = "https://files.pythonhosted.org/packages/ee/d0/bedac20505e8a8f5501ad73d7d15d8e421a563fef59993909a23036929a4/ty-0.0.64-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:652ee3d6d03bea76cd2fe8949c78bb5970394ebd7c5fd270e9518c0dee1b931d", size = 12782439, upload-time = "2026-07-27T18:32:28.642Z" },
|
|
1093
|
+
{ url = "https://files.pythonhosted.org/packages/79/d5/795733f13ceff1378f08b3de0c49d0f518df220ed856b0dfac869f3b7c81/ty-0.0.64-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4838768295774a86e95f9ec5633e739d016adbbb69dcbdc62f3549578a11f624", size = 11814821, upload-time = "2026-07-27T18:32:30.632Z" },
|
|
1094
|
+
{ url = "https://files.pythonhosted.org/packages/52/3e/9d99cd1e1831003434f508ed9f258a56543194afc3bbe051eba2545fa676/ty-0.0.64-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5a3d700669868599edf39ce5125196682f15a8880cc8c669bc2a83b99984d99b", size = 11928678, upload-time = "2026-07-27T18:32:32.888Z" },
|
|
1095
|
+
{ url = "https://files.pythonhosted.org/packages/b8/3d/448f49a3503fb119a34348a5714bb92001f252fadbbb12d645f2e744b557/ty-0.0.64-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b161f0a82a8e2f2432db3bf7702b4d3924fa9486ba0014f6710a160fc157df0d", size = 12202249, upload-time = "2026-07-27T18:32:34.905Z" },
|
|
1096
|
+
{ url = "https://files.pythonhosted.org/packages/dd/9b/75768e562cec990d189dc05807ae72890b20ffbd1e1f43597bb98db63c60/ty-0.0.64-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:39b9dd42908df47c2dc57dda87e656fab97097ffd2618474bbdea986af0d6a9d", size = 12548817, upload-time = "2026-07-27T18:32:36.995Z" },
|
|
1097
|
+
{ url = "https://files.pythonhosted.org/packages/34/89/44cc276ea6ca0245495014758ffeadde1798fb0b5840c9cf36d8d2ed3250/ty-0.0.64-py3-none-win32.whl", hash = "sha256:d0676ab0e0935795e5843baa28dd5e366dc343d7c0945a996df9eab8e0644885", size = 11545474, upload-time = "2026-07-27T18:32:39.389Z" },
|
|
1098
|
+
{ url = "https://files.pythonhosted.org/packages/01/7e/d1c8a871a38d17c8f168b9a6975f6247f7660f8334e517656a5e4b4a4858/ty-0.0.64-py3-none-win_amd64.whl", hash = "sha256:dcb9bd31f54097e362b776c26ab4564d4564cdd1355cb883481167a26c03cc3f", size = 12542987, upload-time = "2026-07-27T18:32:41.412Z" },
|
|
1099
|
+
{ url = "https://files.pythonhosted.org/packages/35/4d/6d18640d0204cacd69abbaca95ad6a34c6d7e9169e9051d0117b17b827ec/ty-0.0.64-py3-none-win_arm64.whl", hash = "sha256:82cc34c1ad9a8feb6059aef193bebcecc656e548f6fab3d518bcd8b57d198d39", size = 11899263, upload-time = "2026-07-27T18:32:43.366Z" },
|
|
1069
1100
|
]
|
|
1070
1101
|
|
|
1071
1102
|
[[package]]
|
|
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.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/conftest.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/test_store.py
RENAMED
|
File without changes
|
{langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/test_sync.py
RENAMED
|
File without changes
|