17
17
from typing_extensions import TypeAlias
18
18
19
19
from 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
+ TransformWallClockExpandedCallback ,
25
+ )
21
26
from quixstreams .core .stream .exceptions import InvalidOperation
22
27
from quixstreams .models .topics .manager import TopicManager
23
28
from quixstreams .state import WindowedPartitionTransaction
42
47
Iterable [Message ],
43
48
]
44
49
50
+ WallClockCallback = Callable [[int , WindowedPartitionTransaction ], Iterable [Message ]]
51
+
45
52
46
53
class Window (abc .ABC ):
47
54
def __init__ (
@@ -69,6 +76,14 @@ def process_window(
69
76
) -> tuple [Iterable [WindowKeyResult ], Iterable [WindowKeyResult ]]:
70
77
pass
71
78
79
+ @abstractmethod
80
+ def process_wall_clock (
81
+ self ,
82
+ timestamp_ms : int ,
83
+ transaction : WindowedPartitionTransaction ,
84
+ ) -> Iterable [WindowKeyResult ]:
85
+ pass
86
+
72
87
def register_store (self ) -> None :
73
88
TopicManager .ensure_topics_copartitioned (* self ._dataframe .topics )
74
89
# Create a config for the changelog topic based on the underlying SDF topics
@@ -83,6 +98,7 @@ def _apply_window(
83
98
self ,
84
99
func : TransformRecordCallbackExpandedWindowed ,
85
100
name : str ,
101
+ wall_clock_func : WallClockCallback ,
86
102
) -> "StreamingDataFrame" :
87
103
self .register_store ()
88
104
@@ -92,12 +108,24 @@ def _apply_window(
92
108
processing_context = self ._dataframe .processing_context ,
93
109
store_name = name ,
94
110
)
111
+ wall_clock_transform_func = _as_wall_clock (
112
+ func = wall_clock_func ,
113
+ stream_id = self ._dataframe .stream_id ,
114
+ processing_context = self ._dataframe .processing_context ,
115
+ store_name = name ,
116
+ )
95
117
# Manually modify the Stream and clone the source StreamingDataFrame
96
118
# to avoid adding "transform" API to it.
97
119
# Transform callbacks can modify record key and timestamp,
98
120
# 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 )
121
+ windowed_stream = self ._dataframe .stream .add_transform (
122
+ func = windowed_func , expand = True
123
+ )
124
+ wall_clock_stream = Stream (
125
+ TransformFunction (wall_clock_transform_func , expand = True , wall_clock = True )
126
+ )
127
+ sdf = self ._dataframe .__dataframe_clone__ (stream = windowed_stream )
128
+ return sdf .concat_wall_clock (wall_clock_stream )
101
129
102
130
def final (self ) -> "StreamingDataFrame" :
103
131
"""
@@ -140,9 +168,17 @@ def window_callback(
140
168
for key , window in expired_windows :
141
169
yield (window , key , window ["start" ], None )
142
170
171
+ def wall_clock_callback (
172
+ timestamp : int , transaction : WindowedPartitionTransaction
173
+ ) -> Iterable [Message ]:
174
+ # TODO: Check if this will work for sliding windows
175
+ for key , window in self .process_wall_clock (timestamp , transaction ):
176
+ yield (window , key , window ["start" ], None )
177
+
143
178
return self ._apply_window (
144
179
func = window_callback ,
145
180
name = self ._name ,
181
+ wall_clock_func = wall_clock_callback ,
146
182
)
147
183
148
184
def current (self ) -> "StreamingDataFrame" :
@@ -188,7 +224,17 @@ def window_callback(
188
224
for key , window in updated_windows :
189
225
yield (window , key , window ["start" ], None )
190
226
191
- return self ._apply_window (func = window_callback , name = self ._name )
227
+ def wall_clock_callback (
228
+ timestamp : int , transaction : WindowedPartitionTransaction
229
+ ) -> Iterable [Message ]:
230
+ # TODO: Implement wall_clock callback
231
+ return []
232
+
233
+ return self ._apply_window (
234
+ func = window_callback ,
235
+ name = self ._name ,
236
+ wall_clock_func = wall_clock_callback ,
237
+ )
192
238
193
239
# Implemented by SingleAggregationWindowMixin and MultiAggregationWindowMixin
194
240
# Single aggregation and multi aggregation windows store aggregations and collections
@@ -424,6 +470,26 @@ def wrapper(
424
470
return wrapper
425
471
426
472
473
+ def _as_wall_clock (
474
+ func : WallClockCallback ,
475
+ processing_context : "ProcessingContext" ,
476
+ store_name : str ,
477
+ stream_id : str ,
478
+ ) -> TransformWallClockExpandedCallback :
479
+ @functools .wraps (func )
480
+ def wrapper (timestamp : int ) -> 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 (timestamp , transaction )
489
+
490
+ return wrapper
491
+
492
+
427
493
class WindowOnLateCallback (Protocol ):
428
494
def __call__ (
429
495
self ,
0 commit comments