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.
Files changed (28) hide show
  1. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/PKG-INFO +9 -33
  2. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/README.md +8 -32
  3. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/__init__.py +12 -5
  4. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/aio.py +12 -5
  5. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/base.py +90 -20
  6. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/shallow.py +2 -2
  7. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/pyproject.toml +5 -1
  8. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/test_async.py +9 -7
  9. langgraph_checkpoint_postgres-3.1.2/tests/test_conformance_delta.py +30 -0
  10. langgraph_checkpoint_postgres-3.1.2/tests/test_delta_plain_value_seed.py +204 -0
  11. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/uv.lock +81 -50
  12. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/.gitignore +0 -0
  13. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/LICENSE +0 -0
  14. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/Makefile +0 -0
  15. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/_ainternal.py +0 -0
  16. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/_internal.py +0 -0
  17. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/checkpoint/postgres/py.typed +0 -0
  18. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/store/postgres/__init__.py +0 -0
  19. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/store/postgres/aio.py +0 -0
  20. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/store/postgres/base.py +0 -0
  21. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/langgraph/store/postgres/py.typed +0 -0
  22. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/__init__.py +0 -0
  23. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/compose-postgres.yml +0 -0
  24. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/conftest.py +0 -0
  25. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/embed_test_utils.py +0 -0
  26. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/test_async_store.py +0 -0
  27. {langgraph_checkpoint_postgres-3.1.1 → langgraph_checkpoint_postgres-3.1.2}/tests/test_store.py +0 -0
  28. {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.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
- "my_key": "meow",
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
- "__start__": 1
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
- "my_key": "meow",
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
- "__start__": 1
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
- "my_key": "meow",
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
- "__start__": 1
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
- "my_key": "meow",
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
- "__start__": 1
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
- """ # noqa
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
- hs_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
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
- stage1_params.extend([ch, ch])
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
- hs_by_i_by_cid,
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
- hs_by_i_by_cid,
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
- hs_by_i_by_cid: list[dict[str, bool]] = [{} for _ in channels]
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
- stage1_params.extend([ch, ch])
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
- hs_by_i_by_cid,
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
- hs_by_i_by_cid,
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] # noqa: F821
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 2K parallel JSONB key lookups.
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
- (checkpoint -> 'channel_values' -> %s) IS NOT NULL AS hs_0,
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
- (checkpoint -> 'channel_values' -> %s) IS NOT NULL AS hs_1
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` / `hs_i` are interpolated into the
219
- SQL string (i is bounded by len(channels) and uses safe identifiers).
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, ch_0, ch_1, ch_1, ...,
222
- thread_id, ns, cursor, cursor, page_size]` when `paged=True`.
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"(checkpoint -> 'channel_values' -> %s) IS NOT NULL AS hs_{i}"
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
- hs_by_i_by_cid: list[dict[str, bool]],
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
- hs_by_i_by_cid[i][cid] = bool(r.get(f"hs_{i}"))
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
- hs_by_i_by_cid: Sequence[Mapping[str, bool]],
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 snapshot for its channel (channel becomes seeded),
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`, `walk_cursor_by_ch`, and
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
- hs_i = hs_by_i_by_cid[i]
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
- if hs_i.get(cur_cid, False):
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. The `seed` key is omitted
425
- when the walk reached root with no snapshot found, or when the
426
- seed blob is sentinel "empty" — in both cases the consumer treats
427
- absence as "start empty".
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
- """ # noqa
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] # noqa: F821
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.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"]
@@ -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
- from typing import Annotated
384
-
385
- from langchain_core.messages import AIMessage, HumanMessage
386
- from langgraph.channels.delta import DeltaChannel
387
- from langgraph.graph import START, StateGraph
388
- from langgraph.graph.message import _messages_delta_reducer
389
- from typing_extensions import TypedDict
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.1"
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/3b/72/5562aabb8dd7181e8e860622a38bea08d17842b99ecd4c91f84ac95251b0/anyio-4.14.1.tar.gz", hash = "sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e", size = 254831, upload-time = "2026-06-24T20:56:06.017Z" }
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/b0/7b/90df4a0a816d98d6ea26f559d87836d494a2cf1fcf063be67df50a7bcc30/anyio-4.14.1-py3-none-any.whl", hash = "sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72", size = 124875, upload-time = "2026-06-24T20:56:04.413Z" },
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.2"
141
+ version = "2.4.3"
142
142
  source = { registry = "https://pypi.org/simple" }
