langgraph-checkpoint-postgres 1.0.7__tar.gz → 1.0.8__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langgraph-checkpoint-postgres
3
- Version: 1.0.7
3
+ Version: 1.0.8
4
4
  Summary: Library with a Postgres implementation of LangGraph checkpoint saver.
5
5
  Home-page: https://www.github.com/langchain-ai/langgraph
6
6
  License: MIT
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3.9
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
- Requires-Dist: langgraph-checkpoint (>=1.0.8,<2.0.0)
14
+ Requires-Dist: langgraph-checkpoint (>=1.0.11,<2.0.0)
15
15
  Requires-Dist: orjson (>=3.10.1)
16
16
  Requires-Dist: psycopg (>=3.0.0,<4.0.0)
17
17
  Requires-Dist: psycopg-pool (>=3.0.0,<4.0.0)
@@ -1,11 +1,11 @@
1
1
  import threading
2
2
  from contextlib import contextmanager
3
- from typing import Any, Iterator, List, Optional, Union
3
+ from typing import Any, Iterator, Optional, Sequence, Union
4
4
 
5
5
  from langchain_core.runnables import RunnableConfig
6
6
  from psycopg import Connection, Cursor, Pipeline
7
7
  from psycopg.errors import UndefinedTable
8
- from psycopg.rows import dict_row
8
+ from psycopg.rows import DictRow, dict_row
9
9
  from psycopg.types.json import Jsonb
10
10
  from psycopg_pool import ConnectionPool
11
11
 
@@ -22,9 +22,11 @@ from langgraph.checkpoint.postgres.base import (
22
22
  )
23
23
  from langgraph.checkpoint.serde.base import SerializerProtocol
24
24
 
25
+ Conn = Union[Connection[DictRow], ConnectionPool[Connection[DictRow]]]
26
+
25
27
 
26
28
  @contextmanager
27
- def _get_connection(conn: Union[Connection, ConnectionPool]) -> Iterator[Connection]:
29
+ def _get_connection(conn: Conn) -> Iterator[Connection[DictRow]]:
28
30
  if isinstance(conn, Connection):
29
31
  yield conn
30
32
  elif isinstance(conn, ConnectionPool):
@@ -39,7 +41,7 @@ class PostgresSaver(BasePostgresSaver):
39
41
 
40
42
  def __init__(
41
43
  self,
42
- conn: Union[Connection, ConnectionPool],
44
+ conn: Conn,
43
45
  pipe: Optional[Pipeline] = None,
44
46
  serde: Optional[SerializerProtocol] = None,
45
47
  ) -> None:
@@ -85,9 +87,13 @@ class PostgresSaver(BasePostgresSaver):
85
87
  """
86
88
  with self._cursor() as cur:
87
89
  try:
88
- version = cur.execute(
90
+ row = cur.execute(
89
91
  "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
90
- ).fetchone()["v"]
92
+ ).fetchone()
93
+ if row is None:
94
+ version = -1
95
+ else:
96
+ version = row["v"]
91
97
  except UndefinedTable:
92
98
  version = -1
93
99
  for v, migration in zip(
@@ -212,7 +218,7 @@ class PostgresSaver(BasePostgresSaver):
212
218
  checkpoint_id = get_checkpoint_id(config)
213
219
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
214
220
  if checkpoint_id:
215
- args = (thread_id, checkpoint_ns, checkpoint_id)
221
+ args: tuple[Any, ...] = (thread_id, checkpoint_ns, checkpoint_id)
216
222
  where = "WHERE thread_id = %s AND checkpoint_ns = %s AND checkpoint_id = %s"
217
223
  else:
218
224
  args = (thread_id, checkpoint_ns)
@@ -306,7 +312,7 @@ class PostgresSaver(BasePostgresSaver):
306
312
  self._dump_blobs(
307
313
  thread_id,
308
314
  checkpoint_ns,
309
- copy.pop("channel_values"),
315
+ copy.pop("channel_values"), # type: ignore[misc]
310
316
  new_versions,
311
317
  ),
312
318
  )
@@ -326,7 +332,7 @@ class PostgresSaver(BasePostgresSaver):
326
332
  def put_writes(
327
333
  self,
328
334
  config: RunnableConfig,
329
- writes: List[tuple[str, Any]],
335
+ writes: Sequence[tuple[str, Any]],
330
336
  task_id: str,
331
337
  ) -> None:
332
338
  """Store intermediate writes linked to a checkpoint.
@@ -356,7 +362,7 @@ class PostgresSaver(BasePostgresSaver):
356
362
  )
357
363
 
358
364
  @contextmanager
359
- def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor]:
365
+ def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor[DictRow]]:
360
366
  with _get_connection(self.conn) as conn:
361
367
  if self.pipe:
362
368
  # a connection in pipeline mode can be used concurrently
@@ -1,11 +1,11 @@
1
1
  import asyncio
2
2
  from contextlib import asynccontextmanager
3
- from typing import Any, AsyncIterator, Iterator, List, Optional, Union
3
+ from typing import Any, AsyncIterator, Iterator, Optional, Sequence, Union
4
4
 
