tetrix-sdk 0.1.1__py3-none-any.whl
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.
- tetrix_aidb/__init__.py +62 -0
- tetrix_aidb/client.py +767 -0
- tetrix_aidb/errors.py +38 -0
- tetrix_aidb/protocol.py +29 -0
- tetrix_aidb/types.py +354 -0
- tetrix_sdk-0.1.1.dist-info/METADATA +60 -0
- tetrix_sdk-0.1.1.dist-info/RECORD +8 -0
- tetrix_sdk-0.1.1.dist-info/WHEEL +4 -0
tetrix_aidb/__init__.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""TetrixAIDb Python SDK — AI-native database client."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.1"
|
|
4
|
+
|
|
5
|
+
from tetrix_aidb.client import TetrixClient, create_client
|
|
6
|
+
from tetrix_aidb.errors import TetrixError
|
|
7
|
+
from tetrix_aidb.types import (
|
|
8
|
+
Category,
|
|
9
|
+
ClientConfig,
|
|
10
|
+
DeleteBatchResponse,
|
|
11
|
+
Entity,
|
|
12
|
+
EntityContent,
|
|
13
|
+
HealthResponse,
|
|
14
|
+
IndexBatchItem,
|
|
15
|
+
IndexBatchOptions,
|
|
16
|
+
IndexBatchResponse,
|
|
17
|
+
IndexBatchResultItem,
|
|
18
|
+
ListCategoriesResponse,
|
|
19
|
+
ListEntityCategoriesResponse,
|
|
20
|
+
ListOrgsResponse,
|
|
21
|
+
ListPermissionsResponse,
|
|
22
|
+
ListTeamMembersResponse,
|
|
23
|
+
ListTeamsResponse,
|
|
24
|
+
ListUsersResponse,
|
|
25
|
+
MetricsResponse,
|
|
26
|
+
Organization,
|
|
27
|
+
Permission,
|
|
28
|
+
RelationRef,
|
|
29
|
+
Team,
|
|
30
|
+
TeamMember,
|
|
31
|
+
User,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"TetrixClient",
|
|
36
|
+
"create_client",
|
|
37
|
+
"Category",
|
|
38
|
+
"ClientConfig",
|
|
39
|
+
"DeleteBatchResponse",
|
|
40
|
+
"Entity",
|
|
41
|
+
"EntityContent",
|
|
42
|
+
"HealthResponse",
|
|
43
|
+
"IndexBatchItem",
|
|
44
|
+
"IndexBatchOptions",
|
|
45
|
+
"IndexBatchResponse",
|
|
46
|
+
"IndexBatchResultItem",
|
|
47
|
+
"ListCategoriesResponse",
|
|
48
|
+
"ListEntityCategoriesResponse",
|
|
49
|
+
"ListOrgsResponse",
|
|
50
|
+
"ListPermissionsResponse",
|
|
51
|
+
"ListTeamMembersResponse",
|
|
52
|
+
"ListTeamsResponse",
|
|
53
|
+
"ListUsersResponse",
|
|
54
|
+
"MetricsResponse",
|
|
55
|
+
"Organization",
|
|
56
|
+
"Permission",
|
|
57
|
+
"RelationRef",
|
|
58
|
+
"Team",
|
|
59
|
+
"TeamMember",
|
|
60
|
+
"TetrixError",
|
|
61
|
+
"User",
|
|
62
|
+
]
|
tetrix_aidb/client.py
ADDED
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
"""TetrixAIDb async client implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import urlparse
|
|
9
|
+
|
|
10
|
+
from tetrix_aidb.errors import ConnectionError, TetrixError
|
|
11
|
+
from tetrix_aidb.protocol import encode_frame, read_frame
|
|
12
|
+
from tetrix_aidb.types import (
|
|
13
|
+
Category,
|
|
14
|
+
ClientConfig,
|
|
15
|
+
DeleteBatchResponse,
|
|
16
|
+
DeleteRequest,
|
|
17
|
+
DeleteResponse,
|
|
18
|
+
EmbedBatchRequest,
|
|
19
|
+
EmbedBatchResponse,
|
|
20
|
+
EntityContent,
|
|
21
|
+
GetRequest,
|
|
22
|
+
GetResponse,
|
|
23
|
+
HealthResponse,
|
|
24
|
+
IndexBatchItem,
|
|
25
|
+
IndexBatchOptions,
|
|
26
|
+
IndexBatchResponse,
|
|
27
|
+
IndexBatchResultItem,
|
|
28
|
+
IndexRequest,
|
|
29
|
+
IndexResponse,
|
|
30
|
+
ListCategoriesResponse,
|
|
31
|
+
ListEntityCategoriesResponse,
|
|
32
|
+
ListOrgsResponse,
|
|
33
|
+
ListPermissionsResponse,
|
|
34
|
+
ListTeamMembersResponse,
|
|
35
|
+
ListTeamsResponse,
|
|
36
|
+
ListUsersResponse,
|
|
37
|
+
MetricsResponse,
|
|
38
|
+
Organization,
|
|
39
|
+
Permission,
|
|
40
|
+
SemanticSearchRequest,
|
|
41
|
+
SemanticSearchResponse,
|
|
42
|
+
Team,
|
|
43
|
+
TeamMember,
|
|
44
|
+
UpdateRequest,
|
|
45
|
+
UpdateResponse,
|
|
46
|
+
User,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
PROTOCOL_VERSION = "1.0"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TetrixClient:
|
|
53
|
+
"""Async client for TetrixAIDb."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: ClientConfig) -> None:
|
|
56
|
+
self._config = config
|
|
57
|
+
self._reader: asyncio.StreamReader | None = None
|
|
58
|
+
self._writer: asyncio.StreamWriter | None = None
|
|
59
|
+
self._lock = asyncio.Lock()
|
|
60
|
+
self._authenticated = False
|
|
61
|
+
|
|
62
|
+
async def connect(self) -> None:
|
|
63
|
+
"""Establish connection and perform AUTH handshake."""
|
|
64
|
+
parsed = urlparse(self._config.endpoint)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
if parsed.scheme == "unix":
|
|
68
|
+
self._reader, self._writer = await asyncio.open_unix_connection(parsed.path)
|
|
69
|
+
else:
|
|
70
|
+
host = parsed.hostname or "localhost"
|
|
71
|
+
port = parsed.port or 7779
|
|
72
|
+
self._reader, self._writer = await asyncio.open_connection(host, port)
|
|
73
|
+
except OSError as e:
|
|
74
|
+
raise ConnectionError(f"failed to connect to {self._config.endpoint}: {e}") from e
|
|
75
|
+
|
|
76
|
+
await self._authenticate()
|
|
77
|
+
|
|
78
|
+
async def _authenticate(self) -> None:
|
|
79
|
+
"""Send AUTH handshake and validate response."""
|
|
80
|
+
auth_frame: dict[str, Any] = {
|
|
81
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
82
|
+
"op": "AUTH",
|
|
83
|
+
"username": self._config.username,
|
|
84
|
+
"password": self._config.password,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
assert self._writer is not None
|
|
88
|
+
assert self._reader is not None
|
|
89
|
+
|
|
90
|
+
self._writer.write(encode_frame(auth_frame))
|
|
91
|
+
await self._writer.drain()
|
|
92
|
+
resp = await read_frame(self._reader)
|
|
93
|
+
|
|
94
|
+
if resp.get("status") != "ok":
|
|
95
|
+
err = resp.get("error", {})
|
|
96
|
+
await self.close()
|
|
97
|
+
raise TetrixError(
|
|
98
|
+
code=err.get("code", "UNAUTHENTICATED"),
|
|
99
|
+
message=err.get("message", "authentication failed"),
|
|
100
|
+
retryable=False,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self._authenticated = True
|
|
104
|
+
|
|
105
|
+
async def close(self) -> None:
|
|
106
|
+
"""Close the connection."""
|
|
107
|
+
if self._writer:
|
|
108
|
+
self._writer.close()
|
|
109
|
+
await self._writer.wait_closed()
|
|
110
|
+
self._writer = None
|
|
111
|
+
self._reader = None
|
|
112
|
+
self._authenticated = False
|
|
113
|
+
|
|
114
|
+
async def _request(self, op: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
115
|
+
"""Send a request and return the response payload."""
|
|
116
|
+
if not self._writer or not self._reader:
|
|
117
|
+
await self.connect()
|
|
118
|
+
|
|
119
|
+
envelope = {
|
|
120
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
121
|
+
"requestId": str(uuid.uuid4()),
|
|
122
|
+
"op": op,
|
|
123
|
+
"payload": payload,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async with self._lock:
|
|
127
|
+
self._writer.write(encode_frame(envelope))
|
|
128
|
+
await self._writer.drain()
|
|
129
|
+
resp = await read_frame(self._reader)
|
|
130
|
+
|
|
131
|
+
if resp.get("status") == "error":
|
|
132
|
+
err = resp.get("error", {})
|
|
133
|
+
raise TetrixError(
|
|
134
|
+
code=err.get("code", "UNKNOWN"),
|
|
135
|
+
message=err.get("message", "unknown error"),
|
|
136
|
+
retryable=err.get("retryable", False),
|
|
137
|
+
details=err.get("details"),
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return resp.get("payload", {})
|
|
141
|
+
|
|
142
|
+
# =================================================================
|
|
143
|
+
# Data Operations
|
|
144
|
+
# =================================================================
|
|
145
|
+
|
|
146
|
+
async def index(self, request: IndexRequest) -> IndexResponse:
|
|
147
|
+
"""Create or index an entity."""
|
|
148
|
+
payload = _index_request_to_dict(request)
|
|
149
|
+
resp = await self._request("INDEX", payload)
|
|
150
|
+
return IndexResponse(
|
|
151
|
+
id=resp["id"],
|
|
152
|
+
version=resp["version"],
|
|
153
|
+
created_at=resp["createdAt"],
|
|
154
|
+
write_consistency=resp.get("writeConsistency", "strong"),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
async def get(self, request: GetRequest) -> GetResponse:
|
|
158
|
+
"""Retrieve an entity by ID."""
|
|
159
|
+
payload = {
|
|
160
|
+
"id": request.id,
|
|
161
|
+
"includeContent": request.include_content,
|
|
162
|
+
"includeRelations": request.include_relations,
|
|
163
|
+
}
|
|
164
|
+
resp = await self._request("GET", payload)
|
|
165
|
+
content = None
|
|
166
|
+
if resp.get("content"):
|
|
167
|
+
c = resp["content"]
|
|
168
|
+
content = EntityContent(
|
|
169
|
+
text=c.get("text", ""),
|
|
170
|
+
raw=c.get("raw"),
|
|
171
|
+
encoding=c.get("encoding"),
|
|
172
|
+
)
|
|
173
|
+
return GetResponse(entity=_dict_to_entity(resp.get("entity", {})), content=content)
|
|
174
|
+
|
|
175
|
+
async def update(self, request: UpdateRequest) -> UpdateResponse:
|
|
176
|
+
"""Update an entity."""
|
|
177
|
+
payload: dict[str, Any] = {"id": request.id}
|
|
178
|
+
if request.expected_version:
|
|
179
|
+
payload["expectedVersion"] = request.expected_version
|
|
180
|
+
if request.patch:
|
|
181
|
+
patch: dict[str, Any] = {}
|
|
182
|
+
if request.patch.name:
|
|
183
|
+
patch["name"] = request.patch.name
|
|
184
|
+
if request.patch.metadata is not None:
|
|
185
|
+
patch["metadata"] = request.patch.metadata
|
|
186
|
+
if request.patch.relations is not None:
|
|
187
|
+
patch["relations"] = [
|
|
188
|
+
{"type": r.type, "targetId": r.target_id}
|
|
189
|
+
for r in request.patch.relations
|
|
190
|
+
]
|
|
191
|
+
payload["patch"] = patch
|
|
192
|
+
if request.content_patch:
|
|
193
|
+
cp: dict[str, Any] = {}
|
|
194
|
+
if request.content_patch.text is not None:
|
|
195
|
+
cp["text"] = request.content_patch.text
|
|
196
|
+
if request.content_patch.raw is not None:
|
|
197
|
+
cp["raw"] = request.content_patch.raw
|
|
198
|
+
if request.content_patch.encoding is not None:
|
|
199
|
+
cp["encoding"] = request.content_patch.encoding
|
|
200
|
+
payload["contentPatch"] = cp
|
|
201
|
+
resp = await self._request("UPDATE", payload)
|
|
202
|
+
return UpdateResponse(
|
|
203
|
+
id=resp["id"],
|
|
204
|
+
version=resp["version"],
|
|
205
|
+
updated_at=resp["updatedAt"],
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
async def delete(self, request: DeleteRequest) -> DeleteResponse:
|
|
209
|
+
"""Delete an entity."""
|
|
210
|
+
payload: dict[str, Any] = {"id": request.id, "deleteMode": request.delete_mode}
|
|
211
|
+
if request.expected_version:
|
|
212
|
+
payload["expectedVersion"] = request.expected_version
|
|
213
|
+
resp = await self._request("DELETE", payload)
|
|
214
|
+
return DeleteResponse(
|
|
215
|
+
id=resp["id"],
|
|
216
|
+
status=resp["status"],
|
|
217
|
+
deleted_at=resp["deletedAt"],
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
async def semantic_search(self, request: SemanticSearchRequest) -> SemanticSearchResponse:
|
|
221
|
+
"""Search for entities by meaning."""
|
|
222
|
+
payload: dict[str, Any] = {"text": request.text}
|
|
223
|
+
if request.filters:
|
|
224
|
+
filters: dict[str, Any] = {}
|
|
225
|
+
if request.filters.type:
|
|
226
|
+
filters["type"] = request.filters.type
|
|
227
|
+
if request.filters.category:
|
|
228
|
+
filters["category"] = request.filters.category
|
|
229
|
+
if request.filters.owner:
|
|
230
|
+
filters["owner"] = request.filters.owner
|
|
231
|
+
if request.filters.metadata:
|
|
232
|
+
filters["metadata"] = request.filters.metadata
|
|
233
|
+
payload["filters"] = filters
|
|
234
|
+
if request.options:
|
|
235
|
+
payload["options"] = {
|
|
236
|
+
"topK": request.options.top_k,
|
|
237
|
+
"offset": request.options.offset,
|
|
238
|
+
"expandGraph": request.options.expand_graph,
|
|
239
|
+
"graphHops": request.options.graph_hops,
|
|
240
|
+
"includeContentPreview": request.options.include_content_preview,
|
|
241
|
+
"includeRelations": request.options.include_relations,
|
|
242
|
+
"useKeyword": request.options.use_keyword,
|
|
243
|
+
}
|
|
244
|
+
resp = await self._request("SEMANTIC_SEARCH", payload)
|
|
245
|
+
items = [_dict_to_search_result(item) for item in resp.get("items", [])]
|
|
246
|
+
return SemanticSearchResponse(
|
|
247
|
+
items=items,
|
|
248
|
+
total=resp.get("total", 0),
|
|
249
|
+
next_offset=resp.get("nextOffset"),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
async def index_batch(
|
|
253
|
+
self,
|
|
254
|
+
items: list[IndexBatchItem],
|
|
255
|
+
options: IndexBatchOptions | None = None,
|
|
256
|
+
) -> IndexBatchResponse:
|
|
257
|
+
"""Index multiple entities in a single batch."""
|
|
258
|
+
batch_items = []
|
|
259
|
+
for item in items:
|
|
260
|
+
entry: dict[str, Any] = {
|
|
261
|
+
"entity": {
|
|
262
|
+
"type": item.entity.type,
|
|
263
|
+
"owner": item.entity.owner,
|
|
264
|
+
"name": item.entity.name,
|
|
265
|
+
},
|
|
266
|
+
"content": {"text": item.content.text},
|
|
267
|
+
}
|
|
268
|
+
if item.entity.id:
|
|
269
|
+
entry["entity"]["id"] = item.entity.id
|
|
270
|
+
if item.entity.metadata:
|
|
271
|
+
entry["entity"]["metadata"] = item.entity.metadata
|
|
272
|
+
if item.entity.relations:
|
|
273
|
+
entry["entity"]["relations"] = [
|
|
274
|
+
{"type": r.type, "targetId": r.target_id}
|
|
275
|
+
for r in item.entity.relations
|
|
276
|
+
]
|
|
277
|
+
if item.content.raw:
|
|
278
|
+
entry["content"]["raw"] = item.content.raw
|
|
279
|
+
if item.content.encoding:
|
|
280
|
+
entry["content"]["encoding"] = item.content.encoding
|
|
281
|
+
if item.category_ids:
|
|
282
|
+
entry["categoryIds"] = item.category_ids
|
|
283
|
+
batch_items.append(entry)
|
|
284
|
+
|
|
285
|
+
payload: dict[str, Any] = {"items": batch_items}
|
|
286
|
+
if options:
|
|
287
|
+
payload["options"] = {"writeConsistency": options.write_consistency}
|
|
288
|
+
|
|
289
|
+
resp = await self._request("INDEX_BATCH", payload)
|
|
290
|
+
results = [
|
|
291
|
+
IndexBatchResultItem(
|
|
292
|
+
id=r.get("id", ""),
|
|
293
|
+
version=r.get("version", ""),
|
|
294
|
+
error=r.get("error"),
|
|
295
|
+
)
|
|
296
|
+
for r in resp.get("results", [])
|
|
297
|
+
]
|
|
298
|
+
return IndexBatchResponse(
|
|
299
|
+
total_count=resp.get("totalCount", 0),
|
|
300
|
+
failed_count=resp.get("failedCount", 0),
|
|
301
|
+
results=results,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
async def delete_batch(self, ids: list[str], delete_mode: str = "soft") -> DeleteBatchResponse:
|
|
305
|
+
"""Delete multiple entities in a single batch."""
|
|
306
|
+
payload: dict[str, Any] = {"ids": ids, "deleteMode": delete_mode}
|
|
307
|
+
resp = await self._request("DELETE_BATCH", payload)
|
|
308
|
+
return DeleteBatchResponse(
|
|
309
|
+
total_count=resp.get("totalCount", 0),
|
|
310
|
+
failed_count=resp.get("failedCount", 0),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
async def list_entity_events(
|
|
314
|
+
self,
|
|
315
|
+
entity_id: str,
|
|
316
|
+
event_types: list[str] | None = None,
|
|
317
|
+
limit: int = 50,
|
|
318
|
+
offset: int = 0,
|
|
319
|
+
) -> dict:
|
|
320
|
+
"""List events for an entity. Returns {events: [...], total: int}."""
|
|
321
|
+
payload: dict[str, Any] = {"entityId": entity_id, "limit": limit, "offset": offset}
|
|
322
|
+
if event_types:
|
|
323
|
+
payload["eventTypes"] = event_types
|
|
324
|
+
return await self._request("LIST_ENTITY_EVENTS", payload)
|
|
325
|
+
|
|
326
|
+
async def embed_batch(self, request: EmbedBatchRequest) -> EmbedBatchResponse:
|
|
327
|
+
"""Generate embedding vectors for multiple texts via the daemon's embedding provider."""
|
|
328
|
+
payload: dict[str, Any] = {"texts": request.texts}
|
|
329
|
+
resp = await self._request("EMBED_BATCH", payload)
|
|
330
|
+
return EmbedBatchResponse(
|
|
331
|
+
embeddings=resp.get("embeddings", []),
|
|
332
|
+
dimensions=resp.get("dimensions", 0),
|
|
333
|
+
model=resp.get("model", ""),
|
|
334
|
+
cached=resp.get("cached", 0),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
async def health(self) -> HealthResponse:
|
|
338
|
+
"""Check daemon health."""
|
|
339
|
+
resp = await self._request("HEALTH", {})
|
|
340
|
+
return HealthResponse(
|
|
341
|
+
status=resp.get("status", "unknown"),
|
|
342
|
+
uptime_seconds=resp.get("uptimeSeconds", 0),
|
|
343
|
+
backends=resp.get("backends", {}),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
async def metrics(self) -> MetricsResponse:
|
|
347
|
+
"""Get daemon metrics."""
|
|
348
|
+
resp = await self._request("METRICS", {})
|
|
349
|
+
return MetricsResponse(data=resp)
|
|
350
|
+
|
|
351
|
+
# =================================================================
|
|
352
|
+
# IAM – Organizations
|
|
353
|
+
# =================================================================
|
|
354
|
+
|
|
355
|
+
async def create_org(self, name: str) -> Organization:
|
|
356
|
+
"""Create a new organization. Requires superadmin or platform_admin."""
|
|
357
|
+
resp = await self._request("CREATE_ORG", {"name": name})
|
|
358
|
+
return Organization(id=resp["id"], name=resp["name"])
|
|
359
|
+
|
|
360
|
+
async def get_org(self, org_id: str = "") -> Organization:
|
|
361
|
+
"""Get organization details. Defaults to caller's org."""
|
|
362
|
+
resp = await self._request("GET_ORG", {"orgId": org_id})
|
|
363
|
+
return Organization(
|
|
364
|
+
id=resp["id"], name=resp["name"], created_at=resp.get("createdAt", ""),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
async def update_org(self, org_id: str, name: str) -> Organization:
|
|
368
|
+
"""Rename an organization. Requires superadmin or platform_admin."""
|
|
369
|
+
resp = await self._request("UPDATE_ORG", {"orgId": org_id, "name": name})
|
|
370
|
+
return Organization(id=resp["id"], name=resp["name"])
|
|
371
|
+
|
|
372
|
+
async def delete_org(self, org_id: str) -> None:
|
|
373
|
+
"""Delete an organization. Requires superadmin or platform_admin."""
|
|
374
|
+
await self._request("DELETE_ORG", {"orgId": org_id})
|
|
375
|
+
|
|
376
|
+
async def list_orgs(self) -> ListOrgsResponse:
|
|
377
|
+
"""List all organizations. Requires superadmin or platform_admin."""
|
|
378
|
+
resp = await self._request("LIST_ORGS", {})
|
|
379
|
+
orgs = [
|
|
380
|
+
Organization(id=o["id"], name=o["name"], created_at=o.get("createdAt", ""))
|
|
381
|
+
for o in resp.get("orgs", [])
|
|
382
|
+
]
|
|
383
|
+
return ListOrgsResponse(orgs=orgs, total=resp.get("total", 0))
|
|
384
|
+
|
|
385
|
+
# =================================================================
|
|
386
|
+
# IAM – Users
|
|
387
|
+
# =================================================================
|
|
388
|
+
|
|
389
|
+
async def create_user(
|
|
390
|
+
self,
|
|
391
|
+
username: str,
|
|
392
|
+
password: str,
|
|
393
|
+
org_id: str = "",
|
|
394
|
+
role: str = "member",
|
|
395
|
+
) -> User:
|
|
396
|
+
"""Create a user. Requires org admin."""
|
|
397
|
+
payload: dict[str, Any] = {
|
|
398
|
+
"username": username,
|
|
399
|
+
"password": password,
|
|
400
|
+
"role": role,
|
|
401
|
+
}
|
|
402
|
+
if org_id:
|
|
403
|
+
payload["orgId"] = org_id
|
|
404
|
+
resp = await self._request("CREATE_USER", payload)
|
|
405
|
+
return User(
|
|
406
|
+
id=resp["id"],
|
|
407
|
+
username=resp["username"],
|
|
408
|
+
org_id=resp["orgId"],
|
|
409
|
+
role=resp["role"],
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
async def get_user(self, user_id: str) -> User:
|
|
413
|
+
"""Get user details. Users can always view themselves."""
|
|
414
|
+
resp = await self._request("GET_USER", {"userId": user_id})
|
|
415
|
+
return User(
|
|
416
|
+
id=resp["id"],
|
|
417
|
+
username=resp["username"],
|
|
418
|
+
org_id=resp["orgId"],
|
|
419
|
+
role=resp["role"],
|
|
420
|
+
platform_role=resp.get("platformRole", ""),
|
|
421
|
+
created_at=resp.get("createdAt", ""),
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
async def update_user(
|
|
425
|
+
self,
|
|
426
|
+
user_id: str,
|
|
427
|
+
role: str = "",
|
|
428
|
+
platform_role: str = "",
|
|
429
|
+
) -> User:
|
|
430
|
+
"""Update a user's role or platform role."""
|
|
431
|
+
payload: dict[str, Any] = {"userId": user_id}
|
|
432
|
+
if role:
|
|
433
|
+
payload["role"] = role
|
|
434
|
+
if platform_role:
|
|
435
|
+
payload["platformRole"] = platform_role
|
|
436
|
+
resp = await self._request("UPDATE_USER", payload)
|
|
437
|
+
return User(
|
|
438
|
+
id=resp["id"],
|
|
439
|
+
username="",
|
|
440
|
+
org_id="",
|
|
441
|
+
role=resp["role"],
|
|
442
|
+
platform_role=resp.get("platformRole", ""),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
async def delete_user(self, user_id: str) -> None:
|
|
446
|
+
"""Delete a user. Requires org admin."""
|
|
447
|
+
await self._request("DELETE_USER", {"userId": user_id})
|
|
448
|
+
|
|
449
|
+
async def list_users(self, org_id: str = "") -> ListUsersResponse:
|
|
450
|
+
"""List users in an organization. Requires org admin."""
|
|
451
|
+
payload: dict[str, Any] = {}
|
|
452
|
+
if org_id:
|
|
453
|
+
payload["orgId"] = org_id
|
|
454
|
+
resp = await self._request("LIST_USERS", payload)
|
|
455
|
+
users = [
|
|
456
|
+
User(
|
|
457
|
+
id=u["id"],
|
|
458
|
+
username=u["username"],
|
|
459
|
+
org_id=u["orgId"],
|
|
460
|
+
role=u["role"],
|
|
461
|
+
platform_role=u.get("platformRole", ""),
|
|
462
|
+
)
|
|
463
|
+
for u in resp.get("users", [])
|
|
464
|
+
]
|
|
465
|
+
return ListUsersResponse(users=users, total=resp.get("total", 0))
|
|
466
|
+
|
|
467
|
+
# =================================================================
|
|
468
|
+
# IAM – Teams
|
|
469
|
+
# =================================================================
|
|
470
|
+
|
|
471
|
+
async def create_team(self, name: str, org_id: str = "") -> Team:
|
|
472
|
+
"""Create a team. Requires org admin."""
|
|
473
|
+
payload: dict[str, Any] = {"name": name}
|
|
474
|
+
if org_id:
|
|
475
|
+
payload["orgId"] = org_id
|
|
476
|
+
resp = await self._request("CREATE_TEAM", payload)
|
|
477
|
+
return Team(id=resp["id"], name=resp["name"], org_id=resp["orgId"])
|
|
478
|
+
|
|
479
|
+
async def get_team(self, team_id: str) -> Team:
|
|
480
|
+
"""Get team details. Requires org admin."""
|
|
481
|
+
resp = await self._request("GET_TEAM", {"teamId": team_id})
|
|
482
|
+
return Team(
|
|
483
|
+
id=resp["id"],
|
|
484
|
+
name=resp["name"],
|
|
485
|
+
org_id=resp["orgId"],
|
|
486
|
+
created_at=resp.get("createdAt", ""),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
async def update_team(self, team_id: str, name: str) -> Team:
|
|
490
|
+
"""Rename a team. Requires org admin."""
|
|
491
|
+
resp = await self._request("UPDATE_TEAM", {"teamId": team_id, "name": name})
|
|
492
|
+
return Team(id=resp["id"], name=resp["name"], org_id="")
|
|
493
|
+
|
|
494
|
+
async def delete_team(self, team_id: str) -> None:
|
|
495
|
+
"""Delete a team. Requires org admin."""
|
|
496
|
+
await self._request("DELETE_TEAM", {"teamId": team_id})
|
|
497
|
+
|
|
498
|
+
async def list_teams(self, org_id: str = "") -> ListTeamsResponse:
|
|
499
|
+
"""List teams in an organization. Requires org admin."""
|
|
500
|
+
payload: dict[str, Any] = {}
|
|
501
|
+
if org_id:
|
|
502
|
+
payload["orgId"] = org_id
|
|
503
|
+
resp = await self._request("LIST_TEAMS", payload)
|
|
504
|
+
teams = [
|
|
505
|
+
Team(id=t["id"], name=t["name"], org_id=t["orgId"])
|
|
506
|
+
for t in resp.get("teams", [])
|
|
507
|
+
]
|
|
508
|
+
return ListTeamsResponse(teams=teams, total=resp.get("total", 0))
|
|
509
|
+
|
|
510
|
+
# =================================================================
|
|
511
|
+
# IAM – Team Members
|
|
512
|
+
# =================================================================
|
|
513
|
+
|
|
514
|
+
async def add_team_member(
|
|
515
|
+
self, team_id: str, user_id: str, role: str = "member",
|
|
516
|
+
) -> None:
|
|
517
|
+
"""Add a user to a team. Requires org admin or team admin."""
|
|
518
|
+
await self._request("ADD_TEAM_MEMBER", {
|
|
519
|
+
"teamId": team_id, "userId": user_id, "role": role,
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
async def remove_team_member(self, team_id: str, user_id: str) -> None:
|
|
523
|
+
"""Remove a user from a team. Requires org admin or team admin."""
|
|
524
|
+
await self._request("REMOVE_TEAM_MEMBER", {
|
|
525
|
+
"teamId": team_id, "userId": user_id,
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
async def list_team_members(self, team_id: str) -> ListTeamMembersResponse:
|
|
529
|
+
"""List members of a team. Requires org admin or team admin."""
|
|
530
|
+
resp = await self._request("LIST_TEAM_MEMBERS", {"teamId": team_id})
|
|
531
|
+
members = [
|
|
532
|
+
TeamMember(
|
|
533
|
+
user_id=m["userId"], username=m["username"], role=m["role"],
|
|
534
|
+
)
|
|
535
|
+
for m in resp.get("members", [])
|
|
536
|
+
]
|
|
537
|
+
return ListTeamMembersResponse(
|
|
538
|
+
team_id=resp.get("teamId", team_id),
|
|
539
|
+
members=members,
|
|
540
|
+
total=resp.get("total", 0),
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# =================================================================
|
|
544
|
+
# IAM – Entity Permissions
|
|
545
|
+
# =================================================================
|
|
546
|
+
|
|
547
|
+
async def grant_permission(
|
|
548
|
+
self,
|
|
549
|
+
entity_id: str,
|
|
550
|
+
grantee_type: str,
|
|
551
|
+
grantee_id: str,
|
|
552
|
+
permission: str = "read",
|
|
553
|
+
) -> None:
|
|
554
|
+
"""Grant access to an entity. Any authenticated user can share."""
|
|
555
|
+
await self._request("GRANT_PERMISSION", {
|
|
556
|
+
"entityId": entity_id,
|
|
557
|
+
"granteeType": grantee_type,
|
|
558
|
+
"granteeId": grantee_id,
|
|
559
|
+
"permission": permission,
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
async def revoke_permission(
|
|
563
|
+
self, entity_id: str, grantee_type: str, grantee_id: str,
|
|
564
|
+
) -> None:
|
|
565
|
+
"""Revoke access to an entity."""
|
|
566
|
+
await self._request("REVOKE_PERMISSION", {
|
|
567
|
+
"entityId": entity_id,
|
|
568
|
+
"granteeType": grantee_type,
|
|
569
|
+
"granteeId": grantee_id,
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
async def list_permissions(self, entity_id: str) -> ListPermissionsResponse:
|
|
573
|
+
"""List all permission grants on an entity."""
|
|
574
|
+
resp = await self._request("LIST_PERMISSIONS", {"entityId": entity_id})
|
|
575
|
+
perms = [
|
|
576
|
+
Permission(
|
|
577
|
+
grantee_type=p["granteeType"],
|
|
578
|
+
grantee_id=p["granteeId"],
|
|
579
|
+
permission=p["permission"],
|
|
580
|
+
granted_by=p.get("grantedBy", ""),
|
|
581
|
+
)
|
|
582
|
+
for p in resp.get("permissions", [])
|
|
583
|
+
]
|
|
584
|
+
return ListPermissionsResponse(
|
|
585
|
+
entity_id=resp.get("entityId", entity_id),
|
|
586
|
+
permissions=perms,
|
|
587
|
+
total=resp.get("total", 0),
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# =================================================================
|
|
591
|
+
# Categories
|
|
592
|
+
# =================================================================
|
|
593
|
+
|
|
594
|
+
async def create_category(
|
|
595
|
+
self, name: str, description: str = "", parent_id: str = "",
|
|
596
|
+
) -> Category:
|
|
597
|
+
payload: dict[str, Any] = {"name": name}
|
|
598
|
+
if description:
|
|
599
|
+
payload["description"] = description
|
|
600
|
+
if parent_id:
|
|
601
|
+
payload["parentId"] = parent_id
|
|
602
|
+
resp = await self._request("CREATE_CATEGORY", payload)
|
|
603
|
+
return Category(
|
|
604
|
+
id=resp.get("id", ""),
|
|
605
|
+
name=resp.get("name", ""),
|
|
606
|
+
org_id=resp.get("orgId", ""),
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
async def get_category(self, category_id: str) -> Category:
|
|
610
|
+
resp = await self._request("GET_CATEGORY", {"categoryId": category_id})
|
|
611
|
+
cat = resp.get("category", {})
|
|
612
|
+
return _dict_to_category(cat)
|
|
613
|
+
|
|
614
|
+
async def update_category(
|
|
615
|
+
self,
|
|
616
|
+
category_id: str,
|
|
617
|
+
*,
|
|
618
|
+
name: str = "",
|
|
619
|
+
description: str = "",
|
|
620
|
+
parent_id: str = "",
|
|
621
|
+
clear_parent: bool = False,
|
|
622
|
+
) -> Category:
|
|
623
|
+
payload: dict[str, Any] = {"categoryId": category_id}
|
|
624
|
+
if name:
|
|
625
|
+
payload["name"] = name
|
|
626
|
+
if description:
|
|
627
|
+
payload["description"] = description
|
|
628
|
+
if parent_id:
|
|
629
|
+
payload["parentId"] = parent_id
|
|
630
|
+
if clear_parent:
|
|
631
|
+
payload["clearParent"] = True
|
|
632
|
+
resp = await self._request("UPDATE_CATEGORY", payload)
|
|
633
|
+
return Category(
|
|
634
|
+
id=resp.get("id", ""),
|
|
635
|
+
name=resp.get("name", ""),
|
|
636
|
+
org_id="",
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
async def delete_category(self, category_id: str) -> None:
|
|
640
|
+
await self._request("DELETE_CATEGORY", {"categoryId": category_id})
|
|
641
|
+
|
|
642
|
+
async def list_categories(
|
|
643
|
+
self, parent_id: str = "", tree: bool = False,
|
|
644
|
+
) -> ListCategoriesResponse:
|
|
645
|
+
payload: dict[str, Any] = {}
|
|
646
|
+
if parent_id:
|
|
647
|
+
payload["parentId"] = parent_id
|
|
648
|
+
if tree:
|
|
649
|
+
payload["tree"] = True
|
|
650
|
+
resp = await self._request("LIST_CATEGORIES", payload)
|
|
651
|
+
cats = [_dict_to_category(c) for c in resp.get("categories", [])]
|
|
652
|
+
return ListCategoriesResponse(categories=cats, total=resp.get("total", 0))
|
|
653
|
+
|
|
654
|
+
async def assign_entity_category(self, entity_id: str, category_id: str) -> None:
|
|
655
|
+
await self._request("ASSIGN_ENTITY_CATEGORY", {
|
|
656
|
+
"entityId": entity_id,
|
|
657
|
+
"categoryId": category_id,
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
async def unassign_entity_category(self, entity_id: str, category_id: str) -> None:
|
|
661
|
+
await self._request("UNASSIGN_ENTITY_CATEGORY", {
|
|
662
|
+
"entityId": entity_id,
|
|
663
|
+
"categoryId": category_id,
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
async def list_entity_categories(self, entity_id: str) -> ListEntityCategoriesResponse:
|
|
667
|
+
resp = await self._request("LIST_ENTITY_CATEGORIES", {"entityId": entity_id})
|
|
668
|
+
cats = [_dict_to_category(c) for c in resp.get("categories", [])]
|
|
669
|
+
return ListEntityCategoriesResponse(
|
|
670
|
+
entity_id=resp.get("entityId", entity_id),
|
|
671
|
+
categories=cats,
|
|
672
|
+
total=resp.get("total", 0),
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def create_client(config: ClientConfig) -> TetrixClient:
|
|
677
|
+
"""Create a new TetrixAIDb client."""
|
|
678
|
+
return TetrixClient(config)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
# --- Internal helpers ---
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _index_request_to_dict(req: IndexRequest) -> dict[str, Any]:
|
|
685
|
+
entity: dict[str, Any] = {
|
|
686
|
+
"type": req.entity.type,
|
|
687
|
+
"owner": req.entity.owner,
|
|
688
|
+
"name": req.entity.name,
|
|
689
|
+
}
|
|
690
|
+
if req.entity.id:
|
|
691
|
+
entity["id"] = req.entity.id
|
|
692
|
+
if req.entity.metadata:
|
|
693
|
+
entity["metadata"] = req.entity.metadata
|
|
694
|
+
if req.entity.relations:
|
|
695
|
+
entity["relations"] = [
|
|
696
|
+
{"type": r.type, "targetId": r.target_id} for r in req.entity.relations
|
|
697
|
+
]
|
|
698
|
+
|
|
699
|
+
content: dict[str, Any] = {"text": req.content.text}
|
|
700
|
+
if req.content.raw:
|
|
701
|
+
content["raw"] = req.content.raw
|
|
702
|
+
if req.content.encoding:
|
|
703
|
+
content["encoding"] = req.content.encoding
|
|
704
|
+
|
|
705
|
+
result: dict[str, Any] = {
|
|
706
|
+
"entity": entity,
|
|
707
|
+
"content": content,
|
|
708
|
+
"writeConsistency": req.write_consistency,
|
|
709
|
+
}
|
|
710
|
+
if req.category_ids:
|
|
711
|
+
result["categoryIds"] = req.category_ids
|
|
712
|
+
return result
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _dict_to_entity(d: dict[str, Any]) -> Any:
|
|
716
|
+
from tetrix_aidb.types import Entity, RelationRef
|
|
717
|
+
|
|
718
|
+
relations = [
|
|
719
|
+
RelationRef(type=r.get("type", ""), target_id=r.get("targetId", ""))
|
|
720
|
+
for r in d.get("relations", [])
|
|
721
|
+
]
|
|
722
|
+
return Entity(
|
|
723
|
+
id=d.get("id", ""),
|
|
724
|
+
type=d.get("type", ""),
|
|
725
|
+
owner=d.get("owner", ""),
|
|
726
|
+
name=d.get("name", ""),
|
|
727
|
+
metadata=d.get("metadata", {}),
|
|
728
|
+
relations=relations,
|
|
729
|
+
version=d.get("version", ""),
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _dict_to_search_result(d: dict[str, Any]) -> Any:
|
|
734
|
+
from tetrix_aidb.types import RelatedEntity, SearchResultItem
|
|
735
|
+
|
|
736
|
+
entity = _dict_to_entity(d.get("entity", {}))
|
|
737
|
+
related = [
|
|
738
|
+
RelatedEntity(
|
|
739
|
+
id=r.get("id", ""),
|
|
740
|
+
type=r.get("type", ""),
|
|
741
|
+
name=r.get("name", ""),
|
|
742
|
+
relation_type=r.get("relationType", ""),
|
|
743
|
+
distance=r.get("distance", 0),
|
|
744
|
+
)
|
|
745
|
+
for r in d.get("relatedEntities", [])
|
|
746
|
+
]
|
|
747
|
+
return SearchResultItem(
|
|
748
|
+
entity=entity,
|
|
749
|
+
score=d.get("score", 0.0),
|
|
750
|
+
source=d.get("source", ""),
|
|
751
|
+
content_preview=d.get("contentPreview"),
|
|
752
|
+
related_entities=related,
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _dict_to_category(d: dict[str, Any]) -> Category:
|
|
757
|
+
children = [_dict_to_category(c) for c in d.get("children", [])]
|
|
758
|
+
return Category(
|
|
759
|
+
id=d.get("id", ""),
|
|
760
|
+
name=d.get("name", ""),
|
|
761
|
+
description=d.get("description", ""),
|
|
762
|
+
org_id=d.get("orgId", ""),
|
|
763
|
+
parent_id=d.get("parentId", ""),
|
|
764
|
+
children=children,
|
|
765
|
+
created_at=d.get("createdAt", ""),
|
|
766
|
+
updated_at=d.get("updatedAt", ""),
|
|
767
|
+
)
|
tetrix_aidb/errors.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Error types for the TetrixAIDb SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TetrixError(Exception):
|
|
9
|
+
"""Base error raised by TetrixAIDb SDK operations."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
code: str,
|
|
14
|
+
message: str,
|
|
15
|
+
retryable: bool = False,
|
|
16
|
+
details: dict[str, Any] | None = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.code = code
|
|
20
|
+
self.retryable = retryable
|
|
21
|
+
self.details = details or {}
|
|
22
|
+
|
|
23
|
+
def __repr__(self) -> str:
|
|
24
|
+
return f"TetrixError(code={self.code!r}, message={self.args[0]!r}, retryable={self.retryable})"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConnectionError(TetrixError):
|
|
28
|
+
"""Raised when the SDK cannot connect to the daemon."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str) -> None:
|
|
31
|
+
super().__init__(code="CONNECTION_ERROR", message=message, retryable=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TimeoutError(TetrixError):
|
|
35
|
+
"""Raised when an operation exceeds the configured timeout."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, message: str) -> None:
|
|
38
|
+
super().__init__(code="TIMEOUT", message=message, retryable=True)
|
tetrix_aidb/protocol.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Wire protocol framing: 4-byte big-endian length prefix + JSON payload."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import struct
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
HEADER_FORMAT = "!I" # big-endian uint32
|
|
10
|
+
HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def encode_frame(payload: dict[str, Any]) -> bytes:
|
|
14
|
+
"""Encode a payload dict into a length-prefixed JSON frame."""
|
|
15
|
+
body = json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
|
16
|
+
header = struct.pack(HEADER_FORMAT, len(body))
|
|
17
|
+
return header + body
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def read_frame(reader: Any, max_size: int = 16 << 20) -> dict[str, Any]:
|
|
21
|
+
"""Read a single length-prefixed frame from an asyncio StreamReader."""
|
|
22
|
+
header = await reader.readexactly(HEADER_SIZE)
|
|
23
|
+
(length,) = struct.unpack(HEADER_FORMAT, header)
|
|
24
|
+
|
|
25
|
+
if length > max_size:
|
|
26
|
+
raise ValueError(f"frame size {length} exceeds max {max_size}")
|
|
27
|
+
|
|
28
|
+
body = await reader.readexactly(length)
|
|
29
|
+
return json.loads(body)
|
tetrix_aidb/types.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Core types matching the TetrixAIDb protocol specification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ClientConfig:
|
|
11
|
+
endpoint: str
|
|
12
|
+
username: str
|
|
13
|
+
password: str
|
|
14
|
+
timeout_ms: int = 5000
|
|
15
|
+
max_connections: int = 256
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class RelationRef:
|
|
20
|
+
type: str
|
|
21
|
+
target_id: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Entity:
|
|
26
|
+
id: str = ""
|
|
27
|
+
type: str = ""
|
|
28
|
+
owner: str = ""
|
|
29
|
+
name: str = ""
|
|
30
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
31
|
+
relations: list[RelationRef] = field(default_factory=list)
|
|
32
|
+
version: str = ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class EntityContent:
|
|
37
|
+
text: str = ""
|
|
38
|
+
raw: str | None = None
|
|
39
|
+
encoding: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class IndexRequest:
|
|
44
|
+
entity: Entity
|
|
45
|
+
content: EntityContent
|
|
46
|
+
idempotency_key: str | None = None
|
|
47
|
+
category_ids: list[str] | None = None
|
|
48
|
+
write_consistency: str = "strong"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class IndexResponse:
|
|
53
|
+
id: str
|
|
54
|
+
version: str
|
|
55
|
+
created_at: str
|
|
56
|
+
write_consistency: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class GetRequest:
|
|
61
|
+
id: str
|
|
62
|
+
include_content: bool = True
|
|
63
|
+
include_relations: bool = True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class GetResponse:
|
|
68
|
+
entity: Entity
|
|
69
|
+
content: EntityContent | None = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class UpdatePatch:
|
|
74
|
+
name: str | None = None
|
|
75
|
+
metadata: dict[str, Any] | None = None
|
|
76
|
+
relations: list[RelationRef] | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class ContentPatch:
|
|
81
|
+
text: str | None = None
|
|
82
|
+
raw: str | None = None
|
|
83
|
+
encoding: str | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class UpdateRequest:
|
|
88
|
+
id: str
|
|
89
|
+
expected_version: str | None = None
|
|
90
|
+
patch: UpdatePatch | None = None
|
|
91
|
+
content_patch: ContentPatch | None = None
|
|
92
|
+
write_consistency: str = "strong"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class UpdateResponse:
|
|
97
|
+
id: str
|
|
98
|
+
version: str
|
|
99
|
+
updated_at: str
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class DeleteRequest:
|
|
104
|
+
id: str
|
|
105
|
+
expected_version: str | None = None
|
|
106
|
+
delete_mode: str = "soft"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class DeleteResponse:
|
|
111
|
+
id: str
|
|
112
|
+
status: str
|
|
113
|
+
deleted_at: str
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class IndexBatchItem:
|
|
118
|
+
entity: Entity
|
|
119
|
+
content: EntityContent
|
|
120
|
+
idempotency_key: str | None = None
|
|
121
|
+
category_ids: list[str] | None = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class IndexBatchOptions:
|
|
126
|
+
write_consistency: str = "strong"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class IndexBatchResultItem:
|
|
131
|
+
id: str
|
|
132
|
+
version: str
|
|
133
|
+
error: str | None = None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class IndexBatchResponse:
|
|
138
|
+
total_count: int
|
|
139
|
+
failed_count: int
|
|
140
|
+
results: list[IndexBatchResultItem]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class DeleteBatchResponse:
|
|
145
|
+
total_count: int
|
|
146
|
+
failed_count: int
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class HealthResponse:
|
|
151
|
+
status: str
|
|
152
|
+
uptime_seconds: int
|
|
153
|
+
backends: dict[str, str] = field(default_factory=dict)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class MetricsResponse:
|
|
158
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass
|
|
162
|
+
class SearchOptions:
|
|
163
|
+
top_k: int = 20
|
|
164
|
+
offset: int = 0
|
|
165
|
+
expand_graph: bool = False
|
|
166
|
+
graph_hops: int = 1
|
|
167
|
+
include_content_preview: bool = True
|
|
168
|
+
include_relations: bool = False
|
|
169
|
+
use_keyword: bool = True
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass
|
|
173
|
+
class SearchFilters:
|
|
174
|
+
type: list[str] | None = None
|
|
175
|
+
category: list[str] | None = None
|
|
176
|
+
owner: list[str] | None = None
|
|
177
|
+
metadata: dict[str, Any] | None = None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class SemanticSearchRequest:
|
|
182
|
+
text: str
|
|
183
|
+
filters: SearchFilters | None = None
|
|
184
|
+
options: SearchOptions | None = None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class RelatedEntity:
|
|
189
|
+
id: str
|
|
190
|
+
type: str
|
|
191
|
+
name: str
|
|
192
|
+
relation_type: str
|
|
193
|
+
distance: int
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class SearchResultItem:
|
|
198
|
+
entity: Entity
|
|
199
|
+
score: float
|
|
200
|
+
source: str # "vector" | "keyword" | "hybrid"
|
|
201
|
+
content_preview: str | None = None
|
|
202
|
+
related_entities: list[RelatedEntity] = field(default_factory=list)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class SemanticSearchResponse:
|
|
207
|
+
items: list[SearchResultItem]
|
|
208
|
+
total: int
|
|
209
|
+
next_offset: int | None = None
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# =====================================================================
|
|
213
|
+
# IAM – Organizations
|
|
214
|
+
# =====================================================================
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass
|
|
218
|
+
class Organization:
|
|
219
|
+
id: str
|
|
220
|
+
name: str
|
|
221
|
+
created_at: str = ""
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass
|
|
225
|
+
class ListOrgsResponse:
|
|
226
|
+
orgs: list[Organization]
|
|
227
|
+
total: int
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# =====================================================================
|
|
231
|
+
# IAM – Users
|
|
232
|
+
# =====================================================================
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@dataclass
|
|
236
|
+
class User:
|
|
237
|
+
id: str
|
|
238
|
+
username: str
|
|
239
|
+
org_id: str
|
|
240
|
+
role: str
|
|
241
|
+
platform_role: str = ""
|
|
242
|
+
created_at: str = ""
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@dataclass
|
|
246
|
+
class ListUsersResponse:
|
|
247
|
+
users: list[User]
|
|
248
|
+
total: int
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# =====================================================================
|
|
252
|
+
# IAM – Teams
|
|
253
|
+
# =====================================================================
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass
|
|
257
|
+
class Team:
|
|
258
|
+
id: str
|
|
259
|
+
name: str
|
|
260
|
+
org_id: str
|
|
261
|
+
created_at: str = ""
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclass
|
|
265
|
+
class ListTeamsResponse:
|
|
266
|
+
teams: list[Team]
|
|
267
|
+
total: int
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# =====================================================================
|
|
271
|
+
# IAM – Team Members
|
|
272
|
+
# =====================================================================
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass
|
|
276
|
+
class TeamMember:
|
|
277
|
+
user_id: str
|
|
278
|
+
username: str
|
|
279
|
+
role: str
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass
|
|
283
|
+
class ListTeamMembersResponse:
|
|
284
|
+
team_id: str
|
|
285
|
+
members: list[TeamMember]
|
|
286
|
+
total: int
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
# =====================================================================
|
|
290
|
+
# IAM – Entity Permissions
|
|
291
|
+
# =====================================================================
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@dataclass
|
|
295
|
+
class Permission:
|
|
296
|
+
grantee_type: str # "user" | "team"
|
|
297
|
+
grantee_id: str
|
|
298
|
+
permission: str # "read" | "write"
|
|
299
|
+
granted_by: str = ""
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@dataclass
|
|
303
|
+
class ListPermissionsResponse:
|
|
304
|
+
entity_id: str
|
|
305
|
+
permissions: list[Permission]
|
|
306
|
+
total: int
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
# =====================================================================
|
|
310
|
+
# EMBED_BATCH
|
|
311
|
+
# =====================================================================
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@dataclass
|
|
315
|
+
class EmbedBatchRequest:
|
|
316
|
+
texts: list[str]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@dataclass
|
|
320
|
+
class EmbedBatchResponse:
|
|
321
|
+
embeddings: list[list[float]]
|
|
322
|
+
dimensions: int
|
|
323
|
+
model: str = ""
|
|
324
|
+
cached: int = 0
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# =====================================================================
|
|
328
|
+
# Categories
|
|
329
|
+
# =====================================================================
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@dataclass
|
|
333
|
+
class Category:
|
|
334
|
+
id: str
|
|
335
|
+
name: str
|
|
336
|
+
org_id: str
|
|
337
|
+
description: str = ""
|
|
338
|
+
parent_id: str = ""
|
|
339
|
+
children: list["Category"] = field(default_factory=list)
|
|
340
|
+
created_at: str = ""
|
|
341
|
+
updated_at: str = ""
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@dataclass
|
|
345
|
+
class ListCategoriesResponse:
|
|
346
|
+
categories: list[Category]
|
|
347
|
+
total: int
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@dataclass
|
|
351
|
+
class ListEntityCategoriesResponse:
|
|
352
|
+
entity_id: str
|
|
353
|
+
categories: list[Category]
|
|
354
|
+
total: int
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tetrix-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: TetrixAIDb Python SDK — AI-native database client
|
|
5
|
+
Project-URL: Repository, https://github.com/deskree-inc/tetrix-ee
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
11
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# TetrixAIDb Python SDK
|
|
15
|
+
|
|
16
|
+
Async Python client for [TetrixAIDb](https://github.com/deskree-inc/tetrix-ee), the AI-native database daemon that unifies vector, keyword, graph, and object storage behind a single binary protocol.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install tetrix-aidb
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Python 3.11+.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import asyncio
|
|
30
|
+
from tetrix_aidb import ClientConfig, TetrixClient
|
|
31
|
+
|
|
32
|
+
async def main():
|
|
33
|
+
client = TetrixClient(ClientConfig(
|
|
34
|
+
endpoint="tcp://localhost:7779",
|
|
35
|
+
username="admin",
|
|
36
|
+
password="secret",
|
|
37
|
+
))
|
|
38
|
+
await client.connect()
|
|
39
|
+
|
|
40
|
+
health = await client.health()
|
|
41
|
+
print(f"Status: {health.status}")
|
|
42
|
+
|
|
43
|
+
await client.close()
|
|
44
|
+
|
|
45
|
+
asyncio.run(main())
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- Async-first API built on `asyncio`
|
|
51
|
+
- TCP, TLS, and Unix socket transports
|
|
52
|
+
- Auth-on-connect with automatic session handling
|
|
53
|
+
- Entity indexing, semantic search, and batch operations
|
|
54
|
+
- IAM operations: organizations, users, teams, permissions
|
|
55
|
+
- Category management
|
|
56
|
+
- Typed request/response models
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
tetrix_aidb/__init__.py,sha256=JagoVNgSnYp-bNaa8gr9d3uL0_A-5lyKkHfV4IWn_F8,1336
|
|
2
|
+
tetrix_aidb/client.py,sha256=FYHRvlr3TQmu-BCBoq1s4xQD3g3wCOMNcz8XJVtUSCw,27839
|
|
3
|
+
tetrix_aidb/errors.py,sha256=ZXqaw_4w8rMQd2sRSuxTCRWuQxZzK_aRcYCSGNnHwtU,1079
|
|
4
|
+
tetrix_aidb/protocol.py,sha256=GPRYGoHpCAYeyYZc1hS98o7qYOTGBww7Z1uNNWnDc04,951
|
|
5
|
+
tetrix_aidb/types.py,sha256=RN2DSwYGKg_sC3vSWJsY1H_fjrDfHQFUW3e6M9AfdqU,6590
|
|
6
|
+
tetrix_sdk-0.1.1.dist-info/METADATA,sha256=KlzCn7Wanwn2ZR-sE244DqdM917OiguCmo-1bk-2WoA,1460
|
|
7
|
+
tetrix_sdk-0.1.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
8
|
+
tetrix_sdk-0.1.1.dist-info/RECORD,,
|