143
- sdist = { url = "https://files.pythonhosted.org/packages/2d/9d/1d0903dff693160f893ca6abcabad545088e7a2ee0a6deae7c24e958be69/codespell-2.4.2.tar.gz", hash = "sha256:3c33be9ae34543807f088aeb4832dfad8cb2dae38da61cac0a7045dd376cfdf3", size = 352058, upload-time = "2026-03-05T18:10:42.936Z" }
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/42/a1/52fa05533e95fe45bcc09bcf8a503874b1c08f221a4e35608017e0938f55/codespell-2.4.2-py3-none-any.whl", hash = "sha256:97e0c1060cf46bd1d5db89a936c98db8c2b804e1fdd4b5c645e82a1ec6b1f886", size = 353715, upload-time = "2026-03-05T18:10:41.398Z" },
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", marker = "python_full_version < '3.13'" },
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.4.8"
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/12/e3/bea6d0080acf183332f24dcd74c208aee5857cf8f783c3fb0bd86027d8fb/langchain_core-1.4.8.tar.gz", hash = "sha256:5bf1f8411077c904182ad8f975943d36adcbf579c4e017b3a118b719229ebf9a", size = 957974, upload-time = "2026-06-18T19:39:23.636Z" }
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/13/d6/bdf6f0481cc57ef300d6b1eb48cf1400c0409be715d6eb3cabadd1142a09/langchain_core-1.4.8-py3-none-any.whl", hash = "sha256:d84c28b05e3ba8d4271d0827aad5b592ccdaaf986e76768c23503f0a2045e8aa", size = 557416, upload-time = "2026-06-18T19:39:21.902Z" },
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.1.1"
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.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.15.20"
991
+ version = "0.16.0"
961
992
  source = { registry = "https://pypi.org/simple" }
