langgraph-checkpoint-sqlite 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-sqlite
3
- Version: 1.0.2
3
+ Version: 1.0.4
4
4
  Summary: Library with a SQLite implementation of LangGraph checkpoint saver.
5
5
  Home-page: https://www.github.com/langchain-ai/langgraph
6
6
  License: MIT
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Requires-Dist: aiosqlite (>=0.20.0,<0.21.0)
15
- Requires-Dist: langgraph-checkpoint (>=1.0.8,<2.0.0)
15
+ Requires-Dist: langgraph-checkpoint (>=1.0.11,<2.0.0)
16
16
  Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
17
17
  Description-Content-Type: text/markdown
18
18
 
@@ -1,7 +1,7 @@
1
+ import random
1
2
  import sqlite3
2
3
  import threading
3
4
  from contextlib import closing, contextmanager
4
- from hashlib import md5
5
5
  from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple
6
6
 
7
7
  from langchain_core.runnables import RunnableConfig
@@ -13,7 +13,6 @@ from langgraph.checkpoint.base import (
13
13
  Checkpoint,
14
14
  CheckpointMetadata,
15
15
  CheckpointTuple,
16
- EmptyChannelError,
17
16
  SerializerProtocol,
18
17
  get_checkpoint_id,
19
18
  )
@@ -32,7 +31,7 @@ _AIO_ERROR_MSG = (
32
31
  )
33
32
 
34
33
 
35
- class SqliteSaver(BaseCheckpointSaver):
34
+ class SqliteSaver(BaseCheckpointSaver[str]):
36
35
  """A checkpoint saver that stores checkpoints in a SQLite database.
37
36
 
38
37
  Note:
@@ -104,10 +103,12 @@ class SqliteSaver(BaseCheckpointSaver):
104
103
  with SqliteSaver.from_conn_string("checkpoints.sqlite") as memory:
105
104
  ...
106
105
  """
