langgraph-checkpointer-couchbase 1.0.9__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.
Files changed (21) hide show
  1. langgraph_checkpointer_couchbase-2.0.0/.github/workflows/ci.yaml +95 -0
  2. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/.github/workflows/release.yaml +3 -3
  3. langgraph_checkpointer_couchbase-2.0.0/AGENTS.md +30 -0
  4. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/PKG-INFO +42 -14
  5. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/README.md +37 -8
  6. langgraph_checkpointer_couchbase-2.0.0/langgraph_checkpointer_couchbase/__about__.py +1 -0
  7. langgraph_checkpointer_couchbase-2.0.0/langgraph_checkpointer_couchbase/__init__.py +16 -0
  8. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/langgraph_checkpointer_couchbase/async_cb_saver.py +4 -3
  9. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/langgraph_checkpointer_couchbase/couchbase_saver.py +4 -3
  10. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/pyproject.toml +3 -4
  11. langgraph_checkpointer_couchbase-2.0.0/tests/test_checkpointer.py +147 -0
  12. langgraph_checkpointer_couchbase-2.0.0/tests/test_python_version_guard.py +18 -0
  13. langgraph_checkpointer_couchbase-1.0.9/langgraph_checkpointer_couchbase/__about__.py +0 -1
  14. langgraph_checkpointer_couchbase-1.0.9/langgraph_checkpointer_couchbase/__init__.py +0 -7
  15. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/.env.example +0 -0
  16. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/.gitignore +0 -0
  17. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/LICENSE +0 -0
  18. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/langgraph_checkpointer_couchbase/telemetry.py +0 -0
  19. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/langgraph_checkpointer_couchbase/utils.py +0 -0
  20. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/tests/__init__.py +0 -0
  21. {langgraph_checkpointer_couchbase-1.0.9 → langgraph_checkpointer_couchbase-2.0.0}/tests/agent_e2e_test.py +0 -0
@@ -0,0 +1,95 @@
1
+ name: CI
2
+ on:
3
+ pull_request:
4
+ push:
5
+ branches:
6
+ - main
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
19
+ services:
20
+ couchbase:
21
+ image: couchbase:enterprise-7.6.4
22
+ ports:
23
+ - 8091-8096:8091-8096
24
+ - 11210:11210
25
+ env:
26
+ CB_CLUSTER: couchbase://localhost
27
+ CB_USERNAME: Administrator
28
+ CB_PASSWORD: password
29
+ CB_BUCKET: test
30
+ CB_SCOPE: langgraph
31
+ steps:
32
+ - name: Checkout Repository
33
+ uses: actions/checkout@v7
34
+
35
+ - name: Set up Python
36
+ uses: actions/setup-python@v7
37
+ with:
38
+ python-version: ${{ matrix.python-version }}
39
+
40
+ - name: Install Dependencies
41
+ run: pip install -e . pytest pytest-asyncio hatch
42
+
43
+ - name: Initialize Couchbase
44
+ env:
45
+ CB_CONTAINER: ${{ job.services.couchbase.id }}
46
+ run: |
47
+ for i in $(seq 1 60); do
48
+ curl -sf http://localhost:8091/ui/index.html > /dev/null && break
49
+ sleep 2
50
+ done
51
+ cli() { docker exec "$CB_CONTAINER" couchbase-cli "$@" -c localhost; }
52
+ cli cluster-init --cluster-username "$CB_USERNAME" --cluster-password "$CB_PASSWORD" \
53
+ --services data,index,query --cluster-ramsize 1024 --cluster-index-ramsize 256
54
+ cli bucket-create -u "$CB_USERNAME" -p "$CB_PASSWORD" --bucket "$CB_BUCKET" \
55
+ --bucket-type couchbase --bucket-ramsize 256 --wait
56
+ cli collection-manage -u "$CB_USERNAME" -p "$CB_PASSWORD" --bucket "$CB_BUCKET" --create-scope "$CB_SCOPE"
57
+
58
+ # A freshly initialized node can reject queries for a short while.
59
+ - name: Wait for Couchbase Query Service
60
+ shell: python
61
+ run: |
62
+ import os, time
63
+ from langgraph_checkpointer_couchbase import CouchbaseSaver
64
+
65
+ deadline = time.time() + 180
66
+ while True:
67
+ try:
68
+ with CouchbaseSaver.from_conn_info(
69
+ cb_conn_str=os.environ["CB_CLUSTER"],
70
+ cb_username=os.environ["CB_USERNAME"],
71
+ cb_password=os.environ["CB_PASSWORD"],
72
+ bucket_name=os.environ["CB_BUCKET"],
73
+ scope_name=os.environ["CB_SCOPE"],
74
+ ) as saver:
75
+ saver.get_tuple({"configurable": {"thread_id": "ci-readiness"}})
76
+ print("Couchbase query service is ready")
77
+ break
78
+ except Exception as e:
79
+ if time.time() > deadline:
80
+ raise
81
+ print(f"Waiting for Couchbase: {type(e).__name__}")
82
+ time.sleep(3)
83
+
84
+ - name: Create Checkpoint Indexes
85
+ run: |
86
+ for c in checkpoints checkpoint_writes; do
87
+ curl -sf -u "$CB_USERNAME:$CB_PASSWORD" http://localhost:8093/query/service \
88
+ --data-urlencode "statement=CREATE INDEX idx_${c}_thread IF NOT EXISTS ON \`$CB_BUCKET\`.\`$CB_SCOPE\`.\`$c\`(thread_id, checkpoint_ns, checkpoint_id)"
89
+ done
90
+
91
+ - name: Run Tests
92
+ run: pytest tests/test_checkpointer.py tests/test_python_version_guard.py -v
93
+
94
+ - name: Hatch build
95
+ run: hatch build
@@ -13,10 +13,10 @@ jobs:
13
13
  contents: write
