langgraph-checkpoint-postgres 1.0.11__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {langgraph_checkpoint_postgres-1.0.11 → langgraph_checkpoint_postgres-2.0.0}/PKG-INFO +2 -2
- {langgraph_checkpoint_postgres-1.0.11 → langgraph_checkpoint_postgres-2.0.0}/langgraph/store/postgres/aio.py +9 -8
- {langgraph_checkpoint_postgres-1.0.11 → langgraph_checkpoint_postgres-2.0.0}/langgraph/store/postgres/base.py +61 -34
- {langgraph_checkpoint_postgres-1.0.11 → langgraph_checkpoint_postgres-2.0.0}/pyproject.toml +2 -2
- {langgraph_checkpoint_postgres-1.0.11 → langgraph_checkpoint_postgres-2.0.0}/README.md +0 -0
- {langgraph_checkpoint_postgres-1.0.11 → langgraph_checkpoint_postgres-2.0.0}/langgraph/checkpoint/postgres/__init__.py +0 -0
- {langgraph_checkpoint_postgres-1.0.11 → langgraph_checkpoint_postgres-2.0.0}/langgraph/checkpoint/postgres/aio.py +0 -0
- {langgraph_checkpoint_postgres-1.0.11 → langgraph_checkpoint_postgres-2.0.0}/langgraph/checkpoint/postgres/base.py +0 -0
- {langgraph_checkpoint_postgres-1.0.11 → langgraph_checkpoint_postgres-2.0.0}/langgraph/checkpoint/py.typed +0 -0
- {langgraph_checkpoint_postgres-1.0.11 → langgraph_checkpoint_postgres-2.0.0}/langgraph/store/postgres/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: langgraph-checkpoint-postgres
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
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 (>=
|
|
14
|
+
Requires-Dist: langgraph-checkpoint (>=2.0.0,<3.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)
|
|
@@ -21,6 +21,7 @@ from langgraph.store.base import GetOp, ListNamespacesOp, Op, PutOp, Result, Sea
|
|
|
21
21
|
from langgraph.store.postgres.base import (
|
|
22
22
|
BasePostgresStore,
|
|
23
23
|
Row,
|
|
24
|
+
_decode_ns_bytes,
|
|
24
25
|
_group_ops,
|
|
25
26
|
_row_to_item,
|
|
26
27
|
)
|
|
@@ -127,17 +128,19 @@ class AsyncPostgresStore(BasePostgresStore[AsyncConnection]):
|
|
|
127
128
|
results: list[Result],
|
|
128
129
|
) -> None:
|
|
129
130
|
queries = self._get_batch_search_queries(search_ops)
|
|
130
|
-
cursors: list[tuple[AsyncCursor[Any], int
|
|
131
|
+
cursors: list[tuple[AsyncCursor[Any], int]] = []
|
|
131
132
|
|
|
132
|
-
for (query, params), (idx,
|
|
133
|
+
for (query, params), (idx, _) in zip(queries, search_ops):
|
|
133
134
|
cur = self.conn.cursor(binary=True)
|
|
134
135
|
await cur.execute(query, params)
|
|
135
|
-
cursors.append((cur, idx
|
|
136
|
+
cursors.append((cur, idx))
|
|
136
137
|
|
|
137
|
-
for cur, idx
|
|
138
|
+
for cur, idx in cursors:
|
|
138
139
|
rows = cast(list[Row], await cur.fetchall())
|
|
139
140
|
items = [
|
|
140
|
-
_row_to_item(
|
|
141
|
+
_row_to_item(
|
|
142
|
+
_decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
|
|
143
|
+
)
|
|
141
144
|
for row in rows
|
|
142
145
|
]
|
|
143
146
|
results[idx] = items
|
|
@@ -156,9 +159,7 @@ class AsyncPostgresStore(BasePostgresStore[AsyncConnection]):
|
|
|
156
159
|
|
|
157
160
|
for cur, idx in cursors:
|
|
158
161
|
rows = cast(list[dict], await cur.fetchall())
|
|
159
|
-
namespaces = [
|
|
160
|
-
tuple(row["truncated_prefix"].decode()[1:].split(".")) for row in rows
|
|
161
|
-
]
|
|
162
|
+
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
|
|
162
163
|
results[idx] = namespaces
|
|
163
164
|
|
|
164
165
|
@classmethod
|
|
@@ -40,12 +40,9 @@ logger = logging.getLogger(__name__)
|
|
|
40
40
|
|
|
41
41
|
MIGRATIONS = [
|
|
42
42
|
"""
|
|
43
|
-
CREATE EXTENSION IF NOT EXISTS ltree;
|
|
44
|
-
""",
|
|
45
|
-
"""
|
|
46
43
|
CREATE TABLE IF NOT EXISTS store (
|
|
47
44
|
-- 'prefix' represents the doc's 'namespace'
|
|
48
|
-
prefix
|
|
45
|
+
prefix text NOT NULL,
|
|
49
46
|
key text NOT NULL,
|
|
50
47
|
value jsonb NOT NULL,
|
|
51
48
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
@@ -54,8 +51,8 @@ CREATE TABLE IF NOT EXISTS store (
|
|
|
54
51
|
);
|
|
55
52
|
""",
|
|
56
53
|
"""
|
|
57
|
-
-- For faster
|
|
58
|
-
CREATE INDEX IF NOT EXISTS store_prefix_idx ON store USING
|
|
54
|
+
-- For faster lookups by prefix
|
|
55
|
+
CREATE INDEX IF NOT EXISTS store_prefix_idx ON store USING btree (prefix text_pattern_ops);
|
|
59
56
|
""",
|
|
60
57
|
]
|
|
61
58
|
|
|
@@ -93,7 +90,7 @@ class BasePostgresStore(BaseStore, Generic[C]):
|
|
|
93
90
|
FROM store
|
|
94
91
|
WHERE prefix = %s AND key IN ({keys_to_query})
|
|
95
92
|
"""
|
|
96
|
-
params = (
|
|
93
|
+
params = (_namespace_to_text(namespace), *keys)
|
|
97
94
|
results.append((query, params, namespace, items))
|
|
98
95
|
return results
|
|
99
96
|
|
|
@@ -120,7 +117,7 @@ class BasePostgresStore(BaseStore, Generic[C]):
|
|
|
120
117
|
query = (
|
|
121
118
|
f"DELETE FROM store WHERE prefix = %s AND key IN ({placeholders})"
|
|
122
119
|
)
|
|
123
|
-
params = (
|
|
120
|
+
params = (_namespace_to_text(namespace), *keys)
|
|
124
121
|
queries.append((query, params))
|
|
125
122
|
if inserts:
|
|
126
123
|
values = []
|
|
@@ -129,7 +126,7 @@ class BasePostgresStore(BaseStore, Generic[C]):
|
|
|
129
126
|
values.append("(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)")
|
|
130
127
|
insertion_params.extend(
|
|
131
128
|
[
|
|
132
|
-
|
|
129
|
+
_namespace_to_text(op.namespace),
|
|
133
130
|
op.key,
|
|
134
131
|
Jsonb(op.value),
|
|
135
132
|
]
|
|
@@ -152,11 +149,11 @@ class BasePostgresStore(BaseStore, Generic[C]):
|
|
|
152
149
|
queries: list[tuple[str, Sequence]] = []
|
|
153
150
|
for _, op in search_ops:
|
|
154
151
|
query = """
|
|
155
|
-
SELECT key, value, created_at, updated_at
|
|
152
|
+
SELECT prefix, key, value, created_at, updated_at
|
|
156
153
|
FROM store
|
|
157
|
-
WHERE prefix
|
|
154
|
+
WHERE prefix LIKE %s
|
|
158
155
|
"""
|
|
159
|
-
params: list = [
|
|
156
|
+
params: list = [f"{_namespace_to_text(op.namespace_prefix)}%"]
|
|
160
157
|
|
|
161
158
|
if op.filter:
|
|
162
159
|
filter_conditions = []
|
|
@@ -181,22 +178,39 @@ class BasePostgresStore(BaseStore, Generic[C]):
|
|
|
181
178
|
) -> list[tuple[str, Sequence]]:
|
|
182
179
|
queries: list[tuple[str, Sequence]] = []
|
|
183
180
|
for _, op in list_ops:
|
|
184
|
-
query = "
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
query = """
|
|
182
|
+
SELECT DISTINCT ON (truncated_prefix) truncated_prefix, prefix
|
|
183
|
+
FROM (
|
|
184
|
+
SELECT
|
|
185
|
+
prefix,
|
|
186
|
+
CASE
|
|
187
|
+
WHEN %s::integer IS NOT NULL THEN
|
|
188
|
+
(SELECT STRING_AGG(part, '.' ORDER BY idx)
|
|
189
|
+
FROM (
|
|
190
|
+
SELECT part, ROW_NUMBER() OVER () AS idx
|
|
191
|
+
FROM UNNEST(REGEXP_SPLIT_TO_ARRAY(prefix, '\.')) AS part
|
|
192
|
+
LIMIT %s::integer
|
|
193
|
+
) subquery
|
|
194
|
+
)
|
|
195
|
+
ELSE prefix
|
|
196
|
+
END AS truncated_prefix
|
|
197
|
+
FROM store
|
|
198
|
+
"""
|
|
199
|
+
params: list[Any] = [op.max_depth, op.max_depth]
|
|
188
200
|
|
|
189
201
|
conditions = []
|
|
190
202
|
if op.match_conditions:
|
|
191
203
|
for condition in op.match_conditions:
|
|
192
204
|
if condition.match_type == "prefix":
|
|
193
|
-
conditions.append("prefix
|
|
194
|
-
|
|
195
|
-
|
|
205
|
+
conditions.append("prefix LIKE %s")
|
|
206
|
+
params.append(
|
|
207
|
+
f"{_namespace_to_text(condition.path, handle_wildcards=True)}%"
|
|
208
|
+
)
|
|
196
209
|
elif condition.match_type == "suffix":
|
|
197
|
-
conditions.append("prefix
|
|
198
|
-
|
|
199
|
-
|
|
210
|
+
conditions.append("prefix LIKE %s")
|
|
211
|
+
params.append(
|
|
212
|
+
f"%{_namespace_to_text(condition.path, handle_wildcards=True)}"
|
|
213
|
+
)
|
|
200
214
|
else:
|
|
201
215
|
logger.warning(
|
|
202
216
|
f"Unknown match_type in list_namespaces: {condition.match_type}"
|
|
@@ -204,11 +218,12 @@ class BasePostgresStore(BaseStore, Generic[C]):
|
|
|
204
218
|
|
|
205
219
|
if conditions:
|
|
206
220
|
query += " WHERE " + " AND ".join(conditions)
|
|
221
|
+
query += ") AS subquery "
|
|
207
222
|
|
|
208
223
|
query += " ORDER BY truncated_prefix LIMIT %s OFFSET %s"
|
|
209
224
|
params.extend([op.limit, op.offset])
|
|
210
|
-
|
|
211
225
|
queries.append((query, params))
|
|
226
|
+
|
|
212
227
|
return queries
|
|
213
228
|
|
|
214
229
|
|
|
@@ -297,17 +312,19 @@ class PostgresStore(BasePostgresStore[Connection]):
|
|
|
297
312
|
results: list[Result],
|
|
298
313
|
) -> None:
|
|
299
314
|
queries = self._get_batch_search_queries(search_ops)
|
|
300
|
-
cursors: list[tuple[Cursor[Any], int
|
|
315
|
+
cursors: list[tuple[Cursor[Any], int]] = []
|
|
301
316
|
|
|
302
|
-
for (query, params), (idx,
|
|
317
|
+
for (query, params), (idx, _) in zip(queries, search_ops):
|
|
303
318
|
cur = self.conn.cursor(binary=True)
|
|
304
319
|
cur.execute(query, params)
|
|
305
|
-
cursors.append((cur, idx
|
|
320
|
+
cursors.append((cur, idx))
|
|
306
321
|
|
|
307
|
-
for cur, idx
|
|
322
|
+
for cur, idx in cursors:
|
|
308
323
|
rows = cast(list[Row], cur.fetchall())
|
|
309
324
|
items = [
|
|
310
|
-
_row_to_item(
|
|
325
|
+
_row_to_item(
|
|
326
|
+
_decode_ns_bytes(row["prefix"]), row, loader=self._deserializer
|
|
327
|
+
)
|
|
311
328
|
for row in rows
|
|
312
329
|
]
|
|
313
330
|
results[idx] = items
|
|
@@ -326,9 +343,7 @@ class PostgresStore(BasePostgresStore[Connection]):
|
|
|
326
343
|
|
|
327
344
|
for cur, idx in cursors:
|
|
328
345
|
rows = cast(list[dict], cur.fetchall())
|
|
329
|
-
namespaces = [
|
|
330
|
-
tuple(row["truncated_prefix"].decode()[1:].split(".")) for row in rows
|
|
331
|
-
]
|
|
346
|
+
namespaces = [_decode_ns_bytes(row["truncated_prefix"]) for row in rows]
|
|
332
347
|
results[idx] = namespaces
|
|
333
348
|
|
|
334
349
|
@classmethod
|
|
@@ -386,13 +401,17 @@ class PostgresStore(BasePostgresStore[Connection]):
|
|
|
386
401
|
class Row(TypedDict):
|
|
387
402
|
key: str
|
|
388
403
|
value: Any
|
|
389
|
-
prefix:
|
|
404
|
+
prefix: str
|
|
390
405
|
created_at: datetime
|
|
391
406
|
updated_at: datetime
|
|
392
407
|
|
|
393
408
|
|
|
394
|
-
def
|
|
395
|
-
|
|
409
|
+
def _namespace_to_text(
|
|
410
|
+
namespace: tuple[str, ...], handle_wildcards: bool = False
|
|
411
|
+
) -> str:
|
|
412
|
+
"""Convert namespace tuple to text string."""
|
|
413
|
+
if handle_wildcards:
|
|
414
|
+
namespace = tuple("%" if val == "*" else val for val in namespace)
|
|
396
415
|
return ".".join(namespace)
|
|
397
416
|
|
|
398
417
|
|
|
@@ -433,3 +452,11 @@ def _json_loads(content: Union[bytes, orjson.Fragment]) -> Any:
|
|
|
433
452
|
else:
|
|
434
453
|
content = content.contents.encode()
|
|
435
454
|
return orjson.loads(cast(bytes, content))
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _decode_ns_bytes(namespace: Union[str, bytes, list]) -> tuple[str, ...]:
|
|
458
|
+
if isinstance(namespace, list):
|
|
459
|
+
return tuple(namespace)
|
|
460
|
+
if isinstance(namespace, bytes):
|
|
461
|
+
namespace = namespace.decode()[1:]
|
|
462
|
+
return tuple(namespace.split("."))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "langgraph-checkpoint-postgres"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.0"
|
|
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 = "^
|
|
13
|
+
langgraph-checkpoint = "^2.0.0"
|
|
14
14
|
orjson = ">=3.10.1"
|
|
15
15
|
psycopg = "^3.0.0"
|
|
16
16
|
psycopg-pool = "^3.0.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|