5
5
  from langchain_core.runnables import RunnableConfig
6
6
  from psycopg import AsyncConnection, AsyncCursor, AsyncPipeline
7
7
  from psycopg.errors import UndefinedTable
8
- from psycopg.rows import dict_row
8
+ from psycopg.rows import DictRow, dict_row
9
9
  from psycopg.types.json import Jsonb
10
10
  from psycopg_pool import AsyncConnectionPool
11
11
 
@@ -20,11 +20,13 @@ from langgraph.checkpoint.base import (
20
20
  from langgraph.checkpoint.postgres.base import BasePostgresSaver
21
21
  from langgraph.checkpoint.serde.base import SerializerProtocol
22
22
 
23
+ Conn = Union[AsyncConnection[DictRow], AsyncConnectionPool[AsyncConnection[DictRow]]]
24
+
23
25
 
24
26
  @asynccontextmanager
25
27
  async def _get_connection(
26
- conn: Union[AsyncConnection, AsyncConnectionPool],
27
- ) -> AsyncIterator[AsyncConnection]:
28
+ conn: Conn,
29
+ ) -> AsyncIterator[AsyncConnection[DictRow]]:
28
30
  if isinstance(conn, AsyncConnection):
29
31
  yield conn
30
32
  elif isinstance(conn, AsyncConnectionPool):
@@ -39,7 +41,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
39
41
 
40
42
  def __init__(
41
43
  self,
42
- conn: Union[AsyncConnection, AsyncConnectionPool],
44
+ conn: Conn,
43
45
  pipe: Optional[AsyncPipeline] = None,
44
46
  serde: Optional[SerializerProtocol] = None,
45
47
  ) -> None:
@@ -93,7 +95,11 @@ class AsyncPostgresSaver(BasePostgresSaver):
93
95
  results = await cur.execute(
94
96
  "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
95
97
  )
96
- version = (await results.fetchone())["v"]
98
+ row = await results.fetchone()
99
+ if row is None:
100
+ version = -1
101
+ else:
102
+ version = row["v"]
97
103
  except UndefinedTable:
98
104
  version = -1
99
105
  for v, migration in zip(
@@ -180,7 +186,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
180
186
  checkpoint_id = get_checkpoint_id(config)
181
187
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
182
188
  if checkpoint_id:
183
- args = (thread_id, checkpoint_ns, checkpoint_id)
189
+ args: tuple[Any, ...] = (thread_id, checkpoint_ns, checkpoint_id)
184
190
  where = "WHERE thread_id = %s AND checkpoint_ns = %s AND checkpoint_id = %s"
185
191
  else:
186
192
  args = (thread_id, checkpoint_ns)
@@ -265,7 +271,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
265
271
  self._dump_blobs,
266
272
  thread_id,
267
273
  checkpoint_ns,
268
- copy.pop("channel_values"),
274
+ copy.pop("channel_values"), # type: ignore[misc]
269
275
  new_versions,
270
276
  ),
271
277
  )
@@ -285,7 +291,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
285
291
  async def aput_writes(
286
292
  self,
287
293
  config: RunnableConfig,
288
- writes: list[tuple[str, Any]],
294
+ writes: Sequence[tuple[str, Any]],
289
295
  task_id: str,
290
296
  ) -> None:
291
297
  """Store intermediate writes linked to a checkpoint asynchronously.
@@ -314,7 +320,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
314
320
  await cur.executemany(query, params)
315
321
 
316
322
  @asynccontextmanager
317
- async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[AsyncCursor]:
323
+ async def _cursor(
324
+ self, *, pipeline: bool = False
325
+ ) -> AsyncIterator[AsyncCursor[DictRow]]:
318
326
  async with _get_connection(self.conn) as conn:
319
327
  if self.pipe:
320
328
  # a connection in pipeline mode can be used concurrently
@@ -365,7 +373,8 @@ class AsyncPostgresSaver(BasePostgresSaver):
365
373
  while True:
366
374
  try:
367
375
  yield asyncio.run_coroutine_threadsafe(
368
- anext(aiter_), self.loop
376
+ anext(aiter_),
377
+ self.loop,
369
378
  ).result()
370
379
  except StopAsyncIteration:
371
380
  break
@@ -384,6 +393,18 @@ class AsyncPostgresSaver(BasePostgresSaver):
384
393
  Returns:
385
394
  Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
386
395
  """
396
+ try:
397
+ # check if we are in the main thread, only bg threads can block
398
+ # we don't check in other methods to avoid the overhead
399
+ if asyncio.get_running_loop() is self.loop:
400
+ raise asyncio.InvalidStateError(
401
+ "Synchronous calls to AsyncPostgresSaver are only allowed from a "
402
+ "different thread. From the main thread, use the async interface."
403
+ "For example, use `await checkpointer.aget_tuple(...)` or `await "
404
+ "graph.ainvoke(...)`."
405
+ )
406
+ except RuntimeError:
407
+ pass
387
408
  return asyncio.run_coroutine_threadsafe(
388
409
  self.aget_tuple(config), self.loop
389
410
  ).result()
