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/types.py CHANGED
@@ -6,20 +6,102 @@ Core types, constants, and data classes for the Flo client SDK.
6
6
  import struct
7
7
  from dataclasses import dataclass
8
8
  from enum import IntEnum
9
- from typing import Optional
9
+ from typing import Any, Generic, Optional, TypeVar
10
10
 
11
11
  # =============================================================================
12
12
  # Protocol Constants
13
13
  # =============================================================================
14
14
 
15
15
  MAGIC: int = 0x004F4C46 # "FLO\0" in little-endian
16
- VERSION: int = 0x01
16
+ VERSION: int = 0x02
17
+ # The hash of the op table this SDK was built for, written into every request
18
+ # header. A server built from another table refuses the request, and its
19
+ # answers carry its own hash, which this SDK refuses. CI's live tests run
20
+ # against the flo commit in .github/flo-ref.env: change both in one commit.
21
+ TABLE_HASH: int = 0x2FBA7DD9AE941830
22
+
23
+
24
+ class Reason(IntEnum):
25
+ """Why the server refused a request. Mirrors the server's list; the table
26
+ hash covers it, so a server can't send one this SDK doesn't know."""
27
+
28
+ UNKNOWN_OP = 1
29
+ MALFORMED = 2
30
+ FIELD_TOO_LONG = 3
31
+ OUT_OF_RANGE = 4
32
+ LEADER_UNKNOWN = 5
33
+ SHARD_BUSY = 6
34
+ TABLE_MISMATCH = 7
35
+ UNKNOWN_FLAG = 8
36
+ INTERNAL = 9
37
+ COMMITTED_NOT_APPLIED = 10
38
+ ANSWER_LOST = 11
39
+ # A refusal from an op family the server hasn't classified yet.
40
+ UNCLASSIFIED = 12
41
+ # What the request names doesn't exist.
42
+ NOT_FOUND = 13
43
+ # The key is in a prefix the server keeps for itself.
44
+ RESERVED_KEY = 14
45
+ # The write's cas_version isn't the key's version.
46
+ CAS_MISMATCH = 15
47
+ # The write's condition didn't hold.
48
+ CONDITION_FAILED = 16
49
+ # No open transaction has this id here.
50
+ NO_SUCH_TRANSACTION = 17
51
+ # The routing key (the key, when none is given) isn't the one the
52
+ # transaction was begun with.
53
+ WRONG_ROUTING_KEY = 18
54
+ # The transaction holds as many ops or bytes as it may.
55
+ TXN_TOO_LARGE = 19
56
+ # The shard holds as many waiting reads as it may.
57
+ TOO_MANY_WAITERS = 20
58
+ # Two fields given together that don't go together.
59
+ CONFLICTING_FIELDS = 21
60
+ # The shard stopped taking writes until the node restarts.
61
+ WRITES_STOPPED = 22
62
+ # Too many writes wait for commit.
63
+ TOO_MANY_PROPOSALS = 23
64
+ # The key's value isn't what the op works on (a counter, a JSON document).
65
+ WRONG_TYPE = 24
66
+ # The shard holds as many open transactions as it may.
67
+ TOO_MANY_TRANSACTIONS = 25
68
+ # An action complete or fail for a run that has moved on (another worker
69
+ # holds it, or it finished). It was dropped; resending it is refused too.
70
+ STALE_REPORT = 26
71
+ # An await from a worker that missed its heartbeats: a heartbeat (or, once
72
+ # it's removed, registering again) lets it take work again.
73
+ WORKER_UNHEALTHY = 27
74
+ # An await from a draining worker: it takes no new work.
75
+ WORKER_DRAINING = 28
76
+
77
+
78
+ class Ran(IntEnum):
79
+ """Whether a refused request took effect: retry logic reads this, not the
80
+ message."""
81
+
82
+ NO = 0
83
+ # It took effect; don't resend it.
84
+ YES = 1
85
+ # It may still take effect; check before resending.
86
+ UNKNOWN = 2
87
+
88
+
17
89
  HEADER_SIZE: int = 32
18
90
 
19
91
  # Size limits (for client-side validation)
20
92
  MAX_NAMESPACE_SIZE: int = 255
21
93
  MAX_KEY_SIZE: int = 64 * 1024 # 64 KB
22
94
  MAX_VALUE_SIZE: int = 16 * 1024 * 1024 # 16 MB practical limit
95
+ # Longest blocking wait (block_ms) the server accepts; checked client-side so
96
+ # it fails before the round trip.
97
+ MAX_BLOCK_MS: int = 300_000 # 5 minutes
98
+ # Longest a KV get may wait (wait_ms).
99
+ MAX_WAIT_MS: int = 300_000
100
+ # Longest value a KV put takes.
101
+ MAX_KV_VALUE_SIZE: int = 253_817
102
+ # A worker always long-polls: block_ms=0 ("don't wait") would turn its poll
103
+ # loop into a busy spin against the server, so a worker reads 0 (or unset) as this.
104
+ _DEFAULT_WORKER_BLOCK_MS: int = 30_000
23
105
 
24
106
 
25
107
  # =============================================================================
@@ -35,11 +117,6 @@ class OpCode(IntEnum):
35
117
 
36
118
  # ── System (0x000 – 0x00F) ──
37
119
  PING = 0x000
38
- PONG = 0x001
39
- ERROR_RESPONSE = 0x002
40
- AUTH = 0x003
41
- SET_DURABILITY = 0x004
42
- OK = 0x005
43
120
 
44
121
  # ── Namespace (0x010 – 0x02F) ──
45
122
  NAMESPACE_CREATE = 0x010
@@ -48,12 +125,6 @@ class OpCode(IntEnum):
48
125
  NAMESPACE_INFO = 0x013
49
126
  NAMESPACE_CONFIG_SET = 0x014
50
127
  NAMESPACE_CONFIG_GET = 0x015