962
- sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" }
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/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" },
965
- { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" },
966
- { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" },
967
- { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" },
968
- { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" },
969
- { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" },
970
- { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" },
971
- { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" },
972
- { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" },
973
- { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" },
974
- { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" },
975
- { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" },
976
- { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" },
977
- { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" },
978
- { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" },
979
- { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" },
980
- { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" },
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.55"
1079
+ version = "0.0.64"
1049
1080
  source = { registry = "https://pypi.org/simple" }
1050
- sdist = { url = "https://files.pythonhosted.org/packages/08/48/f687c8d268e3581f2f104d1f2ac5944d5b5e841b3695c613b3f263e5bbf7/ty-0.0.55.tar.gz", hash = "sha256:88ca87073825a79a8327c550efcc86cec94344890244c5946f84c9e44a969f31", size = 6040230, upload-time = "2026-06-27T00:27:29.385Z" }
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/87/a3/1a90ba7e5a61c6d09adb92346ddba97668095fc257b577af433e5ac4f404/ty-0.0.55-py3-none-linux_armv6l.whl", hash = "sha256:31e83eef512d066542fe990fe1a3b814423abd1616376c54e48af7045b3e1749", size = 11677249, upload-time = "2026-06-27T00:26:52.18Z" },
1053
- { url = "https://files.pythonhosted.org/packages/82/3a/669f9aa478c38243e213a2684db1502086026cfadc15bb1b29b7cbde030d/ty-0.0.55-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab4bca857950608fea73e269e2da369d43e6467131de85160d68e2fa466fa248", size = 11444180, upload-time = "2026-06-27T00:26:54.576Z" },
1054
- { url = "https://files.pythonhosted.org/packages/15/a4/6a4b2507a53ce6530c66c5b4fe0d58551eb1748ffa9e0696c32fdd55bbd4/ty-0.0.55-py3-none-macosx_11_0_arm64.whl", hash = "sha256:55032bfd31bf2c5355ee81bdc6407b144a1cc7ee41e5681dd1368e4cef2ba327", size = 10963134, upload-time = "2026-06-27T00:26:57.348Z" },
1055
- { url = "https://files.pythonhosted.org/packages/ce/ae/a3b1a0f1cc83b7d258662cb98aa80a720c2e671d0e8fa0d17a4d5d057a7a/ty-0.0.55-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef1e049f69ce65b3c269af67624607f435e1c32319786c1e453ef9611502f295", size = 11493517, upload-time = "2026-06-27T00:26:59.26Z" },
1056
- { url = "https://files.pythonhosted.org/packages/0d/9f/311ce39065a979ef40a9b847f685c8e02464e53adf1671e081eea90640ca/ty-0.0.55-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:631409975c681d5a280fc5a99b7b32e9e801f33be7567c6b42ec331362f59d7d", size = 11460590, upload-time = "2026-06-27T00:27:01.425Z" },
1057
- { url = "https://files.pythonhosted.org/packages/cd/8f/3bf29aa77bd78aae48275153135a2052fa7d3ccdf1ecabeb99c8773abd66/ty-0.0.55-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e08cb0436e68b9351555ae8f2697138c9009b4d5b4ae4272232988b2a431a98f", size = 12098430, upload-time = "2026-06-27T00:27:03.596Z" },
1058
- { url = "https://files.pythonhosted.org/packages/bc/6e/e88411a88240b94640bba06fb6d0d92b247fbeef47ee2bc71f39e58c2558/ty-0.0.55-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16c215ad9f823829409b94ee188cfaa4563f6e1384f6ce3fecb1db75f6c7cf7c", size = 12673086, upload-time = "2026-06-27T00:27:05.589Z" },
1059
- { url = "https://files.pythonhosted.org/packages/6c/7e/8f1762fb7f9245a68ba5ae338d73c59403ce57554e5d311b8bb55027b0ec/ty-0.0.55-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b510eb8f4032baf11b7aee2f1d53babc3b4ca03939b9cdcf6a9d15761d575188", size = 12242559, upload-time = "2026-06-27T00:27:07.714Z" },
1060
- { url = "https://files.pythonhosted.org/packages/72/1f/143657daf2670d977dac83435f1fe03d4843efb798d8e1e75950e541aadd/ty-0.0.55-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ddc05e7959709c3b9b83aa627128a80446865e3c1a4882638dcff6d776dc34a", size = 12021409, upload-time = "2026-06-27T00:27:09.881Z" },
1061
- { url = "https://files.pythonhosted.org/packages/6d/30/69487c439dd1fad3a4a3d96f0a472193de297eaba6fc4b8ea687ce434ac2/ty-0.0.55-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:636e8e5078787b8c6916c94e1406719f10189a4ca6b37b813a5922ce5857a8c7", size = 12303807, upload-time = "2026-06-27T00:27:11.986Z" },
1062
- { url = "https://files.pythonhosted.org/packages/e8/ca/cd88b6493dafc7db077f5e17c0438eb3af6e2d6d08f616dbb52a8ddfd567/ty-0.0.55-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ef7d6deaacb73fec603666b5471f1dc5a5699aa84e11a6d4d644dd07ca72121e", size = 11441263, upload-time = "2026-06-27T00:27:14.087Z" },
1063
- { url = "https://files.pythonhosted.org/packages/aa/fe/66b6915671653ab739f71e4f1b0528e69da64429b7ebf3840c625b6e43f2/ty-0.0.55-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9aeea0fe5875d3cf37faf0e44d0fdf9669335467749741b8fc0103916fb5cd32", size = 11484584, upload-time = "2026-06-27T00:27:16.311Z" },
1064
- { url = "https://files.pythonhosted.org/packages/4a/4f/7a9c0bbac8b899e9f6c0ec110c6612f52e4db35f6bb17ddc0ef60384fa3e/ty-0.0.55-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0b699c01310dbd2705a07c97c5f4aaeedef61bd9adeea2e7c46aed32401d3576", size = 11759309, upload-time = "2026-06-27T00:27:18.471Z" },
1065
- { url = "https://files.pythonhosted.org/packages/ca/de/b6f8b1b69aa631b5716ef3f985c3b56de0e46c2499cc00d30c402b41f714/ty-0.0.55-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:32cbeba543e46de2a983ec6d525d8b56514f7422bd1e1b57c44ccf7bfa72c38a", size = 12128755, upload-time = "2026-06-27T00:27:20.55Z" },
1066
- { url = "https://files.pythonhosted.org/packages/7d/90/a912531e51ee7e076b42972479290fa687c0f5e747b7e773f3033164acaa/ty-0.0.55-py3-none-win32.whl", hash = "sha256:52b968e24eb4f7a5c3bd251db1f99f60dd385890356d38fc619d84f1b423446a", size = 11117501, upload-time = "2026-06-27T00:27:22.714Z" },
1067
- { url = "https://files.pythonhosted.org/packages/4c/7a/99d59843bf8908a7f9f4d13fda107dbad07b7faa28ecd7860eacf363fb1c/ty-0.0.55-py3-none-win_amd64.whl", hash = "sha256:bf39cbfdc0add44d94bd3fff1f53c351418d134b6a66b87efdb7876d7b7a2224", size = 12150106, upload-time = "2026-06-27T00:27:24.881Z" },
1068
- { url = "https://files.pythonhosted.org/packages/b3/44/20987505cedf2a865b08482f0eabc181fd9599b062964057ec8a128a4296/ty-0.0.55-py3-none-win_arm64.whl", hash = "sha256:f7f3700a9a060e8f1af11e4fb63fafcaf272b041781f4ccdfda2b3b5c6c1e439", size = 11560157, upload-time = "2026-06-27T00:27:27.332Z" },
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]]