1717from typing_extensions import TypeAlias
1818
1919from quixstreams .context import message_context
20- from quixstreams .core .stream import TransformExpandedCallback
20+ from quixstreams .core .stream import (
21+ Stream ,
22+ TransformExpandedCallback ,
23+ TransformFunction ,
24+ )
2125from quixstreams .core .stream .exceptions import InvalidOperation
2226from quixstreams .models .topics .manager import TopicManager
2327from quixstreams .state import WindowedPartitionTransaction
4246 Iterable [Message ],
4347]
4448
49+ WallClockCallback = Callable [[WindowedPartitionTransaction ], Iterable [Message ]]
50+
4551
4652class Window (abc .ABC ):
4753 def __init__ (
@@ -69,6 +75,13 @@ def process_window(
6975 ) -> tuple [Iterable [WindowKeyResult ], Iterable [WindowKeyResult ]]:
7076 pass
7177
78+ @abstractmethod
79+ def process_wall_clock (
80+ self ,
81+ transaction : WindowedPartitionTransaction ,
82+ ) -> Iterable [WindowKeyResult ]:
83+ pass
84+
7285 def register_store (self ) -> None :
7386 TopicManager .ensure_topics_copartitioned (* self ._dataframe .topics )
7487 # Create a config for the changelog topic based on the underlying SDF topics
@@ -83,6 +96,7 @@ def _apply_window(
8396 self ,
8497 func : TransformRecordCallbackExpandedWindowed ,
8598 name : str ,
99+ wall_clock_func : WallClockCallback ,
86100 ) -> "StreamingDataFrame" :
87101 self .register_store ()
88102
@@ -92,12 +106,24 @@ def _apply_window(
92106 processing_context = self ._dataframe .processing_context ,
93107 store_name = name ,
94108 )
109+ wall_clock_transform_func = _as_wall_clock (
110+ func = wall_clock_func ,
111+ stream_id = self ._dataframe .stream_id ,
112+ processing_context = self ._dataframe .processing_context ,
113+ store_name = name ,
114+ )
95115 # Manually modify the Stream and clone the source StreamingDataFrame
96116 # to avoid adding "transform" API to it.
97117 # Transform callbacks can modify record key and timestamp,
98118 # and it's prone to misuse.
99- stream = self ._dataframe .stream .add_transform (func = windowed_func , expand = True )
100- return self ._dataframe .__dataframe_clone__ (stream = stream )
119+ windowed_stream = self ._dataframe .stream .add_transform (
120+ func = windowed_func , expand = True
121+ )
122+ wall_clock_stream = Stream (
123+ func = TransformFunction (wall_clock_transform_func , expand = True )
124+ )
125+ sdf = self ._dataframe .__dataframe_clone__ (stream = windowed_stream )
126+ return sdf .concat_wall_clock (wall_clock_stream )
101127
102128 def final (self ) -> "StreamingDataFrame" :
103129 """
@@ -140,9 +166,17 @@ def window_callback(
140166 for key , window in expired_windows :
141167 yield (window , key , window ["start" ], None )
142168
169+ def wall_clock_callback (
170+ transaction : WindowedPartitionTransaction ,
171+ ) -> Iterable [Message ]:
172+ # TODO: Check if this will work for sliding windows
173+ for key , window in self .process_wall_clock (transaction ):
174+ yield (window , key , window ["start" ], None )
175+
143176 return self ._apply_window (
144177 func = window_callback ,
145178 name = self ._name ,
179+ wall_clock_func = wall_clock_callback ,
146180 )
147181
148182 def current (self ) -> "StreamingDataFrame" :
@@ -188,7 +222,17 @@ def window_callback(
188222 for key , window in updated_windows :
189223 yield (window , key , window ["start" ], None )
190224
191- return self ._apply_window (func = window_callback , name = self ._name )
225+ def wall_clock_callback (
226+ transaction : WindowedPartitionTransaction ,
227+ ) -> Iterable [Message ]:
228+ # TODO: Implement wall_clock callback
229+ return []
230+
231+ return self ._apply_window (
232+ func = window_callback ,
233+ name = self ._name ,
234+ wall_clock_func = wall_clock_callback ,
235+ )
192236
193237 # Implemented by SingleAggregationWindowMixin and MultiAggregationWindowMixin
194238 # Single aggregation and multi aggregation windows store aggregations and collections
@@ -424,6 +468,28 @@ def wrapper(
424468 return wrapper
425469
426470
471+ def _as_wall_clock (
472+ func : WallClockCallback ,
473+ processing_context : "ProcessingContext" ,
474+ store_name : str ,
475+ stream_id : str ,
476+ ) -> TransformExpandedCallback :
477+ @functools .wraps (func )
478+ def wrapper (
479+ value : Any , key : Any , timestamp : int , headers : Any
480+ ) -> Iterable [Message ]:
481+ ctx = message_context ()
482+ transaction = cast (
483+ WindowedPartitionTransaction ,
484+ processing_context .checkpoint .get_store_transaction (
485+ stream_id = stream_id , partition = ctx .partition , store_name = store_name
486+ ),
487+ )
488+ return func (transaction )
489+
490+ return wrapper
491+
492+
427493class WindowOnLateCallback (Protocol ):
428494 def __call__ (
429495 self ,
0 commit comments