51
- NAMESPACE_CREATE_RESPONSE = 0x020
52
- NAMESPACE_DELETE_RESPONSE = 0x021
53
- NAMESPACE_LIST_RESPONSE = 0x022
54
- NAMESPACE_INFO_RESPONSE = 0x023
55
- NAMESPACE_CONFIG_SET_RESPONSE = 0x024
56
- NAMESPACE_CONFIG_GET_RESPONSE = 0x025
57
128
 
58
129
  # ── Cluster (0x030 – 0x04F) ──
59
130
  CLUSTER_STATUS = 0x030
@@ -63,46 +134,34 @@ class OpCode(IntEnum):
63
134
  CLUSTER_TRANSFER_LEADER = 0x034
64
135
  CLUSTER_ADD_NODE = 0x035
65
136
  CLUSTER_REMOVE_NODE = 0x036
66
- CLUSTER_STATUS_RESPONSE = 0x040
67
- CLUSTER_MEMBERS_RESPONSE = 0x041
68
- CLUSTER_JOIN_RESPONSE = 0x042
69
137
 
70
- # ── KV + Transactions + Snapshots (0x100 – 0x12F) ──
138
+ # ── KV (0x100 – 0x12F) ──
71
139
  KV_PUT = 0x100
72
140
  KV_GET = 0x101
73
141
  KV_MGET = 0x102
74
142
  KV_DELETE = 0x103
75
143
  KV_SCAN = 0x104
76
144
  KV_HISTORY = 0x105
77
- KV_GET_RESPONSE = 0x106
78
- KV_MGET_RESPONSE = 0x107
79
- KV_PUT_RESPONSE = 0x108
80
- KV_SCAN_RESPONSE = 0x109
81
- KV_HISTORY_RESPONSE = 0x10A
145
+ # KV Extended (atomic counters, JSON ops)
146
+ KV_INCR = 0x10B
147
+ KV_JSON_GET = 0x10C
148
+ KV_JSON_SET = 0x10D
149
+ KV_JSON_DEL = 0x10E
150
+ # KV Per-Shard Transactions
82
151
  KV_BEGIN_TXN = 0x110
83
152
  KV_COMMIT_TXN = 0x111
84
153
  KV_ROLLBACK_TXN = 0x112
85
- KV_SNAPSHOT_CREATE = 0x120
86
- KV_SNAPSHOT_GET = 0x121
87
- KV_SNAPSHOT_RELEASE = 0x122
88
- KV_SNAPSHOT_CREATE_RESPONSE = 0x123
154
+ # KV Extended (TTL, exists)
155
+ KV_EXPIRE = 0x113
156
+ KV_EXISTS = 0x115
89
157
 
90
158
  # ── Streams (0x130 – 0x14F) ──
91
159
  STREAM_APPEND = 0x130
92
160
  STREAM_READ = 0x131
93
161
  STREAM_TRIM = 0x132
94
162
  STREAM_INFO = 0x133
95
- STREAM_APPEND_RESPONSE = 0x134
96
- STREAM_READ_RESPONSE = 0x135
97
- STREAM_EVENT = 0x136
98
- STREAM_SUBSCRIBE = 0x137
99
- STREAM_UNSUBSCRIBE = 0x138
100
- STREAM_SUBSCRIBED = 0x139
101
- STREAM_UNSUBSCRIBED = 0x13A
102
163
  STREAM_LIST = 0x13B
103
- STREAM_LIST_RESPONSE = 0x13C
104
164
  STREAM_CREATE = 0x13D
105
- STREAM_CREATE_RESPONSE = 0x13E
106
165
  STREAM_ALTER = 0x13F
107
166
 
108
167
  # ── Stream Consumer Groups (0x150 – 0x16F) ──
@@ -114,7 +173,6 @@ class OpCode(IntEnum):
114
173
  STREAM_GROUP_CLAIM = 0x155
115
174
  STREAM_GROUP_PENDING = 0x156
116
175
  STREAM_GROUP_CONFIGURE_SWEEPER = 0x157
117
- STREAM_GROUP_READ_RESPONSE = 0x158
118
176
  STREAM_GROUP_NACK = 0x159
119
177
  STREAM_GROUP_TOUCH = 0x15A
120
178
  STREAM_GROUP_INFO = 0x15B
@@ -124,29 +182,14 @@ class OpCode(IntEnum):
124
182
  QUEUE_ENQUEUE = 0x170
125
183
  QUEUE_DEQUEUE = 0x171
126
184
  QUEUE_COMPLETE = 0x172
127
- QUEUE_EXTEND_LEASE = 0x173
128
185
  QUEUE_FAIL = 0x174
129
- QUEUE_FAIL_AUTO = 0x175
130
186
  QUEUE_DLQ_LIST = 0x176
131
187
  QUEUE_DLQ_DELETE = 0x177
132
188
  QUEUE_DLQ_REQUEUE = 0x178
133
- QUEUE_DLQ_STATS = 0x179
134
- QUEUE_PROMOTE_DUE = 0x17A
135
189
  QUEUE_STATS = 0x17B
136
190
  QUEUE_PEEK = 0x17C
137
- QUEUE_TOUCH = 0x17D
138
- QUEUE_BATCH_ENQUEUE = 0x17E
139
191
  QUEUE_PURGE = 0x17F
140
- QUEUE_ENQUEUE_RESPONSE = 0x190
141
- QUEUE_DEQUEUE_RESPONSE = 0x191
142
- QUEUE_DLQ_LIST_RESPONSE = 0x192
143
- QUEUE_STATS_RESPONSE = 0x193
144
- QUEUE_PEEK_RESPONSE = 0x194
145
- QUEUE_TOUCH_RESPONSE = 0x195
146
- QUEUE_BATCH_ENQUEUE_RESPONSE = 0x196
147
- QUEUE_PURGE_RESPONSE = 0x197
148
192
  QUEUE_LIST = 0x198