14
14
  steps:
15
15
  - name: Checkout Repository
16
- uses: actions/checkout@v3
16
+ uses: actions/checkout@v7
17
17
 
18
18
  - name: Set up Python
19
- uses: actions/setup-python@v4
19
+ uses: actions/setup-python@v7
20
20
  with:
21
21
  python-version: "3.11"
22
22
 
@@ -34,7 +34,7 @@ jobs:
34
34
 
35
35
  - name: Create GitHub Release
36
36
  id: create_release
37
- uses: softprops/action-gh-release@v2
37
+ uses: softprops/action-gh-release@v3
38
38
  with:
39
39
  tag_name: ${{ github.ref_name }}
40
40
  draft: false
@@ -0,0 +1,30 @@
1
+ # Agent Notes
2
+
3
+ - Packaging uses hatch/pip (`pyproject.toml`, `hatchling`). Do not add a lockfile or switch package managers.
4
+ - PR validation is `.github/workflows/ci.yaml`: it starts a Couchbase container, creates the bucket/scope, creates the recommended indexes, runs `pytest tests/test_checkpointer.py`, then `hatch build`.
5
+ - Local validation needs a Couchbase cluster with an existing bucket and scope (see "Running the Tests" in `README.md`). The saver creates its own collections but not indexes; without the README indexes, sequential-scan reads can miss just-written checkpoints and tests flake.
6
+ - `tests/agent_e2e_test.py` is the README agent flow and needs `OPENAI_API_KEY`; it is not run in CI.
7
+
8
+ ## Release Process
9
+
10
+ Maintainers release this package in two steps:
11
+
12
+ 1. A PR bumps `__version__` in `langgraph_checkpointer_couchbase/__about__.py`.
13
+ 2. After that PR merges, a maintainer runs the **Publish Package to PyPI** workflow (`.github/workflows/release.yaml`) from the Actions tab on `main`. It builds with hatch, publishes to PyPI, and creates a GitHub release. Pushing a `v*` tag also triggers it.
14
+
15
+ Agents prepare step 1 only. Do not create tags or releases, and do not dispatch the release workflow.
16
+
17
+ ### Version bumps in dependency upgrade PRs
18
+
19
+ Any PR that changes `dependencies` or `requires-python` in `pyproject.toml`, or changes code to follow a dependency upgrade, must bump `__version__` in the same PR. The maintainer can then approve, merge, and run the release workflow without a separate bump PR.
20
+
21
+ - Check the current release first (`https://pypi.org/pypi/langgraph-checkpointer-couchbase/json`) and bump from that version. Never reuse a published version.
22
+ - Pick the level with semver:
23
+ - **Major** (`X.0.0`): the release stops working for some current users, for example raising `requires-python`, moving a dependency floor across a major version, or changing public API in an incompatible way.
24
+ - **Minor** (`x.Y.0`): new backward-compatible features.
25
+ - **Patch** (`x.y.Z`): backward-compatible dependency floor raises and bug fixes.
26
+ - For a major bump, fence the incompatibility so users get a clear error instead of a confusing failure: declare it in `pyproject.toml` (`requires-python`, dependency floors, classifiers) and, where install metadata cannot enforce it, add a runtime check with a message that names the requirement.
27
+ - Put `Make new Release vX.Y.Z` at the start of the PR title so maintainers can tell release PRs apart (for example `Make new Release v1.0.10: bump langgraph floor`).
28
+ - In the PR body, state the old and new version, the bump level and the reason for it, and that a maintainer must run the release workflow after merging.
29
+
30
+ PRs that only touch CI, docs, or tests do not need a version bump.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: langgraph-checkpointer-couchbase
3
- Version: 1.0.9
3
+ Version: 2.0.0
4
4
  Project-URL: Documentation, https://github.com/couchbase-ecosystem/langgraph-checkpointer-couchbase#readme
