flo-python 0.1.0.dev4__py3-none-any.whl → 0.1.2__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.
- flo/__init__.py +60 -15
- flo/actions.py +20 -27
- flo/client.py +193 -46
- flo/exceptions.py +83 -23
- flo/kv.py +373 -165
- flo/kv_body.py +554 -0
- flo/kv_txn.py +237 -0
- flo/processing.py +3 -67
- flo/queue.py +25 -101
- flo/retry.py +124 -0
- flo/streams.py +146 -31
- flo/types.py +431 -221
- flo/wire.py +263 -208
- flo/worker.py +684 -250
- {flo_python-0.1.0.dev4.dist-info → flo_python-0.1.2.dist-info}/METADATA +165 -98
- flo_python-0.1.2.dist-info/RECORD +19 -0
- {flo_python-0.1.0.dev4.dist-info → flo_python-0.1.2.dist-info}/WHEEL +1 -1
- flo_python-0.1.2.dist-info/licenses/LICENSE +202 -0
- flo_python-0.1.0.dev4.dist-info/RECORD +0 -16
- flo_python-0.1.0.dev4.dist-info/licenses/LICENSE +0 -21
flo/kv_body.py
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
"""KV ops, action.status and worker.list, as table ops.
|
|
2
|
+
|
|
3
|
+
The request is ``[namespace][key][body]``, the body coded by the op's
|
|
4
|
+
schema, field by field in order. An optional field is a presence byte then
|
|
5
|
+
its value; a field with a default is sent present. A byte string or list has
|
|
6
|
+
a length prefix as wide as its bound needs. An ok answer's data is the op's
|
|
7
|
+
response fields in order, and is decoded whole.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import struct
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from typing import TypeVar
|
|
13
|
+
|
|
14
|
+
from .exceptions import BlockTooLongError, IncompleteResponseError, ValidationError
|
|
15
|
+
from .types import (
|
|
16
|
+
MAX_KV_VALUE_SIZE,
|
|
17
|
+
MAX_WAIT_MS,
|
|
18
|
+
ActionRunStatus,
|
|
19
|
+
Condition,
|
|
20
|
+
GetOptions,
|
|
21
|
+
GetResult,
|
|
22
|
+
MGetEntry,
|
|
23
|
+
OpCode,
|
|
24
|
+
PutOptions,
|
|
25
|
+
PutResult,
|
|
26
|
+
RunStatus,
|
|
27
|
+
VersionEntry,
|
|
28
|
+
WaitFor,
|
|
29
|
+
WorkerInfo,
|
|
30
|
+
WorkerStatus,
|
|
31
|
+
WorkerType,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# The longest routing key; its length prefix is a u16.
|
|
35
|
+
_MAX_ROUTING_KEY = 4096
|
|
36
|
+
_MAX_JSON_PATH = 1024
|
|
37
|
+
_MAX_JSON_DOCUMENT = 252_819
|
|
38
|
+
_MAX_SCAN_CURSOR = 64
|
|
39
|
+
MAX_SCAN = 1024
|
|
40
|
+
MAX_MGET = 256
|
|
41
|
+
# The most bytes of mget key elements (each [len:u16][key]) in one request:
|
|
42
|
+
# the frame less the count and the longest routing key.
|
|
43
|
+
MAX_MGET_KEYS_BYTES = 253_848
|
|
44
|
+
MAX_HISTORY = 65
|
|
45
|
+
MAX_WORKER_LIST = 1024
|
|
46
|
+
_MAX_WORKER_ID = 4096
|
|
47
|
+
# The longest TTL whose nanoseconds fit a u64.
|
|
48
|
+
MAX_TTL_MS = 18_446_744_073_709
|
|
49
|
+
_U64_MAX = (1 << 64) - 1
|
|
50
|
+
_I64_MIN = -(1 << 63)
|
|
51
|
+
_I64_MAX = (1 << 63) - 1
|
|
52
|
+
|
|
53
|
+
_TABLE_OPS = frozenset(
|
|
54
|
+
{
|
|
55
|
+
OpCode.KV_PUT,
|
|
56
|
+
OpCode.KV_GET,
|
|
57
|
+
OpCode.KV_MGET,
|
|
58
|
+
OpCode.KV_DELETE,
|
|
59
|
+
OpCode.KV_SCAN,
|
|
60
|
+
OpCode.KV_HISTORY,
|
|
61
|
+
OpCode.KV_INCR,
|
|
62
|
+
OpCode.KV_JSON_GET,
|
|
63
|
+
OpCode.KV_JSON_SET,
|
|
64
|
+
OpCode.KV_JSON_DEL,
|
|
65
|
+
OpCode.KV_BEGIN_TXN,
|
|
66
|
+
OpCode.KV_COMMIT_TXN,
|
|
67
|
+
OpCode.KV_ROLLBACK_TXN,
|
|
68
|
+
OpCode.KV_EXPIRE,
|
|
69
|
+
OpCode.KV_EXISTS,
|
|
70
|
+
OpCode.ACTION_STATUS,
|
|
71
|
+
OpCode.WORKER_LIST,
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_table_op(op_code: int) -> bool:
|
|
77
|
+
"""Whether the op's request carries a schema-coded body, not a value and options."""
|
|
78
|
+
return op_code in _TABLE_OPS
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ── Request fields ────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _u64(name: str, v: int) -> bytes:
|
|
85
|
+
if not isinstance(v, int) or isinstance(v, bool) or not 0 <= v <= _U64_MAX:
|
|
86
|
+
raise ValidationError(f"{name} must be a u64, got {v!r}")
|
|
87
|
+
return struct.pack("<Q", v)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _opt_u64(name: str, v: int | None) -> bytes:
|
|
91
|
+
if v is None:
|
|
92
|
+
return b"\x00"
|
|
93
|
+
return b"\x01" + _u64(name, v)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _limit(name: str, v: int, most: int) -> bytes:
|
|
97
|
+
"""A defaulted u32 limit in 1..most, sent present."""
|
|
98
|
+
if not isinstance(v, int) or isinstance(v, bool) or not 1 <= v <= most:
|
|
99
|
+
raise ValidationError(f"{name} must be 1..{most}, got {v!r}")
|
|
100
|
+
return b"\x01" + struct.pack("<I", v)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _bytes16(name: str, b: bytes, most: int) -> bytes:
|
|
104
|
+
if len(b) > most:
|
|
105
|
+
raise ValidationError(f"{name} is {len(b)} bytes, at most {most}")
|
|
106
|
+
return struct.pack("<H", len(b)) + b
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _bytes32(name: str, b: bytes, most: int) -> bytes:
|
|
110
|
+
if len(b) > most:
|
|
111
|
+
raise ValidationError(f"{name} is {len(b)} bytes, at most {most}")
|
|
112
|
+
return struct.pack("<I", len(b)) + bytes(b)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _routing_key(k: str | None) -> bytes:
|
|
116
|
+
if not k:
|
|
117
|
+
return b"\x00"
|
|
118
|
+
return b"\x01" + _bytes16("routing key", k.encode("utf-8"), _MAX_ROUTING_KEY)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _path(path: str | None) -> bytes:
|
|
122
|
+
return b"\x01" + _bytes16("JSON path", (path or "$").encode("utf-8"), _MAX_JSON_PATH)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def encode_get_body(opts: GetOptions, txn: int | None = None) -> bytes:
|
|
126
|
+
"""kv.get's request: wait_ms, wait_for, routing_key, txn."""
|
|
127
|
+
wait_ms = opts.wait_ms
|
|
128
|
+
if not isinstance(wait_ms, int) or isinstance(wait_ms, bool) or wait_ms < 0:
|
|
129
|
+
raise ValidationError(f"wait_ms must be a whole number of ms, got {wait_ms!r}")
|
|
130
|
+
if wait_ms > MAX_WAIT_MS:
|
|
131
|
+
raise BlockTooLongError(
|
|
132
|
+
f"a wait (wait_ms) is at most {MAX_WAIT_MS} ms (5 minutes), got {wait_ms}"
|
|
133
|
+
)
|
|
134
|
+
wait_for = WaitFor(opts.wait_for)
|
|
135
|
+
return (
|
|
136
|
+
b"\x01"
|
|
137
|
+
+ struct.pack("<Q", wait_ms)
|
|
138
|
+
+ b"\x01"
|
|
139
|
+
+ bytes([wait_for])
|
|
140
|
+
+ _routing_key(opts.routing_key)
|
|
141
|
+
+ _opt_u64("txn", txn)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def encode_set_body(value: bytes, opts: PutOptions, txn: int | None = None) -> bytes:
|
|
146
|
+
"""kv.set's request: value, ttl_ms, condition, cas_version, routing_key, txn."""
|
|
147
|
+
if len(value) > MAX_KV_VALUE_SIZE:
|
|
148
|
+
raise ValidationError(f"value is {len(value)} bytes, at most {MAX_KV_VALUE_SIZE}")
|
|
149
|
+
condition = Condition(opts.condition)
|
|
150
|
+
ttl = opts.ttl_ms or None
|
|
151
|
+
return (
|
|
152
|
+
struct.pack("<I", len(value))
|
|
153
|
+
+ bytes(value)
|
|
154
|
+
+ _opt_u64("ttl_ms", ttl)
|
|
155
|
+
+ b"\x01"
|
|
156
|
+
+ bytes([condition])
|
|
157
|
+
+ _opt_u64("cas_version", opts.cas_version)
|
|
158
|
+
+ _routing_key(opts.routing_key)
|
|
159
|
+
+ _opt_u64("txn", txn)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def encode_delete_body(
|
|
164
|
+
cas_version: int | None, routing_key: str | None, txn: int | None = None
|
|
165
|
+
) -> bytes:
|
|
166
|
+
"""kv.delete's request: cas_version, routing_key, txn."""
|
|
167
|
+
return _opt_u64("cas_version", cas_version) + _routing_key(routing_key) + _opt_u64("txn", txn)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def encode_scan_body(limit: int, cursor: bytes | None) -> bytes:
|
|
171
|
+
"""kv.scan's request: limit, cursor (empty for the first page)."""
|
|
172
|
+
cursor = cursor or b""
|
|
173
|
+
if len(cursor) > _MAX_SCAN_CURSOR:
|
|
174
|
+
raise ValidationError(f"scan cursor is {len(cursor)} bytes, at most {_MAX_SCAN_CURSOR}")
|
|
175
|
+
return _limit("scan limit", limit, MAX_SCAN) + bytes([len(cursor)]) + cursor
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def encode_history_body(limit: int, routing_key: str | None) -> bytes:
|
|
179
|
+
"""kv.history's request: limit, routing_key."""
|
|
180
|
+
return _limit("history limit", limit, MAX_HISTORY) + _routing_key(routing_key)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def encode_mget_body(keys: Sequence[bytes], routing_key: str | None = None) -> bytes:
|
|
184
|
+
"""kv.mget's request: keys, routing_key."""
|
|
185
|
+
if not 1 <= len(keys) <= MAX_MGET:
|
|
186
|
+
raise ValidationError(f"mget takes 1..{MAX_MGET} keys, got {len(keys)}")
|
|
187
|
+
elements = b"".join(_bytes16("mget key", k, _MAX_ROUTING_KEY) for k in keys)
|
|
188
|
+
if len(elements) > MAX_MGET_KEYS_BYTES:
|
|
189
|
+
raise ValidationError(f"mget keys are {len(elements)} bytes, at most {MAX_MGET_KEYS_BYTES}")
|
|
190
|
+
return struct.pack("<H", len(keys)) + elements + _routing_key(routing_key)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def split_mget_keys(keys: Sequence[bytes]) -> list[Sequence[bytes]]:
|
|
194
|
+
"""The keys in runs that each fit one request frame; refuses a bad key list
|
|
195
|
+
before any round trip."""
|
|
196
|
+
if not 1 <= len(keys) <= MAX_MGET:
|
|
197
|
+
raise ValidationError(f"mget takes 1..{MAX_MGET} keys, got {len(keys)}")
|
|
198
|
+
runs: list[Sequence[bytes]] = []
|
|
199
|
+
start, size = 0, 0
|
|
200
|
+
for i, k in enumerate(keys):
|
|
201
|
+
if len(k) > _MAX_ROUTING_KEY:
|
|
202
|
+
raise ValidationError(f"mget key is {len(k)} bytes, at most {_MAX_ROUTING_KEY}")
|
|
203
|
+
if size + 2 + len(k) > MAX_MGET_KEYS_BYTES:
|
|
204
|
+
runs.append(keys[start:i])
|
|
205
|
+
start, size = i, 0
|
|
206
|
+
size += 2 + len(k)
|
|
207
|
+
runs.append(keys[start:])
|
|
208
|
+
return runs
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def encode_incr_body(delta: int, routing_key: str | None, txn: int | None = None) -> bytes:
|
|
212
|
+
"""kv.incr's request: delta, routing_key, txn."""
|
|
213
|
+
if not isinstance(delta, int) or isinstance(delta, bool) or not _I64_MIN <= delta <= _I64_MAX:
|
|
214
|
+
raise ValidationError(f"delta must be an i64, got {delta!r}")
|
|
215
|
+
return b"\x01" + struct.pack("<q", delta) + _routing_key(routing_key) + _opt_u64("txn", txn)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def encode_expire_body(
|
|
219
|
+
ttl_ms: int, cas_version: int | None, routing_key: str | None, txn: int | None = None
|
|
220
|
+
) -> bytes:
|
|
221
|
+
"""kv.expire's request: ttl_ms (0 clears the TTL), cas_version, routing_key, txn."""
|
|
222
|
+
if not isinstance(ttl_ms, int) or isinstance(ttl_ms, bool) or not 0 <= ttl_ms <= MAX_TTL_MS:
|
|
223
|
+
raise ValidationError(f"ttl_ms must be 0..{MAX_TTL_MS}, got {ttl_ms!r}")
|
|
224
|
+
return (
|
|
225
|
+
struct.pack("<Q", ttl_ms)
|
|
226
|
+
+ _opt_u64("cas_version", cas_version)
|
|
227
|
+
+ _routing_key(routing_key)
|
|
228
|
+
+ _opt_u64("txn", txn)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def encode_exists_body(routing_key: str | None, txn: int | None = None) -> bytes:
|
|
233
|
+
"""kv.exists's request: routing_key, txn."""
|
|
234
|
+
return _routing_key(routing_key) + _opt_u64("txn", txn)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def encode_json_get_body(path: str | None, routing_key: str | None) -> bytes:
|
|
238
|
+
"""kv.json_get's request: path, routing_key."""
|
|
239
|
+
return _path(path) + _routing_key(routing_key)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def encode_json_set_body(path: str | None, document: bytes, routing_key: str | None) -> bytes:
|
|
243
|
+
"""kv.json_set's request: path, document, routing_key."""
|
|
244
|
+
return (
|
|
245
|
+
_path(path)
|
|
246
|
+
+ _bytes32("JSON document", document, _MAX_JSON_DOCUMENT)
|
|
247
|
+
+ _routing_key(routing_key)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def encode_json_del_body(path: str | None, routing_key: str | None) -> bytes:
|
|
252
|
+
"""kv.json_del's request: path, routing_key."""
|
|
253
|
+
return _path(path) + _routing_key(routing_key)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def encode_worker_list_body(limit: int, cursor: bytes | None) -> bytes:
|
|
257
|
+
"""worker.list's request: limit, cursor (empty starts)."""
|
|
258
|
+
return _limit("limit", limit, MAX_WORKER_LIST) + _bytes16(
|
|
259
|
+
"cursor", cursor or b"", _MAX_WORKER_ID
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def encode_txn_body(txn: int) -> bytes:
|
|
264
|
+
"""kv.txn_commit's and kv.txn_rollback's request: txn."""
|
|
265
|
+
return _u64("txn", txn)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ── Answers ───────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class _Answer:
|
|
272
|
+
"""Reads an answer's fields in order; ``done`` refuses bytes left over."""
|
|
273
|
+
|
|
274
|
+
def __init__(self, op: str, data: bytes):
|
|
275
|
+
self._op = op
|
|
276
|
+
self._data = bytes(data)
|
|
277
|
+
self._at = 0
|
|
278
|
+
|
|
279
|
+
def _take(self, n: int) -> bytes:
|
|
280
|
+
if self._at + n > len(self._data):
|
|
281
|
+
raise IncompleteResponseError(f"{self._op} answer is cut short")
|
|
282
|
+
b = self._data[self._at : self._at + n]
|
|
283
|
+
self._at += n
|
|
284
|
+
return b
|
|
285
|
+
|
|
286
|
+
def u8(self) -> int:
|
|
287
|
+
return self._take(1)[0]
|
|
288
|
+
|
|
289
|
+
def u16(self) -> int:
|
|
290
|
+
return int(struct.unpack("<H", self._take(2))[0])
|
|
291
|
+
|
|
292
|
+
def u32(self) -> int:
|
|
293
|
+
return int(struct.unpack("<I", self._take(4))[0])
|
|
294
|
+
|
|
295
|
+
def u64(self) -> int:
|
|
296
|
+
return int(struct.unpack("<Q", self._take(8))[0])
|
|
297
|
+
|
|
298
|
+
def i64(self) -> int:
|
|
299
|
+
return int(struct.unpack("<q", self._take(8))[0])
|
|
300
|
+
|
|
301
|
+
def flag(self) -> bool:
|
|
302
|
+
b = self.u8()
|
|
303
|
+
if b > 1:
|
|
304
|
+
raise IncompleteResponseError(f"{self._op} answer has a bool of {b}")
|
|
305
|
+
return b == 1
|
|
306
|
+
|
|
307
|
+
def present(self) -> bool:
|
|
308
|
+
"""An optional field's presence byte."""
|
|
309
|
+
return self.flag()
|
|
310
|
+
|
|
311
|
+
def bytes8(self) -> bytes:
|
|
312
|
+
return self._take(self.u8())
|
|
313
|
+
|
|
314
|
+
def bytes16(self) -> bytes:
|
|
315
|
+
return self._take(self.u16())
|
|
316
|
+
|
|
317
|
+
def bytes32(self) -> bytes:
|
|
318
|
+
return self._take(self.u32())
|
|
319
|
+
|
|
320
|
+
def done(self) -> None:
|
|
321
|
+
if self._at != len(self._data):
|
|
322
|
+
raise IncompleteResponseError(
|
|
323
|
+
f"{self._op} answer has {len(self._data) - self._at} bytes left over"
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def decode_get_answer(data: bytes) -> GetResult:
|
|
328
|
+
"""kv.get's answer: version, value."""
|
|
329
|
+
a = _Answer("kv.get", data)
|
|
330
|
+
version = a.u64()
|
|
331
|
+
value = a.bytes32()
|
|
332
|
+
a.done()
|
|
333
|
+
return GetResult(value=value, version=version)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def decode_set_answer(data: bytes) -> PutResult:
|
|
337
|
+
"""kv.set's answer: its version, absent (0) inside a transaction."""
|
|
338
|
+
a = _Answer("kv.set", data)
|
|
339
|
+
version = a.u64() if a.present() else 0
|
|
340
|
+
a.done()
|
|
341
|
+
return PutResult(version=version)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def decode_delete_answer(data: bytes) -> bool | None:
|
|
345
|
+
"""kv.delete's answer: whether the key existed, None inside a transaction."""
|
|
346
|
+
a = _Answer("kv.delete", data)
|
|
347
|
+
existed = a.flag() if a.present() else None
|
|
348
|
+
a.done()
|
|
349
|
+
return existed
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def decode_scan_answer(data: bytes) -> tuple[list[str], bytes | None]:
|
|
353
|
+
"""kv.scan's answer: names, then the next page's cursor (None on the last page)."""
|
|
354
|
+
a = _Answer("kv.scan", data)
|
|
355
|
+
try:
|
|
356
|
+
names = [a.bytes16().decode("utf-8") for _ in range(a.u16())]
|
|
357
|
+
except UnicodeDecodeError as e:
|
|
358
|
+
raise IncompleteResponseError("kv.scan answer has a name that isn't UTF-8") from e
|
|
359
|
+
cursor = a.bytes8() if a.present() else None
|
|
360
|
+
a.done()
|
|
361
|
+
return names, cursor
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def decode_history_answer(data: bytes) -> list[VersionEntry]:
|
|
365
|
+
"""kv.history's answer: versions, newest first."""
|
|
366
|
+
a = _Answer("kv.history", data)
|
|
367
|
+
versions = [
|
|
368
|
+
VersionEntry(version=a.u64(), at_ms=a.i64(), value=a.bytes32()) for _ in range(a.u8())
|
|
369
|
+
]
|
|
370
|
+
a.done()
|
|
371
|
+
return versions
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def decode_mget_answer(data: bytes, keys: Sequence[str]) -> tuple[list[MGetEntry], int | None]:
|
|
375
|
+
"""kv.mget's answer: one entry per answered key, then the index of the first
|
|
376
|
+
key not answered (None when all were)."""
|
|
377
|
+
a = _Answer("kv.mget", data)
|
|
378
|
+
count = a.u16()
|
|
379
|
+
if count > len(keys):
|
|
380
|
+
raise IncompleteResponseError(f"kv.mget answered {count} entries for {len(keys)} keys")
|
|
381
|
+
entries = []
|
|
382
|
+
for key in keys[:count]:
|
|
383
|
+
found = a.flag()
|
|
384
|
+
version = a.u64()
|
|
385
|
+
entries.append(MGetEntry(key=key, found=found, version=version, value=a.bytes32()))
|
|
386
|
+
next_index = a.u16() if a.present() else None
|
|
387
|
+
a.done()
|
|
388
|
+
if next_index == len(keys):
|
|
389
|
+
next_index = None # resumes past the last key: all were answered
|
|
390
|
+
if next_index is None and count != len(keys):
|
|
391
|
+
raise IncompleteResponseError(f"kv.mget answered {count} of {len(keys)} keys")
|
|
392
|
+
if next_index is not None and next_index != count:
|
|
393
|
+
raise IncompleteResponseError(
|
|
394
|
+
f"kv.mget answered {count} entries but resumes at key {next_index}"
|
|
395
|
+
)
|
|
396
|
+
return entries, next_index
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def decode_incr_answer(data: bytes) -> tuple[int | None, int | None]:
|
|
400
|
+
"""kv.incr's answer: version, value (both None inside a transaction)."""
|
|
401
|
+
a = _Answer("kv.incr", data)
|
|
402
|
+
version = a.u64() if a.present() else None
|
|
403
|
+
value = a.i64() if a.present() else None
|
|
404
|
+
a.done()
|
|
405
|
+
return version, value
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def decode_empty_answer(op: str, data: bytes) -> None:
|
|
409
|
+
"""An answer with no fields."""
|
|
410
|
+
_Answer(op, data).done()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def decode_exists_answer(data: bytes) -> tuple[bool, int]:
|
|
414
|
+
"""kv.exists's answer: exists, version (0 when absent or buffered in a transaction)."""
|
|
415
|
+
a = _Answer("kv.exists", data)
|
|
416
|
+
exists = a.flag()
|
|
417
|
+
version = a.u64()
|
|
418
|
+
a.done()
|
|
419
|
+
return exists, version
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def decode_json_get_answer(data: bytes) -> tuple[int, bytes]:
|
|
423
|
+
"""kv.json_get's answer: version, document."""
|
|
424
|
+
a = _Answer("kv.json_get", data)
|
|
425
|
+
version = a.u64()
|
|
426
|
+
document = a.bytes32()
|
|
427
|
+
a.done()
|
|
428
|
+
return version, document
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def decode_json_set_answer(data: bytes) -> int:
|
|
432
|
+
"""kv.json_set's answer: version."""
|
|
433
|
+
a = _Answer("kv.json_set", data)
|
|
434
|
+
version = a.u64()
|
|
435
|
+
a.done()
|
|
436
|
+
return version
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def decode_json_del_answer(data: bytes) -> int | None:
|
|
440
|
+
"""kv.json_del's answer: version, None when the path was "$" and the key is gone."""
|
|
441
|
+
a = _Answer("kv.json_del", data)
|
|
442
|
+
version = a.u64() if a.present() else None
|
|
443
|
+
a.done()
|
|
444
|
+
return version
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def decode_txn_begin_answer(data: bytes) -> tuple[int, int]:
|
|
448
|
+
"""kv.txn_begin's answer: txn, pinned."""
|
|
449
|
+
a = _Answer("kv.txn_begin", data)
|
|
450
|
+
txn, pinned = a.u64(), a.u64()
|
|
451
|
+
a.done()
|
|
452
|
+
return txn, pinned
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def decode_txn_commit_answer(data: bytes) -> tuple[int, int]:
|
|
456
|
+
"""kv.txn_commit's answer: commit_index, ops."""
|
|
457
|
+
a = _Answer("kv.txn_commit", data)
|
|
458
|
+
commit_index, ops = a.u64(), a.u16()
|
|
459
|
+
a.done()
|
|
460
|
+
return commit_index, ops
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def decode_action_status_answer(run_id: str, data: bytes) -> ActionRunStatus:
|
|
464
|
+
"""action.status's answer: status, created_at, started_at?, completed_at?,
|
|
465
|
+
attempt, output?, error_message?, worker_id?, with u16 lengths."""
|
|
466
|
+
a = _Answer("action.status", data)
|
|
467
|
+
code = a.u8()
|
|
468
|
+
try:
|
|
469
|
+
status = RunStatus(code)
|
|
470
|
+
except ValueError:
|
|
471
|
+
raise IncompleteResponseError(
|
|
472
|
+
f"action.status answer has an unknown status {code}"
|
|
473
|
+
) from None
|
|
474
|
+
created_at_ms = a.i64()
|
|
475
|
+
started_at_ms = a.i64() if a.present() else None
|
|
476
|
+
completed_at_ms = a.i64() if a.present() else None
|
|
477
|
+
attempt = a.u32()
|
|
478
|
+
output = a.bytes16() if a.present() else None
|
|
479
|
+
error = a.bytes16() if a.present() else None
|
|
480
|
+
worker_id = a.bytes16() if a.present() else None
|
|
481
|
+
a.done()
|
|
482
|
+
return ActionRunStatus(
|
|
483
|
+
run_id=run_id,
|
|
484
|
+
status=status,
|
|
485
|
+
created_at_ms=created_at_ms,
|
|
486
|
+
attempt=attempt,
|
|
487
|
+
started_at_ms=started_at_ms,
|
|
488
|
+
completed_at_ms=completed_at_ms,
|
|
489
|
+
output=output,
|
|
490
|
+
error_message=error.decode("utf-8", errors="replace") if error is not None else None,
|
|
491
|
+
worker_id=worker_id.decode("utf-8", errors="replace") if worker_id is not None else None,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def decode_action_invoke_answer(data: bytes) -> str:
|
|
496
|
+
"""action.invoke's answer: [run_id_len:u16][run_id][has_output:u8]; the
|
|
497
|
+
server never sets output."""
|
|
498
|
+
a = _Answer("action.invoke", data)
|
|
499
|
+
run_id = a.bytes16()
|
|
500
|
+
if not run_id:
|
|
501
|
+
raise IncompleteResponseError("action.invoke answer has no run id")
|
|
502
|
+
if a.present():
|
|
503
|
+
raise IncompleteResponseError("action.invoke answer carries an output")
|
|
504
|
+
a.done()
|
|
505
|
+
return run_id.decode("utf-8")
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
_E = TypeVar("_E", WorkerType, WorkerStatus)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def _enum8(a: _Answer, op: str, kind: type[_E]) -> _E:
|
|
512
|
+
code = a.u8()
|
|
513
|
+
try:
|
|
514
|
+
return kind(code)
|
|
515
|
+
except ValueError:
|
|
516
|
+
raise IncompleteResponseError(
|
|
517
|
+
f"{op} answer has an unknown {kind.__name__} {code}"
|
|
518
|
+
) from None
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def decode_worker_list_answer(data: bytes) -> tuple[list[WorkerInfo], bytes | None]:
|
|
522
|
+
"""worker.list's answer: workers in id order, then the next page's cursor
|
|
523
|
+
(None on the last page)."""
|
|
524
|
+
op = "worker.list"
|
|
525
|
+
a = _Answer(op, data)
|
|
526
|
+
count = a.u16()
|
|
527
|
+
if count > MAX_WORKER_LIST:
|
|
528
|
+
raise IncompleteResponseError(f"{op} answer lists {count} workers")
|
|
529
|
+
workers = []
|
|
530
|
+
for _ in range(count):
|
|
531
|
+
worker_id = a.bytes16().decode("utf-8", errors="replace")
|
|
532
|
+
worker_type = _enum8(a, op, WorkerType)
|
|
533
|
+
status = _enum8(a, op, WorkerStatus)
|
|
534
|
+
current_load, max_concurrency = a.u32(), a.u32()
|
|
535
|
+
tasks_completed, tasks_failed = a.u64(), a.u64()
|
|
536
|
+
registered_at_ms, last_heartbeat_ms = a.i64(), a.i64()
|
|
537
|
+
machine_id = a.bytes8().decode("utf-8", errors="replace") if a.present() else None
|
|
538
|
+
workers.append(
|
|
539
|
+
WorkerInfo(
|
|
540
|
+
worker_id=worker_id,
|
|
541
|
+
worker_type=worker_type,
|
|
542
|
+
status=status,
|
|
543
|
+
current_load=current_load,
|
|
544
|
+
max_concurrency=max_concurrency,
|
|
545
|
+
tasks_completed=tasks_completed,
|
|
546
|
+
tasks_failed=tasks_failed,
|
|
547
|
+
registered_at_ms=registered_at_ms,
|
|
548
|
+
last_heartbeat_ms=last_heartbeat_ms,
|
|
549
|
+
machine_id=machine_id,
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
cursor = a.bytes16() if a.present() else None
|
|
553
|
+
a.done()
|
|
554
|
+
return workers, cursor
|