149
- QUEUE_LIST_RESPONSE = 0x199
150
193
 
151
194
  # ── Time-Series (0x1A0 – 0x1BF) ──
152
195
  TS_WRITE = 0x1A0
@@ -156,13 +199,6 @@ class OpCode(IntEnum):
156
199
  TS_LIST = 0x1A4
157
200
  TS_DELETE = 0x1A5
158
201
  TS_RETENTION = 0x1A6
159
- TS_WRITE_RESPONSE = 0x1A7
160
- TS_READ_RESPONSE = 0x1A8
161
- TS_QUERY_RESPONSE = 0x1A9
162
- TS_FLOQL_RESPONSE = 0x1AA
163
- TS_LIST_RESPONSE = 0x1AB
164
- TS_DELETE_RESPONSE = 0x1AC
165
- TS_RETENTION_RESPONSE = 0x1AD
166
202
 
167
203
  # ── Actions (0x300 – 0x31F) ──
168
204
  ACTION_REGISTER = 0x300
@@ -175,12 +211,6 @@ class OpCode(IntEnum):
175
211
  ACTION_COMPLETE = 0x307
176
212
  ACTION_FAIL = 0x308
177
213
  ACTION_TOUCH = 0x309
178
- ACTION_REGISTER_RESPONSE = 0x310
179
- ACTION_INVOKE_RESPONSE = 0x311
180
- ACTION_STATUS_RESPONSE = 0x312
181
- ACTION_LIST_RESPONSE = 0x313
182
- ACTION_LIST_RUNS_RESPONSE = 0x314
183
- ACTION_TASK_ASSIGNMENT = 0x315
184
214
 
185
215
  # ── Workers (0x320 – 0x33F) ──
186
216
  WORKER_REGISTER = 0x320
@@ -189,10 +219,6 @@ class OpCode(IntEnum):
189
219
  WORKER_LIST = 0x323
190
220
  WORKER_INFO = 0x324
191
221
  WORKER_DRAIN = 0x325
192
- WORKER_REGISTER_RESPONSE = 0x330
193
- WORKER_LIST_RESPONSE = 0x331
194
- WORKER_INFO_RESPONSE = 0x332
195
- WORKER_DRAIN_RESPONSE = 0x333
196
222
 
197
223
  # ── Workflows (0x340 – 0x35F) ──
198
224
  WORKFLOW_CREATE = 0x340
@@ -206,15 +232,6 @@ class OpCode(IntEnum):
206
232
  WORKFLOW_DISABLE = 0x348
207
233
  WORKFLOW_ENABLE = 0x349
208
234
  WORKFLOW_LIST_DEFINITIONS = 0x34A
209
- WORKFLOW_CREATE_RESPONSE = 0x350
210
- WORKFLOW_START_RESPONSE = 0x351
211
- WORKFLOW_STATUS_RESPONSE = 0x352
212
- WORKFLOW_HISTORY_RESPONSE = 0x353
213
- WORKFLOW_LIST_RUNS_RESPONSE = 0x354
214
- WORKFLOW_GET_DEFINITION_RESPONSE = 0x355
215
- WORKFLOW_DISABLE_RESPONSE = 0x356
216
- WORKFLOW_ENABLE_RESPONSE = 0x357
217
- WORKFLOW_LIST_DEFINITIONS_RESPONSE = 0x358
218
235
 
219
236
  # ── Processing (0x360 – 0x37F) ──
220
237
  PROCESSING_SUBMIT = 0x360
@@ -222,17 +239,6 @@ class OpCode(IntEnum):
222
239
  PROCESSING_CANCEL = 0x362
223
240
  PROCESSING_STATUS = 0x363
224
241
  PROCESSING_LIST = 0x364
225
- PROCESSING_SAVEPOINT = 0x365
226
- PROCESSING_RESTORE = 0x366
227
- PROCESSING_RESCALE = 0x367
228
- PROCESSING_SUBMIT_RESPONSE = 0x370
229
- PROCESSING_STOP_RESPONSE = 0x371
230
- PROCESSING_CANCEL_RESPONSE = 0x372
231
- PROCESSING_STATUS_RESPONSE = 0x373
232
- PROCESSING_LIST_RESPONSE = 0x374
233
- PROCESSING_SAVEPOINT_RESPONSE = 0x375
234
- PROCESSING_RESTORE_RESPONSE = 0x376
235
- PROCESSING_RESCALE_RESPONSE = 0x377
236
242
 
237
243
 
238
244
  class StatusCode(IntEnum):
@@ -250,6 +256,23 @@ class StatusCode(IntEnum):
250
256
  INTERNAL_ERROR = 9
251
257
  OVERLOADED = 10
252
258
  RATE_LIMITED = 11
259
+ UNAVAILABLE = 12
260
+
261
+ @classmethod
262
+ def _missing_(cls, value: object) -> "StatusCode | None":
263
+ # A status this SDK doesn't know still has a body on the wire; failing
264
+ # here would abandon it unread, so it becomes a pseudo-member instead.
265
+ if not isinstance(value, int) or not 0 <= value <= 255:
266
+ return None
267
+ member = int.__new__(cls, value)
268
+ member._name_ = f"UNKNOWN_{value}"
269
+ member._value_ = value
270
+ return member
271
+
272
+ def __reduce_ex__(self, proto: object) -> tuple[type["StatusCode"], tuple[int]]:
273
+ # Rebuilt from its value, not its name: a pseudo-member such as
274
+ # UNKNOWN_200 has no name to look up, so pickle and copy would fail.
275
+ return self.__class__, (int(self),)
253
276
 
254
277
  def message(self) -> str:
255
278
  """Get human-readable error message."""
@@ -266,8 +289,11 @@ class StatusCode(IntEnum):
266
289
  StatusCode.INTERNAL_ERROR: "Internal server error",
267
290
  StatusCode.OVERLOADED: "Server overloaded",
268
291
  StatusCode.RATE_LIMITED: "Request rate limit exceeded",
