2626import os
2727import uuid
2828from abc import ABC , abstractmethod
29+ from collections .abc import Sequence
2930from dataclasses import dataclass
3031from functools import cached_property
3132from 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