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.
- nio_db_py-1.0.1/LICENSE +21 -0
- nio_db_py-1.0.1/PKG-INFO +70 -0
- nio_db_py-1.0.1/README.md +56 -0
- nio_db_py-1.0.1/nio_db_py.egg-info/PKG-INFO +70 -0
- nio_db_py-1.0.1/nio_db_py.egg-info/SOURCES.txt +11 -0
- nio_db_py-1.0.1/nio_db_py.egg-info/dependency_links.txt +1 -0
- nio_db_py-1.0.1/nio_db_py.egg-info/top_level.txt +1 -0
- nio_db_py-1.0.1/niodb/__init__.py +9 -0
- nio_db_py-1.0.1/niodb/client.py +284 -0
- nio_db_py-1.0.1/pyproject.toml +17 -0
- nio_db_py-1.0.1/setup.cfg +4 -0
- nio_db_py-1.0.1/setup.py +18 -0
- nio_db_py-1.0.1/tests/test_client.py +189 -0
nio_db_py-1.0.1/LICENSE
ADDED
|
@@ -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.
|
nio_db_py-1.0.1/PKG-INFO
ADDED
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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*"]
|
nio_db_py-1.0.1/setup.py
ADDED
|
@@ -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()
|