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.
- axonbase_sdk-0.1.1/LICENSE +21 -0
- axonbase_sdk-0.1.1/PKG-INFO +35 -0
- axonbase_sdk-0.1.1/README.md +11 -0
- axonbase_sdk-0.1.1/axonbase/__init__.py +30 -0
- axonbase_sdk-0.1.1/axonbase/client.py +286 -0
- axonbase_sdk-0.1.1/axonbase/codec.py +33 -0
- axonbase_sdk-0.1.1/axonbase/dbapi.py +549 -0
- axonbase_sdk-0.1.1/axonbase/errors.py +69 -0
- axonbase_sdk-0.1.1/axonbase/integrations/__init__.py +1 -0
- axonbase_sdk-0.1.1/axonbase/integrations/agno.py +137 -0
- axonbase_sdk-0.1.1/axonbase/integrations/langchain.py +77 -0
- axonbase_sdk-0.1.1/axonbase/migration/__init__.py +5 -0
- axonbase_sdk-0.1.1/axonbase/migration/migrator.py +122 -0
- axonbase_sdk-0.1.1/axonbase/openai_embeddings.py +36 -0
- axonbase_sdk-0.1.1/axonbase/protocol.py +42 -0
- axonbase_sdk-0.1.1/axonbase/saga.py +119 -0
- axonbase_sdk-0.1.1/axonbase/sqlalchemy.py +113 -0
- axonbase_sdk-0.1.1/axonbase/sqlalchemy_dialect.py +145 -0
- axonbase_sdk-0.1.1/axonbase/vectorstore.py +181 -0
- axonbase_sdk-0.1.1/axonbase_sdk.egg-info/PKG-INFO +35 -0
- axonbase_sdk-0.1.1/axonbase_sdk.egg-info/SOURCES.txt +30 -0
- axonbase_sdk-0.1.1/axonbase_sdk.egg-info/dependency_links.txt +1 -0
- axonbase_sdk-0.1.1/axonbase_sdk.egg-info/requires.txt +14 -0
- axonbase_sdk-0.1.1/axonbase_sdk.egg-info/top_level.txt +1 -0
- axonbase_sdk-0.1.1/pyproject.toml +33 -0
- axonbase_sdk-0.1.1/setup.cfg +4 -0
- axonbase_sdk-0.1.1/tests/test_basic.py +210 -0
- axonbase_sdk-0.1.1/tests/test_certificates.py +78 -0
- axonbase_sdk-0.1.1/tests/test_optional_adapters.py +154 -0
- axonbase_sdk-0.1.1/tests/test_sqlalchemy_saga.py +112 -0
- axonbase_sdk-0.1.1/tests/test_vectorstore.py +121 -0
- 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,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)
|