107
- with sqlite3.connect(
108
- conn_string,
109
- # https://ricardoanderegg.com/posts/python-sqlite-thread-safety/
110
- check_same_thread=False,
106
+ with closing(
107
+ sqlite3.connect(
108
+ conn_string,
109
+ # https://ricardoanderegg.com/posts/python-sqlite-thread-safety/
110
+ check_same_thread=False,
111
+ )
111
112
  ) as conn:
112
113
  yield SqliteSaver(conn)
113
114
 
@@ -163,14 +164,15 @@ class SqliteSaver(BaseCheckpointSaver):
163
164
  Yields:
164
165
  sqlite3.Cursor: A cursor for the SQLite database.
165
166
  """
166
- self.setup()
167
- cur = self.conn.cursor()
168
- try:
169
- yield cur
170
- finally:
171
- if transaction:
172
- self.conn.commit()
173
- cur.close()
167
+ with self.lock:
168
+ self.setup()
169
+ cur = self.conn.cursor()
170
+ try:
171
+ yield cur
172
+ finally:
173
+ if transaction:
174
+ self.conn.commit()
175
+ cur.close()
174
176
 
175
177
  def get_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:
176
178
  """Get a checkpoint tuple from the database.
@@ -396,7 +398,7 @@ class SqliteSaver(BaseCheckpointSaver):
396
398
  checkpoint_ns = config["configurable"]["checkpoint_ns"]
397
399
  type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
398
400
  serialized_metadata = self.jsonplus_serde.dumps(metadata)
399
- with self.lock, self.cursor() as cur:
401
+ with self.cursor() as cur:
400
402
  cur.execute(
401
403
  "INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
402
404
  (
@@ -432,9 +434,14 @@ class SqliteSaver(BaseCheckpointSaver):
432
434
  writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
433
435
  task_id (str): Identifier for the task creating the writes.
434
436
  """
435
- with self.lock, self.cursor() as cur:
437
+ query = (
438
+ "INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
439
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
440
+ else "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
441
+ )
442
+ with self.cursor() as cur:
436
443
  cur.executemany(
437
- "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
444
+ query,
438
445
  [
439
446
  (
440
447
  str(config["configurable"]["thread_id"]),
@@ -480,6 +487,7 @@ class SqliteSaver(BaseCheckpointSaver):
480
487
  config: RunnableConfig,
481
488
  checkpoint: Checkpoint,
482
489
  metadata: CheckpointMetadata,
490
+ new_versions: ChannelVersions,
483
491
  ) -> RunnableConfig:
484
492
  """Save a checkpoint to the database asynchronously.
485
493
 
@@ -503,11 +511,10 @@ class SqliteSaver(BaseCheckpointSaver):
503
511
  """
504
512
  if current is None:
505
513
  current_v = 0
514
+ elif isinstance(current, int):
515
+ current_v = current
506
516
  else:
507
517
  current_v = int(current.split(".")[0])
508
518
  next_v = current_v + 1
509
- try:
510
- next_h = md5(self.serde.dumps_typed(channel.checkpoint())[1]).hexdigest()
511
- except EmptyChannelError:
512
- next_h = ""
513
- return f"{next_v:032}.{next_h}"
519
+ next_h = random.random()
520
+ return f"{next_v:032}.{next_h:016}"
@@ -1,11 +1,12 @@
1
1
  import asyncio
2
+ import random
2
3
  from contextlib import asynccontextmanager
3
4
  from typing import (
4
5
  Any,
5
6
  AsyncIterator,
7
+ Callable,
6
8
  Dict,
7
9
  Iterator,
8
- List,
9
10
  Optional,
10
11
  Sequence,
11
12
  Tuple,
@@ -26,12 +27,13 @@ from langgraph.checkpoint.base import (
26
27
  get_checkpoint_id,
27
28
  )
28
29
  from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
30
+ from langgraph.checkpoint.serde.types import ChannelProtocol
29
31
  from langgraph.checkpoint.sqlite.utils import search_where
30
32
 
31
- T = TypeVar("T", bound=callable)
33
+ T = TypeVar("T", bound=Callable)
32
34
 
33
35
 
34
- class AsyncSqliteSaver(BaseCheckpointSaver):
36
+ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
35
37
  """An asynchronous checkpoint saver that stores checkpoints in a SQLite database.
36
38
 
37
39
  This class provides an asynchronous interface for saving and retrieving checkpoints
@@ -151,6 +153,18 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
151
153
  Returns:
152
154
  Optional[CheckpointTuple]: The retrieved checkpoint tuple, or None if no matching checkpoint was found.
153
155
  """
156
+ try:
157
+ # check if we are in the main thread, only bg threads can block
158
+ # we don't check in other methods to avoid the overhead
159
+ if asyncio.get_running_loop() is self.loop:
160
+ raise asyncio.InvalidStateError(
161
+ "Synchronous calls to AsyncSqliteSaver are only allowed from a "
162
+ "different thread. From the main thread, use the async interface."
163
+ "For example, use `await checkpointer.aget_tuple(...)` or `await "
164
+ "graph.ainvoke(...)`."
165
+ )
166
+ except RuntimeError:
167
+ pass
154
168
  return asyncio.run_coroutine_threadsafe(
155
169
  self.aget_tuple(config), self.loop
156
170
  ).result()
@@ -181,7 +195,8 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
181
195
  while True:
182
196
  try:
183
197
  yield asyncio.run_coroutine_threadsafe(
184
- anext(aiter_), self.loop
198
+ anext(aiter_),
199
+ self.loop,
185
200
  ).result()
186
201
  except StopAsyncIteration:
187
202
  break
@@ -212,7 +227,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
212
227
  ).result()
213
228
 
214
229
  def put_writes(
215
- self, config: RunnableConfig, writes: List[Tuple[str, Any]], task_id: str
230
+ self, config: RunnableConfig, writes: Sequence[Tuple[str, Any]], task_id: str
216
231
  ) -> None:
217
232
  return asyncio.run_coroutine_threadsafe(
218
233
  self.aput_writes(config, writes, task_id), self.loop
@@ -276,7 +291,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
276
291
  """
277
292
  await self.setup()
278
293
  checkpoint_ns = config["configurable"].get("checkpoint_ns", "")
279
- async with self.conn.cursor() as cur:
294
+ async with self.lock, self.conn.cursor() as cur:
280
295
  # find the latest checkpoint for the thread_id
281
296
  if checkpoint_id := get_checkpoint_id(config):
282
297
  await cur.execute(
@@ -371,7 +386,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
371
386
  ORDER BY checkpoint_id DESC"""
372
387
  if limit:
373
388
  query += f" LIMIT {limit}"
374
- async with self.conn.execute(query, params) as cur, self.conn.cursor() as wcur:
389
+ async with self.lock, self.conn.execute(
390
+ query, params
391
+ ) as cur, self.conn.cursor() as wcur:
375
392
  async for (
376
393
  thread_id,
377
394
  checkpoint_ns,
@@ -438,7 +455,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
438
455
  checkpoint_ns = config["configurable"]["checkpoint_ns"]
439
456
  type_, serialized_checkpoint = self.serde.dumps_typed(checkpoint)
440
457
  serialized_metadata = self.jsonplus_serde.dumps(metadata)
441
- async with self.conn.execute(
458
+ async with self.lock, self.conn.execute(
442
459
  "INSERT OR REPLACE INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, parent_checkpoint_id, type, checkpoint, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)",
443
460
  (
444
461
  str(config["configurable"]["thread_id"]),
@@ -474,10 +491,15 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
474
491
  writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
475
492
  task_id (str): Identifier for the task creating the writes.
476
493
  """
494
+ query = (
495
+ "INSERT OR REPLACE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
496
+ if all(w[0] in WRITES_IDX_MAP for w in writes)
497
+ else "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
498
+ )
477
499
  await self.setup()
478
- async with self.conn.cursor() as cur:
500
+ async with self.lock, self.conn.cursor() as cur:
479
501
  await cur.executemany(
480
- "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
502
+ query,
481
503
  [
482
504
  (
483
505
  str(config["configurable"]["thread_id"]),
@@ -491,3 +513,25 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
491
513
  for idx, (channel, value) in enumerate(writes)
492
514
  ],
493
515
  )
516
+
517
+ def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
518
+ """Generate the next version ID for a channel.
519
+
520
+ This method creates a new version identifier for a channel based on its current version.
521
+
522
+ Args:
523
+ current (Optional[str]): The current version identifier of the channel.
524
+ channel (BaseChannel): The channel being versioned.
525
+
526
+ Returns:
527
+ str: The next version identifier, which is guaranteed to be monotonically increasing.
528
+ """
529
+ if current is None:
530
+ current_v = 0
531
+ elif isinstance(current, int):
532
+ current_v = current
533
+ else:
534
+ current_v = int(current.split(".")[0])
535
+ next_v = current_v + 1
536
+ next_h = random.random()
537
+ return f"{next_v:032}.{next_h:016}"
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "langgraph-checkpoint-sqlite"
3
- version = "1.0.2"
3
+ version = "1.0.4"
4
4
  description = "Library with a SQLite 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"
13
- langgraph-checkpoint = "^1.0.8"
13
+ langgraph-checkpoint = "^1.0.11"
14
14
  aiosqlite = "^0.20.0"
15
15
 
16
16
  [tool.poetry.group.dev.dependencies]
@@ -53,3 +53,13 @@ now = true
53
53
  delay = 0.1
54
54
  runner_args = ["--ff", "-v", "--tb", "short"]
55
55
  patterns = ["*.py"]
56
+
57
+ [tool.mypy]
58
+ # https://mypy.readthedocs.io/en/stable/config_file.html
59
+ disallow_untyped_defs = "True"
60
+ explicit_package_bases = "True"
61
+ warn_no_return = "False"
62
+ warn_unused_ignores = "True"
63
+ warn_redundant_casts = "True"
64
+ allow_redefinition = "True"
65
+ disable_error_code = "typeddict-item, return-value"