292
+ StatusCode.UNAVAILABLE: (
293
+ "Unavailable: no leader or the shard isn't taking writes; retry"
294
+ ),
269
295
  }
270
- return messages.get(self, "Unknown error")
296
+ return messages.get(self, f"Unknown status {int(self)}")
271
297
 
272
298
 
273
299
  class OptionTag(IntEnum):
@@ -276,26 +302,13 @@ class OptionTag(IntEnum):
276
302
  Organized by feature area, matching proto.zig definitions.
277
303
  """
278
304
 
279
- # KV Options (0x01 - 0x0F)
280
- TTL_SECONDS = 0x01 # u64: Time-to-live in seconds (0 = no expiration)
281
- CAS_VERSION = 0x02 # u64: Expected version for compare-and-swap
282
- IF_NOT_EXISTS = 0x03 # void: Only set if key doesn't exist (NX)
283
- IF_EXISTS = 0x04 # void: Only set if key exists (XX)
284
- LIMIT = 0x05 # u32: Maximum number of results for scan/list operations
285
- KEYS_ONLY = 0x06 # u8: Skip values in scan response (0/1)
286
- CURSOR = 0x07 # bytes: Pagination cursor (ShardWalker format)
287
- ROUTING_KEY = 0x08 # string: Explicit routing key for shard co-location
305
+ # General Options (0x01 - 0x0F)
306
+ LIMIT = 0x05 # u32: Maximum number of results for list operations
288
307
 
289
308
  # Queue Options (0x10 - 0x1F)
290
- PRIORITY = 0x10 # u8: Message priority (0-255, higher = more urgent)
291
- DELAY_MS = 0x11 # u64: Delay before message becomes visible
292
- VISIBILITY_TIMEOUT_MS = 0x12 # u32: How long message is invisible after dequeue
293
- DEDUP_KEY = 0x13 # string: Deduplication key
294
- MAX_RETRIES = 0x14 # u8: Maximum retry attempts before DLQ
309
+ PRIORITY = 0x10 # u8: Message priority (0-255, lower is taken first)
295
310
  COUNT = 0x15 # u32: Number of messages to dequeue
296
- SEND_TO_DLQ = 0x16 # u8: Whether to send failed messages to DLQ (0/1)
297
- BLOCK_MS = 0x17 # u32: Block timeout - wait until exists (0=forever)
298
- WAIT_MS = 0x18 # u32: Watch timeout - wait for NEXT version change (0=forever)
311
+ BLOCK_MS = 0x17 # u32: Block timeout - wait for data (0=don't wait, max 300000)
299
312
 
300
313
  # Stream Options (0x20 - 0x2F) - StreamID-native ONLY
301
314
  # All stream positioning uses StreamID (timestamp_ms + sequence)
@@ -306,36 +319,11 @@ class OptionTag(IntEnum):
306
319
  PARTITION = 0x24 # u32: Explicit partition index
307
320
  PARTITION_KEY = 0x25 # string: Key for partition routing
308
321
  MAX_AGE_SECONDS = 0x26 # u64: Maximum age in seconds for retention
309
- MAX_BYTES = 0x27 # u64: Maximum size in bytes for retention
310
322
  DRY_RUN = 0x28 # void: Flag to preview what would be deleted
311
- RETENTION_COUNT = 0x29 # u64: Retention policy - max event count
312
- RETENTION_AGE = 0x2A # u64: Retention policy - max age in seconds
313
- RETENTION_BYTES = 0x2B # u64: Retention policy - max bytes
314
323
 
315
324
  # Consumer Group Options (0x30 - 0x3F)
316
325
  ACK_TIMEOUT_MS = 0x30 # u32: Time before unacked message auto-redelivers
317
326
  MAX_DELIVER = 0x31 # u8: Max delivery attempts before DLQ (default: 10)
318
- SUBSCRIPTION_MODE = 0x32 # u8: 0=shared, 1=exclusive, 2=key_shared
319
- REDELIVERY_DELAY_MS = 0x33 # u32: Delay before NACK'd message becomes visible
320
- CONSUMER_TIMEOUT_MS = 0x34 # u32: Remove consumer from group if no activity
321
- NO_ACK = 0x35 # void: Auto-ack on delivery (at-most-once)
322
- IDLE_TIMEOUT_MS = 0x36 # u64: Min idle time for claiming stuck messages
323
- MAX_ACK_PENDING = 0x37 # u32: Max unacked messages per consumer
324
- EXTEND_ACK_MS = 0x38 # u32: Amount of time to extend ack deadline
325
- MAX_STANDBYS = 0x39 # u16: Max standby consumers in exclusive mode
326
- NUM_SLOTS = 0x3A # u16: Number of hash slots for key_shared mode
327
-
328
- # Worker/Action Options (0x40 - 0x4F)
329
- WORKER_ID = 0x40 # string: Worker identifier
330
- EXTEND_MS = 0x41 # u32: Lease extension time in milliseconds
331
- MAX_TASKS = 0x42 # u32: Maximum tasks to return in batch
332
- RETRY = 0x43 # u8: Whether to retry on failure (0/1)
333
-
334
- # Workflow Options (0x50 - 0x5F)
335
- TIMEOUT_MS = 0x50 # u64: Workflow/activity timeout
336
- RETRY_POLICY = 0x51 # bytes: Serialized retry policy
337
- CORRELATION_ID = 0x52 # string: Correlation ID for tracing
338
- SUBSCRIPTION_ID = 0x53 # u64: Subscription ID for stream subscriptions
339
327
 
340
328
  # Time-Series Options (0x60 - 0x6F)
341
329
  TS_FROM_MS = 0x60 # i64: Start of time range (inclusive, unix ms)
@@ -344,11 +332,9 @@ class OptionTag(IntEnum):
344
332
  TS_AGGREGATION = 0x63 # string: Aggregation function name (avg, sum, count, min, max)
345
333
  TS_FIELD = 0x64 # string: Field name filter (empty = "value")
346
334
  TS_TAGS = 0x65 # string: Comma-separated tag filters "key=val,key2=val2"
347
- TS_PRECISION = 0x66 # u8: Timestamp precision (0=ns, 1=us, 2=ms, 3=s)
348
335
  TS_TIMESTAMP = 0x67 # i64: Explicit timestamp for write (0 = server-assigned)
349
336
  TS_RAW_TTL = 0x68 # string: Raw data TTL (e.g., "7d")
350
337
  TS_DOWNSAMPLE = 0x69 # string: Downsample rule (e.g., "1m:avg:30d")
351
- TS_BATCH = 0x6A # void: Flag indicating batch/line-protocol mode
352
338
 
353
339
 
354
340
  # =============================================================================
@@ -357,29 +343,114 @@ class OptionTag(IntEnum):
357
343
 
358
344
 
359
345
  @dataclass
360
- class KVEntry:
361
- """KV entry from scan results."""
346
+ class ScanResult:
347
+ """A page of a KV scan: the keys' names, and the cursor for the next page."""
362
348
 
