flo-python 0.1.0.dev3__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flo/__init__.py +31 -1
- flo/client.py +37 -3
- flo/kv.py +360 -35
- flo/kv_txn.py +340 -0
- flo/streams.py +116 -5
- flo/types.py +169 -6
- flo/wire.py +179 -5
- flo/worker.py +136 -12
- {flo_python-0.1.0.dev3.dist-info → flo_python-0.1.1.dist-info}/METADATA +3 -3
- flo_python-0.1.1.dist-info/RECORD +17 -0
- {flo_python-0.1.0.dev3.dist-info → flo_python-0.1.1.dist-info}/WHEEL +1 -1
- flo_python-0.1.0.dev3.dist-info/RECORD +0 -16
- {flo_python-0.1.0.dev3.dist-info → flo_python-0.1.1.dist-info}/licenses/LICENSE +0 -0
flo/types.py
CHANGED
|
@@ -6,7 +6,7 @@ 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
|
|
@@ -67,7 +67,7 @@ class OpCode(IntEnum):
|
|
|
67
67
|
CLUSTER_MEMBERS_RESPONSE = 0x041
|
|
68
68
|
CLUSTER_JOIN_RESPONSE = 0x042
|
|
69
69
|
|
|
70
|
-
# ── KV
|
|
70
|
+
# ── KV (0x100 – 0x12F) ──
|
|
71
71
|
KV_PUT = 0x100
|
|
72
72
|
KV_GET = 0x101
|
|
73
73
|
KV_MGET = 0x102
|
|
@@ -79,13 +79,23 @@ class OpCode(IntEnum):
|
|
|
79
79
|
KV_PUT_RESPONSE = 0x108
|
|
80
80
|
KV_SCAN_RESPONSE = 0x109
|
|
81
81
|
KV_HISTORY_RESPONSE = 0x10A
|
|
82
|
+
# KV Extended (atomic counters, JSON ops)
|
|
83
|
+
KV_INCR = 0x10B
|
|
84
|
+
KV_JSON_GET = 0x10C
|
|
85
|
+
KV_JSON_SET = 0x10D
|
|
86
|
+
KV_JSON_DEL = 0x10E
|
|
87
|
+
# KV Per-Shard Transactions
|
|
82
88
|
KV_BEGIN_TXN = 0x110
|
|
83
89
|
KV_COMMIT_TXN = 0x111
|
|
84
90
|
KV_ROLLBACK_TXN = 0x112
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
# KV Extended (TTL lifecycle, exists)
|
|
92
|
+
KV_TOUCH = 0x113
|
|
93
|
+
KV_PERSIST = 0x114
|
|
94
|
+
KV_EXISTS = 0x115
|
|
95
|
+
KV_INCR_RESPONSE = 0x116
|
|
96
|
+
KV_JSON_RESPONSE = 0x117
|
|
97
|
+
KV_EXISTS_RESPONSE = 0x118
|
|
98
|
+
KV_TXN_RESPONSE = 0x119
|
|
89
99
|
|
|
90
100
|
# ── Streams (0x130 – 0x14F) ──
|
|
91
101
|
STREAM_APPEND = 0x130
|
|
@@ -285,6 +295,7 @@ class OptionTag(IntEnum):
|
|
|
285
295
|
KEYS_ONLY = 0x06 # u8: Skip values in scan response (0/1)
|
|
286
296
|
CURSOR = 0x07 # bytes: Pagination cursor (ShardWalker format)
|
|
287
297
|
ROUTING_KEY = 0x08 # string: Explicit routing key for shard co-location
|
|
298
|
+
TXN_ID = 0x09 # u64: Transaction ID for per-shard transactions
|
|
288
299
|
|
|
289
300
|
# Queue Options (0x10 - 0x1F)
|
|
290
301
|
PRIORITY = 0x10 # u8: Message priority (0-255, higher = more urgent)
|
|
@@ -382,6 +393,88 @@ class VersionEntry:
|
|
|
382
393
|
value: bytes
|
|
383
394
|
|
|
384
395
|
|
|
396
|
+
@dataclass
|
|
397
|
+
class PutResult:
|
|
398
|
+
"""Result of a successful KV put.
|
|
399
|
+
|
|
400
|
+
The ``version`` field is the new version assigned by the server, suitable
|
|
401
|
+
for CAS on the next write via :class:`PutOptions.cas_version`.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
version: int
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@dataclass
|
|
408
|
+
class KVBeginResult:
|
|
409
|
+
"""Result of a successful KV transaction begin.
|
|
410
|
+
|
|
411
|
+
``txn_id`` is the server-assigned transaction handle. ``pinned_hash`` is
|
|
412
|
+
the partition hash this transaction is bound to — every key written or
|
|
413
|
+
read inside the transaction must hash to the same partition.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
txn_id: int
|
|
417
|
+
pinned_hash: int
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@dataclass
|
|
421
|
+
class KVCommitResult:
|
|
422
|
+
"""Result of a successful KV transaction commit.
|
|
423
|
+
|
|
424
|
+
``commit_index`` is the Raft log index of the committed batch and
|
|
425
|
+
``op_count`` is the number of buffered operations applied atomically.
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
commit_index: int
|
|
429
|
+
op_count: int
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@dataclass
|
|
433
|
+
class GetResult:
|
|
434
|
+
"""Result of a KV get that found a key.
|
|
435
|
+
|
|
436
|
+
``kv.get`` returns ``None`` when the key is missing; check for ``None``
|
|
437
|
+
before dereferencing.
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
value: bytes
|
|
441
|
+
version: int
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
T = TypeVar("T")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@dataclass
|
|
448
|
+
class GetJsonResult(Generic[T]):
|
|
449
|
+
"""Result of a :meth:`KV.json_get` that found the key and path.
|
|
450
|
+
|
|
451
|
+
Unlike :class:`GetResult`, ``value`` is the already-parsed JSON (the
|
|
452
|
+
``KV_JSON_GET`` wire body is always a JSON document, so it is decoded for
|
|
453
|
+
you). ``version`` is the document's current version. ``kv.json_get``
|
|
454
|
+
returns ``None`` when the key or path is missing.
|
|
455
|
+
|
|
456
|
+
The value type defaults to ``Any``; the SDK does not validate the shape at
|
|
457
|
+
runtime.
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
value: Any
|
|
461
|
+
version: int
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@dataclass
|
|
465
|
+
class MGetEntry:
|
|
466
|
+
"""One entry in a :meth:`KV.mget` response.
|
|
467
|
+
|
|
468
|
+
``found`` is ``False`` when the key did not exist; in that case ``value``
|
|
469
|
+
is ``b''`` and ``version`` is ``0``.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
key: str
|
|
473
|
+
value: bytes
|
|
474
|
+
version: int
|
|
475
|
+
found: bool
|
|
476
|
+
|
|
477
|
+
|
|
385
478
|
@dataclass
|
|
386
479
|
class Message:
|
|
387
480
|
"""Queue message."""
|
|
@@ -440,6 +533,7 @@ class StreamRecord:
|
|
|
440
533
|
|
|
441
534
|
id: StreamID = None # type: ignore[assignment]
|
|
442
535
|
tier: StorageTier = StorageTier.HOT
|
|
536
|
+
stream: str = ""
|
|
443
537
|
payload: bytes = b""
|
|
444
538
|
headers: dict[str, str] | None = None
|
|
445
539
|
|
|
@@ -458,6 +552,35 @@ class StreamReadResult:
|
|
|
458
552
|
records: list[StreamRecord]
|
|
459
553
|
|
|
460
554
|
|
|
555
|
+
@dataclass
|
|
556
|
+
class PendingEntry:
|
|
557
|
+
"""One entry in a consumer group's Pending Entry List (PEL).
|
|
558
|
+
|
|
559
|
+
A delivered-but-unacked message. ``delivery_count`` is how many times the
|
|
560
|
+
entry has been delivered; ``consumer`` is the consumer that currently owns
|
|
561
|
+
it. (FLO-102)
|
|
562
|
+
"""
|
|
563
|
+
|
|
564
|
+
id: StreamID = None # type: ignore[assignment]
|
|
565
|
+
consumer: str = ""
|
|
566
|
+
delivery_count: int = 0
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
@dataclass
|
|
570
|
+
class StreamClaimResult:
|
|
571
|
+
"""Result of a :meth:`StreamOperations.group_claim` cursor page (FLO-102).
|
|
572
|
+
|
|
573
|
+
``records`` carry payload + headers (same shape as ``group_read``).
|
|
574
|
+
``next_cursor`` is the ``start_id`` to pass on the next ``group_claim``
|
|
575
|
+
call; when ``done`` is ``True`` the PEL has been fully scanned and the loop
|
|
576
|
+
should stop.
|
|
577
|
+
"""
|
|
578
|
+
|
|
579
|
+
records: list[StreamRecord]
|
|
580
|
+
next_cursor: StreamID = None # type: ignore[assignment]
|
|
581
|
+
done: bool = False
|
|
582
|
+
|
|
583
|
+
|
|
461
584
|
@dataclass
|
|
462
585
|
class StreamInfo:
|
|
463
586
|
"""Stream metadata."""
|
|
@@ -498,6 +621,7 @@ class DeleteOptions:
|
|
|
498
621
|
"""Options for KV delete operations."""
|
|
499
622
|
|
|
500
623
|
namespace: str | None = None
|
|
624
|
+
if_match: int | None = None # CAS: only delete when current version equals if_match
|
|
501
625
|
|
|
502
626
|
|
|
503
627
|
@dataclass
|
|
@@ -518,6 +642,43 @@ class HistoryOptions:
|
|
|
518
642
|
limit: int | None = None
|
|
519
643
|
|
|
520
644
|
|
|
645
|
+
@dataclass
|
|
646
|
+
class KVIncrOptions:
|
|
647
|
+
"""Options for KV incr operations."""
|
|
648
|
+
|
|
649
|
+
namespace: str | None = None
|
|
650
|
+
delta: int | None = None # default +1 when None
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@dataclass
|
|
654
|
+
class KVTouchOptions:
|
|
655
|
+
"""Options for KV touch / persist operations."""
|
|
656
|
+
|
|
657
|
+
namespace: str | None = None
|
|
658
|
+
if_match: int | None = None # CAS: only succeed when current version equals if_match
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
@dataclass
|
|
662
|
+
class KVExistsOptions:
|
|
663
|
+
"""Options for KV exists operations."""
|
|
664
|
+
|
|
665
|
+
namespace: str | None = None
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
@dataclass
|
|
669
|
+
class KVJsonOptions:
|
|
670
|
+
"""Options for KV JSON.* operations."""
|
|
671
|
+
|
|
672
|
+
namespace: str | None = None
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
@dataclass
|
|
676
|
+
class KVMGetOptions:
|
|
677
|
+
"""Options for KV mget operations."""
|
|
678
|
+
|
|
679
|
+
namespace: str | None = None
|
|
680
|
+
|
|
681
|
+
|
|
521
682
|
@dataclass
|
|
522
683
|
class EnqueueOptions:
|
|
523
684
|
"""Options for queue enqueue operations."""
|
|
@@ -769,6 +930,8 @@ class TaskAssignment:
|
|
|
769
930
|
payload: bytes
|
|
770
931
|
created_at: int
|
|
771
932
|
attempt: int
|
|
933
|
+
caller_run_id: str = ""
|
|
934
|
+
caller_workflow_name: str = ""
|
|
772
935
|
|
|
773
936
|
|
|
774
937
|
# Alias for backwards compatibility
|
flo/wire.py
CHANGED
|
@@ -30,6 +30,7 @@ from .types import (
|
|
|
30
30
|
Message,
|
|
31
31
|
OpCode,
|
|
32
32
|
OptionTag,
|
|
33
|
+
PendingEntry,
|
|
33
34
|
ScanResult,
|
|
34
35
|
StatusCode,
|
|
35
36
|
StorageTier,
|
|
@@ -597,7 +598,8 @@ def parse_stream_read_response(data: bytes) -> StreamReadResult:
|
|
|
597
598
|
key_present = data[pos]
|
|
598
599
|
pos += 1
|
|
599
600
|
|
|
600
|
-
#
|
|
601
|
+
# Read key (stream name) if present
|
|
602
|
+
stream_name = ""
|
|
601
603
|
if key_present != 0:
|
|
602
604
|
if pos + 4 > len(data):
|
|
603
605
|
raise IncompleteResponseError("Stream record: missing key length")
|
|
@@ -605,6 +607,7 @@ def parse_stream_read_response(data: bytes) -> StreamReadResult:
|
|
|
605
607
|
pos += 4
|
|
606
608
|
if pos + key_len > len(data):
|
|
607
609
|
raise IncompleteResponseError("Stream record: missing key data")
|
|
610
|
+
stream_name = data[pos : pos + key_len].decode("utf-8")
|
|
608
611
|
pos += key_len
|
|
609
612
|
|
|
610
613
|
# Read payload
|
|
@@ -618,17 +621,41 @@ def parse_stream_read_response(data: bytes) -> StreamReadResult:
|
|
|
618
621
|
payload = data[pos : pos + payload_len]
|
|
619
622
|
pos += payload_len
|
|
620
623
|
|
|
621
|
-
#
|
|
624
|
+
# Read headers: [header_count:u32]([key_len:u32][key][val_len:u32][val])*
|
|
622
625
|
if pos + 4 > len(data):
|
|
623
626
|
raise IncompleteResponseError("Stream record: missing header count")
|
|
627
|
+
header_count = struct.unpack("<I", data[pos : pos + 4])[0]
|
|
624
628
|
pos += 4
|
|
625
629
|
|
|
630
|
+
headers = None
|
|
631
|
+
if header_count > 0:
|
|
632
|
+
headers = {}
|
|
633
|
+
for _ in range(header_count):
|
|
634
|
+
if pos + 4 > len(data):
|
|
635
|
+
raise IncompleteResponseError("Stream record: missing header key length")
|
|
636
|
+
hkey_len = struct.unpack("<I", data[pos : pos + 4])[0]
|
|
637
|
+
pos += 4
|
|
638
|
+
if pos + hkey_len > len(data):
|
|
639
|
+
raise IncompleteResponseError("Stream record: missing header key")
|
|
640
|
+
hkey = data[pos : pos + hkey_len].decode("utf-8")
|
|
641
|
+
pos += hkey_len
|
|
642
|
+
if pos + 4 > len(data):
|
|
643
|
+
raise IncompleteResponseError("Stream record: missing header value length")
|
|
644
|
+
hval_len = struct.unpack("<I", data[pos : pos + 4])[0]
|
|
645
|
+
pos += 4
|
|
646
|
+
if pos + hval_len > len(data):
|
|
647
|
+
raise IncompleteResponseError("Stream record: missing header value")
|
|
648
|
+
hval = data[pos : pos + hval_len].decode("utf-8")
|
|
649
|
+
pos += hval_len
|
|
650
|
+
headers[hkey] = hval
|
|
651
|
+
|
|
626
652
|
records.append(
|
|
627
653
|
StreamRecord(
|
|
628
654
|
id=StreamID(timestamp_ms=timestamp_ms, sequence=sequence),
|
|
629
655
|
tier=tier,
|
|
656
|
+
stream=stream_name,
|
|
630
657
|
payload=payload,
|
|
631
|
-
headers=
|
|
658
|
+
headers=headers,
|
|
632
659
|
)
|
|
633
660
|
)
|
|
634
661
|
|
|
@@ -661,6 +688,33 @@ def parse_stream_info_response(data: bytes) -> StreamInfo:
|
|
|
661
688
|
)
|
|
662
689
|
|
|
663
690
|
|
|
691
|
+
def build_stream_batch_value(
|
|
692
|
+
payload: bytes,
|
|
693
|
+
headers: dict[str, str] | None = None,
|
|
694
|
+
) -> bytes:
|
|
695
|
+
"""Frame a single record for stream_append (batch of one).
|
|
696
|
+
|
|
697
|
+
Wire format:
|
|
698
|
+
[record_count:u32=1][payload_len:u32][payload]
|
|
699
|
+
[header_count:u16]([key_len:u16][key][val_len:u16][val])*
|
|
700
|
+
"""
|
|
701
|
+
hdrs = headers or {}
|
|
702
|
+
parts: list[bytes] = [
|
|
703
|
+
struct.pack("<I", 1),
|
|
704
|
+
struct.pack("<I", len(payload)),
|
|
705
|
+
payload,
|
|
706
|
+
struct.pack("<H", len(hdrs)),
|
|
707
|
+
]
|
|
708
|
+
for key, val in hdrs.items():
|
|
709
|
+
key_bytes = key.encode("utf-8")
|
|
710
|
+
val_bytes = val.encode("utf-8")
|
|
711
|
+
parts.append(struct.pack("<H", len(key_bytes)))
|
|
712
|
+
parts.append(key_bytes)
|
|
713
|
+
parts.append(struct.pack("<H", len(val_bytes)))
|
|
714
|
+
parts.append(val_bytes)
|
|
715
|
+
return b"".join(parts)
|
|
716
|
+
|
|
717
|
+
|
|
664
718
|
def serialize_group_value(group: str, consumer: str) -> bytes:
|
|
665
719
|
"""Serialize group and consumer names for consumer group operations.
|
|
666
720
|
|
|
@@ -697,6 +751,92 @@ def serialize_group_ack_value(group: str, ids: list[StreamID], consumer: str = "
|
|
|
697
751
|
return bytes(result)
|
|
698
752
|
|
|
699
753
|
|
|
754
|
+
def serialize_group_pending_value(group: str, consumer: str = "") -> bytes:
|
|
755
|
+
"""Serialize group (and optional consumer filter) for STREAM_GROUP_PENDING.
|
|
756
|
+
|
|
757
|
+
Format: [group_len:u16][group]([consumer_len:u16][consumer])?
|
|
758
|
+
|
|
759
|
+
An empty ``consumer`` returns the whole group's PEL; a non-empty one filters
|
|
760
|
+
to that consumer's entries.
|
|
761
|
+
"""
|
|
762
|
+
group_bytes = group.encode("utf-8")
|
|
763
|
+
|
|
764
|
+
result = bytearray()
|
|
765
|
+
result.extend(struct.pack("<H", len(group_bytes)))
|
|
766
|
+
result.extend(group_bytes)
|
|
767
|
+
if consumer:
|
|
768
|
+
consumer_bytes = consumer.encode("utf-8")
|
|
769
|
+
result.extend(struct.pack("<H", len(consumer_bytes)))
|
|
770
|
+
result.extend(consumer_bytes)
|
|
771
|
+
return bytes(result)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def serialize_group_claim_value(
|
|
775
|
+
group: str,
|
|
776
|
+
consumer: str,
|
|
777
|
+
min_idle_ms: int,
|
|
778
|
+
start_id: StreamID,
|
|
779
|
+
count: int,
|
|
780
|
+
) -> bytes:
|
|
781
|
+
"""Serialize the request value for STREAM_GROUP_CLAIM (FLO-102).
|
|
782
|
+
|
|
783
|
+
Format: [group_len:u16][group][consumer_len:u16][consumer]
|
|
784
|
+
[min_idle_ms:u32][start_ts:u64][start_seq:u64][count:u32]
|
|
785
|
+
"""
|
|
786
|
+
group_bytes = group.encode("utf-8")
|
|
787
|
+
consumer_bytes = consumer.encode("utf-8")
|
|
788
|
+
|
|
789
|
+
result = bytearray()
|
|
790
|
+
result.extend(struct.pack("<H", len(group_bytes)))
|
|
791
|
+
result.extend(group_bytes)
|
|
792
|
+
result.extend(struct.pack("<H", len(consumer_bytes)))
|
|
793
|
+
result.extend(consumer_bytes)
|
|
794
|
+
result.extend(struct.pack("<I", min_idle_ms))
|
|
795
|
+
result.extend(struct.pack("<Q", start_id.timestamp_ms))
|
|
796
|
+
result.extend(struct.pack("<Q", start_id.sequence))
|
|
797
|
+
result.extend(struct.pack("<I", count))
|
|
798
|
+
return bytes(result)
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def parse_pending_entries(data: bytes) -> list[PendingEntry]:
|
|
802
|
+
"""Parse the STREAM_GROUP_PENDING response (FLO-102).
|
|
803
|
+
|
|
804
|
+
Format: [count:u32]([ts:u64][seq:u64][delivery_count:u32]
|
|
805
|
+
[consumer_len:u16][consumer])*
|
|
806
|
+
"""
|
|
807
|
+
if len(data) < 4:
|
|
808
|
+
return []
|
|
809
|
+
|
|
810
|
+
pos = 0
|
|
811
|
+
count = struct.unpack("<I", data[pos : pos + 4])[0]
|
|
812
|
+
pos += 4
|
|
813
|
+
|
|
814
|
+
entries: list[PendingEntry] = []
|
|
815
|
+
for _ in range(count):
|
|
816
|
+
if pos + 8 + 8 + 4 + 2 > len(data):
|
|
817
|
+
raise IncompleteResponseError("Pending entry: truncated header")
|
|
818
|
+
ts = struct.unpack("<Q", data[pos : pos + 8])[0]
|
|
819
|
+
pos += 8
|
|
820
|
+
seq = struct.unpack("<Q", data[pos : pos + 8])[0]
|
|
821
|
+
pos += 8
|
|
822
|
+
delivery_count = struct.unpack("<I", data[pos : pos + 4])[0]
|
|
823
|
+
pos += 4
|
|
824
|
+
clen = struct.unpack("<H", data[pos : pos + 2])[0]
|
|
825
|
+
pos += 2
|
|
826
|
+
if pos + clen > len(data):
|
|
827
|
+
raise IncompleteResponseError("Pending entry: truncated consumer")
|
|
828
|
+
consumer = data[pos : pos + clen].decode("utf-8")
|
|
829
|
+
pos += clen
|
|
830
|
+
entries.append(
|
|
831
|
+
PendingEntry(
|
|
832
|
+
id=StreamID(timestamp_ms=ts, sequence=seq),
|
|
833
|
+
consumer=consumer,
|
|
834
|
+
delivery_count=delivery_count,
|
|
835
|
+
)
|
|
836
|
+
)
|
|
837
|
+
return entries
|
|
838
|
+
|
|
839
|
+
|
|
700
840
|
# =============================================================================
|
|
701
841
|
# Action/Worker Value Serialization
|
|
702
842
|
# =============================================================================
|
|
@@ -760,7 +900,8 @@ def serialize_action_invoke_value(
|
|
|
760
900
|
"""Serialize action invoke value.
|
|
761
901
|
|
|
762
902
|
Format: [priority:u8][delay_ms:i64][has_caller:u8]
|
|
763
|
-
[has_idempotency_key:u8][key_len:u16]?[key]?
|
|
903
|
+
[has_idempotency_key:u8][key_len:u16]?[key]?
|
|
904
|
+
[has_labels:u8][labels_len:u16]?[labels]?[input...]
|
|
764
905
|
"""
|
|
765
906
|
result = bytearray()
|
|
766
907
|
|
|
@@ -782,6 +923,9 @@ def serialize_action_invoke_value(
|
|
|
782
923
|
else:
|
|
783
924
|
result.append(0)
|
|
784
925
|
|
|
926
|
+
# labels (none)
|
|
927
|
+
result.append(0)
|
|
928
|
+
|
|
785
929
|
# input
|
|
786
930
|
result.extend(input_data)
|
|
787
931
|
|
|
@@ -945,7 +1089,9 @@ def parse_task_assignment(data: bytes) -> "types.TaskAssignment":
|
|
|
945
1089
|
"""Parse task assignment from server response.
|
|
946
1090
|
|
|
947
1091
|
Format: [task_id_len:u16][task_id][task_type_len:u16][task_type]
|
|
948
|
-
[created_at:i64][attempt:u32]
|
|
1092
|
+
[created_at:i64][attempt:u32]
|
|
1093
|
+
[has_caller:u8]([run_id_len:u16][run_id][wf_name_len:u16][wf_name])?
|
|
1094
|
+
[payload...]
|
|
949
1095
|
"""
|
|
950
1096
|
if len(data) < 10:
|
|
951
1097
|
raise ValueError("Incomplete task assignment response")
|
|
@@ -982,6 +1128,32 @@ def parse_task_assignment(data: bytes) -> "types.TaskAssignment":
|
|
|
982
1128
|
attempt = struct.unpack_from("<I", data, pos)[0]
|
|
983
1129
|
pos += 4
|
|
984
1130
|
|
|
1131
|
+
# caller block: [has_caller:u8][run_id_len:u16][run_id][wf_name_len:u16][wf_name]
|
|
1132
|
+
if pos + 1 > len(data):
|
|
1133
|
+
raise ValueError("Incomplete task assignment: missing has_caller")
|
|
1134
|
+
has_caller = data[pos]
|
|
1135
|
+
pos += 1
|
|
1136
|
+
caller_run_id = ""
|
|
1137
|
+
caller_workflow_name = ""
|
|
1138
|
+
if has_caller == 1:
|
|
1139
|
+
if pos + 2 > len(data):
|
|
1140
|
+
raise ValueError("Incomplete task assignment: missing caller_run_id length")
|
|
1141
|
+
crid_len = struct.unpack_from("<H", data, pos)[0]
|
|
1142
|
+
pos += 2
|
|
1143
|
+
if pos + crid_len > len(data):
|
|
1144
|
+
raise ValueError("Incomplete task assignment: missing caller_run_id")
|
|
1145
|
+
caller_run_id = data[pos : pos + crid_len].decode("utf-8")
|
|
1146
|
+
pos += crid_len
|
|
1147
|
+
|
|
1148
|
+
if pos + 2 > len(data):
|
|
1149
|
+
raise ValueError("Incomplete task assignment: missing caller_workflow_name length")
|
|
1150
|
+
cwf_len = struct.unpack_from("<H", data, pos)[0]
|
|
1151
|
+
pos += 2
|
|
1152
|
+
if pos + cwf_len > len(data):
|
|
1153
|
+
raise ValueError("Incomplete task assignment: missing caller_workflow_name")
|
|
1154
|
+
caller_workflow_name = data[pos : pos + cwf_len].decode("utf-8")
|
|
1155
|
+
pos += cwf_len
|
|
1156
|
+
|
|
985
1157
|
# payload (rest of data)
|
|
986
1158
|
payload = data[pos:]
|
|
987
1159
|
|
|
@@ -991,4 +1163,6 @@ def parse_task_assignment(data: bytes) -> "types.TaskAssignment":
|
|
|
991
1163
|
payload=payload,
|
|
992
1164
|
created_at=created_at,
|
|
993
1165
|
attempt=attempt,
|
|
1166
|
+
caller_run_id=caller_run_id,
|
|
1167
|
+
caller_workflow_name=caller_workflow_name,
|
|
994
1168
|
)
|