@@ -416,7 +437,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
416
437
  def put_writes(
417
438
  self,
418
439
  config: RunnableConfig,
419
- writes: List[tuple[str, Any]],
440
+ writes: Sequence[tuple[str, Any]],
420
441
  task_id: str,
421
442
  ) -> None:
422
443
  """Store intermediate writes linked to a checkpoint.
@@ -1,5 +1,5 @@
1
1
  import random
2
- from typing import Any, List, Optional, Tuple
2
+ from typing import Any, List, Optional, Sequence, Tuple, cast
3
3
 
4
4
  from langchain_core.runnables import RunnableConfig
5
5
  from psycopg.types.json import Jsonb
@@ -7,7 +7,9 @@ from psycopg.types.json import Jsonb
7
7
  from langgraph.checkpoint.base import (
8
8
  WRITES_IDX_MAP,
9
9
  BaseCheckpointSaver,
10
+ ChannelVersions,
10
11
  Checkpoint,
12
+ CheckpointMetadata,
11
13
  get_checkpoint_id,
12
14
  )
13
15
  from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
@@ -122,7 +124,7 @@ INSERT_CHECKPOINT_WRITES_SQL = """
122
124
  """
123
125
 
124
126
 
125
- class BasePostgresSaver(BaseCheckpointSaver):
127
+ class BasePostgresSaver(BaseCheckpointSaver[str]):
126
128
  SELECT_SQL = SELECT_SQL
127
129
  MIGRATIONS = MIGRATIONS
128
130
  UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
@@ -165,8 +167,8 @@ class BasePostgresSaver(BaseCheckpointSaver):
165
167
  thread_id: str,
166
168
  checkpoint_ns: str,
167
169
  values: dict[str, Any],
168
- versions: dict[str, str],
169
- ) -> list[tuple[str, str, str, str, str, bytes]]:
170
+ versions: ChannelVersions,
171
+ ) -> list[tuple[str, str, str, str, str, Optional[bytes]]]:
170
172
  if not versions:
171
173
  return []
172
174
 
@@ -175,7 +177,7 @@ class BasePostgresSaver(BaseCheckpointSaver):
175
177
  thread_id,
176
178
  checkpoint_ns,
177
179
  k,
178
- ver,
180
+ cast(str, ver),
179
181
  *(
180
182
  self.serde.dumps_typed(values[k])
181
183
  if k in values
@@ -207,8 +209,8 @@ class BasePostgresSaver(BaseCheckpointSaver):
207
209
  checkpoint_ns: str,
208
210
  checkpoint_id: str,
209
211
  task_id: str,
210
- writes: list[tuple[str, Any]],
211
- ) -> list[tuple[str, str, str, int, str, str, bytes]]:
212
+ writes: Sequence[tuple[str, Any]],
213
+ ) -> list[tuple[str, str, str, str, int, str, str, bytes]]:
212
214
  return [
213
215
  (
214
216
  thread_id,
@@ -222,10 +224,10 @@ class BasePostgresSaver(BaseCheckpointSaver):
222
224
  for idx, (channel, value) in enumerate(writes)
223
225
  ]
224
226
 
225
- def _load_metadata(self, metadata: dict[str, Any]) -> dict[str, Any]:
227
+ def _load_metadata(self, metadata: dict[str, Any]) -> CheckpointMetadata:
226
228
  return self.jsonplus_serde.loads(self.jsonplus_serde.dumps(metadata))
227
229
 
228
- def _dump_metadata(self, metadata) -> str:
230
+ def _dump_metadata(self, metadata: CheckpointMetadata) -> str:
229
231
  serialized_metadata = self.jsonplus_serde.dumps(metadata)
230
232
  return serialized_metadata.decode()
231
233
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "langgraph-checkpoint-postgres"
3
- version = "1.0.7"
3
+ version = "1.0.8"
4
4
  description = "Library with a Postgres implementation of LangGraph checkpoint saver."
5
5
  authors = []
6
6
  license = "MIT"
@@ -10,7 +10,7 @@ packages = [{ include = "langgraph" }]
10
10
 
11
11
  [tool.poetry.dependencies]
12
12
  python = "^3.9.0,<4.0"
13
- langgraph-checkpoint = "^1.0.8"
13
+ langgraph-checkpoint = "^1.0.11"
14
14
  orjson = ">=3.10.1"
15
15
  psycopg = "^3.0.0"
16
16
  psycopg-pool = "^3.0.0"
@@ -51,3 +51,13 @@ lint.select = [
51
51
  "I", # isort
52
52
  ]
53
53
  lint.ignore = ["E501", "B008", "UP007", "UP006"]
54
+
55
+ [tool.mypy]
56
+ # https://mypy.readthedocs.io/en/stable/config_file.html
57
+ disallow_untyped_defs = "True"
58
+ explicit_package_bases = "True"
59
+ warn_no_return = "False"
60
+ warn_unused_ignores = "True"
61
+ warn_redundant_casts = "True"
62
+ allow_redefinition = "True"
63
+ disable_error_code = "typeddict-item, return-value"