axonbase-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.
- axonbase/__init__.py +30 -0
- axonbase/client.py +286 -0
- axonbase/codec.py +33 -0
- axonbase/dbapi.py +549 -0
- axonbase/errors.py +69 -0
- axonbase/integrations/__init__.py +1 -0
- axonbase/integrations/agno.py +137 -0
- axonbase/integrations/langchain.py +77 -0
- axonbase/migration/__init__.py +5 -0
- axonbase/migration/migrator.py +122 -0
- axonbase/openai_embeddings.py +36 -0
- axonbase/protocol.py +42 -0
- axonbase/saga.py +119 -0
- axonbase/sqlalchemy.py +113 -0
- axonbase/sqlalchemy_dialect.py +145 -0
- axonbase/vectorstore.py +181 -0
- axonbase_sdk-0.1.1.dist-info/METADATA +35 -0
- axonbase_sdk-0.1.1.dist-info/RECORD +21 -0
- axonbase_sdk-0.1.1.dist-info/WHEEL +5 -0
- axonbase_sdk-0.1.1.dist-info/licenses/LICENSE +21 -0
- axonbase_sdk-0.1.1.dist-info/top_level.txt +1 -0
axonbase/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from .client import Axon
|
|
2
|
+
from .protocol import (
|
|
3
|
+
CertificateChallenge, CertificateCompletion, PROTOCOL_VERSION, METHODS,
|
|
4
|
+
ERR_PARSE, ERR_INVALID_REQ, ERR_METHOD_NOT_FOUND, ERR_INVALID_PARAMS,
|
|
5
|
+
ERR_INTERNAL, ERR_AUTH, ERR_RUNTIME, ERR_TXN_CONFLICT,
|
|
6
|
+
ERR_NOT_LEADER, ERR_NO_QUORUM, ERR_RATE_LIMIT,
|
|
7
|
+
)
|
|
8
|
+
from .errors import (
|
|
9
|
+
AxonSdkError, NotLeaderError, NoQuorumError, RateLimitError,
|
|
10
|
+
ProtocolMismatchError, AuthError, ConflictError, from_rpc_error,
|
|
11
|
+
)
|
|
12
|
+
from .codec import encode_typed, is_typed_value
|
|
13
|
+
from .openai_embeddings import OpenAIEmbedder
|
|
14
|
+
from .vectorstore import AxonBaseVectorStore, Embedder, SearchResult, VectorRecord
|
|
15
|
+
from .saga import SagaTransaction
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Axon",
|
|
19
|
+
"CertificateChallenge", "CertificateCompletion",
|
|
20
|
+
"PROTOCOL_VERSION", "METHODS",
|
|
21
|
+
"ERR_PARSE", "ERR_INVALID_REQ", "ERR_METHOD_NOT_FOUND", "ERR_INVALID_PARAMS",
|
|
22
|
+
"ERR_INTERNAL", "ERR_AUTH", "ERR_RUNTIME", "ERR_TXN_CONFLICT",
|
|
23
|
+
"ERR_NOT_LEADER", "ERR_NO_QUORUM", "ERR_RATE_LIMIT",
|
|
24
|
+
"AxonSdkError", "NotLeaderError", "NoQuorumError", "RateLimitError",
|
|
25
|
+
"ProtocolMismatchError", "AuthError", "ConflictError", "from_rpc_error",
|
|
26
|
+
"encode_typed", "is_typed_value",
|
|
27
|
+
"OpenAIEmbedder",
|
|
28
|
+
"AxonBaseVectorStore", "Embedder", "SearchResult", "VectorRecord",
|
|
29
|
+
"SagaTransaction",
|
|
30
|
+
]
|
axonbase/client.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import ssl
|
|
5
|
+
from typing import Any, Callable
|
|
6
|
+
|
|
7
|
+
import websockets
|
|
8
|
+
|
|
9
|
+
from .protocol import CertificateChallenge, CertificateCompletion, PROTOCOL_VERSION
|
|
10
|
+
from .errors import AxonSdkError, NotLeaderError, from_rpc_error
|
|
11
|
+
from .codec import encode_typed
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("axonbase")
|
|
14
|
+
|
|
15
|
+
LiveHandler = Callable[[str, str, Any], None]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Axon:
|
|
19
|
+
def __init__(self, url: str, *,
|
|
20
|
+
timeout: float = 30.0,
|
|
21
|
+
reconnect: bool = True,
|
|
22
|
+
reconnect_interval: float = 1.0,
|
|
23
|
+
max_reconnect_attempts: int = 10,
|
|
24
|
+
ssl_context: ssl.SSLContext | None = None):
|
|
25
|
+
self.url = url
|
|
26
|
+
self.timeout = timeout
|
|
27
|
+
self.reconnect = reconnect
|
|
28
|
+
self.reconnect_interval = reconnect_interval
|
|
29
|
+
self.max_reconnect_attempts = max_reconnect_attempts
|
|
30
|
+
self.ssl_context = ssl_context
|
|
31
|
+
|
|
32
|
+
self._ws = None
|
|
33
|
+
self._next_id = 1
|
|
34
|
+
self._pending = {}
|
|
35
|
+
self._live_handlers = {}
|
|
36
|
+
self._handshake_received = False
|
|
37
|
+
self._namespace = ""
|
|
38
|
+
self._database = ""
|
|
39
|
+
self._auth: str | None = None
|
|
40
|
+
self._hello = None
|
|
41
|
+
self._closed = False
|
|
42
|
+
self._loop = None
|
|
43
|
+
|
|
44
|
+
async def _open_ws(self):
|
|
45
|
+
self._ws = await websockets.connect(self.url, ssl=self.ssl_context)
|
|
46
|
+
hello = await asyncio.wait_for(self._ws.recv(), self.timeout)
|
|
47
|
+
parsed = json.loads(hello)
|
|
48
|
+
if "hello" not in parsed:
|
|
49
|
+
raise AxonSdkError("handshake hello esperado como primeiro frame")
|
|
50
|
+
self._hello = parsed["hello"]
|
|
51
|
+
self._handshake_received = True
|
|
52
|
+
self._loop = asyncio.get_running_loop()
|
|
53
|
+
asyncio.ensure_future(self._listen())
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
async def connect(cls, url: str, **kwargs):
|
|
57
|
+
axon = cls(url, **kwargs)
|
|
58
|
+
await axon._open_ws()
|
|
59
|
+
return axon
|
|
60
|
+
|
|
61
|
+
async def _send_recv(self, method: str, params: list) -> Any:
|
|
62
|
+
if not self._ws:
|
|
63
|
+
raise AxonSdkError("conexão não aberta")
|
|
64
|
+
_id = self._next_id
|
|
65
|
+
self._next_id += 1
|
|
66
|
+
request = {
|
|
67
|
+
"id": _id,
|
|
68
|
+
"method": method,
|
|
69
|
+
"params": [encode_typed(p) for p in params],
|
|
70
|
+
"version": PROTOCOL_VERSION,
|
|
71
|
+
}
|
|
72
|
+
fut = self._loop.create_future()
|
|
73
|
+
self._pending[_id] = fut
|
|
74
|
+
try:
|
|
75
|
+
await self._ws.send(json.dumps(request))
|
|
76
|
+
return await asyncio.wait_for(fut, self.timeout)
|
|
77
|
+
finally:
|
|
78
|
+
self._pending.pop(_id, None)
|
|
79
|
+
|
|
80
|
+
async def _call(self, method: str, params: list, retries: int = 3) -> Any:
|
|
81
|
+
for attempt in range(retries):
|
|
82
|
+
try:
|
|
83
|
+
response = await self._send_recv(method, params)
|
|
84
|
+
if "error" in response:
|
|
85
|
+
error = response["error"]
|
|
86
|
+
exc = from_rpc_error(error)
|
|
87
|
+
if isinstance(exc, NotLeaderError) and exc.leader_address and attempt < retries - 1:
|
|
88
|
+
new_url = f"ws://{exc.leader_address}/rpc/ws"
|
|
89
|
+
await self._reconnect_to(new_url)
|
|
90
|
+
continue
|
|
91
|
+
raise exc
|
|
92
|
+
return response.get("result")
|
|
93
|
+
except (websockets.ConnectionClosed, BrokenPipeError) as e:
|
|
94
|
+
if attempt < retries - 1 and self.reconnect:
|
|
95
|
+
await asyncio.sleep(self.reconnect_interval)
|
|
96
|
+
await self._open_ws()
|
|
97
|
+
if self._namespace:
|
|
98
|
+
await self._send_recv("use", [self._namespace, self._database])
|
|
99
|
+
if self._auth:
|
|
100
|
+
await self._send_recv("authenticate", [self._auth])
|
|
101
|
+
continue
|
|
102
|
+
raise AxonSdkError(f"conexão perdida: {e}") from e
|
|
103
|
+
raise AxonSdkError("falha após tentativas de reconexão")
|
|
104
|
+
|
|
105
|
+
async def _reconnect_to(self, new_url: str):
|
|
106
|
+
if self._ws:
|
|
107
|
+
await self._ws.close()
|
|
108
|
+
self._handshake_received = False
|
|
109
|
+
self._hello = None
|
|
110
|
+
self.url = new_url
|
|
111
|
+
await self._open_ws()
|
|
112
|
+
if self._namespace:
|
|
113
|
+
await self._send_recv("use", [self._namespace, self._database])
|
|
114
|
+
if self._auth:
|
|
115
|
+
await self._send_recv("authenticate", [self._auth])
|
|
116
|
+
|
|
117
|
+
async def _listen(self):
|
|
118
|
+
while not self._closed and self._ws:
|
|
119
|
+
try:
|
|
120
|
+
msg = await self._ws.recv()
|
|
121
|
+
except websockets.ConnectionClosed:
|
|
122
|
+
break
|
|
123
|
+
try:
|
|
124
|
+
parsed = json.loads(msg)
|
|
125
|
+
except json.JSONDecodeError:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
if isinstance(parsed, dict):
|
|
129
|
+
if "notification" in parsed:
|
|
130
|
+
n = parsed["notification"]
|
|
131
|
+
handler = self._live_handlers.get(n["id"])
|
|
132
|
+
if handler:
|
|
133
|
+
handler(n["id"], n["action"], n["result"])
|
|
134
|
+
continue
|
|
135
|
+
rpc_id = parsed.get("id")
|
|
136
|
+
if rpc_id is not None and rpc_id in self._pending:
|
|
137
|
+
fut = self._pending.pop(rpc_id)
|
|
138
|
+
if not fut.done():
|
|
139
|
+
fut.set_result(parsed)
|
|
140
|
+
|
|
141
|
+
# ============================================================
|
|
142
|
+
# RPC methods
|
|
143
|
+
# ============================================================
|
|
144
|
+
|
|
145
|
+
async def ping(self) -> bool:
|
|
146
|
+
return await self._call("ping", [])
|
|
147
|
+
|
|
148
|
+
async def use(self, ns: str, db: str) -> dict:
|
|
149
|
+
result = await self._call("use", [ns, db])
|
|
150
|
+
self._namespace = result.get("namespace", ns)
|
|
151
|
+
self._database = result.get("database", db)
|
|
152
|
+
return result
|
|
153
|
+
|
|
154
|
+
async def query(self, sql: str, vars: dict | None = None) -> Any:
|
|
155
|
+
params = [sql, vars] if vars else [sql]
|
|
156
|
+
return await self._call("query", params)
|
|
157
|
+
|
|
158
|
+
async def signin(self, user: str, passwd: str, access: str | None = None) -> str:
|
|
159
|
+
cred = {"user": user, "pass": passwd}
|
|
160
|
+
if access:
|
|
161
|
+
cred["access"] = access
|
|
162
|
+
token = await self._call("signin", [cred])
|
|
163
|
+
self._auth = token
|
|
164
|
+
return token
|
|
165
|
+
|
|
166
|
+
async def authenticate(self, token: str):
|
|
167
|
+
await self._call("authenticate", [token])
|
|
168
|
+
self._auth = token
|
|
169
|
+
|
|
170
|
+
async def signup(self, user: str, passwd: str) -> str:
|
|
171
|
+
token = await self._call("signup", [{"user": user, "pass": passwd}])
|
|
172
|
+
self._auth = token
|
|
173
|
+
return token
|
|
174
|
+
|
|
175
|
+
async def invalidate(self):
|
|
176
|
+
await self._call("invalidate", [])
|
|
177
|
+
self._auth = None
|
|
178
|
+
|
|
179
|
+
async def certificate_begin(self, store: str) -> CertificateChallenge:
|
|
180
|
+
result = await self._call("certificate.begin", [{"store": store}])
|
|
181
|
+
return CertificateChallenge(
|
|
182
|
+
id=result["id"],
|
|
183
|
+
challenge=result["challenge"],
|
|
184
|
+
expires_at=result["expires_at"],
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
async def certificate_complete(self, completion: CertificateCompletion) -> str:
|
|
188
|
+
return await self._call("certificate.complete", [{
|
|
189
|
+
"id": completion.id,
|
|
190
|
+
"challenge": completion.challenge,
|
|
191
|
+
"store": completion.store,
|
|
192
|
+
"user": completion.user,
|
|
193
|
+
"chain": completion.chain,
|
|
194
|
+
"signature": completion.signature,
|
|
195
|
+
}])
|
|
196
|
+
|
|
197
|
+
async def version(self) -> str:
|
|
198
|
+
return await self._call("version", [])
|
|
199
|
+
|
|
200
|
+
async def select(self, what: str) -> Any:
|
|
201
|
+
return await self._call("select", [what])
|
|
202
|
+
|
|
203
|
+
async def create(self, what: str, data: dict) -> Any:
|
|
204
|
+
return await self._call("create", [what, data])
|
|
205
|
+
|
|
206
|
+
async def update(self, what: str, data: dict) -> Any:
|
|
207
|
+
return await self._call("update", [what, data])
|
|
208
|
+
|
|
209
|
+
async def upsert(self, what: str, data: dict) -> Any:
|
|
210
|
+
return await self._call("upsert", [what, data])
|
|
211
|
+
|
|
212
|
+
async def delete(self, what: str) -> Any:
|
|
213
|
+
return await self._call("delete", [what])
|
|
214
|
+
|
|
215
|
+
async def insert(self, table: str, data: dict) -> Any:
|
|
216
|
+
return await self._call("insert", [table, data])
|
|
217
|
+
|
|
218
|
+
async def relate(self, from_: str, kind: str, to: str, data: dict | None = None) -> Any:
|
|
219
|
+
return await self._call("relate", [from_, kind, to, data or {}])
|
|
220
|
+
|
|
221
|
+
async def let_var(self, name: str, value: Any):
|
|
222
|
+
clean = name[1:] if name.startswith("$") else name
|
|
223
|
+
await self._send_recv("let", [clean, value])
|
|
224
|
+
|
|
225
|
+
async def unset(self, name: str):
|
|
226
|
+
clean = name[1:] if name.startswith("$") else name
|
|
227
|
+
await self._send_recv("unset", [clean])
|
|
228
|
+
|
|
229
|
+
async def begin(self):
|
|
230
|
+
await self._call("begin", [])
|
|
231
|
+
|
|
232
|
+
async def commit(self):
|
|
233
|
+
await self._call("commit", [])
|
|
234
|
+
|
|
235
|
+
async def cancel(self):
|
|
236
|
+
await self._call("cancel", [])
|
|
237
|
+
|
|
238
|
+
async def kv_get(self, ns: str, db: str, key: str) -> Any:
|
|
239
|
+
return await self._call("kv_get", [ns, db, key])
|
|
240
|
+
|
|
241
|
+
async def kv_set(self, ns: str, db: str, key: str, value: Any, ttl: int | None = None) -> Any:
|
|
242
|
+
return await self._call("kv_set", [ns, db, key, value, ttl])
|
|
243
|
+
|
|
244
|
+
async def kv_del(self, ns: str, db: str, key: str) -> bool:
|
|
245
|
+
return await self._call("kv_del", [ns, db, key])
|
|
246
|
+
|
|
247
|
+
async def kv_scan(self, ns: str, db: str, prefix: str) -> list:
|
|
248
|
+
return await self._call("kv_scan", [ns, db, prefix])
|
|
249
|
+
|
|
250
|
+
async def live(self, table: str, handler: LiveHandler, diff: bool = False) -> str:
|
|
251
|
+
id_ = await self._call("live", [table, diff])
|
|
252
|
+
self._live_handlers[id_] = handler
|
|
253
|
+
return id_
|
|
254
|
+
|
|
255
|
+
async def kill(self, id_: str) -> bool:
|
|
256
|
+
ok = await self._call("kill", [id_])
|
|
257
|
+
if ok:
|
|
258
|
+
self._live_handlers.pop(id_, None)
|
|
259
|
+
return ok
|
|
260
|
+
|
|
261
|
+
# ============================================================
|
|
262
|
+
# Properties
|
|
263
|
+
# ============================================================
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def namespace(self) -> str:
|
|
267
|
+
return self._namespace
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def database(self) -> str:
|
|
271
|
+
return self._database
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def is_connected(self) -> bool:
|
|
275
|
+
return self._ws is not None and not self._ws.closed
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def hello_info(self) -> dict | None:
|
|
279
|
+
return self._hello
|
|
280
|
+
|
|
281
|
+
async def close(self):
|
|
282
|
+
self._closed = True
|
|
283
|
+
self._live_handlers.clear()
|
|
284
|
+
if self._ws:
|
|
285
|
+
await self._ws.close()
|
|
286
|
+
self._ws = None
|
axonbase/codec.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
TYPED_KEYS = frozenset({
|
|
2
|
+
"$decimal", "$bytes", "$datetime", "$duration", "$record", "$uuid", "$table",
|
|
3
|
+
})
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_typed_value(v):
|
|
7
|
+
if not isinstance(v, dict):
|
|
8
|
+
return False
|
|
9
|
+
if len(v) != 1:
|
|
10
|
+
return False
|
|
11
|
+
key = next(iter(v))
|
|
12
|
+
return key in TYPED_KEYS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def typed_value_to_string(v):
|
|
16
|
+
key = next(iter(v))
|
|
17
|
+
return f"{key}={v[key]}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def encode_typed(value):
|
|
21
|
+
if value is None:
|
|
22
|
+
return None
|
|
23
|
+
if isinstance(value, (bool, int, float)):
|
|
24
|
+
return value
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
return value
|
|
27
|
+
if isinstance(value, (list, tuple)):
|
|
28
|
+
return [encode_typed(v) for v in value]
|
|
29
|
+
if isinstance(value, dict):
|
|
30
|
+
if is_typed_value(value):
|
|
31
|
+
return value
|
|
32
|
+
return {k: encode_typed(v) for k, v in value.items()}
|
|
33
|
+
return str(value)
|