363
- key: bytes
364
- value: bytes | None # None if keys_only=True
349
+ names: list[str]
350
+ cursor: bytes | None # None on the last page
351
+
352
+ @property
353
+ def has_more(self) -> bool:
354
+ return self.cursor is not None
365
355
 
366
356
 
367
357
  @dataclass
368
- class ScanResult:
369
- """Result of a KV scan operation."""
358
+ class VersionEntry:
359
+ """One version of a key from KV history."""
370
360
 
371
- entries: list[KVEntry]
372
- cursor: bytes | None # None if no more pages
373
- has_more: bool
361
+ version: int
362
+ at_ms: int # When it was written, unix ms
363
+ value: bytes
374
364
 
375
365
 
376
366
  @dataclass
377
- class VersionEntry:
378
- """KV version entry from history."""
367
+ class IncrResult:
368
+ """A KV incr's new counter value and the key's new version."""
369
+
370
+ value: int
371
+ version: int
372
+
373
+
374
+ @dataclass
375
+ class PutResult:
376
+ """Result of a successful KV put.
377
+
378
+ The ``version`` field is the new version assigned by the server, suitable
379
+ for CAS on the next write via :class:`PutOptions.cas_version`.
380
+ """
381
+
382
+ version: int
383
+
384
+
385
+ @dataclass
386
+ class KVBeginResult:
387
+ """Result of a successful KV transaction begin.
388
+
389
+ ``txn_id`` is the server-assigned transaction handle. ``pinned_hash`` is
390
+ the partition hash this transaction is bound to — every key written or
391
+ read inside the transaction must hash to the same partition.
392
+ """
393
+
394
+ txn_id: int
395
+ pinned_hash: int
396
+
397
+
398
+ @dataclass
399
+ class KVCommitResult:
400
+ """Result of a successful KV transaction commit.
401
+
402
+ ``commit_index`` is the Raft log index of the committed batch and
403
+ ``op_count`` is the number of buffered operations applied atomically.
404
+ """
405
+
406
+ commit_index: int
407
+ op_count: int
408
+
409
+
410
+ @dataclass
411
+ class GetResult:
412
+ """Result of a KV get that found a key.
413
+
414
+ ``kv.get`` returns ``None`` when the key is missing; check for ``None``
415
+ before dereferencing.
416
+ """
379
417
 
418
+ value: bytes
380
419
  version: int
381
- timestamp: int
420
+
421
+
422
+ T = TypeVar("T")
423
+
424
+
425
+ @dataclass
426
+ class GetJsonResult(Generic[T]):
427
+ """Result of a :meth:`KV.json_get` that found the key and path.
428
+
429
+ Unlike :class:`GetResult`, ``value`` is the already-parsed JSON (the
430
+ ``KV_JSON_GET`` wire body is always a JSON document, so it is decoded for
431
+ you). ``version`` is the document's current version. ``kv.json_get``
432
+ returns ``None`` when the key or path is missing.
433
+
434
+ The value type defaults to ``Any``; the SDK does not validate the shape at
435
+ runtime.
436
+ """
437
+
438
+ value: Any
439
+ version: int
440
+
441
+
442
+ @dataclass
443
+ class MGetEntry:
444
+ """One entry in a :meth:`KV.mget` response.
445
+
446
+ ``found`` is ``False`` when the key did not exist; in that case ``value``
447
+ is ``b''`` and ``version`` is ``0``.
448
+ """
449
+
450
+ key: str
382
451
  value: bytes
452
+ version: int
453
+ found: bool
383
454
 
384
455
 
385
456
  @dataclass
@@ -388,6 +459,9 @@ class Message:
388
459
 
389
460
  seq: int
390
461
  payload: bytes
462
+ enqueued_at_ms: int = 0
463
+ delivery_count: int = 0
464
+ priority: int = 0
391
465
 
392
466
 
393
467
  @dataclass
@@ -426,12 +500,10 @@ class StreamID:
426
500
 
427
501
 
428
502
  class StorageTier(IntEnum):
429
- """Storage tier of a stream record."""
503
+ """The tier a stream record was served from; the server sends only these two."""
430
504
 
431
- HOT = 0
432
- PENDING = 1
433
- WARM = 2
434
- COLD = 3
505
+ HOT = 0 # the in-memory log
506
+ WARM = 1 # the warm store, after the record left the log
435
507
 
436
508
 
437
509
  @dataclass
@@ -440,6 +512,7 @@ class StreamRecord:
440
512
 
441
513
  id: StreamID = None # type: ignore[assignment]
442
514
  tier: StorageTier = StorageTier.HOT
515
+ stream: str = ""
443
516
  payload: bytes = b""
444
517
  headers: dict[str, str] | None = None
445
518
 
