langgraph-checkpoint-postgres 1.0.2__tar.gz → 1.0.4__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.2
3
+ Version: 1.0.4
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
@@ -13,7 +13,8 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Requires-Dist: langgraph-checkpoint (>=1.0.1,<2.0.0)
15
15
  Requires-Dist: orjson (>=3.10.1)
16
- Requires-Dist: psycopg[binary] (>=3.1.19)
16
+ Requires-Dist: psycopg (>=3.0.0,<4.0.0)
17
+ Requires-Dist: psycopg-pool (>=3.0.0,<4.0.0)
17
18
  Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
18
19
  Description-Content-Type: text/markdown
19
20
 
@@ -21,11 +22,18 @@ Description-Content-Type: text/markdown
21
22
 
22
23
  Implementation of LangGraph CheckpointSaver that uses Postgres.
23
24
 
25
+ ## Dependencies
26
+
27
+ By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`).
28
+
24
29
  ## Usage
25
30
 
26
31
  > [!IMPORTANT]
27
32
  > When using Postgres checkpointers for the first time, make sure to call `.setup()` method on them to create required tables. See example below.
28
33
 
34
+ > [!IMPORTANT]
35
+ > When manually creating Postgres connections and passing them to `PostgresSaver` or `AsyncPostgresSaver`, make sure to include `autocommit=True` and `row_factory=dict_row` (`from psycopg.rows import dict_row`). See a full example in this [how-to guide](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/).
36
+
29
37
  ```python
30
38
  from langgraph.checkpoint.postgres import PostgresSaver
31
39
 
@@ -60,7 +68,6 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
60
68
  }
61
69
  },
62
70
  "pending_sends": [],
63
- "current_tasks": {}
64
71
  }
65
72
 
66
73
  # store checkpoint
@@ -103,7 +110,6 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
103
110
  }
104
111
  },
105
112
  "pending_sends": [],
106
- "current_tasks": {}
107
113
  }
108
114
 
109
115
  # store checkpoint
@@ -115,3 +121,4 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
115
121
  # list checkpoints
116
122
  [c async for c in checkpointer.alist(read_config)]
117
123
  ```
124
+
@@ -2,11 +2,18 @@
2
2
 
3
3
  Implementation of LangGraph CheckpointSaver that uses Postgres.
4
4
 
5
+ ## Dependencies
6
+
7
+ By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`).
8
+
5
9
  ## Usage
6
10
 
7
11
  > [!IMPORTANT]
8
12
  > When using Postgres checkpointers for the first time, make sure to call `.setup()` method on them to create required tables. See example below.
9
13
 
