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 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)