@@ -458,6 +531,35 @@ class StreamReadResult:
458
531
  records: list[StreamRecord]
459
532
 
460
533
 
534
+ @dataclass
535
+ class PendingEntry:
536
+ """One entry in a consumer group's Pending Entry List (PEL).
537
+
538
+ A delivered-but-unacked message. ``delivery_count`` is how many times the
539
+ entry has been delivered; ``consumer`` is the consumer that currently owns
540
+ it.
541
+ """
542
+
543
+ id: StreamID = None # type: ignore[assignment]
544
+ consumer: str = ""
545
+ delivery_count: int = 0
546
+
547
+
548
+ @dataclass
549
+ class StreamClaimResult:
550
+ """Result of a :meth:`StreamOperations.group_claim` cursor page.
551
+
552
+ ``records`` carry payload + headers (same shape as ``group_read``).
553
+ ``next_cursor`` is the ``start_id`` to pass on the next ``group_claim``
554
+ call; when ``done`` is ``True`` the PEL has been fully scanned and the loop
555
+ should stop.
556
+ """
557
+
558
+ records: list[StreamRecord]
559
+ next_cursor: StreamID = None # type: ignore[assignment]
560
+ done: bool = False
561
+
562
+
461
563
  @dataclass
462
564
  class StreamInfo:
463
565
  """Stream metadata."""
@@ -474,12 +576,34 @@ class StreamInfo:
474
576
  # =============================================================================
475
577
 
476
578
 
579
+ class WaitFor(IntEnum):
580
+ """What a KV get with wait_ms waits for."""
581
+
582
+ # The key to exist; one that exists answers at once.
583
+ EXISTS = 0
584
+ # The key's next version.
585
+ CHANGE = 1
586
+
587
+
588
+ class Condition(IntEnum):
589
+ """When a KV put may write."""
590
+
591
+ ALWAYS = 0
592
+ IF_ABSENT = 1
593
+ IF_PRESENT = 2
594
+
595
+
477
596
  @dataclass
478
597
  class GetOptions:
479
598
  """Options for KV get operations."""
480
599
 
481
600
  namespace: str | None = None
482
- block_ms: int | None = None # Block until value available (0 = infinite)
601
+ # Wait up to this long for what wait_for names (0 = don't wait, max 300000).
602
+ wait_ms: int = 0
603
+ # The key to exist (the default), or its next version.
604
+ wait_for: WaitFor = WaitFor.EXISTS
605
+ # Routes the request in place of the key.
606
+ routing_key: str | None = None
483
607
 
484
608
 
485
609
  @dataclass
@@ -487,10 +611,13 @@ class PutOptions:
487
611
  """Options for KV put operations."""
488
612
 
489
613
  namespace: str | None = None
490
- ttl_seconds: int | None = None
614
+ ttl_ms: int | None = None # Time-to-live in milliseconds (0 = no expiration)
615
+ # When the write may happen.
616
+ condition: Condition = Condition.ALWAYS
617
+ # The key's version the write requires; 0 means it doesn't exist.
491
618
  cas_version: int | None = None
492
- if_not_exists: bool = False
493
- if_exists: bool = False
619
+ # Routes the request in place of the key.
620
+ routing_key: str | None = None
494
621
 
495
622
 
496
623
  @dataclass
@@ -498,6 +625,10 @@ class DeleteOptions:
498
625
  """Options for KV delete operations."""
499
626
 
500
627
  namespace: str | None = None
628
+ # Only delete when the key's version is this.
629
+ cas_version: int | None = None
630
+ # Routes the request in place of the key.
631
+ routing_key: str | None = None
501
632
 
502
633
 
503
634
  @dataclass
@@ -505,9 +636,9 @@ class ScanOptions:
505
636
  """Options for KV scan operations."""
506
637
 
507
638
  namespace: str | None = None
639
+ # The cursor from the previous page; None for the first.
508
640
  cursor: bytes | None = None
509
- limit: int | None = None
510
- keys_only: bool = False
641
+ limit: int = 1024 # 1..1024 names per page
511
642
 
512
643
 
513
644
  @dataclass
@@ -515,7 +646,58 @@ class HistoryOptions:
515
646
  """Options for KV history operations."""
516
647
 
517
648
  namespace: str | None = None
518
- limit: int | None = None
649
+ limit: int = 10 # 1..65 versions, newest first
650
+ # Routes the request in place of the key.
651
+ routing_key: str | None = None
652
+
653
+
654
+ @dataclass
655
+ class KVIncrOptions:
656
+ """Options for KV incr operations."""
657
+
658
+ namespace: str | None = None
659
+ delta: int = 1
660
+ # Routes the request in place of the key.
661
+ routing_key: str | None = None
662
+
663
+
664
+ @dataclass
665
+ class KVExpireOptions:
666
+ """Options for KV expire operations."""
667
+
668
+ namespace: str | None = None
669
+ # Only succeed when the key's version is this.
670
+ cas_version: int | None = None
671
+ # Routes the request in place of the key.
672
+ routing_key: str | None = None
673
+
674
+
675
+ @dataclass
676
+ class KVExistsOptions:
677
+ """Options for KV exists operations."""
678
+
679
+ namespace: str | None = None
680
+ # Routes the request in place of the key.
681
+ routing_key: str | None = None
682
+
683
+
684
+ @dataclass
685
+ class KVJsonOptions:
686
+ """Options for KV JSON.* operations."""
687
+
688
+ namespace: str | None = None
689
+ # Routes the request in place of the key.
690
+ routing_key: str | None = None
691
+
692
+
693
+ @dataclass
694
+ class KVMGetOptions:
695
+ """Options for KV mget operations."""
696
+
697
+ namespace: str | None = None
698
+ # The routing key every key was written with: all are looked up on its
699
+ # shard. Without it, each key is looked up on its own.
700
+ routing_key: str | None = None
519
701
 
520
702
 
521
703
  @dataclass
