langgraph-store-cosmosdb 0.1.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.
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ .venv/
6
+ dist/
7
+ build/
8
+ uv.lock
9
+ .env
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.5
2
+ Name: langgraph-store-cosmosdb
3
+ Version: 0.1.0
4
+ Summary: Azure CosmosDB long-term-memory store (BaseStore) for LangGraph — namespaced key/value memory with filters, list_namespaces and native semantic (Cosmos DB vector) search
5
+ Project-URL: Homepage, https://github.com/skamalj/langgraph-store
6
+ Project-URL: Repository, https://github.com/skamalj/langgraph-store.git
7
+ Author-email: Kamal <skamalj@gmail.com>
8
+ Keywords: agent-memory,azure,basestore,cosmosdb,langgraph,long-term-memory,store
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: azure-cosmos
15
+ Requires-Dist: langgraph-store-core>=0.1.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ # langgraph-store-cosmosdb
19
+
20
+ An **Azure Cosmos DB (NoSQL)** long-term-memory store (`BaseStore`) for [LangGraph](https://langchain-ai.github.io/langgraph/) — namespaced key/value memory with prefix search, filters, `list_namespaces`, and **native semantic search via Cosmos DB vector search**.
21
+
22
+ ```bash
23
+ pip install langgraph-store-cosmosdb
24
+ ```
25
+
26
+ ```python
27
+ from langgraph_store_cosmosdb import CosmosDBStore
28
+
29
+ store = CosmosDBStore(
30
+ endpoint="https://<acct>.documents.azure.com:443/", key="<key>",
31
+ database_name="langgraph", container_name="store",
32
+ )
33
+ store.put(("users", "1", "memories"), "food", {"text": "loves sushi", "kind": "pref"})
34
+ item = store.get(("users", "1", "memories"), "food")
35
+ hits = store.search(("users", "1"), filter={"kind": "pref"})
36
+ ```
37
+
38
+ Use it as a LangGraph store: `graph.compile(store=CosmosDBStore(...))`. Async methods work too (sync calls run in a thread).
39
+
40
+ ## Semantic search (Cosmos DB vector search)
41
+
42
+ Pass a LangGraph `IndexConfig` and the store embeds the configured fields on `put` and ranks `search(query=...)` by cosine similarity using `VectorDistance` in the container's SQL:
43
+
44
+ ```python
45
+ from langgraph_store_core import bedrock_titan_embeddings
46
+ from langgraph_store_cosmosdb import CosmosDBStore
47
+
48
+ store = CosmosDBStore(endpoint=..., key=..., database_name="langgraph", container_name="memory",
49
+ index={"dims": 1024, "embed": bedrock_titan_embeddings(dimensions=1024), "fields": ["text"]})
50
+ store.put(("memories", "kamal"), "k1", {"text": "the user loves sushi", "kind": "pref"})
51
+ hits = store.search(("memories", "kamal"), query="what food does the user like?", filter={"kind": "pref"})
52
+ print(hits[0].score, hits[0].value)
53
+ ```
54
+
55
+ `embed` may be any LangChain `Embeddings`, a `list[str] -> list[list[float]]` callable, or a provider string. `fields` defaults to `["$"]` (whole value as JSON). `put(..., index=False)` skips embedding for one item.
56
+
57
+ With `index` the container is created with a vector embedding policy on `/embedding` (cosine, `dims`) and a `diskANN` vector index (`vector_index_type="quantizedFlat"` / `"flat"` to change). The policy must be set at container creation, so use a **new container name** when enabling semantic search on an existing store. The account needs the **Vector Search for NoSQL** capability enabled (`az cosmosdb update ... --capabilities EnableNoSQLVectorSearch`, keeping any existing capabilities). Namespace prefix and plain-equality filters run inside the query; operator filters (`$gt`, `$in`, …) are applied on the returned candidates.
58
+
59
+ ## Data model
60
+
61
+ A container partitioned by `/prefix` (the namespace), each item `{id, prefix, key, value, created_at, updated_at[, embedding]}` (`id` is the URL-encoded key). Database and container are auto-created under key-based auth. Search is a `STARTSWITH` prefix query; filters and namespace matching are evaluated in the [core](https://pypi.org/project/langgraph-store-core/).
62
+
63
+ Docs: <https://skamalj.github.io/agentstate-reducer/>
64
+
65
+ ## License
66
+
67
+ MIT
@@ -0,0 +1,50 @@
1
+ # langgraph-store-cosmosdb
2
+
3
+ An **Azure Cosmos DB (NoSQL)** long-term-memory store (`BaseStore`) for [LangGraph](https://langchain-ai.github.io/langgraph/) — namespaced key/value memory with prefix search, filters, `list_namespaces`, and **native semantic search via Cosmos DB vector search**.
4
+
5
+ ```bash
6
+ pip install langgraph-store-cosmosdb
7
+ ```
8
+
9
+ ```python
10
+ from langgraph_store_cosmosdb import CosmosDBStore
11
+
12
+ store = CosmosDBStore(
13
+ endpoint="https://<acct>.documents.azure.com:443/", key="<key>",
14
+ database_name="langgraph", container_name="store",
15
+ )
16
+ store.put(("users", "1", "memories"), "food", {"text": "loves sushi", "kind": "pref"})
17
+ item = store.get(("users", "1", "memories"), "food")
18
+ hits = store.search(("users", "1"), filter={"kind": "pref"})
19
+ ```
20
+
21
+ Use it as a LangGraph store: `graph.compile(store=CosmosDBStore(...))`. Async methods work too (sync calls run in a thread).
22
+
23
+ ## Semantic search (Cosmos DB vector search)
24
+
25
+ Pass a LangGraph `IndexConfig` and the store embeds the configured fields on `put` and ranks `search(query=...)` by cosine similarity using `VectorDistance` in the container's SQL:
26
+
27
+ ```python
28
+ from langgraph_store_core import bedrock_titan_embeddings
29
+ from langgraph_store_cosmosdb import CosmosDBStore
30
+
31
+ store = CosmosDBStore(endpoint=..., key=..., database_name="langgraph", container_name="memory",
32
+ index={"dims": 1024, "embed": bedrock_titan_embeddings(dimensions=1024), "fields": ["text"]})
33
+ store.put(("memories", "kamal"), "k1", {"text": "the user loves sushi", "kind": "pref"})
34
+ hits = store.search(("memories", "kamal"), query="what food does the user like?", filter={"kind": "pref"})
35
+ print(hits[0].score, hits[0].value)
36
+ ```
37
+
38
+ `embed` may be any LangChain `Embeddings`, a `list[str] -> list[list[float]]` callable, or a provider string. `fields` defaults to `["$"]` (whole value as JSON). `put(..., index=False)` skips embedding for one item.
39
+
40
+ With `index` the container is created with a vector embedding policy on `/embedding` (cosine, `dims`) and a `diskANN` vector index (`vector_index_type="quantizedFlat"` / `"flat"` to change). The policy must be set at container creation, so use a **new container name** when enabling semantic search on an existing store. The account needs the **Vector Search for NoSQL** capability enabled (`az cosmosdb update ... --capabilities EnableNoSQLVectorSearch`, keeping any existing capabilities). Namespace prefix and plain-equality filters run inside the query; operator filters (`$gt`, `$in`, …) are applied on the returned candidates.
41
+
42
+ ## Data model
43
+
44
+ A container partitioned by `/prefix` (the namespace), each item `{id, prefix, key, value, created_at, updated_at[, embedding]}` (`id` is the URL-encoded key). Database and container are auto-created under key-based auth. Search is a `STARTSWITH` prefix query; filters and namespace matching are evaluated in the [core](https://pypi.org/project/langgraph-store-core/).
45
+
46
+ Docs: <https://skamalj.github.io/agentstate-reducer/>
47
+
48
+ ## License
49
+
50
+ MIT
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "langgraph-store-cosmosdb"
7
+ version = "0.1.0"
8
+ description = "Azure CosmosDB long-term-memory store (BaseStore) for LangGraph — namespaced key/value memory with filters, list_namespaces and native semantic (Cosmos DB vector) search"
9
+ authors = [{name = "Kamal", email = "skamalj@gmail.com"}]
10
+ readme = "README.md"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "langgraph-store-core>=0.1.0",
14
+ "azure-cosmos",
15
+ ]
16
+ keywords = ["langgraph", "store", "basestore", "cosmosdb", "azure", "long-term-memory", "agent-memory"]
17
+ classifiers = [
18
+ "Programming Language :: Python :: 3",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/skamalj/langgraph-store"
26
+ Repository = "https://github.com/skamalj/langgraph-store.git"
27
+
28
+ [dependency-groups]
29
+ dev = ["pytest>=7.0", "pytest-asyncio"]
30
+
31
+ [tool.pytest.ini_options]
32
+ asyncio_mode = "auto"
33
+
34
+ [tool.hatch.build.targets.wheel]
35
+ packages = ["src/langgraph_store_cosmosdb"]
@@ -0,0 +1,161 @@
1
+ """Azure CosmosDB implementation of the LangGraph long-term-memory ``BaseStore``.
2
+
3
+ ``CosmosDBStore`` persists namespaced key/value memory in a container partitioned
4
+ by namespace, and supports get / put / delete / search (namespace-prefix + filter)
5
+ and ``list_namespaces``. With an ``IndexConfig`` it also does **semantic search**
6
+ natively via Cosmos DB NoSQL vector search (``VectorDistance``, cosine).
7
+ Built on ``langgraph-store-core``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import List, Optional, Tuple
13
+ from urllib.parse import quote
14
+
15
+ from azure.cosmos import CosmosClient, PartitionKey
16
+ from azure.cosmos.exceptions import CosmosResourceNotFoundError
17
+
18
+ from langgraph_store_core import IndexConfig, KVStore
19
+
20
+ __all__ = ["CosmosDBStore"]
21
+
22
+ _FIELDS = ("prefix", "key", "value", "created_at", "updated_at")
23
+ _PROJECTION = ", ".join(f'c["{f}"]' for f in _FIELDS) # value/key are reserved words
24
+
25
+
26
+ class CosmosDBStore(KVStore):
27
+ """LangGraph ``BaseStore`` backed by Azure CosmosDB.
28
+
29
+ Example:
30
+ ```python
31
+ from langgraph_store_cosmosdb import CosmosDBStore
32
+
33
+ store = CosmosDBStore(
34
+ endpoint="https://<acct>.documents.azure.com:443/", key="<key>",
35
+ database_name="langgraph", container_name="store",
36
+ )
37
+ store.put(("users", "1"), "profile", {"name": "Kamal"})
38
+
39
+ # semantic search
40
+ store = CosmosDBStore(..., index={"dims": 1024, "embed": my_embedder, "fields": ["text"]})
41
+ hits = store.search(("users", "1"), query="what does the user like?")
42
+ ```
43
+
44
+ The container is partitioned by ``/prefix`` (the namespace) and auto-created
45
+ under key-based auth. With ``index`` the container is created with a vector
46
+ embedding policy on ``/embedding`` (cosine, ``dims``) and a ``diskANN``
47
+ vector index; the policy must be set at container creation, so use a new
48
+ container name when enabling semantic search on an existing store.
49
+ """
50
+
51
+ supports_native_vector_search = True
52
+
53
+ def __init__(
54
+ self,
55
+ *,
56
+ endpoint: str,
57
+ key: str,
58
+ database_name: str,
59
+ container_name: str,
60
+ index: Optional[IndexConfig] = None,
61
+ vector_index_type: str = "diskANN",
62
+ ) -> None:
63
+ super().__init__(index=index)
64
+ client = CosmosClient(endpoint, credential=key)
65
+ db = client.create_database_if_not_exists(database_name)
66
+ kwargs = {}
67
+ if self.dims:
68
+ kwargs["vector_embedding_policy"] = {
69
+ "vectorEmbeddings": [
70
+ {
71
+ "path": "/embedding",
72
+ "dataType": "float32",
73
+ "distanceFunction": "cosine",
74
+ "dimensions": self.dims,
75
+ }
76
+ ]
77
+ }
78
+ kwargs["indexing_policy"] = {
79
+ "automatic": True,
80
+ "indexingMode": "consistent",
81
+ "includedPaths": [{"path": "/*"}],
82
+ # vector paths must be excluded from the range index
83
+ "excludedPaths": [{"path": "/embedding/*"}, {"path": '/"_etag"/?'}],
84
+ "vectorIndexes": [{"path": "/embedding", "type": vector_index_type}],
85
+ }
86
+ self._container = db.create_container_if_not_exists(
87
+ id=container_name, partition_key=PartitionKey(path="/prefix"), **kwargs
88
+ )
89
+
90
+ @staticmethod
91
+ def _id(key: str) -> str:
92
+ return quote(key, safe="") # Cosmos ids may not contain # / \ ?
93
+
94
+ def _row(self, item: dict) -> dict:
95
+ row = {f: item[f] for f in _FIELDS}
96
+ row["embedding"] = item.get("embedding")
97
+ return row
98
+
99
+ def _read(self, prefix: str, key: str) -> Optional[dict]:
100
+ try:
101
+ item = self._container.read_item(item=self._id(key), partition_key=prefix)
102
+ except CosmosResourceNotFoundError:
103
+ return None
104
+ return self._row(item)
105
+
106
+ def _write(self, row: dict) -> None:
107
+ doc = {"id": self._id(row["key"]), **{f: row[f] for f in _FIELDS}}
108
+ if self.dims and row.get("embedding") is not None:
109
+ doc["embedding"] = row["embedding"]
110
+ self._container.upsert_item(doc)
111
+
112
+ def _remove(self, prefix: str, key: str) -> None:
113
+ try:
114
+ self._container.delete_item(item=self._id(key), partition_key=prefix)
115
+ except CosmosResourceNotFoundError:
116
+ pass
117
+
118
+ def _scan(self, prefix: str) -> List[dict]:
119
+ proj = _PROJECTION + (', c["embedding"]' if self.dims else "")
120
+ if prefix == "":
121
+ query = f"SELECT {proj} FROM c"
122
+ params: list = []
123
+ else:
124
+ query = f'SELECT {proj} FROM c WHERE STARTSWITH(c["prefix"], @p)'
125
+ params = [{"name": "@p", "value": prefix}]
126
+ items = self._container.query_items(
127
+ query=query, parameters=params, enable_cross_partition_query=True
128
+ )
129
+ return [self._row(i) for i in items]
130
+
131
+ def _vector_search(
132
+ self, prefix: str, vector: List[float], filter: Optional[dict], limit: int
133
+ ) -> List[Tuple[dict, float]]:
134
+ """Native Cosmos vector search: ORDER BY VectorDistance, prefix + equality filters in SQL."""
135
+ where = ['IS_DEFINED(c["embedding"])']
136
+ params: list = [{"name": "@q", "value": vector}, {"name": "@k", "value": int(limit)}]
137
+ if prefix != "":
138
+ where.append('STARTSWITH(c["prefix"], @p)')
139
+ params.append({"name": "@p", "value": prefix})
140
+ # Push plain-equality filters into SQL; operator filters are applied by core.
141
+ n = 0
142
+ for field, expected in (filter or {}).items():
143
+ if not isinstance(expected, dict):
144
+ n += 1
145
+ where.append(f'c["value"][@f{n}] = @v{n}')
146
+ params += [{"name": f"@f{n}", "value": field}, {"name": f"@v{n}", "value": expected}]
147
+ query = (
148
+ f"SELECT TOP @k {_PROJECTION}, "
149
+ f'VectorDistance(c["embedding"], @q) AS score '
150
+ f"FROM c WHERE {' AND '.join(where)} "
151
+ f'ORDER BY VectorDistance(c["embedding"], @q)'
152
+ )
153
+ items = self._container.query_items(
154
+ query=query, parameters=params, enable_cross_partition_query=True
155
+ )
156
+ out: List[Tuple[dict, float]] = []
157
+ for i in items:
158
+ row = {f: i[f] for f in _FIELDS}
159
+ row["embedding"] = None # not projected; not needed after ranking
160
+ out.append((row, float(i["score"]))) # cosine: similarity, higher is better
161
+ return out
@@ -0,0 +1,107 @@
1
+ """Semantic-search tests for CosmosDBStore (native VectorDistance path) against a real Cosmos DB account."""
2
+ import os
3
+ import time
4
+ import uuid
5
+
6
+ import pytest
7
+
8
+ from langgraph_store_core.testing import FakeEmbeddings
9
+ from langgraph_store_cosmosdb import CosmosDBStore
10
+
11
+ EP = os.environ.get("COSMOS_ENDPOINT")
12
+ KEY = os.environ.get("COSMOS_KEY")
13
+ DB = os.environ.get("LG_COSMOS_DB", "langgraph_store_test")
14
+ DIMS = 64
15
+
16
+ pytestmark = pytest.mark.skipif(not (EP and KEY), reason="COSMOS_ENDPOINT/COSMOS_KEY not set")
17
+
18
+
19
+ @pytest.fixture(scope="module")
20
+ def store():
21
+ cname = f"vstore_{uuid.uuid4().hex[:8]}"
22
+ s = CosmosDBStore(endpoint=EP, key=KEY, database_name=DB, container_name=cname,
23
+ index={"dims": DIMS, "embed": FakeEmbeddings(DIMS), "fields": ["text"]})
24
+ yield s
25
+ s._container.database_link # noqa: B018 — keep proxy alive
26
+ from azure.cosmos import CosmosClient
27
+ CosmosClient(EP, credential=KEY).get_database_client(DB).delete_container(cname)
28
+
29
+
30
+ @pytest.fixture(autouse=True)
31
+ def _clean(store):
32
+ yield
33
+ for row in store._scan(""):
34
+ store._remove(row["prefix"], row["key"])
35
+
36
+
37
+ def _seed(s):
38
+ s.put(("memories", "kamal"), "food", {"text": "the user loves sushi and japanese food", "kind": "pref"})
39
+ s.put(("memories", "kamal"), "lang", {"text": "the user writes python every day", "kind": "fact"})
40
+ s.put(("memories", "kamal"), "city", {"text": "the user lives by the sea in a big city", "kind": "fact"})
41
+ s.put(("memories", "priya"), "food", {"text": "loves sushi", "kind": "pref"})
42
+
43
+
44
+ def _search(s, *a, **k):
45
+ # vector index population is asynchronous; retry briefly until results appear
46
+ for _ in range(12):
47
+ hits = s.search(*a, **k)
48
+ if hits:
49
+ return hits
50
+ time.sleep(2)
51
+ return s.search(*a, **k)
52
+
53
+
54
+ def test_container_has_vector_policy(store):
55
+ props = store._container.read()
56
+ assert props.get("vectorEmbeddingPolicy", {}).get("vectorEmbeddings", [{}])[0].get("path") == "/embedding"
57
+ assert any(v.get("path") == "/embedding" for v in props["indexingPolicy"].get("vectorIndexes", []))
58
+
59
+
60
+ def test_put_embeds_and_index_false_skips(store):
61
+ store.put(("n",), "k", {"text": "hello world"})
62
+ assert len(store._read("n", "k")["embedding"]) == DIMS
63
+ store.put(("n",), "k2", {"text": "hello"}, index=False)
64
+ assert store._read("n", "k2")["embedding"] is None
65
+
66
+
67
+ def test_query_ranks_and_scores(store):
68
+ _seed(store)
69
+ hits = _search(store, ("memories", "kamal"), query="sushi and japanese food")
70
+ assert hits[0].key == "food" and hits[0].score is not None
71
+ assert hits[0].score >= hits[-1].score
72
+ assert all(h.namespace == ("memories", "kamal") for h in hits)
73
+
74
+
75
+ def test_query_prefix_children_and_filter(store):
76
+ _seed(store)
77
+ keys = {(h.namespace, h.key) for h in _search(store, ("memories",), query="sushi")}
78
+ assert (("memories", "kamal"), "food") in keys and (("memories", "priya"), "food") in keys
79
+ facts = _search(store, ("memories", "kamal"), query="user", filter={"kind": "fact"})
80
+ assert facts and all(h.value["kind"] == "fact" for h in facts)
81
+
82
+
83
+ def test_query_limit_offset(store):
84
+ _seed(store)
85
+ first = _search(store, ("memories", "kamal"), query="user", limit=2)
86
+ rest = store.search(("memories", "kamal"), query="user", limit=2, offset=2)
87
+ assert len(first) == 2 and len(rest) == 1 and not {h.key for h in first} & {h.key for h in rest}
88
+
89
+
90
+ def test_no_query_is_filter_only(store):
91
+ _seed(store)
92
+ hits = store.search(("memories", "kamal"))
93
+ assert [h.key for h in hits] == ["city", "food", "lang"] and all(h.score is None for h in hits)
94
+
95
+
96
+ def test_native_path_used(store, monkeypatch):
97
+ _seed(store)
98
+ called = []
99
+ orig = store._container.query_items
100
+
101
+ def spy(query, **kw):
102
+ called.append(query)
103
+ return orig(query=query, **kw)
104
+
105
+ monkeypatch.setattr(store._container, "query_items", spy)
106
+ _search(store, ("memories",), query="sushi")
107
+ assert any("VectorDistance" in q for q in called)
@@ -0,0 +1,83 @@
1
+ """Tests for CosmosDBStore (LangGraph BaseStore) against real CosmosDB.
2
+
3
+ Requires COSMOS_ENDPOINT + COSMOS_KEY. A unique container per test gives clean
4
+ isolation (deleted on teardown).
5
+ """
6
+ import os
7
+ import uuid
8
+
9
+ import pytest
10
+
11
+ from langgraph_store_cosmosdb import CosmosDBStore
12
+
13
+ EP = os.environ.get("COSMOS_ENDPOINT")
14
+ KEY = os.environ.get("COSMOS_KEY")
15
+ DB = os.environ.get("LG_COSMOS_DB", "langgraph_store_test")
16
+
17
+ pytestmark = pytest.mark.skipif(not (EP and KEY), reason="COSMOS_ENDPOINT/COSMOS_KEY not set")
18
+
19
+
20
+ @pytest.fixture()
21
+ def store():
22
+ cname = f"test_{uuid.uuid4().hex[:8]}"
23
+ s = CosmosDBStore(endpoint=EP, key=KEY, database_name=DB, container_name=cname)
24
+ yield s
25
+ from azure.cosmos import CosmosClient
26
+ CosmosClient(EP, credential=KEY).get_database_client(DB).delete_container(cname)
27
+
28
+
29
+ def test_put_get(store):
30
+ store.put(("users", "1"), "food", {"text": "sushi"})
31
+ it = store.get(("users", "1"), "food")
32
+ assert it is not None and it.value == {"text": "sushi"}
33
+ assert it.namespace == ("users", "1") and it.key == "food"
34
+
35
+
36
+ def test_get_missing_is_none(store):
37
+ assert store.get(("nope",), "x") is None
38
+
39
+
40
+ def test_update_preserves_created_at(store):
41
+ store.put(("u",), "k", {"v": 1})
42
+ a = store.get(("u",), "k")
43
+ store.put(("u",), "k", {"v": 2})
44
+ b = store.get(("u",), "k")
45
+ assert b.value == {"v": 2}
46
+ assert b.created_at == a.created_at
47
+ assert b.updated_at >= a.updated_at
48
+
49
+
50
+ def test_delete_both_ways(store):
51
+ store.put(("u",), "k", {"v": 1})
52
+ store.delete(("u",), "k")
53
+ assert store.get(("u",), "k") is None
54
+ store.put(("u",), "k2", {"v": 1})
55
+ store.put(("u",), "k2", None)
56
+ assert store.get(("u",), "k2") is None
57
+
58
+
59
+ def test_search_prefix_and_filter(store):
60
+ store.put(("users", "1", "mem"), "a", {"kind": "pref"})
61
+ store.put(("users", "1", "mem"), "b", {"kind": "fact"})
62
+ store.put(("users", "2", "mem"), "c", {"kind": "pref"})
63
+ assert sorted(i.key for i in store.search(("users", "1"))) == ["a", "b"]
64
+ assert [i.key for i in store.search(("users", "1"), filter={"kind": "pref"})] == ["a"]
65
+
66
+
67
+ def test_list_namespaces(store):
68
+ store.put(("users", "1", "mem"), "a", {})
69
+ store.put(("users", "2", "mem"), "b", {})
70
+ store.put(("orgs", "x"), "c", {})
71
+ allns = store.list_namespaces()
72
+ assert ("users", "1", "mem") in allns and ("orgs", "x") in allns
73
+ assert all(n[0] == "users" for n in store.list_namespaces(prefix=("users",)))
74
+ depth1 = store.list_namespaces(max_depth=1)
75
+ assert ("users",) in depth1 and ("orgs",) in depth1
76
+
77
+
78
+ async def test_async_surface(store):
79
+ await store.aput(("u",), "k", {"v": 1})
80
+ it = await store.aget(("u",), "k")
81
+ assert it.value == {"v": 1}
82
+ await store.adelete(("u",), "k")
83
+ assert await store.aget(("u",), "k") is None