Skip to content

Commit 8682e0f

Browse files
committed
typed methods for state
1 parent 4723e2b commit 8682e0f

1 file changed

Lines changed: 95 additions & 4 deletions

File tree

vgi/function.py

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import os
2727
import uuid
2828
from abc import ABC, abstractmethod
29+
from collections.abc import Sequence
2930
from dataclasses import dataclass
3031
from functools import cached_property
3132
from typing import (
@@ -407,11 +408,13 @@ def finalize(self) -> OutputGenerator:
407408
return [state_class.deserialize(data) for data in state_bytes_list]
408409

409410
def enqueue_work(self, work_items: list[bytes]) -> int:
410-
"""Add work items to the queue for this invocation.
411+
"""Add work items to the queue for this invocation (low-level bytes API).
411412
412413
Call this during initialization (initialize_global_state or setup) to populate
413414
the work queue that workers will pull from during process().
414415
416+
For a typed alternative, see enqueue_work_items().
417+
415418
Args:
416419
work_items: List of opaque bytes representing work items.
417420
The function is responsible for serializing/deserializing
@@ -421,7 +424,7 @@ def enqueue_work(self, work_items: list[bytes]) -> int:
421424
Number of items enqueued.
422425
423426
Raises:
424-
ValueError: If execution_identifier has not been set.
427+
ExecutionIdentifierError: If execution_identifier has not been set.
425428
426429
Example:
427430
def initialize_global_state(self, init_input: pa.RecordBatch) -> InitResult:
@@ -439,20 +442,59 @@ def initialize_global_state(self, init_input: pa.RecordBatch) -> InitResult:
439442
)
440443
return self.storage.queue_push(self.execution_identifier, work_items)
441444

445+
def enqueue_work_items(self, work_items: Sequence[Serializable]) -> int:
446+
"""Add typed work items to the queue for this invocation.
447+
448+
This is a typed convenience method that handles serialization automatically.
449+
Work items must implement the Serializable protocol.
450+
451+
Args:
452+
work_items: List of Serializable objects to enqueue.
453+
454+
Returns:
455+
Number of items enqueued.
456+
457+
Raises:
458+
ExecutionIdentifierError: If execution_identifier has not been set.
459+
460+
Example:
461+
@dataclass
462+
class FileRange:
463+
path: str
464+
start: int
465+
end: int
466+
467+
def serialize(self) -> bytes:
468+
return pickle.dumps((self.path, self.start, self.end))
469+
470+
@classmethod
471+
def deserialize(cls, data: bytes) -> Self:
472+
path, start, end = pickle.loads(data)
473+
return cls(path, start, end)
474+
475+
def setup(self):
476+
ranges = [FileRange("a.csv", 0, 1000), FileRange("b.csv", 0, 500)]
477+
self.enqueue_work_items(ranges)
478+
479+
"""
480+
return self.enqueue_work([item.serialize() for item in work_items])
481+
442482
def dequeue_work(self) -> bytes | None:
443-
"""Claim and return the next work item from the queue.
483+
"""Claim and return the next work item from the queue (low-level bytes API).
444484
445485
Each call atomically claims one item from the queue. Returns None
446486
when the queue is empty (all work has been claimed).
447487
448488
Multiple workers can safely call this concurrently - each item
449489
will be returned to exactly one worker.
450490
491+
For a typed alternative, see dequeue_work_item().
492+
451493
Returns:
452494
Opaque bytes representing a work item, or None if queue is empty.
453495
454496
Raises:
455-
ValueError: If execution_identifier has not been set.
497+
ExecutionIdentifierError: If execution_identifier has not been set.
456498
457499
Example:
458500
def process(self) -> OutputGenerator:
@@ -472,6 +514,55 @@ def process(self) -> OutputGenerator:
472514
)
473515
return self.storage.queue_pop(self.execution_identifier)
474516

517+
def dequeue_work_item(self, item_class: type[StateT]) -> StateT | None:
518+
"""Claim and deserialize the next work item from the queue.
519+
520+
This is a typed convenience method that handles deserialization automatically.
521+
The item_class must implement the Serializable protocol (have a deserialize
522+
classmethod).
523+
524+
Each call atomically claims one item from the queue. Returns None
525+
when the queue is empty (all work has been claimed).
526+
527+
Multiple workers can safely call this concurrently - each item
528+
will be returned to exactly one worker.
529+
530+
Args:
531+
item_class: The class to use for deserializing the work item.
532+
Must have a deserialize(bytes) classmethod.
533+
534+
Returns:
535+
Deserialized work item, or None if queue is empty.
536+
537+
Raises:
538+
ExecutionIdentifierError: If execution_identifier has not been set.
539+
540+
Example:
541+
@dataclass
542+
class FileRange:
543+
path: str
544+
start: int
545+
end: int
546+
547+
def serialize(self) -> bytes:
548+
return pickle.dumps((self.path, self.start, self.end))
549+
550+
@classmethod
551+
def deserialize(cls, data: bytes) -> Self:
552+
path, start, end = pickle.loads(data)
553+
return cls(path, start, end)
554+
555+
def process(self) -> OutputGenerator:
556+
while item := self.dequeue_work_item(FileRange):
557+
# item is FileRange, fully typed
558+
yield Output(self.process_range(item.path, item.start, item.end))
559+
560+
"""
561+
data = self.dequeue_work()
562+
if data is None:
563+
return None
564+
return item_class.deserialize(data)
565+
475566
@final
476567
@cached_property
477568
def empty_output_batch(self) -> pa.RecordBatch:

0 commit comments

Comments
 (0)