flo-python 0.1.0.dev2__py3-none-any.whl → 0.1.0.dev3__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 +80 -8
- flo/actions.py +44 -15
- flo/client.py +137 -16
- flo/exceptions.py +21 -0
- flo/kv.py +6 -6
- flo/processing.py +341 -0
- flo/streams.py +17 -16
- flo/types.py +440 -190
- flo/wire.py +107 -49
- flo/worker.py +610 -42
- flo/workflows.py +463 -0
- {flo_python-0.1.0.dev2.dist-info → flo_python-0.1.0.dev3.dist-info}/METADATA +29 -4
- flo_python-0.1.0.dev3.dist-info/RECORD +16 -0
- {flo_python-0.1.0.dev2.dist-info → flo_python-0.1.0.dev3.dist-info}/WHEEL +1 -1
- flo_python-0.1.0.dev2.dist-info/RECORD +0 -14
- {flo_python-0.1.0.dev2.dist-info → flo_python-0.1.0.dev3.dist-info}/licenses/LICENSE +0 -0
flo/wire.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Flo Wire Protocol
|
|
2
2
|
|
|
3
3
|
Binary serialization/deserialization for the Flo protocol.
|
|
4
|
-
Header:
|
|
4
|
+
Header: 32 bytes, little-endian, CRC32 validated.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import struct
|
|
@@ -34,16 +34,22 @@ from .types import (
|
|
|
34
34
|
StatusCode,
|
|
35
35
|
StorageTier,
|
|
36
36
|
StreamAppendResult,
|
|
37
|
+
StreamID,
|
|
37
38
|
StreamInfo,
|
|
38
39
|
StreamReadResult,
|
|
39
40
|
StreamRecord,
|
|
40
41
|
VersionEntry,
|
|
41
42
|
)
|
|
42
43
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
# Request header (32 bytes):
|
|
45
|
+
# magic(u32) + payload_len(u32) + request_id(u64)
|
|
46
|
+
# + crc32(u32) + op_code(u16) + version(u8) + flags(u8) + reserved(8s)
|
|
47
|
+
REQUEST_HEADER_FORMAT = "<IIQIHBB8s"
|
|
48
|
+
|
|
49
|
+
# Response header (32 bytes):
|
|
50
|
+
# magic(u32) + data_len(u32) + request_id(u64)
|
|
51
|
+
# + crc32(u32) + version(u8) + status(u8) + flags(u8) + _pad(u8) + reserved(8s)
|
|
52
|
+
RESPONSE_HEADER_FORMAT = "<IIQIBBBB8s"
|
|
47
53
|
|
|
48
54
|
|
|
49
55
|
# =============================================================================
|
|
@@ -172,11 +178,11 @@ def compute_crc32(header_bytes: bytes, payload: bytes) -> int:
|
|
|
172
178
|
|
|
173
179
|
The CRC32 is computed over:
|
|
174
180
|
- Header bytes 0-15 (magic, payload_length, request_id)
|
|
175
|
-
- Header bytes 20-
|
|
181
|
+
- Header bytes 20-31 (op_code/version/flags/reserved or version/status/flags/pad/reserved)
|
|
176
182
|
- Payload bytes
|
|
177
183
|
"""
|
|
178
|
-
# Hash bytes 0-15 and 20-
|
|
179
|
-
crc_data = header_bytes[0:16] + header_bytes[20:
|
|
184
|
+
# Hash bytes 0-15 and 20-31 of header (skip crc32 field at 16-19)
|
|
185
|
+
crc_data = header_bytes[0:16] + header_bytes[20:32] + payload
|
|
180
186
|
return crc32(crc_data) & 0xFFFFFFFF
|
|
181
187
|
|
|
182
188
|
|
|
@@ -244,10 +250,10 @@ def serialize_request(
|
|
|
244
250
|
len(payload_bytes),
|
|
245
251
|
request_id,
|
|
246
252
|
0, # CRC32 placeholder
|
|
247
|
-
VERSION,
|
|
248
253
|
op_code,
|
|
254
|
+
VERSION,
|
|
249
255
|
0, # flags
|
|
250
|
-
|
|
256
|
+
b"\x00" * 8, # reserved
|
|
251
257
|
)
|
|
252
258
|
|
|
253
259
|
# Compute CRC32
|
|
@@ -260,10 +266,10 @@ def serialize_request(
|
|
|
260
266
|
len(payload_bytes),
|
|
261
267
|
request_id,
|
|
262
268
|
crc,
|
|
263
|
-
VERSION,
|
|
264
269
|
op_code,
|
|
270
|
+
VERSION,
|
|
265
271
|
0, # flags
|
|
266
|
-
|
|
272
|
+
b"\x00" * 8, # reserved
|
|
267
273
|
)
|
|
268
274
|
|
|
269
275
|
return header + payload_bytes
|
|
@@ -309,7 +315,7 @@ def parse_response_header(data: bytes) -> tuple[StatusCode, int, int, int]:
|
|
|
309
315
|
if len(data) < HEADER_SIZE:
|
|
310
316
|
raise IncompleteResponseError(f"Response too short: {len(data)} < {HEADER_SIZE}")
|
|
311
317
|
|
|
312
|
-
magic, data_len, request_id, crc, version, status, flags, reserved = struct.unpack(
|
|
318
|
+
magic, data_len, request_id, crc, version, status, flags, _pad, reserved = struct.unpack(
|
|
313
319
|
RESPONSE_HEADER_FORMAT, data[:HEADER_SIZE]
|
|
314
320
|
)
|
|
315
321
|
|
|
@@ -538,7 +544,7 @@ def parse_stream_append_response(data: bytes) -> StreamAppendResult:
|
|
|
538
544
|
sequence = struct.unpack("<Q", data[0:8])[0]
|
|
539
545
|
timestamp_ms = struct.unpack("<q", data[8:16])[0]
|
|
540
546
|
|
|
541
|
-
return StreamAppendResult(
|
|
547
|
+
return StreamAppendResult(id=StreamID(timestamp_ms=timestamp_ms, sequence=sequence))
|
|
542
548
|
|
|
543
549
|
|
|
544
550
|
def parse_stream_read_response(data: bytes) -> StreamReadResult:
|
|
@@ -619,8 +625,7 @@ def parse_stream_read_response(data: bytes) -> StreamReadResult:
|
|
|
619
625
|
|
|
620
626
|
records.append(
|
|
621
627
|
StreamRecord(
|
|
622
|
-
sequence=sequence,
|
|
623
|
-
timestamp_ms=timestamp_ms,
|
|
628
|
+
id=StreamID(timestamp_ms=timestamp_ms, sequence=sequence),
|
|
624
629
|
tier=tier,
|
|
625
630
|
payload=payload,
|
|
626
631
|
headers=None,
|
|
@@ -633,20 +638,23 @@ def parse_stream_read_response(data: bytes) -> StreamReadResult:
|
|
|
633
638
|
def parse_stream_info_response(data: bytes) -> StreamInfo:
|
|
634
639
|
"""Parse stream info response data.
|
|
635
640
|
|
|
636
|
-
Format: [
|
|
641
|
+
Format: [first_ts:u64][first_seq:u64][last_ts:u64][last_seq:u64]
|
|
642
|
+
[count:u64][bytes:u64][partition_count:u32]
|
|
637
643
|
"""
|
|
638
|
-
if len(data) <
|
|
644
|
+
if len(data) < 52:
|
|
639
645
|
raise IncompleteResponseError("Stream info response too short")
|
|
640
646
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
647
|
+
first_ts = struct.unpack("<Q", data[0:8])[0]
|
|
648
|
+
first_seq = struct.unpack("<Q", data[8:16])[0]
|
|
649
|
+
last_ts = struct.unpack("<Q", data[16:24])[0]
|
|
650
|
+
last_seq = struct.unpack("<Q", data[24:32])[0]
|
|
651
|
+
count = struct.unpack("<Q", data[32:40])[0]
|
|
652
|
+
bytes_size = struct.unpack("<Q", data[40:48])[0]
|
|
653
|
+
partition_count = struct.unpack("<I", data[48:52])[0]
|
|
646
654
|
|
|
647
655
|
return StreamInfo(
|
|
648
|
-
|
|
649
|
-
|
|
656
|
+
first_id=StreamID(timestamp_ms=first_ts, sequence=first_seq),
|
|
657
|
+
last_id=StreamID(timestamp_ms=last_ts, sequence=last_seq),
|
|
650
658
|
count=count,
|
|
651
659
|
bytes_size=bytes_size,
|
|
652
660
|
partition_count=partition_count,
|
|
@@ -669,10 +677,10 @@ def serialize_group_value(group: str, consumer: str) -> bytes:
|
|
|
669
677
|
return bytes(result)
|
|
670
678
|
|
|
671
679
|
|
|
672
|
-
def serialize_group_ack_value(group: str,
|
|
673
|
-
"""Serialize group name, consumer, and
|
|
680
|
+
def serialize_group_ack_value(group: str, ids: list[StreamID], consumer: str = "") -> bytes:
|
|
681
|
+
"""Serialize group name, consumer, and StreamIDs for group ack/nack.
|
|
674
682
|
|
|
675
|
-
Format: [group_len:u16][group][consumer_len:u16][consumer][count:u32][seq:u64]*
|
|
683
|
+
Format: [group_len:u16][group][consumer_len:u16][consumer][count:u32][ts:u64][seq:u64]*
|
|
676
684
|
"""
|
|
677
685
|
group_bytes = group.encode("utf-8")
|
|
678
686
|
consumer_bytes = consumer.encode("utf-8")
|
|
@@ -682,9 +690,10 @@ def serialize_group_ack_value(group: str, seqs: list[int], consumer: str = "") -
|
|
|
682
690
|
result.extend(group_bytes)
|
|
683
691
|
result.extend(struct.pack("<H", len(consumer_bytes)))
|
|
684
692
|
result.extend(consumer_bytes)
|
|
685
|
-
result.extend(struct.pack("<I", len(
|
|
686
|
-
for
|
|
687
|
-
result.extend(struct.pack("<Q",
|
|
693
|
+
result.extend(struct.pack("<I", len(ids)))
|
|
694
|
+
for sid in ids:
|
|
695
|
+
result.extend(struct.pack("<Q", sid.timestamp_ms))
|
|
696
|
+
result.extend(struct.pack("<Q", sid.sequence))
|
|
688
697
|
return bytes(result)
|
|
689
698
|
|
|
690
699
|
|
|
@@ -703,7 +712,7 @@ def serialize_action_register_value(
|
|
|
703
712
|
|
|
704
713
|
Format: [action_type:u8][timeout_ms:u32][max_retries:u32]
|
|
705
714
|
[has_desc:u8][desc_len:u16]?[desc]?
|
|
706
|
-
[has_wasm_module:u8]...(
|
|
715
|
+
[has_wasm_module:u8=0]...(reserved zero fields for wire compat)
|
|
707
716
|
"""
|
|
708
717
|
result = bytearray()
|
|
709
718
|
|
|
@@ -725,13 +734,13 @@ def serialize_action_register_value(
|
|
|
725
734
|
else:
|
|
726
735
|
result.append(0)
|
|
727
736
|
|
|
728
|
-
# wasm_module (
|
|
737
|
+
# wasm_module (reserved, always zero)
|
|
729
738
|
result.append(0)
|
|
730
739
|
|
|
731
|
-
# wasm_entrypoint (
|
|
740
|
+
# wasm_entrypoint (reserved, always zero)
|
|
732
741
|
result.append(0)
|
|
733
742
|
|
|
734
|
-
# wasm_memory_limit (
|
|
743
|
+
# wasm_memory_limit (reserved, always zero)
|
|
735
744
|
result.append(0)
|
|
736
745
|
|
|
737
746
|
# trigger_stream (optional, not used)
|
|
@@ -787,24 +796,55 @@ def serialize_action_list_value(limit: int = 100) -> bytes:
|
|
|
787
796
|
return struct.pack("<I", limit)
|
|
788
797
|
|
|
789
798
|
|
|
790
|
-
def serialize_worker_register_value(
|
|
799
|
+
def serialize_worker_register_value(
|
|
800
|
+
task_types: list[str],
|
|
801
|
+
*,
|
|
802
|
+
worker_type: int = 0,
|
|
803
|
+
max_concurrency: int = 10,
|
|
804
|
+
metadata: str | None = None,
|
|
805
|
+
machine_id: str | None = None,
|
|
806
|
+
) -> bytes:
|
|
791
807
|
"""Serialize worker register value.
|
|
792
808
|
|
|
793
|
-
|
|
809
|
+
Server format:
|
|
810
|
+
[type:u8][max_concurrency:u32][process_count:u16]
|
|
811
|
+
([name_len:u16][name][kind:u8])*
|
|
812
|
+
[has_metadata:u8]([metadata_len:u16][metadata])?
|
|
813
|
+
[has_machine_id:u8]([machine_id_len:u16][machine_id])?
|
|
794
814
|
"""
|
|
795
815
|
result = bytearray()
|
|
796
816
|
|
|
797
|
-
#
|
|
798
|
-
result.
|
|
817
|
+
# worker type: 0=action, 1=stream
|
|
818
|
+
result.append(worker_type & 0xFF)
|
|
799
819
|
|
|
800
|
-
#
|
|
820
|
+
# max_concurrency
|
|
821
|
+
result.extend(struct.pack("<I", max_concurrency))
|
|
822
|
+
|
|
823
|
+
# process_count + process entries
|
|
824
|
+
result.extend(struct.pack("<H", len(task_types)))
|
|
801
825
|
for tt in task_types:
|
|
802
826
|
tt_bytes = tt.encode("utf-8")
|
|
803
827
|
result.extend(struct.pack("<H", len(tt_bytes)))
|
|
804
828
|
result.extend(tt_bytes)
|
|
829
|
+
result.append(worker_type & 0xFF) # kind matches worker type
|
|
805
830
|
|
|
806
|
-
#
|
|
807
|
-
|
|
831
|
+
# optional metadata
|
|
832
|
+
if metadata:
|
|
833
|
+
result.append(1)
|
|
834
|
+
meta_bytes = metadata.encode("utf-8")
|
|
835
|
+
result.extend(struct.pack("<H", len(meta_bytes)))
|
|
836
|
+
result.extend(meta_bytes)
|
|
837
|
+
else:
|
|
838
|
+
result.append(0)
|
|
839
|
+
|
|
840
|
+
# optional machine_id
|
|
841
|
+
if machine_id:
|
|
842
|
+
result.append(1)
|
|
843
|
+
mid_bytes = machine_id.encode("utf-8")
|
|
844
|
+
result.extend(struct.pack("<H", len(mid_bytes)))
|
|
845
|
+
result.extend(mid_bytes)
|
|
846
|
+
else:
|
|
847
|
+
result.append(0)
|
|
808
848
|
|
|
809
849
|
return bytes(result)
|
|
810
850
|
|
|
@@ -828,14 +868,17 @@ def serialize_worker_await_value(task_types: list[str]) -> bytes:
|
|
|
828
868
|
return bytes(result)
|
|
829
869
|
|
|
830
870
|
|
|
831
|
-
def serialize_worker_touch_value(task_id: str, extend_ms: int = 30000) -> bytes:
|
|
871
|
+
def serialize_worker_touch_value(action_name: str, task_id: str, extend_ms: int = 30000) -> bytes:
|
|
832
872
|
"""Serialize worker touch value.
|
|
833
873
|
|
|
834
|
-
Format: [task_id_len:u16][task_id][extend_ms:u32]
|
|
874
|
+
Format: [action_name_len:u16][action_name][task_id_len:u16][task_id][extend_ms:u32]
|
|
835
875
|
"""
|
|
876
|
+
action_bytes = action_name.encode("utf-8")
|
|
836
877
|
task_id_bytes = task_id.encode("utf-8")
|
|
837
878
|
|
|
838
879
|
result = bytearray()
|
|
880
|
+
result.extend(struct.pack("<H", len(action_bytes)))
|
|
881
|
+
result.extend(action_bytes)
|
|
839
882
|
result.extend(struct.pack("<H", len(task_id_bytes)))
|
|
840
883
|
result.extend(task_id_bytes)
|
|
841
884
|
result.extend(struct.pack("<I", extend_ms))
|
|
@@ -843,33 +886,48 @@ def serialize_worker_touch_value(task_id: str, extend_ms: int = 30000) -> bytes:
|
|
|
843
886
|
return bytes(result)
|
|
844
887
|
|
|
845
888
|
|
|
846
|
-
def serialize_worker_complete_value(
|
|
889
|
+
def serialize_worker_complete_value(
|
|
890
|
+
action_name: str, task_id: str, result_data: bytes, outcome: str = "success"
|
|
891
|
+
) -> bytes:
|
|
847
892
|
"""Serialize worker complete value.
|
|
848
893
|
|
|
849
|
-
Format: [task_id_len:u16][task_id]
|
|
894
|
+
Format: [action_name_len:u16][action_name][task_id_len:u16][task_id]
|
|
895
|
+
[outcome_len:u16][outcome][result_len:u16][result]
|
|
850
896
|
"""
|
|
897
|
+
action_bytes = action_name.encode("utf-8")
|
|
851
898
|
task_id_bytes = task_id.encode("utf-8")
|
|
899
|
+
outcome_bytes = outcome.encode("utf-8")
|
|
852
900
|
|
|
853
901
|
result = bytearray()
|
|
902
|
+
result.extend(struct.pack("<H", len(action_bytes)))
|
|
903
|
+
result.extend(action_bytes)
|
|
854
904
|
result.extend(struct.pack("<H", len(task_id_bytes)))
|
|
855
905
|
result.extend(task_id_bytes)
|
|
906
|
+
result.extend(struct.pack("<H", len(outcome_bytes)))
|
|
907
|
+
result.extend(outcome_bytes)
|
|
908
|
+
result.extend(struct.pack("<H", len(result_data)))
|
|
856
909
|
result.extend(result_data)
|
|
857
910
|
|
|
858
911
|
return bytes(result)
|
|
859
912
|
|
|
860
913
|
|
|
861
|
-
def serialize_worker_fail_value(
|
|
914
|
+
def serialize_worker_fail_value(
|
|
915
|
+
action_name: str, task_id: str, error_message: str, retry: bool = True
|
|
916
|
+
) -> bytes:
|
|
862
917
|
"""Serialize worker fail value.
|
|
863
918
|
|
|
864
|
-
Format: [task_id_len:u16][task_id][error_message...]
|
|
865
|
-
Note: retry flag is handled via TLV options, not in payload (matches Go SDK).
|
|
919
|
+
Format: [action_name_len:u16][action_name][task_id_len:u16][task_id][retry:u8][error_message...]
|
|
866
920
|
"""
|
|
921
|
+
action_bytes = action_name.encode("utf-8")
|
|
867
922
|
task_id_bytes = task_id.encode("utf-8")
|
|
868
923
|
error_bytes = error_message.encode("utf-8")
|
|
869
924
|
|
|
870
925
|
result = bytearray()
|
|
926
|
+
result.extend(struct.pack("<H", len(action_bytes)))
|
|
927
|
+
result.extend(action_bytes)
|
|
871
928
|
result.extend(struct.pack("<H", len(task_id_bytes)))
|
|
872
929
|
result.extend(task_id_bytes)
|
|
930
|
+
result.extend(struct.pack("<B", 1 if retry else 0))
|
|
873
931
|
result.extend(error_bytes)
|
|
874
932
|
|
|
875
933
|
return bytes(result)
|