5
5
  Project-URL: Issues, https://github.com/couchbase-ecosystem/langgraph-checkpointer-couchbase/issues
6
6
  Project-URL: Source, https://github.com/couchbase-ecosystem/langgraph-checkpointer-couchbase
@@ -10,15 +10,14 @@ License-File: LICENSE
10
10
  Keywords: checkpointer,couchbase,langchain,langgraph,persistence
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Programming Language :: Python
13
- Classifier: Programming Language :: Python :: 3.8
14
- Classifier: Programming Language :: Python :: 3.9
15
13
  Classifier: Programming Language :: Python :: 3.10
16
14
  Classifier: Programming Language :: Python :: 3.11
17
15
  Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
18
17
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
18
  Classifier: Programming Language :: Python :: Implementation :: PyPy
20
- Requires-Python: >=3.8
21
- Requires-Dist: couchbase>=4.5.0
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: couchbase>=4.6.3
22
21
  Requires-Dist: langchain-openai>=1.1.3
23
22
  Requires-Dist: langchain>=1.1.3
24
23
  Requires-Dist: langgraph>=1.0.5
@@ -50,10 +49,11 @@ pip install langgraph-checkpointer-couchbase
50
49
 
51
50
  ## Requirements
52
51
 
53
- - Python 3.8+
52
+ - Python 3.10+
54
53
  - Couchbase Server (7.0+ recommended)
55
- - LangGraph 0.3.22+
56
- - LangChain OpenAI 0.3.11+
54
+ - Couchbase Python SDK 4.6.3+
55
+ - LangGraph 1.0.5+
56
+ - LangChain 1.1.3+ and LangChain OpenAI 1.1.3+
57
57
 
58
58
  ## Prerequisites
59
59
 
@@ -67,6 +67,7 @@ First, set up your agent tools and model:
67
67
 
68
68
  ```python
69
69
  from typing import Literal
70
+ from langchain_core.tools import tool
70
71
  from langchain_openai import ChatOpenAI
71
72
 
72
73
  @tool
@@ -81,7 +82,7 @@ def get_weather(city: Literal["nyc", "sf"]):
81
82
 
82
83
 
83
84
  tools = [get_weather]
84
- model = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
85
+ model = ChatOpenAI(model="gpt-5-mini", temperature=0)
85
86
  ```
86
87
 
87
88
  ### Synchronous Usage
@@ -89,7 +90,7 @@ model = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
89
90
  ```python
90
91
  import os
91
92
  from langgraph_checkpointer_couchbase import CouchbaseSaver
92
- from langgraph.graph import create_react_agent
93
+ from langchain.agents import create_agent
93
94
 
94
95
  with CouchbaseSaver.from_conn_info(
95
96
  cb_conn_str=os.getenv("CB_CLUSTER") or "couchbase://localhost",
@@ -99,7 +100,7 @@ with CouchbaseSaver.from_conn_info(
99
100
  scope_name=os.getenv("CB_SCOPE") or "langgraph",
100
101
  ) as checkpointer:
101
102
  # Create the agent with checkpointing
102
- graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
103
+ graph = create_agent(model, tools=tools, checkpointer=checkpointer)
103
104
 
104
105
  # Configure with a unique thread ID
105
106
  config = {"configurable": {"thread_id": "1"}}
@@ -125,7 +126,7 @@ from acouchbase.cluster import Cluster as ACluster
125
126
  from couchbase.auth import PasswordAuthenticator
126
127
  from couchbase.options import ClusterOptions
127
128
  from langgraph_checkpointer_couchbase import AsyncCouchbaseSaver
128
- from langgraph.graph import create_react_agent
129
+ from langchain.agents import create_agent
129
130
 
130
131
  auth = PasswordAuthenticator(
131
132
  os.getenv("CB_USERNAME") or "Administrator",
@@ -143,7 +144,7 @@ async with AsyncCouchbaseSaver.from_cluster(
143
144
  scope_name=scope_name,
144
145
  ) as checkpointer:
145
146
  # Create the agent with checkpointing
146
- graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
147
+ graph = create_agent(model, tools=tools, checkpointer=checkpointer)
147
148
 
148
149
  # Configure with a unique thread ID
149
150
  config = {"configurable": {"thread_id": "2"}}
@@ -166,6 +167,17 @@ async with AsyncCouchbaseSaver.from_cluster(
166
167
  await cluster.close()
167
168
  ```
168
169
 