14
+ > [!IMPORTANT]
15
+ > When manually creating Postgres connections and passing them to `PostgresSaver` or `AsyncPostgresSaver`, make sure to include `autocommit=True` and `row_factory=dict_row` (`from psycopg.rows import dict_row`). See a full example in this [how-to guide](https://langchain-ai.github.io/langgraph/how-tos/persistence_postgres/).
16
+
10
17
  ```python
11
18
  from langgraph.checkpoint.postgres import PostgresSaver
12
19
 
@@ -41,7 +48,6 @@ with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
41
48
  }
42
49
  },
43
50
  "pending_sends": [],
44
- "current_tasks": {}
45
51
  }
46
52
 
47
53
  # store checkpoint
@@ -84,7 +90,6 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
84
90
  }
85
91
  },
86
92
  "pending_sends": [],
87
- "current_tasks": {}
88
93
  }
89
94
 
90
95
  # store checkpoint
@@ -95,4 +100,4 @@ async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
95
100
 
96
101
  # list checkpoints
97
102
  [c async for c in checkpointer.alist(read_config)]
98
- ```
103
+ ```
@@ -1,12 +1,13 @@
1
1
  import threading
2
2
  from contextlib import contextmanager
3
- from typing import Any, Iterator, List, Optional
3
+ from typing import Any, Iterator, List, Optional, 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
8
  from psycopg.rows import dict_row
9
9
  from psycopg.types.json import Jsonb
10
+ from psycopg_pool import ConnectionPool
10
11
 
11
12
  from langgraph.checkpoint.base import (
12
13
  ChannelVersions,
@@ -21,16 +22,32 @@ from langgraph.checkpoint.postgres.base import (
21
22
  from langgraph.checkpoint.serde.base import SerializerProtocol
22
23
 
23
24
 
25
+ @contextmanager
26
+ def _get_connection(conn: Union[Connection, ConnectionPool]) -> Iterator[Connection]:
27
+ if isinstance(conn, Connection):
28
+ yield conn
29
+ elif isinstance(conn, ConnectionPool):
30
+ with conn.connection() as conn:
31
+ yield conn
32
+ else:
33
+ raise TypeError(f"Invalid connection type: {type(conn)}")
34
+
35
+
24
36
  class PostgresSaver(BasePostgresSaver):
25
37
  lock: threading.Lock
26
38
 
27
39
  def __init__(
28
40
  self,
29
- conn: Connection,
41
+ conn: Union[Connection, ConnectionPool],
30
42
  pipe: Optional[Pipeline] = None,
31
43
  serde: Optional[SerializerProtocol] = None,
32
44
  ) -> None:
33
45
  super().__init__(serde=serde)
46
+ if isinstance(conn, ConnectionPool) and pipe is not None:
47
+ raise ValueError(
48
+ "Pipeline should be used only with a single Connection, not ConnectionPool."
49
+ )
50
+
34
51
  self.conn = conn
35
52
  self.pipe = pipe
36
53
  self.lock = threading.Lock()
@@ -65,22 +82,21 @@ class PostgresSaver(BasePostgresSaver):
65
82
  already exist and runs database migrations. It MUST be called directly by the user
66
83
  the first time checkpointer is used.
67
84
  """
68
- with self.lock:
69
- with self.conn.cursor(binary=True) as cur:
70
- try:
71
- version = cur.execute(
72
- "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
73
- ).fetchone()["v"]
74
- except UndefinedTable:
75
- version = -1
76
- for v, migration in zip(
77
- range(version + 1, len(self.MIGRATIONS)),
78
- self.MIGRATIONS[version + 1 :],
79
- ):
80
- cur.execute(migration)
81
- cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
82
- if self.pipe:
83
- self.pipe.sync()
85
+ with self._cursor() as cur:
86
+ try:
87
+ version = cur.execute(
88
+ "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
89
+ ).fetchone()["v"]
90
+ except UndefinedTable:
91
+ version = -1
92
+ for v, migration in zip(
93
+ range(version + 1, len(self.MIGRATIONS)),
94
+ self.MIGRATIONS[version + 1 :],
95
+ ):
96
+ cur.execute(migration)
97
+ cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
98
+ if self.pipe:
99
+ self.pipe.sync()
84
100
 
85
101
  def list(
86
102
  self,
@@ -127,30 +143,33 @@ class PostgresSaver(BasePostgresSaver):
127
143
  if limit:
128
144
  query += f" LIMIT {limit}"
129
145
  # if we change this to use .stream() we need to make sure to close the cursor
130
- for value in self.conn.execute(query, args, binary=True):
131
- yield CheckpointTuple(
132
- {
133
- "configurable": {
134
- "thread_id": value["thread_id"],
135
- "checkpoint_ns": value["checkpoint_ns"],
136
- "checkpoint_id": value["checkpoint_id"],
137
- }
138
- },
139
- {
140
- **self._load_checkpoint(value["checkpoint"]),
141
- "channel_values": self._load_blobs(value["channel_values"]),
142
- },
143
- self._load_metadata(value["metadata"]),
144
- {
145
- "configurable": {
146
- "thread_id": value["thread_id"],
147
- "checkpoint_ns": value["checkpoint_ns"],
148
- "checkpoint_id": value["parent_checkpoint_id"],
146
+ with self._cursor() as cur:
147
+ cur.execute(query, args, binary=True)
148
+ for value in cur:
149
+ yield CheckpointTuple(
150
+ {
151
+ "configurable": {
152
+ "thread_id": value["thread_id"],
153
+ "checkpoint_ns": value["checkpoint_ns"],
154
+ "checkpoint_id": value["checkpoint_id"],
155
+ }
156
+ },
157
+ {
158
+ **self._load_checkpoint(value["checkpoint"]),
159
+ "channel_values": self._load_blobs(value["channel_values"]),
160
+ },
161
+ self._load_metadata(value["metadata"]),
162
+ {
163
+ "configurable": {
164
+ "thread_id": value["thread_id"],
165
+ "checkpoint_ns": value["checkpoint_ns"],
166
+ "checkpoint_id": value["parent_checkpoint_id"],
167
+ }
149
168
  }
150
- }
151
- if value["parent_checkpoint_id"]
152
- else None,
153
- )
169
+ if value["parent_checkpoint_id"]
170
+ else None,
171
+ self._load_writes(value["pending_writes"]),
172
+ )
154
173
 
155
174
  def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
156
175
  """Get a checkpoint tuple from the database.
@@ -198,7 +217,7 @@ class PostgresSaver(BasePostgresSaver):
198
217
  where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1"
199
218
 
200
219
  with self._cursor() as cur:
201
- cur = self.conn.execute(
220
+ cur.execute(
202
221
  self.SELECT_SQL + where,
203
222
  args,
204
223
  binary=True,
@@ -316,7 +335,7 @@ class PostgresSaver(BasePostgresSaver):
316
335
  writes (List[Tuple[str, Any]]): List of writes to store.
317
336
  task_id (str): Identifier for the task creating the writes.
318
337
  """
319
- with self._cursor() as cur:
338
+ with self._cursor(pipeline=True) as cur:
320
339
  cur.executemany(
321
340
  self.UPSERT_CHECKPOINT_WRITES_SQL,
322
341
  self._dump_writes(
@@ -330,21 +349,24 @@ class PostgresSaver(BasePostgresSaver):
330
349
 
331
350
  @contextmanager
332
351
  def _cursor(self, *, pipeline: bool = False) -> Iterator[Cursor]:
333
- if self.pipe:
334
- # a connection in pipeline mode can be used concurrently
335
- # in multiple threads/coroutines, but only one cursor can be
336
- # used at a time
337
- try:
338
- with self.conn.cursor(binary=True) as cur:
352
+ with _get_connection(self.conn) as conn:
353
+ if self.pipe:
354
+ # a connection in pipeline mode can be used concurrently
355
+ # in multiple threads/coroutines, but only one cursor can be
356
+ # used at a time
357
+ try:
358
+ with conn.cursor(binary=True, row_factory=dict_row) as cur:
359
+ yield cur
360
+ finally:
361
+ if pipeline:
362
+ self.pipe.sync()
363
+ elif pipeline:
364
+ # a connection not in pipeline mode can only be used by one
365
+ # thread/coroutine at a time, so we acquire a lock
366
+ with self.lock, conn.pipeline(), conn.cursor(
367
+ binary=True, row_factory=dict_row
368
+ ) as cur:
369
+ yield cur
370
+ else:
371
+ with self.lock, conn.cursor(binary=True, row_factory=dict_row) as cur:
339
372
  yield cur
340
- finally:
341
- if pipeline:
342
- self.pipe.sync()
343
- elif pipeline:
344
- # a connection not in pipeline mode can only be used by one
345
- # thread/coroutine at a time, so we acquire a lock
346
- with self.lock, self.conn.pipeline(), self.conn.cursor(binary=True) as cur:
347
- yield cur
348
- else:
349
- with self.lock, self.conn.cursor(binary=True) as cur:
350
- yield cur
@@ -1,12 +1,13 @@
1
1
  import asyncio
2
2
  from contextlib import asynccontextmanager
3
- from typing import Any, AsyncIterator, Optional
3
+ from typing import Any, AsyncIterator, Optional, 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
8
  from psycopg.rows import dict_row
9
9
  from psycopg.types.json import Jsonb
10
+ from psycopg_pool import AsyncConnectionPool
10
11
 
11
12
  from langgraph.checkpoint.base import (
12
13
  ChannelVersions,
@@ -19,16 +20,34 @@ from langgraph.checkpoint.postgres.base import BasePostgresSaver
19
20
  from langgraph.checkpoint.serde.base import SerializerProtocol
20
21
 
21
22
 
23
+ @asynccontextmanager
24
+ async def _get_connection(
25
+ conn: Union[AsyncConnection, AsyncConnectionPool],
26
+ ) -> AsyncIterator[AsyncConnection]:
27
+ if isinstance(conn, AsyncConnection):
28
+ yield conn
29
+ elif isinstance(conn, AsyncConnectionPool):
30
+ async with conn.connection() as conn:
31
+ yield conn
32
+ else:
33
+ raise TypeError(f"Invalid connection type: {type(conn)}")
34
+
35
+
22
36
  class AsyncPostgresSaver(BasePostgresSaver):
23
37
  lock: asyncio.Lock
24
38
 
25
39
  def __init__(
26
40
  self,
27
- conn: AsyncConnection,
41
+ conn: Union[AsyncConnection, AsyncConnectionPool],
28
42
  pipe: Optional[AsyncPipeline] = None,
29
43
  serde: Optional[SerializerProtocol] = None,
30
44
  ) -> None:
31
45
  super().__init__(serde=serde)
46
+ if isinstance(conn, AsyncConnectionPool) and pipe is not None:
47
+ raise ValueError(
48
+ "Pipeline should be used only with a single AsyncConnection, not AsyncConnectionPool."
49
+ )
50
+
32
51
  self.conn = conn
33
52
  self.pipe = pipe
34
53
  self.lock = asyncio.Lock()
@@ -63,26 +82,22 @@ class AsyncPostgresSaver(BasePostgresSaver):
63
82
  already exist and runs database migrations. It MUST be called directly by the user
64
83
  the first time checkpointer is used.
65
84
  """
66
- async with self.lock:
67
- async with self.conn.cursor(binary=True) as cur:
68
- try:
69
- version = (
70
- await cur.execute(
71
- "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
72
- )
73
- ).fetchone()["v"]
74
- except UndefinedTable:
75
- version = -1
76
- for v, migration in zip(
77
- range(version + 1, len(self.MIGRATIONS)),
78
- self.MIGRATIONS[version + 1 :],
79
- ):
80
- await cur.execute(migration)
81
- await cur.execute(
82
- f"INSERT INTO checkpoint_migrations (v) VALUES ({v})"
83
- )
84
- if self.pipe:
85
- await self.pipe.sync()
85
+ async with self._cursor() as cur:
86
+ try:
87
+ results = await cur.execute(
88
+ "SELECT v FROM checkpoint_migrations ORDER BY v DESC LIMIT 1"
89
+ )
90
+ version = (await results.fetchone())["v"]
91
+ except UndefinedTable:
92
+ version = -1
93
+ for v, migration in zip(
94
+ range(version + 1, len(self.MIGRATIONS)),
95
+ self.MIGRATIONS[version + 1 :],
96
+ ):
97
+ await cur.execute(migration)
98
+ await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
99
+ if self.pipe:
100
+ await self.pipe.sync()
86
101
 
87
102
  async def alist(
88
103
  self,
@@ -111,32 +126,35 @@ class AsyncPostgresSaver(BasePostgresSaver):
111
126
  if limit:
112
127
  query += f" LIMIT {limit}"
113
128
  # if we change this to use .stream() we need to make sure to close the cursor
114
- async for value in await self.conn.execute(query, args, binary=True):
115
- yield CheckpointTuple(
116
- {
117
- "configurable": {
118
- "thread_id": value["thread_id"],
119
- "checkpoint_ns": value["checkpoint_ns"],
120
- "checkpoint_id": value["checkpoint_id"],
121
- }
122
- },
123
- {
124
- **self._load_checkpoint(value["checkpoint"]),
125
- "channel_values": await asyncio.to_thread(
126
- self._load_blobs, value["channel_values"]
127
- ),
128
- },
129
- self._load_metadata(value["metadata"]),
130
- {
131
- "configurable": {
132
- "thread_id": value["thread_id"],
133
- "checkpoint_ns": value["checkpoint_ns"],
134
- "checkpoint_id": value["parent_checkpoint_id"],
129
+ async with self._cursor() as cur:
130
+ await cur.execute(query, args, binary=True)
131
+ async for value in cur:
132
+ yield CheckpointTuple(
133
+ {
134
+ "configurable": {
135
+ "thread_id": value["thread_id"],
136
+ "checkpoint_ns": value["checkpoint_ns"],
137
+ "checkpoint_id": value["checkpoint_id"],
138
+ }
139
+ },
140
+ {
141
+ **self._load_checkpoint(value["checkpoint"]),
142
+ "channel_values": await asyncio.to_thread(
143
+ self._load_blobs, value["channel_values"]
144
+ ),
145
+ },
146
+ self._load_metadata(value["metadata"]),
147
+ {
148
+ "configurable": {
149
+ "thread_id": value["thread_id"],
150
+ "checkpoint_ns": value["checkpoint_ns"],
151
+ "checkpoint_id": value["parent_checkpoint_id"],
152
+ }
135
153
  }
136
- }
137
- if value["parent_checkpoint_id"]
138
- else None,
139
- )
154
+ if value["parent_checkpoint_id"]
155
+ else None,
156
+ await asyncio.to_thread(self._load_writes, value["pending_writes"]),
157
+ )
140
158
 
141
159
  async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
142
160
  """Get a checkpoint tuple from the database asynchronously.
@@ -163,7 +181,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
163
181
  where = "WHERE thread_id = %s AND checkpoint_ns = %s ORDER BY checkpoint_id DESC LIMIT 1"
164
182
 
165
183
  async with self._cursor() as cur:
166
- cur = await self.conn.execute(
184
+ await cur.execute(
167
185
  self.SELECT_SQL + where,
168
186
  args,
169
187
  binary=True,
@@ -273,7 +291,7 @@ class AsyncPostgresSaver(BasePostgresSaver):
273
291
  writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
274
292
  task_id (str): Identifier for the task creating the writes.
275
293
  """
276
- async with self._cursor() as cur:
294
+ async with self._cursor(pipeline=True) as cur:
277
295
  await cur.executemany(
278
296
  self.UPSERT_CHECKPOINT_WRITES_SQL,
279
297
  await asyncio.to_thread(
@@ -288,23 +306,26 @@ class AsyncPostgresSaver(BasePostgresSaver):
288
306
 
289
307
  @asynccontextmanager
290
308
  async def _cursor(self, *, pipeline: bool = False) -> AsyncIterator[AsyncCursor]:
291
- if self.pipe:
292
- # a connection in pipeline mode can be used concurrently
293
- # in multiple threads/coroutines, but only one cursor can be
294
- # used at a time
295
- try:
296
- async with self.conn.cursor(binary=True) as cur:
309
+ async with _get_connection(self.conn) as conn:
310
+ if self.pipe:
311
+ # a connection in pipeline mode can be used concurrently
312
+ # in multiple threads/coroutines, but only one cursor can be
313
+ # used at a time
314
+ try:
315
+ async with conn.cursor(binary=True, row_factory=dict_row) as cur:
316
+ yield cur
317
+ finally:
318
+ if pipeline:
319
+ await self.pipe.sync()
320
+ elif pipeline:
321
+ # a connection not in pipeline mode can only be used by one
322
+ # thread/coroutine at a time, so we acquire a lock
323
+ async with self.lock, conn.pipeline(), conn.cursor(
324
+ binary=True, row_factory=dict_row
325
+ ) as cur:
326
+ yield cur
327
+ else:
328
+ async with self.lock, conn.cursor(
329
+ binary=True, row_factory=dict_row
330
+ ) as cur:
297
331
  yield cur
298
- finally:
299
- if pipeline:
300
- await self.pipe.sync()
301
- elif pipeline:
302
- # a connection not in pipeline mode can only be used by one
303
- # thread/coroutine at a time, so we acquire a lock
304
- async with self.lock, self.conn.pipeline(), self.conn.cursor(
305
- binary=True
306
- ) as cur:
307
- yield cur
308
- else:
309
- async with self.lock, self.conn.cursor(binary=True) as cur:
310
- yield cur
@@ -6,6 +6,7 @@ from langchain_core.runnables import RunnableConfig
6
6
  from psycopg.types.json import Jsonb
7
7
 
8
8
  from langgraph.checkpoint.base import (
9
+ WRITES_IDX_MAP,
9
10
  BaseCheckpointSaver,
10
11
  Checkpoint,
11
12
  EmptyChannelError,
@@ -112,6 +113,7 @@ class BasePostgresSaver(BaseCheckpointSaver):
112
113
  UPSERT_CHECKPOINT_BLOBS_SQL = UPSERT_CHECKPOINT_BLOBS_SQL
113
114
  UPSERT_CHECKPOINTS_SQL = UPSERT_CHECKPOINTS_SQL
114
115
  UPSERT_CHECKPOINT_WRITES_SQL = UPSERT_CHECKPOINT_WRITES_SQL
116
+
115
117
  jsonplus_serde = JsonPlusSerializer()
116
118
 
117
119
  def _load_checkpoint(self, checkpoint: dict[str, Any]) -> Checkpoint:
@@ -199,7 +201,7 @@ class BasePostgresSaver(BaseCheckpointSaver):
199
201
  checkpoint_ns,
200
202
  checkpoint_id,
201
203
  task_id,
202
- idx,
204
+ WRITES_IDX_MAP.get(channel, idx),
203
205
  channel,
204
206
  *self.serde.dumps_typed(value),
205
207
  )
@@ -253,9 +255,13 @@ class BasePostgresSaver(BaseCheckpointSaver):
253
255
  if config:
254
256
  wheres.append("thread_id = %s ")
255
257
  param_values.append(config["configurable"]["thread_id"])
256
- checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
257
- wheres.append("checkpoint_ns = %s")
258
- param_values.append(checkpoint_ns)
258
+ if checkpoint_ns := config["configurable"].get("checkpoint_ns"):
259
+ wheres.append("checkpoint_ns = %s")
260
+ param_values.append(checkpoint_ns)
261
+
262
+ if checkpoint_id := get_checkpoint_id(config):
263
+ wheres.append("checkpoint_id = %s ")
264
+ param_values.append(checkpoint_id)
259
265
 
260
266
  # construct predicate for metadata filter
261
267
  if filter:
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "langgraph-checkpoint-postgres"
3
- version = "1.0.2"
3
+ version = "1.0.4"
4
4
  description = "Library with a Postgres implementation of LangGraph checkpoint saver."
5
5
  authors = []
6
6
  license = "MIT"
@@ -12,7 +12,8 @@ packages = [{ include = "langgraph" }]
12
12
  python = "^3.9.0,<4.0"
13
13
  langgraph-checkpoint = "^1.0.1"
14
14
  orjson = ">=3.10.1"
15
- psycopg = {extras = ["binary"], version = ">=3.1.19"}
15
+ psycopg = "^3.0.0"
16
+ psycopg-pool = "^3.0.0"
16
17
 
17
18
  [tool.poetry.group.dev.dependencies]
18
19
  ruff = "^0.1.4"
@@ -23,7 +24,7 @@ pytest-asyncio = "^0.21.1"
23
24
  pytest-mock = "^3.11.1"
24
25
  pytest-watch = "^4.2.0"
25
26
  mypy = "^1.10.0"
26
- psycopg-pool = "^3.2.2"
27
+ psycopg = {extras = ["binary"], version = ">=3.0.0"}
27
28
  langgraph-checkpoint = {path = "../checkpoint", develop = true}
28
29
 
29
30
  [tool.pytest.ini_options]