axonbase-sdk 0.1.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.
Files changed (32) hide show
  1. axonbase_sdk-0.1.1/LICENSE +21 -0
  2. axonbase_sdk-0.1.1/PKG-INFO +35 -0
  3. axonbase_sdk-0.1.1/README.md +11 -0
  4. axonbase_sdk-0.1.1/axonbase/__init__.py +30 -0
  5. axonbase_sdk-0.1.1/axonbase/client.py +286 -0
  6. axonbase_sdk-0.1.1/axonbase/codec.py +33 -0
  7. axonbase_sdk-0.1.1/axonbase/dbapi.py +549 -0
  8. axonbase_sdk-0.1.1/axonbase/errors.py +69 -0
  9. axonbase_sdk-0.1.1/axonbase/integrations/__init__.py +1 -0
  10. axonbase_sdk-0.1.1/axonbase/integrations/agno.py +137 -0
  11. axonbase_sdk-0.1.1/axonbase/integrations/langchain.py +77 -0
  12. axonbase_sdk-0.1.1/axonbase/migration/__init__.py +5 -0
  13. axonbase_sdk-0.1.1/axonbase/migration/migrator.py +122 -0
  14. axonbase_sdk-0.1.1/axonbase/openai_embeddings.py +36 -0
  15. axonbase_sdk-0.1.1/axonbase/protocol.py +42 -0
  16. axonbase_sdk-0.1.1/axonbase/saga.py +119 -0
  17. axonbase_sdk-0.1.1/axonbase/sqlalchemy.py +113 -0
  18. axonbase_sdk-0.1.1/axonbase/sqlalchemy_dialect.py +145 -0
  19. axonbase_sdk-0.1.1/axonbase/vectorstore.py +181 -0
  20. axonbase_sdk-0.1.1/axonbase_sdk.egg-info/PKG-INFO +35 -0
  21. axonbase_sdk-0.1.1/axonbase_sdk.egg-info/SOURCES.txt +30 -0
  22. axonbase_sdk-0.1.1/axonbase_sdk.egg-info/dependency_links.txt +1 -0
  23. axonbase_sdk-0.1.1/axonbase_sdk.egg-info/requires.txt +14 -0
  24. axonbase_sdk-0.1.1/axonbase_sdk.egg-info/top_level.txt +1 -0
  25. axonbase_sdk-0.1.1/pyproject.toml +33 -0
  26. axonbase_sdk-0.1.1/setup.cfg +4 -0
  27. axonbase_sdk-0.1.1/tests/test_basic.py +210 -0
  28. axonbase_sdk-0.1.1/tests/test_certificates.py +78 -0
  29. axonbase_sdk-0.1.1/tests/test_optional_adapters.py +154 -0
  30. axonbase_sdk-0.1.1/tests/test_sqlalchemy_saga.py +112 -0
  31. axonbase_sdk-0.1.1/tests/test_vectorstore.py +121 -0
  32. axonbase_sdk-0.1.1/tests/test_vectorstore_integration.py +37 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AxonBase
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: axonbase-sdk
3
+ Version: 0.1.1
4
+ Summary: AxonBase WebSocket JSON-RPC client for Python
5
+ Author: AxonBase
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/axonbase/axonbase
8
+ Project-URL: Repository, https://github.com/axonbase/axonbase
9
+ Project-URL: Issues, https://github.com/axonbase/axonbase/issues
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: websockets>=14.0
14
+ Provides-Extra: test
15
+ Requires-Dist: pytest>=8.0; extra == "test"
16
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
17
+ Provides-Extra: openai
18
+ Requires-Dist: openai>=1.0; extra == "openai"
19
+ Provides-Extra: langchain
20
+ Requires-Dist: langchain-core>=0.2; extra == "langchain"
21
+ Provides-Extra: agno
22
+ Requires-Dist: agno>=1.0; extra == "agno"
23
+ Dynamic: license-file
24
+
25
+ # AxonBase Python SDK
26
+
27
+ WebSocket JSON-RPC client for AxonBase.
28
+
29
+ ```python
30
+ from axonbase import Axon
31
+
32
+ axon = await Axon.connect("ws://localhost:8000")
33
+ ```
34
+
35
+ See https://github.com/axonbase/axonbase for documentation and examples.
@@ -0,0 +1,11 @@
1
+ # AxonBase Python SDK
2
+
3
+ WebSocket JSON-RPC client for AxonBase.
4
+
5
+ ```python
6
+ from axonbase import Axon
7
+
8
+ axon = await Axon.connect("ws://localhost:8000")
9
+ ```
10
+
11
+ See https://github.com/axonbase/axonbase for documentation and examples.
@@ -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
+ ]
@@ -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
@@ -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)