170
+ ## Recommended Indexes
171
+
172
+ The saver creates the `checkpoints` and `checkpoint_writes` collections on first use, but not indexes. Without them, queries fall back to sequential scans, which are slow on large collections and may not return a checkpoint written immediately before. Create these indexes once per scope (replace `test` and `langgraph` with your bucket and scope):
173
+
174
+ ```sql
175
+ CREATE INDEX idx_checkpoints_thread IF NOT EXISTS
176
+ ON `test`.`langgraph`.`checkpoints`(thread_id, checkpoint_ns, checkpoint_id);
177
+ CREATE INDEX idx_checkpoint_writes_thread IF NOT EXISTS
178
+ ON `test`.`langgraph`.`checkpoint_writes`(thread_id, checkpoint_ns, checkpoint_id);
179
+ ```
180
+
169
181
  ## Configuration Options
170
182
 
171
183
  | Parameter | Description | Default |
@@ -176,6 +188,22 @@ await cluster.close()
176
188
  | CB_BUCKET | Bucket to store checkpoints | test |
177
189
  | CB_SCOPE | Scope within bucket | langgraph |
178
190
 
191
+ ## Running the Tests
192
+
193
+ The checkpointer tests run against a live Couchbase cluster and do not need an LLM. Create the bucket, scope, and the [recommended indexes](#recommended-indexes) first, then:
194
+
195
+ ```bash
196
+ pip install -e . pytest pytest-asyncio
197
+ export CB_CLUSTER=couchbase://localhost CB_USERNAME=Administrator CB_PASSWORD=password CB_BUCKET=test CB_SCOPE=langgraph
198
+ pytest tests/test_checkpointer.py
199
+ ```
200
+
201
+ The end-to-end agent example in `tests/agent_e2e_test.py` additionally requires `OPENAI_API_KEY`:
202
+
203
+ ```bash
204
+ python tests/agent_e2e_test.py
205
+ ```
206
+
179
207
  ## Usage Data
180
208
 
181
209
  This product automatically collects usage and performance data (such as product name and version) and browser information (such as IP address) (collectively, "Usage Data"). Couchbase uses Usage Data, along with other data you may provide to Couchbase (such as your user name or email address), to develop and improve our products as well as inform our sales and marketing programs. We do not access or collect any data you store in Couchbase products. We use Usage Data to understand aggregate usage patterns and make our products more useful to you. For more information on how Couchbase collects, protects, and processes information, please refer to the Couchbase Privacy Policy viewable at https://www.couchbase.com/privacy-policy.
@@ -20,10 +20,11 @@ pip install langgraph-checkpointer-couchbase
20
20
 
21
21
  ## Requirements
22
22
 
23
- - Python 3.8+
23
+ - Python 3.10+
24
24
  - Couchbase Server (7.0+ recommended)
25
- - LangGraph 0.3.22+
26
- - LangChain OpenAI 0.3.11+
25
+ - Couchbase Python SDK 4.6.3+
26
+ - LangGraph 1.0.5+
27
+ - LangChain 1.1.3+ and LangChain OpenAI 1.1.3+
27
28
 
28
29
  ## Prerequisites
29
30
 
@@ -37,6 +38,7 @@ First, set up your agent tools and model:
37
38
 
38
39
  ```python
39
40
  from typing import Literal
41
+ from langchain_core.tools import tool
40
42
  from langchain_openai import ChatOpenAI
41
43
 
42
44
  @tool
@@ -51,7 +53,7 @@ def get_weather(city: Literal["nyc", "sf"]):
51
53
 
52
54
 
53
55
  tools = [get_weather]
54
- model = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
56
+ model = ChatOpenAI(model="gpt-5-mini", temperature=0)
55
57
  ```
56
58
 
57
59
  ### Synchronous Usage
@@ -59,7 +61,7 @@ model = ChatOpenAI(model_name="gpt-4o-mini", temperature=0)
59
61
  ```python
60
62
  import os
61
63
  from langgraph_checkpointer_couchbase import CouchbaseSaver
62
- from langgraph.graph import create_react_agent
64
+ from langchain.agents import create_agent
63
65
 
64
66
  with CouchbaseSaver.from_conn_info(
65
67
  cb_conn_str=os.getenv("CB_CLUSTER") or "couchbase://localhost",
@@ -69,7 +71,7 @@ with CouchbaseSaver.from_conn_info(
69
71
  scope_name=os.getenv("CB_SCOPE") or "langgraph",
70
72
  ) as checkpointer:
71
73
  # Create the agent with checkpointing
72
- graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
74
+ graph = create_agent(model, tools=tools, checkpointer=checkpointer)
73
75
 
74
76
  # Configure with a unique thread ID
75
77
  config = {"configurable": {"thread_id": "1"}}
@@ -95,7 +97,7 @@ from acouchbase.cluster import Cluster as ACluster
95
97
  from couchbase.auth import PasswordAuthenticator
96
98
  from couchbase.options import ClusterOptions
97
99
  from langgraph_checkpointer_couchbase import AsyncCouchbaseSaver
98
- from langgraph.graph import create_react_agent
100
+ from langchain.agents import create_agent
99
101
 
100
102
  auth = PasswordAuthenticator(
101
103
  os.getenv("CB_USERNAME") or "Administrator",
@@ -113,7 +115,7 @@ async with AsyncCouchbaseSaver.from_cluster(
113
115
  scope_name=scope_name,
114
116
  ) as checkpointer:
115
117
  # Create the agent with checkpointing
116
- graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)
118
+ graph = create_agent(model, tools=tools, checkpointer=checkpointer)
117
119
 
118
120
  # Configure with a unique thread ID
119
121
  config = {"configurable": {"thread_id": "2"}}
@@ -136,6 +138,17 @@ async with AsyncCouchbaseSaver.from_cluster(
136
138
  await cluster.close()
137
139
  ```
138
140
 
141
+ ## Recommended Indexes
142
+
143
+ The saver creates the `checkpoints` and `checkpoint_writes` collections on first use, but not indexes. Without them, queries fall back to sequential scans, which are slow on large collections and may not return a checkpoint written immediately before. Create these indexes once per scope (replace `test` and `langgraph` with your bucket and scope):
144
+
145
+ ```sql
146
+ CREATE INDEX idx_checkpoints_thread IF NOT EXISTS
147
+ ON `test`.`langgraph`.`checkpoints`(thread_id, checkpoint_ns, checkpoint_id);
148
+ CREATE INDEX idx_checkpoint_writes_thread IF NOT EXISTS
149
+ ON `test`.`langgraph`.`checkpoint_writes`(thread_id, checkpoint_ns, checkpoint_id);
150
+ ```
151
+
139
152
  ## Configuration Options
140
153
 
141
154
  | Parameter | Description | Default |
@@ -146,6 +159,22 @@ await cluster.close()
146
159
  | CB_BUCKET | Bucket to store checkpoints | test |
147
160
  | CB_SCOPE | Scope within bucket | langgraph |
148
161
 
162
+ ## Running the Tests
163
+
164
+ The checkpointer tests run against a live Couchbase cluster and do not need an LLM. Create the bucket, scope, and the [recommended indexes](#recommended-indexes) first, then:
165
+
166
+ ```bash
167
+ pip install -e . pytest pytest-asyncio
168
+ export CB_CLUSTER=couchbase://localhost CB_USERNAME=Administrator CB_PASSWORD=password CB_BUCKET=test CB_SCOPE=langgraph
169
+ pytest tests/test_checkpointer.py
170
+ ```
171
+
172
+ The end-to-end agent example in `tests/agent_e2e_test.py` additionally requires `OPENAI_API_KEY`:
173
+
174
+ ```bash
175
+ python tests/agent_e2e_test.py
176
+ ```
177
+
149
178
  ## Usage Data
150
179
 
151
180
  This product automatically collects usage and performance data (such as product name and version) and browser information (such as IP address) (collectively, "Usage Data"). Couchbase uses Usage Data, along with other data you may provide to Couchbase (such as your user name or email address), to develop and improve our products as well as inform our sales and marketing programs. We do not access or collect any data you store in Couchbase products. We use Usage Data to understand aggregate usage patterns and make our products more useful to you. For more information on how Couchbase collects, protects, and processes information, please refer to the Couchbase Privacy Policy viewable at https://www.couchbase.com/privacy-policy.
@@ -0,0 +1,16 @@
1
+ import sys
2
+
3
+ if sys.version_info < (3, 10):
4
+ raise ImportError(
5
+ "langgraph-checkpointer-couchbase 2.x requires Python 3.10 or newer "
6
+ f"(found {sys.version_info[0]}.{sys.version_info[1]}). "
7
+ "Upgrade Python, or pin langgraph-checkpointer-couchbase<2 to stay on an older release."
8
+ )
9
+
10
+ from .async_cb_saver import AsyncCouchbaseSaver
11
+ from .couchbase_saver import CouchbaseSaver
12
+
13
+ __all__ = ["CouchbaseSaver", "AsyncCouchbaseSaver"]
14
+
15
+ # --- Package-level telemetry (non-blocking, fire-and-forget) ---
16
+ import langgraph_checkpointer_couchbase.telemetry # noqa: F401, E402
@@ -9,6 +9,7 @@ from acouchbase.bucket import Bucket as ABucket
9
9
  from couchbase.auth import PasswordAuthenticator
10
10
  from couchbase.options import ClusterOptions, QueryOptions, UpsertOptions
11
11
  from couchbase.exceptions import CollectionAlreadyExistsException
12
+ from couchbase.n1ql import QueryScanConsistency
12
13
 
13
14
  from langgraph.checkpoint.base import (
14
15
  BaseCheckpointSaver,
@@ -179,7 +180,7 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
179
180
  query = f'SELECT * FROM `{self.bucket_name}`.`{self.scope_name}`.`{self.checkpoints_collection_name}` WHERE thread_id = $1 AND checkpoint_ns = $2 ORDER BY checkpoint_id DESC LIMIT 1'
180
181
  query_params = [thread_id, checkpoint_ns]
181
182
 
182
- result = self.cluster.query(query, QueryOptions(positional_parameters=query_params))
183
+ result = self.cluster.query(query, QueryOptions(positional_parameters=query_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
183
184
 
184
185
  async for row in result:
185
186
  doc = row[self.checkpoints_collection_name]
@@ -193,7 +194,7 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
193
194
  serialized_writes_query = f'SELECT * FROM `{self.bucket_name}`.`{self.scope_name}`.`{self.checkpoint_writes_collection_name}` WHERE thread_id = $1 AND checkpoint_ns = $2 AND checkpoint_id = $3'
194
195
  serialized_writes_params = [thread_id, checkpoint_ns, doc["checkpoint_id"] or ""]
195
196
 
196
- serialized_writes_result = self.cluster.query(serialized_writes_query, QueryOptions(positional_parameters=serialized_writes_params))
197
+ serialized_writes_result = self.cluster.query(serialized_writes_query, QueryOptions(positional_parameters=serialized_writes_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
197
198
 
198
199
  pending_writes = []
199
200
  async for write_doc in serialized_writes_result:
@@ -273,7 +274,7 @@ class AsyncCouchbaseSaver(BaseCheckpointSaver):
273
274
  if limit is not None:
274
275
  query += f" LIMIT {limit}"
275
276
 
276
- result = self.cluster.query(query, QueryOptions(positional_parameters=query_params))
277
+ result = self.cluster.query(query, QueryOptions(positional_parameters=query_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
277
278
 
278
279
  async for row in result:
279
280
  doc = row[self.checkpoints_collection_name]
@@ -9,6 +9,7 @@ from couchbase.bucket import Bucket
9
9
  from couchbase.auth import PasswordAuthenticator
10
10
  from couchbase.options import ClusterOptions, QueryOptions, UpsertOptions
11
11
  from couchbase.exceptions import CollectionAlreadyExistsException
12
+ from couchbase.n1ql import QueryScanConsistency
12
13
 
13
14
  from langgraph.checkpoint.base import (
14
15
  BaseCheckpointSaver,
@@ -163,7 +164,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
163
164
  query = f'SELECT * FROM `{self.bucket_name}`.`{self.scope_name}`.`{self.checkpoints_collection_name}` WHERE thread_id = $1 AND checkpoint_ns = $2 ORDER BY checkpoint_id DESC LIMIT 1'
164
165
  query_params = [thread_id, checkpoint_ns]
165
166
 
166
- result = self.cluster.query(query, QueryOptions(positional_parameters=query_params))
167
+ result = self.cluster.query(query, QueryOptions(positional_parameters=query_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
167
168
 
168
169
  for row in result:
169
170
  doc = row[self.checkpoints_collection_name]
@@ -179,7 +180,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
179
180
 
180
181
  serialized_writes_query = f'SELECT * FROM `{self.bucket_name}`.`{self.scope_name}`.`{self.checkpoint_writes_collection_name}` WHERE thread_id = $1 AND checkpoint_ns = $2 AND checkpoint_id = $3'
181
182
  serialized_writes_params = [thread_id, checkpoint_ns, doc["checkpoint_id"] or ""]
182
- serialized_writes_result = self.cluster.query(serialized_writes_query, QueryOptions(positional_parameters=serialized_writes_params))
183
+ serialized_writes_result = self.cluster.query(serialized_writes_query, QueryOptions(positional_parameters=serialized_writes_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
183
184
 
184
185
  pending_writes = []
185
186
  for write_doc in serialized_writes_result:
@@ -265,7 +266,7 @@ class CouchbaseSaver(BaseCheckpointSaver):
265
266
  if limit is not None:
266
267
  query += f" LIMIT {limit}"
267
268
 
268
- result = self.cluster.query(query, QueryOptions(positional_parameters=query_params))
269
+ result = self.cluster.query(query, QueryOptions(positional_parameters=query_params, scan_consistency=QueryScanConsistency.REQUEST_PLUS))
269
270
 
270
271
  for row in result:
271
272
  doc = row[self.checkpoints_collection_name]
@@ -7,7 +7,7 @@ name = "langgraph-checkpointer-couchbase"
7
7
  dynamic = ["version"]
8
8
  description = ''
9
9
  readme = "README.md"
10
- requires-python = ">=3.8"
10
+ requires-python = ">=3.10"
11
11
  license = "MIT"
12
12
  keywords = ["langchain","langgraph", "couchbase", "checkpointer", "persistence"]
13
13
  authors = [
@@ -16,16 +16,15 @@ authors = [
16
16
  classifiers = [
17
17
  "Development Status :: 4 - Beta",
18
18
  "Programming Language :: Python",
19
- "Programming Language :: Python :: 3.8",
20
- "Programming Language :: Python :: 3.9",
21
19
  "Programming Language :: Python :: 3.10",
22
20
  "Programming Language :: Python :: 3.11",
23
21
  "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
24
23
  "Programming Language :: Python :: Implementation :: CPython",
25
24
  "Programming Language :: Python :: Implementation :: PyPy",
26
25
  ]
27
26
  dependencies = [
28
- "couchbase>=4.5.0",
27
+ "couchbase>=4.6.3",
29
28
  "langgraph>=1.0.5",
30
29
  "langchain-openai>=1.1.3",
31
30
  "pydantic>=2.12.5",
@@ -0,0 +1,147 @@
1
+ """Checkpointer integration tests against a live Couchbase cluster.
2
+
3
+ These tests do not need an LLM. They exercise CouchbaseSaver and
4
+ AsyncCouchbaseSaver directly and through a minimal LangGraph StateGraph.
5
+
6
+ Required environment variables: CB_CLUSTER, CB_USERNAME, CB_PASSWORD,
7
+ CB_BUCKET, CB_SCOPE. The bucket and scope must already exist; the saver
8
+ creates its collections. Create the indexes listed in the README so reads
9
+ see checkpoints written immediately before.
10
+ """
11
+ import operator
12
+ import os
13
+ import uuid
14
+ from typing import Annotated, TypedDict
15
+
16
+ import pytest
17
+ from langgraph.checkpoint.base import empty_checkpoint
18
+ from langgraph.graph import END, START, StateGraph
19
+
20
+ from langgraph_checkpointer_couchbase import AsyncCouchbaseSaver, CouchbaseSaver
21
+
22
+ REQUIRED_ENV = ["CB_CLUSTER", "CB_USERNAME", "CB_PASSWORD", "CB_BUCKET", "CB_SCOPE"]
23
+
24
+ pytestmark = pytest.mark.skipif(
25
+ any(not os.getenv(name) for name in REQUIRED_ENV),
26
+ reason=f"Couchbase connection not configured ({', '.join(REQUIRED_ENV)})",
27
+ )
28
+
29
+
30
+ def conn_info():
31
+ return dict(
32
+ cb_conn_str=os.environ["CB_CLUSTER"],
33
+ cb_username=os.environ["CB_USERNAME"],
34
+ cb_password=os.environ["CB_PASSWORD"],
35
+ bucket_name=os.environ["CB_BUCKET"],
36
+ scope_name=os.environ["CB_SCOPE"],
37
+ )
38
+
39
+
40
+ def new_thread_config():
41
+ return {"configurable": {"thread_id": f"test-{uuid.uuid4()}", "checkpoint_ns": ""}}
42
+
43
+
44
+ def make_checkpoint(step):
45
+ checkpoint = empty_checkpoint()
46
+ checkpoint["channel_values"] = {"step": step}
47
+ checkpoint["channel_versions"] = {"step": step}
48
+ return checkpoint
49
+
50
+
51
+ class State(TypedDict):
52
+ items: Annotated[list, operator.add]
53
+
54
+
55
+ def build_graph(checkpointer):
56
+ builder = StateGraph(State)
57
+ builder.add_node("first", lambda state: {"items": ["first"]})
58
+ builder.add_node("second", lambda state: {"items": ["second"]})
59
+ builder.add_edge(START, "first")
60
+ builder.add_edge("first", "second")
61
+ builder.add_edge("second", END)
62
+ return builder.compile(checkpointer=checkpointer)
63
+
64
+
65
+ def test_sync_put_get_list_and_writes():
66
+ with CouchbaseSaver.from_conn_info(**conn_info()) as saver:
67
+ config = new_thread_config()
68
+ assert saver.get_tuple(config) is None
69
+
70
+ first = make_checkpoint(1)
71
+ first_config = saver.put(config, first, {"source": "input", "step": 1}, {})
72
+ second = make_checkpoint(2)
73
+ second_config = saver.put(first_config, second, {"source": "loop", "step": 2}, {})
74
+ saver.put_writes(second_config, [("items", "a"), ("items", "b")], "task-1")
75
+
76
+ latest = saver.get_tuple(config)
77
+ assert latest.config["configurable"]["checkpoint_id"] == second["id"]
78
+ assert latest.checkpoint["channel_values"] == {"step": 2}
79
+ assert latest.metadata == {"source": "loop", "step": 2}
80
+ assert latest.parent_config["configurable"]["checkpoint_id"] == first["id"]
81
+ assert sorted(latest.pending_writes) == [("task-1", "items", "a"), ("task-1", "items", "b")]
82
+
83
+ by_id = saver.get_tuple(first_config)
84
+ assert by_id.checkpoint["id"] == first["id"]
85
+ assert by_id.parent_config is None
86
+
87
+ listed = list(saver.list(config))
88
+ assert [t.checkpoint["id"] for t in listed] == [second["id"], first["id"]]
89
+ assert [t.checkpoint["id"] for t in saver.list(config, limit=1)] == [second["id"]]
90
+ assert [t.checkpoint["id"] for t in saver.list(config, before=second_config)] == [first["id"]]
91
+
92
+
93
+ @pytest.mark.asyncio
94
+ async def test_async_put_get_list_and_writes():
95
+ async with AsyncCouchbaseSaver.from_conn_info(**conn_info()) as saver:
96
+ config = new_thread_config()
97
+ assert await saver.aget_tuple(config) is None
98
+
99
+ first = make_checkpoint(1)
100
+ first_config = await saver.aput(config, first, {"source": "input", "step": 1}, {})
101
+ second = make_checkpoint(2)
102
+ second_config = await saver.aput(first_config, second, {"source": "loop", "step": 2}, {})
103
+ await saver.aput_writes(second_config, [("items", "a")], "task-1")
104
+
105
+ latest = await saver.aget_tuple(config)
106
+ assert latest.config["configurable"]["checkpoint_id"] == second["id"]
107
+ assert latest.checkpoint["channel_values"] == {"step": 2}
108
+ assert latest.metadata == {"source": "loop", "step": 2}
109
+ assert latest.parent_config["configurable"]["checkpoint_id"] == first["id"]
110
+ assert latest.pending_writes == [("task-1", "items", "a")]
111
+
112
+ listed = [t async for t in saver.alist(config)]
113
+ assert [t.checkpoint["id"] for t in listed] == [second["id"], first["id"]]
114
+ limited = [t async for t in saver.alist(config, limit=1)]
115
+ assert [t.checkpoint["id"] for t in limited] == [second["id"]]
116
+
117
+
118
+ def test_sync_graph_persists_state():
119
+ with CouchbaseSaver.from_conn_info(**conn_info()) as saver:
120
+ graph = build_graph(saver)
121
+ config = new_thread_config()
122
+
123
+ result = graph.invoke({"items": ["start"]}, config)
124
+ assert result == {"items": ["start", "first", "second"]}
125
+
126
+ state = graph.get_state(config)
127
+ assert state.values == {"items": ["start", "first", "second"]}
128
+ assert len(list(graph.get_state_history(config))) >= 3
129
+
130
+ # A second run on the same thread continues from the persisted state.
131
+ result = graph.invoke({"items": ["again"]}, config)
132
+ assert result["items"] == ["start", "first", "second", "again", "first", "second"]
133
+
134
+
135
+ @pytest.mark.asyncio
136
+ async def test_async_graph_persists_state():
137
+ async with AsyncCouchbaseSaver.from_conn_info(**conn_info()) as saver:
138
+ graph = build_graph(saver)
139
+ config = new_thread_config()
140
+
141
+ result = await graph.ainvoke({"items": ["start"]}, config)
142
+ assert result == {"items": ["start", "first", "second"]}
143
+
144
+ state = await graph.aget_state(config)
145
+ assert state.values == {"items": ["start", "first", "second"]}
146
+ history = [s async for s in graph.aget_state_history(config)]
147
+ assert len(history) >= 3
@@ -0,0 +1,18 @@
1
+ """The package refuses to import on Python versions older than 3.10."""
2
+ import importlib
3
+ import sys
4
+
5
+ import pytest
6
+
7
+ import langgraph_checkpointer_couchbase
8
+
9
+
10
+ def test_import_fails_with_clear_error_on_old_python(monkeypatch):
11
+ monkeypatch.setattr(sys, "version_info", (3, 9, 18, "final", 0))
12
+ with pytest.raises(ImportError, match="requires Python 3.10 or newer"):
13
+ importlib.reload(langgraph_checkpointer_couchbase)
14
+
15
+
16
+ def test_import_succeeds_on_supported_python():
17
+ importlib.reload(langgraph_checkpointer_couchbase)
18
+ assert langgraph_checkpointer_couchbase.CouchbaseSaver
@@ -1,7 +0,0 @@
1
- from .async_cb_saver import AsyncCouchbaseSaver
2
- from .couchbase_saver import CouchbaseSaver
3
-
4
- __all__ = ["CouchbaseSaver", "AsyncCouchbaseSaver"]
5
-
6
- # --- Package-level telemetry (non-blocking, fire-and-forget) ---
7
- import langgraph_checkpointer_couchbase.telemetry # noqa: F401, E402