@@ -523,9 +705,7 @@ class EnqueueOptions:
523
705
  """Options for queue enqueue operations."""
524
706
 
525
707
  namespace: str | None = None
526
- priority: int = 0
527
- delay_ms: int | None = None
528
- dedup_key: str | None = None
708
+ priority: int = 0 # 0-255, lower is taken first
529
709
 
530
710
 
531
711
  @dataclass
@@ -533,8 +713,7 @@ class DequeueOptions:
533
713
  """Options for queue dequeue operations."""
534
714
 
535
715
  namespace: str | None = None
536
- visibility_timeout_ms: int | None = None
537
- block_ms: int | None = None
716
+ block_ms: int | None = None # Wait for messages (0 = don't wait, max 300000)
538
717
 
539
718
 
540
719
  @dataclass
@@ -549,7 +728,6 @@ class NackOptions:
549
728
  """Options for queue nack operations."""
550
729
 
551
730
  namespace: str | None = None
552
- to_dlq: bool = False
553
731
 
554
732
 
555
733
  @dataclass
@@ -557,7 +735,6 @@ class DlqListOptions:
557
735
  """Options for DLQ list operations."""
558
736
 
559
737
  namespace: str | None = None
560
- limit: int = 100
561
738
 
562
739
 
563
740
  @dataclass
@@ -574,13 +751,6 @@ class PeekOptions:
574
751
  namespace: str | None = None
575
752
 
576
753
 
577
- @dataclass
578
- class TouchOptions:
579
- """Options for queue touch (lease renewal) operations."""
580
-
581
- namespace: str | None = None
582
-
583
-
584
754
  # =============================================================================
585
755
  # Stream Option Types
586
756
  # =============================================================================
@@ -607,7 +777,7 @@ class StreamReadOptions:
607
777
  tail: bool = False # Start from end of stream (mutually exclusive with start)
608
778
  partition: int | None = None # Explicit partition index
609
779
  count: int | None = None # Maximum number of records to return
610
- block_ms: int | None = None # Blocking timeout (0 = infinite)
780
+ block_ms: int | None = None # Blocking timeout (0 = don't wait, max 300000)
611
781
 
612
782
 
613
783
  @dataclass
@@ -615,10 +785,18 @@ class StreamTrimOptions:
615
785
  """Options for stream trim operations."""
616
786
 
617
787
  namespace: str | None = None
618
- max_len: int | None = None # Retention policy - max event count
619
- max_age_seconds: int | None = None # Retention policy - max age in seconds
620
- max_bytes: int | None = None # Retention policy - max bytes
621
- dry_run: bool = False # Preview what would be deleted
788
+ before: Optional["StreamID"] = None # Remove records up to and including this id
789
+ max_len: int | None = None # Keep only the newest N records (> 0)
790
+ max_age_seconds: int | None = None # Remove records older than this (> 0)
791
+ dry_run: bool = False # Report what would be removed, removing nothing
792
+
793
+
794
+ @dataclass
795
+ class StreamTrimResult:
796
+ """What a trim removed, or with dry_run would remove."""
797
+
798
+ removed: int # Records removed
799
+ first_seq: int # Sequence of the first record left; meaningful only when records remain
622
800
 
623
801
 
624
802
  @dataclass
@@ -641,7 +819,7 @@ class StreamGroupReadOptions:
641
819
 
642
820
  namespace: str | None = None
643
821
  count: int | None = None # Max records to read
644
- block_ms: int | None = None # Block waiting for records
822
+ block_ms: int | None = None # Block waiting for records (0 = don't wait, max 300000)
645
823
 
646
824
 
647
825
  @dataclass
@@ -649,7 +827,7 @@ class StreamGroupAckOptions:
649
827
  """Options for acknowledging records in a consumer group."""
650
828
 
651
829
  namespace: str | None = None
652
- consumer: str = "" # Consumer ID (required for correct ack matching)
830
+ consumer: str = "" # Sent, but the server doesn't check it: any consumer can ack an entry
653
831
 
654
832
 
655
833
  @dataclass
@@ -657,8 +835,7 @@ class StreamGroupNackOptions:
657
835
  """Options for negatively acknowledging records in a consumer group."""
658
836
 
659
837
  namespace: str | None = None
660
- consumer: str = "" # Consumer ID (required for correct nack matching)
661
- redelivery_delay_ms: int | None = None # Delay before message becomes visible again
838
+ consumer: str = "" # Sent, but the server doesn't check it: any consumer can nack an entry
662
839
 
663
840
 
664
841
  # =============================================================================
@@ -683,14 +860,34 @@ class ActionInfo:
683
860
  description: str | None = None
684
861
 
685
862
 
863
+ class RunStatus(IntEnum):
864
+ """An action run's status."""
865
+
866
+ PENDING = 0
867
+ RUNNING = 1
868
+ COMPLETED = 2
869
+ FAILED = 3
870
+ CANCELLED = 4
871
+ TIMED_OUT = 5
872
+
873
+
686
874
  @dataclass
687
875
  class ActionRunStatus:
688
- """Status of an action run."""
876
+ """Status of an action run. Times are ms since the epoch; ``attempt``
877
+ counts from 1 once a worker has taken the run. ``output`` is a completed
878
+ run's result, and ``error_message`` the last failure's message."""
689
879
 
690
880
  run_id: str
691
- status: str # "pending", "running", "completed", "failed"
692
- result: bytes | None = None
693
- error: str | None = None
881
+ status: RunStatus
882
+ created_at_ms: int
883
+ attempt: int
884
+ started_at_ms: int | None = None
885
+ completed_at_ms: int | None = None
886
+ output: bytes | None = None
887
+ error_message: str | None = None
888
+ # The worker holding the run, or that last held it; None until one takes
889
+ # it. Only the leader learns of a take at once.
890
+ worker_id: str | None = None
694
891
 
695
892
 
696
893
  @dataclass
