langgraph-checkpoint-sqlite 1.0.3__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.3
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:
@@ -435,9 +434,14 @@ class SqliteSaver(BaseCheckpointSaver):
435
434
  writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
436
435
  task_id (str): Identifier for the task creating the writes.
437
436
  """
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
+ )
438
442
  with self.cursor() as cur:
439
443
  cur.executemany(
440
- "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
444
+ query,
441
445
  [
442
446
  (
443
447
  str(config["configurable"]["thread_id"]),
@@ -483,6 +487,7 @@ class SqliteSaver(BaseCheckpointSaver):
483
487
  config: RunnableConfig,
484
488
  checkpoint: Checkpoint,
485
489
  metadata: CheckpointMetadata,
490
+ new_versions: ChannelVersions,
486
491
  ) -> RunnableConfig:
487
492
  """Save a checkpoint to the database asynchronously.
488
493
 
@@ -506,11 +511,10 @@ class SqliteSaver(BaseCheckpointSaver):
506
511
  """
507
512
  if current is None:
508
513
  current_v = 0
514
+ elif isinstance(current, int):
515
+ current_v = current
509
516
  else:
510
517
  current_v = int(current.split(".")[0])
511
518
  next_v = current_v + 1
512
- try:
513
- next_h = md5(self.serde.dumps_typed(channel.checkpoint())[1]).hexdigest()
514
- except EmptyChannelError:
515
- next_h = ""
516
- 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
@@ -476,10 +491,15 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
476
491
  writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair.
477
492
  task_id (str): Identifier for the task creating the writes.
478
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
+ )
479
499
  await self.setup()
480
500
  async with self.lock, self.conn.cursor() as cur:
481
501
  await cur.executemany(
482
- "INSERT OR IGNORE INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, value) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
502
+ query,
483
503
  [
484
504
  (
485
505
  str(config["configurable"]["thread_id"]),
@@ -493,3 +513,25 @@ class AsyncSqliteSaver(BaseCheckpointSaver):
493
513
  for idx, (channel, value) in enumerate(writes)
494
514
  ],
495
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.3"
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"