nio-db-py 1.0.1__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nio Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: nio-db-py
3
+ Version: 1.0.1
4
+ Summary: Ultra-lightweight Python client for NioDB (The Agentic Database)
5
+ Author: Nio Labs
6
+ License: MIT
7
+ Keywords: niodb,database,ai-agents,vector-search,sqlite,rag
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: author
12
+ Dynamic: license-file
13
+ Dynamic: requires-python
14
+
15
+ # nio-db-py
16
+
17
+ Ultra-lightweight Python client SDK for **NioDB** — the Agentic Database.
18
+
19
+ - **Zero dependencies** (uses Python standard library `urllib`)
20
+ - **Fast and typed**
21
+ - **Compatible with Python >= 3.8**
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install nio-db-py
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Quickstart
34
+
35
+ ```python
36
+ from niodb import NioDB
37
+
38
+ db = NioDB(url="http://127.0.0.1:7432", token="client-token")
39
+
40
+ # 1. Insert & Query Documents
41
+ db.collection("tasks").insert({
42
+ "title": "Fix auth race condition",
43
+ "status": "pending",
44
+ "ttl": 3600 # 1 hour auto-expiry
45
+ })
46
+
47
+ records = db.collection("tasks").find(limit=10)
48
+
49
+ # 2. Pure-Rust Vector Similarity Search
50
+ results = db.collection("knowledge").search_vector(
51
+ vector=[0.012, -0.045, 0.089, ...],
52
+ top_k=5,
53
+ min_score=0.75
54
+ )
55
+
56
+ # 3. Multi-Agent Sessions
57
+ session = db.session("session_104")
58
+ session.append_turn(
59
+ agent="claude",
60
+ model="claude-3-7-sonnet",
61
+ summary="Refactored database pool",
62
+ cost_usd=0.012
63
+ )
64
+
65
+ manifest = session.get_manifest()
66
+ print(manifest["manifest_text"])
67
+
68
+ # 4. Safe SQL Queries
69
+ sql_data = db.query("SELECT * FROM tasks WHERE status = 'pending'")
70
+ ```
@@ -0,0 +1,56 @@
1
+ # nio-db-py
2
+
3
+ Ultra-lightweight Python client SDK for **NioDB** — the Agentic Database.
4
+
5
+ - **Zero dependencies** (uses Python standard library `urllib`)
6
+ - **Fast and typed**
7
+ - **Compatible with Python >= 3.8**
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install nio-db-py
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Quickstart
20
+
21
+ ```python
22
+ from niodb import NioDB
23
+
24
+ db = NioDB(url="http://127.0.0.1:7432", token="client-token")
25
+
26
+ # 1. Insert & Query Documents
27
+ db.collection("tasks").insert({
28
+ "title": "Fix auth race condition",
29
+ "status": "pending",
30
+ "ttl": 3600 # 1 hour auto-expiry
31
+ })
32
+
33
+ records = db.collection("tasks").find(limit=10)
34
+
35
+ # 2. Pure-Rust Vector Similarity Search
36
+ results = db.collection("knowledge").search_vector(
37
+ vector=[0.012, -0.045, 0.089, ...],
38
+ top_k=5,
39
+ min_score=0.75
40
+ )
41
+
42
+ # 3. Multi-Agent Sessions
43
+ session = db.session("session_104")
44
+ session.append_turn(
45
+ agent="claude",
46
+ model="claude-3-7-sonnet",
47
+ summary="Refactored database pool",
48
+ cost_usd=0.012
49
+ )
50
+
51
+ manifest = session.get_manifest()
52
+ print(manifest["manifest_text"])
53
+
54
+ # 4. Safe SQL Queries
55
+ sql_data = db.query("SELECT * FROM tasks WHERE status = 'pending'")
56
+ ```
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: nio-db-py
3
+ Version: 1.0.1
4
+ Summary: Ultra-lightweight Python client for NioDB (The Agentic Database)
5
+ Author: Nio Labs
6
+ License: MIT
7
+ Keywords: niodb,database,ai-agents,vector-search,sqlite,rag
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Dynamic: author
12
+ Dynamic: license-file
13
+ Dynamic: requires-python
14
+
15
+ # nio-db-py
16
+
17
+ Ultra-lightweight Python client SDK for **NioDB** — the Agentic Database.
18
+
19
+ - **Zero dependencies** (uses Python standard library `urllib`)
20
+ - **Fast and typed**
21
+ - **Compatible with Python >= 3.8**
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install nio-db-py
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Quickstart
34
+
35
+ ```python
36
+ from niodb import NioDB
37
+
38
+ db = NioDB(url="http://127.0.0.1:7432", token="client-token")
39
+
40
+ # 1. Insert & Query Documents
41
+ db.collection("tasks").insert({
42
+ "title": "Fix auth race condition",
43
+ "status": "pending",
44
+ "ttl": 3600 # 1 hour auto-expiry
45
+ })
46
+
47
+ records = db.collection("tasks").find(limit=10)
48
+
49
+ # 2. Pure-Rust Vector Similarity Search
50
+ results = db.collection("knowledge").search_vector(
51
+ vector=[0.012, -0.045, 0.089, ...],
52
+ top_k=5,
53
+ min_score=0.75
54
+ )
55
+
56
+ # 3. Multi-Agent Sessions
57
+ session = db.session("session_104")
58
+ session.append_turn(
59
+ agent="claude",
60
+ model="claude-3-7-sonnet",
61
+ summary="Refactored database pool",
62
+ cost_usd=0.012
63
+ )
64
+
65
+ manifest = session.get_manifest()
66
+ print(manifest["manifest_text"])
67
+
68
+ # 4. Safe SQL Queries
69
+ sql_data = db.query("SELECT * FROM tasks WHERE status = 'pending'")
70
+ ```
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ nio_db_py.egg-info/PKG-INFO
6
+ nio_db_py.egg-info/SOURCES.txt
7
+ nio_db_py.egg-info/dependency_links.txt
8
+ nio_db_py.egg-info/top_level.txt
9
+ niodb/__init__.py
10
+ niodb/client.py
11
+ tests/test_client.py
@@ -0,0 +1 @@
1
+ niodb
@@ -0,0 +1,9 @@
1
+ """
2
+ nio-db-py — Lightweight Python SDK for NioDB (The Agentic Database)
3
+ """
4
+
5
+ from .client import NioDB, NioDBError, Collection, Session, Sessions, Tasks
6
+
7
+ __all__ = ["NioDB", "NioDBError", "Collection", "Session", "Sessions", "Tasks"]
8
+ __version__ = "0.1.1"
9
+
@@ -0,0 +1,284 @@
1
+ """
2
+ NioDB Python Client SDK — Zero-dependency Python wrapper for NioDB.
3
+ """
4
+
5
+ import json
6
+ import urllib.request
7
+ import urllib.error
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ class NioDBError(Exception):
11
+ def __init__(self, message: str, status_code: Optional[int] = None, data: Optional[Dict[str, Any]] = None):
12
+ super().__init__(message)
13
+ self.status_code = status_code
14
+ self.data = data
15
+
16
+
17
+ class Collection:
18
+ def __init__(self, client: "NioDB", name: str):
19
+ self._client = client
20
+ self.name = name
21
+
22
+ def insert(self, data: Dict[str, Any], ttl: Optional[int] = None, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
23
+ payload = {"collection": self.name, "data": dict(data)}
24
+ if ttl is not None:
25
+ payload["data"]["ttl"] = ttl
26
+ headers = {}
27
+ if idempotency_key:
28
+ headers["idempotency-key"] = idempotency_key
29
+ return self._client._request("/api/v1/records", method="POST", body=payload, extra_headers=headers)
30
+
31
+ def find(self, limit: Optional[int] = None, cursor: Optional[str] = None) -> Dict[str, Any]:
32
+ path = f"/api/v1/records?type={urllib.parse.quote(self.name)}"
33
+ if limit is not None:
34
+ path += f"&limit={limit}"
35
+ if cursor is not None:
36
+ path += f"&cursor={urllib.parse.quote(cursor)}"
37
+ return self._client._request(path, method="GET")
38
+
39
+ def get(self, record_id: str) -> Dict[str, Any]:
40
+ return self._client._request(f"/api/v1/records/{urllib.parse.quote(record_id)}", method="GET")
41
+
42
+ def update(self, record_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
43
+ payload = data if "data" in data else {"data": data}
44
+ return self._client._request(f"/api/v1/records/{urllib.parse.quote(record_id)}", method="PATCH", body=payload)
45
+
46
+ def delete(self, record_id: str) -> Dict[str, Any]:
47
+ return self._client._request(f"/api/v1/records/{urllib.parse.quote(record_id)}", method="DELETE")
48
+
49
+ def insert_many(self, records: List[Dict[str, Any]]) -> Dict[str, Any]:
50
+ formatted = []
51
+ for r in records:
52
+ if "data" in r:
53
+ formatted.append(r)
54
+ else:
55
+ formatted.append({"collection": self.name, "data": r})
56
+ payload = {
57
+ "collection": self.name,
58
+ "action": "insert",
59
+ "records": formatted
60
+ }
61
+ return self._client._request("/api/v1/records/bulk", method="POST", body=payload)
62
+
63
+ def bulk_insert(self, records: List[Dict[str, Any]]) -> Dict[str, Any]:
64
+ return self.insert_many(records)
65
+
66
+ def update_many(self, records: List[Dict[str, Any]]) -> Dict[str, Any]:
67
+ formatted = []
68
+ for r in records:
69
+ rec_id = r.get("id") or r.get("record_id")
70
+ data = r.get("data") if "data" in r else r
71
+ formatted.append({"id": rec_id, "data": data})
72
+ payload = {
73
+ "action": "update",
74
+ "records": formatted
75
+ }
76
+ return self._client._request("/api/v1/records/bulk", method="POST", body=payload)
77
+
78
+ def bulk_update(self, records: List[Dict[str, Any]]) -> Dict[str, Any]:
79
+ return self.update_many(records)
80
+
81
+ def delete_many(self, ids: List[Union[str, Dict[str, Any]]]) -> Dict[str, Any]:
82
+ formatted_ids = []
83
+ for item in ids:
84
+ if isinstance(item, dict):
85
+ rec_id = item.get("id") or item.get("record_id")
86
+ if rec_id:
87
+ formatted_ids.append(rec_id)
88
+ else:
89
+ formatted_ids.append(str(item))
90
+ payload = {
91
+ "action": "delete",
92
+ "ids": formatted_ids
93
+ }
94
+ return self._client._request("/api/v1/records/bulk", method="POST", body=payload)
95
+
96
+ def bulk_delete(self, ids: List[Union[str, Dict[str, Any]]]) -> Dict[str, Any]:
97
+ return self.delete_many(ids)
98
+
99
+ def search_vector(self, vector: List[float], top_k: int = 5, min_score: float = 0.0) -> Dict[str, Any]:
100
+ payload = {
101
+ "collection": self.name,
102
+ "vector": vector,
103
+ "top_k": top_k,
104
+ "min_score": min_score
105
+ }
106
+ return self._client._request("/api/v1/records/search", method="POST", body=payload)
107
+
108
+
109
+ class Session:
110
+ def __init__(self, client: "NioDB", session_id: str):
111
+ self._client = client
112
+ self.session_id = session_id
113
+
114
+ def create(self, title: str, goal: str = "", model_tier: str = "strong", metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
115
+ payload = {
116
+ "title": title,
117
+ "goal": goal,
118
+ "model_tier": model_tier,
119
+ "metadata": metadata or {}
120
+ }
121
+ return self._client._request("/api/v1/sessions", method="POST", body=payload)
122
+
123
+ def get(self) -> Dict[str, Any]:
124
+ return self._client._request(f"/api/v1/sessions/{urllib.parse.quote(self.session_id)}", method="GET")
125
+
126
+ def append_turn(
127
+ self,
128
+ agent: str,
129
+ model: str,
130
+ summary: str = "",
131
+ files_touched: Optional[List[str]] = None,
132
+ tokens: Optional[Dict[str, Any]] = None,
133
+ cost_usd: float = 0.0
134
+ ) -> Dict[str, Any]:
135
+ payload = {
136
+ "agent": agent,
137
+ "model": model,
138
+ "summary": summary,
139
+ "files_touched": files_touched or [],
140
+ "tokens": tokens,
141
+ "cost_usd": cost_usd
142
+ }
143
+ return self._client._request(f"/api/v1/sessions/{urllib.parse.quote(self.session_id)}/turns", method="POST", body=payload)
144
+
145
+ def get_manifest(self) -> Dict[str, Any]:
146
+ return self._client._request(f"/api/v1/sessions/{urllib.parse.quote(self.session_id)}/manifest", method="GET")
147
+
148
+ def log_dead_end(self, hypothesis: str, reason: str, agent: str = "unknown") -> Dict[str, Any]:
149
+ payload = {
150
+ "hypothesis": hypothesis,
151
+ "reason": reason,
152
+ "agent": agent
153
+ }
154
+ return self._client._request(f"/api/v1/sessions/{urllib.parse.quote(self.session_id)}/dead-ends", method="POST", body=payload)
155
+
156
+ def get_dead_ends(self) -> Dict[str, Any]:
157
+ return self._client._request(f"/api/v1/sessions/{urllib.parse.quote(self.session_id)}/dead-ends", method="GET")
158
+
159
+
160
+ class Sessions:
161
+ def __init__(self, client: "NioDB"):
162
+ self._client = client
163
+
164
+ def list(self) -> Dict[str, Any]:
165
+ return self._client._request("/api/v1/sessions", method="GET")
166
+
167
+ def create(self, title: str, goal: str = "", model_tier: str = "strong", metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
168
+ payload = {
169
+ "title": title,
170
+ "goal": goal,
171
+ "model_tier": model_tier,
172
+ "metadata": metadata or {}
173
+ }
174
+ return self._client._request("/api/v1/sessions", method="POST", body=payload)
175
+
176
+ def get(self, session_id: str) -> Session:
177
+ return Session(self._client, session_id)
178
+
179
+
180
+ class Tasks:
181
+ def __init__(self, client: "NioDB"):
182
+ self._client = client
183
+
184
+ def list(self) -> Dict[str, Any]:
185
+ return self._client._request("/api/v1/tasks", method="GET")
186
+
187
+ def create(self, title: str, prompt: str, priority: str = "normal", metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
188
+ payload = {
189
+ "title": title,
190
+ "prompt": prompt,
191
+ "priority": priority,
192
+ "metadata": metadata or {}
193
+ }
194
+ return self._client._request("/api/v1/tasks", method="POST", body=payload)
195
+
196
+ def get(self, task_id: str) -> Dict[str, Any]:
197
+ return self._client._request(f"/api/v1/tasks/{urllib.parse.quote(task_id)}", method="GET")
198
+
199
+ def update(self, task_id: str, status: Optional[str] = None, progress: Optional[int] = None, log_line: Optional[str] = None, result: Any = None) -> Dict[str, Any]:
200
+ payload = {}
201
+ if status is not None: payload["status"] = status
202
+ if progress is not None: payload["progress"] = progress
203
+ if log_line is not None: payload["log_line"] = log_line
204
+ if result is not None: payload["result"] = result
205
+ return self._client._request(f"/api/v1/tasks/{urllib.parse.quote(task_id)}", method="PATCH", body=payload)
206
+
207
+
208
+ class NioDB:
209
+ def __init__(self, url: str = "http://127.0.0.1:7432", token: Optional[str] = None, client_token: Optional[str] = None, secret_token: Optional[str] = None, workspace_id: Optional[str] = None):
210
+ self.url = url.rstrip("/")
211
+ if token:
212
+ self.token = token
213
+ elif client_token and secret_token:
214
+ self.token = f"{client_token}:{secret_token}"
215
+ elif client_token or secret_token:
216
+ self.token = client_token or secret_token
217
+ else:
218
+ self.token = None
219
+ self.workspace_id = workspace_id
220
+ self.sessions = Sessions(self)
221
+ self.tasks = Tasks(self)
222
+
223
+ def _request(self, path: str, method: str = "GET", body: Optional[Dict[str, Any]] = None, extra_headers: Optional[Dict[str, str]] = None) -> Any:
224
+ url = f"{self.url}{path}"
225
+ if self.workspace_id and "workspace_id=" not in url:
226
+ sep = "&" if "?" in url else "?"
227
+ url += f"{sep}workspace_id={urllib.parse.quote(self.workspace_id)}"
228
+
229
+ headers = {
230
+ "Accept": "application/json",
231
+ "User-Agent": "nio-db-py/0.1.1"
232
+ }
233
+ if self.token:
234
+ headers["Authorization"] = f"Bearer {self.token}"
235
+ if extra_headers:
236
+ headers.update(extra_headers)
237
+
238
+ data_bytes = None
239
+ if body is not None and method != "GET":
240
+ headers["Content-Type"] = "application/json"
241
+ data_bytes = json.dumps(body).encode("utf-8")
242
+
243
+ req = urllib.request.Request(url, data=data_bytes, headers=headers, method=method)
244
+
245
+ try:
246
+ with urllib.request.urlopen(req) as resp:
247
+ if resp.status == 204:
248
+ return None
249
+ resp_bytes = resp.read()
250
+ if not resp_bytes:
251
+ return {}
252
+ return json.loads(resp_bytes.decode("utf-8"))
253
+ except urllib.error.HTTPError as e:
254
+ try:
255
+ err_data = json.loads(e.read().decode("utf-8"))
256
+ msg = err_data.get("error", {}).get("message", str(e))
257
+ except Exception:
258
+ err_data = None
259
+ msg = str(e)
260
+ raise NioDBError(msg, status_code=e.code, data=err_data) from e
261
+ except urllib.error.URLError as e:
262
+ raise NioDBError(f"Connection failed: {e}") from e
263
+
264
+ def collection(self, name: str) -> Collection:
265
+ return Collection(self, name)
266
+
267
+ def session(self, session_id: str) -> Session:
268
+ return Session(self, session_id)
269
+
270
+ def bulk(self, operations: Union[List[Dict[str, Any]], Dict[str, Any]]) -> Dict[str, Any]:
271
+ if isinstance(operations, list):
272
+ payload = {"operations": operations}
273
+ else:
274
+ payload = operations
275
+ return self._request("/api/v1/records/bulk", method="POST", body=payload)
276
+
277
+ def query(self, sql: str) -> Dict[str, Any]:
278
+ return self._request("/api/v1/query", method="POST", body={"sql": sql})
279
+
280
+ def vacuum(self) -> Dict[str, Any]:
281
+ return self._request("/api/v1/admin/vacuum", method="POST", body={})
282
+
283
+ def get_tools(self) -> Dict[str, Any]:
284
+ return self._request("/api/v1/agent/tools", method="GET")
@@ -0,0 +1,17 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "nio-db-py"
7
+ version = "1.0.1"
8
+ description = "Ultra-lightweight Python client for NioDB (The Agentic Database)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ keywords = ["niodb", "database", "ai-agents", "vector-search", "sqlite", "rag"]
13
+ dependencies = []
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["."]
17
+ include = ["niodb*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="nio-db-py",
5
+ version="1.0.1",
6
+ description="Ultra-lightweight Python client for NioDB (The Agentic Database)",
7
+ long_description=open("README.md", encoding="utf-8").read(),
8
+ long_description_content_type="text/markdown",
9
+ author="Nio Labs",
10
+ license="MIT",
11
+ packages=find_packages(exclude=["tests*"]),
12
+ python_requires=">=3.8",
13
+ keywords=["niodb", "database", "ai-agents", "vector-search", "sqlite", "rag"],
14
+ classifiers=[
15
+ "Programming Language :: Python :: 3",
16
+ "Operating System :: OS Independent",
17
+ ],
18
+ )
@@ -0,0 +1,189 @@
1
+ import unittest
2
+ import json
3
+ import threading
4
+ from http.server import HTTPServer, BaseHTTPRequestHandler
5
+ import sys
6
+ import os
7
+
8
+ # Add package dir to sys.path
9
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
10
+ from niodb import NioDB, NioDBError
11
+
12
+
13
+ class MockNioDBHandler(BaseHTTPRequestHandler):
14
+ def do_GET(self):
15
+ self.send_response(200)
16
+ self.send_header("Content-Type", "application/json")
17
+ self.end_headers()
18
+
19
+ if "/manifest" in self.path:
20
+ self.wfile.write(json.dumps({"session_id": "s1", "manifest_text": "Manifest ok"}).encode("utf-8"))
21
+ elif "/dead-ends" in self.path:
22
+ self.wfile.write(json.dumps({"session_id": "s1", "dead_ends": [{"hypothesis": "test"}], "count": 1}).encode("utf-8"))
23
+ elif self.path.startswith("/api/v1/sessions"):
24
+ self.wfile.write(json.dumps({"items": [{"id": "s1"}], "count": 1}).encode("utf-8"))
25
+ elif self.path.startswith("/api/v1/tasks/task_1"):
26
+ self.wfile.write(json.dumps({"id": "task_1", "status": "pending"}).encode("utf-8"))
27
+ elif self.path.startswith("/api/v1/tasks"):
28
+ self.wfile.write(json.dumps({"items": [{"id": "task_1"}], "count": 1}).encode("utf-8"))
29
+ elif self.path.startswith("/api/v1/agent/tools"):
30
+ self.wfile.write(json.dumps({"tools": [{"name": "query_database"}]}).encode("utf-8"))
31
+ elif self.path.startswith("/api/v1/records/rec_1"):
32
+ self.wfile.write(json.dumps({"id": "rec_1", "data": {"key": "value"}}).encode("utf-8"))
33
+ elif self.path.startswith("/api/v1/records"):
34
+ self.wfile.write(json.dumps({"items": [{"id": "rec_1"}], "count": 1}).encode("utf-8"))
35
+ else:
36
+ self.wfile.write(json.dumps({"status": "ok"}).encode("utf-8"))
37
+
38
+ def do_POST(self):
39
+ length = int(self.headers.get("Content-Length", 0))
40
+ body = json.loads(self.rfile.read(length).decode("utf-8")) if length > 0 else {}
41
+
42
+ self.send_response(200)
43
+ self.send_header("Content-Type", "application/json")
44
+ self.end_headers()
45
+
46
+ if self.path.startswith("/api/v1/records/search"):
47
+ self.wfile.write(json.dumps({"items": [{"id": "rec_1", "score": 0.99}], "count": 1}).encode("utf-8"))
48
+ elif self.path.startswith("/api/v1/records/bulk"):
49
+ act = body.get("action", "insert")
50
+ recs = body.get("records", [])
51
+ ids = body.get("ids", [])
52
+ self.wfile.write(json.dumps({
53
+ "success": True,
54
+ "inserted": len(recs) if act == "insert" else 0,
55
+ "updated": len(recs) if act == "update" else 0,
56
+ "deleted": len(ids) if act == "delete" else 0,
57
+ "records": recs,
58
+ "deleted_ids": ids
59
+ }).encode("utf-8"))
60
+ elif self.path.startswith("/api/v1/records"):
61
+ self.wfile.write(json.dumps({"id": "rec_new", **body}).encode("utf-8"))
62
+ elif "/turns" in self.path:
63
+ self.wfile.write(json.dumps({"success": True, "session_id": "s1", "turn": body}).encode("utf-8"))
64
+ elif "/dead-ends" in self.path:
65
+ self.wfile.write(json.dumps({"success": True, "count": 1}).encode("utf-8"))
66
+ elif self.path.startswith("/api/v1/sessions"):
67
+ self.wfile.write(json.dumps({"id": "s1", "title": body.get("title", "")}).encode("utf-8"))
68
+ elif self.path.startswith("/api/v1/tasks"):
69
+ self.wfile.write(json.dumps({"id": "task_1", **body}).encode("utf-8"))
70
+ elif self.path.startswith("/api/v1/admin/vacuum"):
71
+ self.wfile.write(json.dumps({"success": True, "stats": {"compacted_bytes": 500}}).encode("utf-8"))
72
+ elif self.path.startswith("/api/v1/query"):
73
+ self.wfile.write(json.dumps({"items": [{"cnt": 10}]}).encode("utf-8"))
74
+ else:
75
+ self.wfile.write(json.dumps({"status": "ok"}).encode("utf-8"))
76
+
77
+ def do_PATCH(self):
78
+ length = int(self.headers.get("Content-Length", 0))
79
+ body = json.loads(self.rfile.read(length).decode("utf-8")) if length > 0 else {}
80
+
81
+ self.send_response(200)
82
+ self.send_header("Content-Type", "application/json")
83
+ self.end_headers()
84
+ self.wfile.write(json.dumps({"id": "task_1", **body}).encode("utf-8"))
85
+
86
+ def do_DELETE(self):
87
+ self.send_response(200)
88
+ self.send_header("Content-Type", "application/json")
89
+ self.end_headers()
90
+ self.wfile.write(json.dumps({"success": True, "deleted": True, "id": "rec_1"}).encode("utf-8"))
91
+
92
+ def log_message(self, format, *args):
93
+ pass # Quiet logger
94
+
95
+
96
+ class TestNioDBPy(unittest.TestCase):
97
+ @classmethod
98
+ def setUpClass(cls):
99
+ cls.server = HTTPServer(("127.0.0.1", 0), MockNioDBHandler)
100
+ cls.port = cls.server.server_port
101
+ cls.server_thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
102
+ cls.server_thread.start()
103
+ cls.client = NioDB(f"http://127.0.0.1:{cls.port}", token="tok-123", workspace_id="ws-py")
104
+
105
+ @classmethod
106
+ def tearDownClass(cls):
107
+ cls.server.shutdown()
108
+ cls.server.server_close()
109
+
110
+ def test_collection_crud_and_vector(self):
111
+ c_comb = NioDB("http://127.0.0.1:7432", client_token="cli-py", secret_token="sec-py")
112
+ self.assertEqual(c_comb.token, "cli-py:sec-py")
113
+
114
+ col = self.client.collection("memories")
115
+ inserted = col.insert({"text": "python test"}, ttl=300)
116
+ self.assertEqual(inserted["id"], "rec_new")
117
+ self.assertEqual(inserted["data"]["ttl"], 300)
118
+
119
+ found = col.find(limit=5)
120
+ self.assertEqual(found["count"], 1)
121
+
122
+ rec = col.get("rec_1")
123
+ self.assertEqual(rec["id"], "rec_1")
124
+
125
+ updated = col.update("rec_1", {"key": "val2"})
126
+ self.assertEqual(updated["id"], "task_1")
127
+
128
+ deleted = col.delete("rec_1")
129
+ self.assertTrue(deleted["success"])
130
+
131
+ bulk_ins = col.insert_many([{"text": "a"}, {"text": "b"}])
132
+ self.assertEqual(bulk_ins["inserted"], 2)
133
+
134
+ bulk_upd = col.update_many([{"id": "rec_1", "text": "a2"}])
135
+ self.assertEqual(bulk_upd["updated"], 1)
136
+
137
+ bulk_del = col.delete_many(["rec_1", "rec_2"])
138
+ self.assertEqual(bulk_del["deleted"], 2)
139
+
140
+ bulk_all = self.client.bulk([{"action": "insert", "collection": "test", "data": {}}])
141
+ self.assertTrue(bulk_all["success"])
142
+
143
+ search_res = col.search_vector([0.1, 0.2, 0.3], top_k=3)
144
+ self.assertEqual(search_res["count"], 1)
145
+ self.assertEqual(search_res["items"][0]["score"], 0.99)
146
+
147
+ def test_sessions_and_dead_ends(self):
148
+ sess = self.client.sessions.create("Py Task", goal="Test python client")
149
+ self.assertEqual(sess["id"], "s1")
150
+
151
+ sessions_list = self.client.sessions.list()
152
+ self.assertEqual(sessions_list["count"], 1)
153
+
154
+ s1 = self.client.session("s1")
155
+ turn_res = s1.append_turn(agent="worker", model="gpt-4o", summary="Completed step")
156
+ self.assertTrue(turn_res["success"])
157
+
158
+ manifest = s1.get_manifest()
159
+ self.assertEqual(manifest["manifest_text"], "Manifest ok")
160
+
161
+ dead_end_res = s1.log_dead_end(hypothesis="hypothesis 1", reason="failed validation")
162
+ self.assertTrue(dead_end_res["success"])
163
+
164
+ dead_ends = s1.get_dead_ends()
165
+ self.assertEqual(dead_ends["count"], 1)
166
+
167
+ def test_tasks_and_admin(self):
168
+ task = self.client.tasks.create("Run py workflow", prompt="Execute job")
169
+ self.assertEqual(task["id"], "task_1")
170
+
171
+ tasks_list = self.client.tasks.list()
172
+ self.assertEqual(tasks_list["count"], 1)
173
+
174
+ updated = self.client.tasks.update("task_1", status="running", progress=75)
175
+ self.assertEqual(updated["status"], "running")
176
+ self.assertEqual(updated["progress"], 75)
177
+
178
+ query_res = self.client.query("SELECT COUNT(*) FROM artifacts")
179
+ self.assertEqual(query_res["items"][0]["cnt"], 10)
180
+
181
+ vac_res = self.client.vacuum()
182
+ self.assertTrue(vac_res["success"])
183
+
184
+ tools = self.client.get_tools()
185
+ self.assertEqual(len(tools["tools"]), 1)
186
+
187
+
188
+ if __name__ == "__main__":
189
+ unittest.main()