@@ -715,12 +912,23 @@ class ActionListResult:
715
912
 
716
913
  @dataclass
717
914
  class ActionRegisterOptions:
718
- """Options for registering an action."""
915
+ """Options for registering an action. Registering an action replaces
916
+ the settings it had: the last registration wins."""
719
917
 
720
918
  namespace: str | None = None
919
+ # How long a run may go without its worker completing, failing or
920
+ # touching it before it's taken back; 0 means never.
721
921
  timeout_ms: int = 30000
922
+ # Attempts in all, counting the first.
722
923
  max_retries: int = 3
723
- description: str | None = None
924
+ owner: str | None = None
925
+
926
+ def __post_init__(self) -> None:
927
+ # Both go on the wire as u32.
928
+ for name in ("timeout_ms", "max_retries"):
929
+ value = getattr(self, name)
930
+ if not 0 <= value < 2**32:
931
+ raise ValueError(f"{name} must be in [0, 2**32), not {value}")
724
932
 
725
933
 
726
934
  @dataclass
@@ -728,8 +936,9 @@ class ActionInvokeOptions:
728
936
  """Options for invoking an action."""
729
937
 
730
938
  namespace: str | None = None
731
- priority: int = 10
732
- idempotency_key: str | None = None
939
+ # Required worker labels: only workers registered with every key/value
940
+ # here receive the run.
941
+ labels: dict[str, Any] | None = None
733
942
 
734
943
 
735
944
  @dataclass
@@ -769,10 +978,8 @@ class TaskAssignment:
769
978
  payload: bytes
770
979
  created_at: int
771
980
  attempt: int
772
-
773
-
774
- # Alias for backwards compatibility
775
- WorkerTask = TaskAssignment
981
+ caller_run_id: str = ""
982
+ caller_workflow_name: str = ""
776
983
 
777
984
 
778
985
  @dataclass
@@ -782,19 +989,44 @@ class WorkerAwaitResult:
782
989
  task: TaskAssignment | None = None # None if no task available
783
990
 
784
991
 
992
+ class WorkerType(IntEnum):
993
+ """What a worker runs."""
994
+
995
+ ACTION = 0
996
+ STREAM = 1
997
+
998
+
999
+ class WorkerStatus(IntEnum):
1000
+ """A registered worker's status."""
1001
+
1002
+ ACTIVE = 0
1003
+ IDLE = 1
1004
+ DRAINING = 2
1005
+ UNHEALTHY = 3
1006
+
1007
+
785
1008
  @dataclass
786
1009
  class WorkerInfo:
787
- """Information about a registered worker."""
1010
+ """A registered worker, as worker.list reports it. Times are ms since the epoch."""
788
1011
 
789
1012
  worker_id: str
790
- task_types: list[str]
1013
+ worker_type: WorkerType
1014
+ status: WorkerStatus
1015
+ current_load: int
1016
+ max_concurrency: int
1017
+ tasks_completed: int
1018
+ tasks_failed: int
1019
+ registered_at_ms: int
1020
+ last_heartbeat_ms: int
1021
+ machine_id: str | None = None
791
1022
 
792
1023
 
793
1024
  @dataclass
794
1025
  class WorkerListResult:
795
- """Result of listing workers."""
1026
+ """A page of workers, in id order. ``cursor`` gives the next page; None on the last."""
796
1027
 
797
1028
  workers: list[WorkerInfo]
1029
+ cursor: bytes | None = None
798
1030
 
799
1031
 
800
1032
  # =============================================================================
@@ -817,8 +1049,7 @@ class WorkerAwaitOptions:
817
1049
  """Options for awaiting a task."""
818
1050
 
819
1051
  namespace: str | None = None
820
- block_ms: int | None = None # Block waiting for task (0 = infinite)
821
- timeout_ms: int | None = None
1052
+ block_ms: int | None = None # Block waiting for task (None = 30000, 0 = don't wait, max 300000)
822
1053
 
823
1054
 
824
1055
  @dataclass
@@ -847,10 +1078,12 @@ class WorkerFailOptions:
847
1078
 
848
1079
  @dataclass
849
1080
  class WorkerListOptions:
850
- """Options for listing workers."""
1081
+ """Options for listing workers: at most ``limit`` (1..1024) after ``cursor``,
1082
+ the last page's cursor (None starts)."""
851
1083
 
852
1084
  namespace: str | None = None
853
1085
  limit: int = 100
1086
+ cursor: bytes | None = None
854
1087
 
855
1088
 
856
1089
  # =============================================================================
@@ -1005,27 +1238,6 @@ class ProcessingCancelOptions:
1005
1238
  namespace: str | None = None
1006
1239
 
1007
1240
 
1008
- @dataclass
1009
- class ProcessingSavepointOptions:
1010
- """Options for triggering a savepoint."""
1011
-
1012
- namespace: str | None = None
1013
-
1014
-
1015
- @dataclass
1016
- class ProcessingRestoreOptions:
1017
- """Options for restoring from a savepoint."""
1018
-
1019
- namespace: str | None = None
1020
-
1021
-
1022
- @dataclass
1023
- class ProcessingRescaleOptions:
1024
- """Options for rescaling a processing job."""
1025
-
1026
- namespace: str | None = None
1027
-
1028
-
1029
1241
  @dataclass
1030
1242
  class ProcessingSyncOptions:
1031
1243
  """Options for declarative processing sync."""
@@ -1040,7 +1252,6 @@ class ProcessingStatusResult:
1040
1252
  job_id: str
1041
1253
  name: str
1042
1254
  status: str
1043
- parallelism: int
1044
1255
  batch_size: int
1045
1256
  records_processed: int
1046
1257
  created_at: int
@@ -1053,7 +1264,6 @@ class ProcessingListEntry:
1053
1264
  name: str
1054
1265
  job_id: str
1055
1266
  status: str
1056
- parallelism: int
1057
1267
  created_at: int
1058
1268
 
1059
1269