calc-flow-python 4.0.0__cp313-abi3-win_amd64.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.
@@ -0,0 +1,1155 @@
1
+ """The row, ts, cs, window, table, and linalg declaration namespaces.
2
+
3
+ Every namespace function only constructs declaration nodes. Domain checks
4
+ run before node construction so a wrongly typed operand never produces a
5
+ wrongly typed expression.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from collections.abc import Sequence
12
+ from typing import TYPE_CHECKING, Literal
13
+
14
+ from calc_flow.symbolic.domains import (
15
+ is_strict_scalar_type,
16
+ namespace_error,
17
+ type_name,
18
+ )
19
+ from calc_flow.symbolic.expr import (
20
+ ArrayExpr,
21
+ ColumnExpr,
22
+ Parameter,
23
+ TableExpr,
24
+ )
25
+ from calc_flow.symbolic.nodes import (
26
+ CBool,
27
+ CDType,
28
+ CEnum,
29
+ CFloat,
30
+ CInt,
31
+ CMap,
32
+ CSeq,
33
+ CStr,
34
+ CValue,
35
+ Node,
36
+ build,
37
+ literal_value,
38
+ )
39
+ from calc_flow.symbolic.types import (
40
+ check_table_field_type,
41
+ require_int,
42
+ require_non_empty_str,
43
+ require_positive_int,
44
+ )
45
+ from calc_flow.symbolic.windows import (
46
+ CrossSectionGroup,
47
+ DurationFrame,
48
+ RowFrame,
49
+ )
50
+
51
+ if TYPE_CHECKING:
52
+ from calc_flow.pipeline import JoinStateLimits, JoinTimeBounds
53
+
54
+
55
+ def _column(value: object, function: str, parameter: str, /) -> ColumnExpr:
56
+ if not isinstance(value, ColumnExpr):
57
+ raise namespace_error(function, parameter, "ColumnExpr", value)
58
+ return value
59
+
60
+
61
+ def _table(value: object, function: str, parameter: str, /) -> TableExpr:
62
+ if not isinstance(value, TableExpr):
63
+ raise namespace_error(function, parameter, "TableExpr", value)
64
+ return value
65
+
66
+
67
+ def _array(value: object, function: str, parameter: str, /) -> ArrayExpr:
68
+ if not isinstance(value, ArrayExpr):
69
+ raise namespace_error(function, parameter, "ArrayExpr", value)
70
+ return value
71
+
72
+
73
+ def _column_operand(value: object, function: str, parameter: str, /) -> Node:
74
+ if isinstance(value, ColumnExpr):
75
+ return value._node
76
+ if is_strict_scalar_type(value):
77
+ return build("literal", (), {"value": literal_value(value)})
78
+ raise namespace_error(function, parameter, "ColumnOperand", value)
79
+
80
+
81
+ def _str_sequence(values: object, function: str, parameter: str, /) -> CSeq:
82
+ if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
83
+ raise namespace_error(function, parameter, "Sequence[str]", values)
84
+ return CSeq(
85
+ tuple(
86
+ CStr(
87
+ require_non_empty_str(
88
+ value, f"calc_flow.symbolic.{function}.{parameter}[{index}]"
89
+ )
90
+ )
91
+ for index, value in enumerate(values)
92
+ )
93
+ )
94
+
95
+
96
+ def _frame(value: object, function: str, /) -> CMap:
97
+ if isinstance(value, RowFrame):
98
+ return CMap.from_mapping(
99
+ {"frame": CEnum("frame", "rows"), "size": CInt(value.size)}
100
+ )
101
+ if isinstance(value, DurationFrame):
102
+ return CMap.from_mapping(
103
+ {"frame": CEnum("frame", "duration"), "micros": CInt(value.micros)}
104
+ )
105
+ raise namespace_error(function, "window", "RowFrame | DurationFrame", value)
106
+
107
+
108
+ def _min_periods(value: object, frame: object, function: str, /) -> int:
109
+ path = f"calc_flow.symbolic.{function}.min_periods"
110
+ periods = require_positive_int(value, path)
111
+ if isinstance(frame, RowFrame) and periods > frame.size:
112
+ raise ValueError(
113
+ f"{path}: invalid_literal: min_periods must not exceed the row-frame size"
114
+ )
115
+ return periods
116
+
117
+
118
+ def _min_samples(value: object, function: str, /) -> int:
119
+ return require_positive_int(value, f"calc_flow.symbolic.{function}.min_samples")
120
+
121
+
122
+ def _include_ties(value: object, function: str, /) -> bool:
123
+ path = f"calc_flow.symbolic.{function}.include_ties"
124
+ if type(value) is not bool:
125
+ raise TypeError(f"{path} must be a boolean; got {type_name(value)}")
126
+ return value
127
+
128
+
129
+ def _ddof(value: object, function: str, /) -> int:
130
+ path = f"calc_flow.symbolic.{function}.ddof"
131
+ ddof = require_int(value, path)
132
+ if ddof not in (0, 1):
133
+ raise ValueError(f"{path}: invalid_literal: ddof must be 0 or 1")
134
+ return ddof
135
+
136
+
137
+ def _enum_value(
138
+ value: object,
139
+ family: str,
140
+ variants: tuple[str, ...],
141
+ function: str,
142
+ parameter: str,
143
+ /,
144
+ ) -> CEnum:
145
+ path = f"calc_flow.symbolic.{function}.{parameter}"
146
+ if type(value) is not str:
147
+ raise TypeError(f"{path} must be a string; got {type_name(value)}")
148
+ if value not in variants:
149
+ raise ValueError(
150
+ f"{path}: invalid_literal: {parameter} must be one of"
151
+ f" {', '.join(repr(variant) for variant in variants)}"
152
+ )
153
+ return CEnum(family, value)
154
+
155
+
156
+ class RowNamespace:
157
+ """Row-local expressions over column operands."""
158
+
159
+ __slots__ = ()
160
+
161
+ def where(
162
+ self,
163
+ condition: ColumnExpr,
164
+ when_true: object,
165
+ when_false: object,
166
+ /,
167
+ ) -> ColumnExpr:
168
+ return ColumnExpr(
169
+ build(
170
+ "where",
171
+ (
172
+ _column(condition, "row.where", "condition")._node,
173
+ _column_operand(when_true, "row.where", "when_true"),
174
+ _column_operand(when_false, "row.where", "when_false"),
175
+ ),
176
+ {},
177
+ )
178
+ )
179
+
180
+ def coalesce(self, *values: object) -> ColumnExpr:
181
+ if not values:
182
+ raise ValueError(
183
+ "calc_flow.symbolic.row.coalesce.values: invalid_literal:"
184
+ " coalesce requires at least one value"
185
+ )
186
+ return ColumnExpr(
187
+ build(
188
+ "coalesce",
189
+ tuple(
190
+ _column_operand(value, "row.coalesce", f"values[{index}]")
191
+ for index, value in enumerate(values)
192
+ ),
193
+ {},
194
+ )
195
+ )
196
+
197
+ def log(self, value: object, /) -> ColumnExpr:
198
+ return ColumnExpr(
199
+ build(
200
+ "log",
201
+ (_column_operand(value, "row.log", "value"),),
202
+ {},
203
+ )
204
+ )
205
+
206
+ def exp(self, value: object, /) -> ColumnExpr:
207
+ return ColumnExpr(
208
+ build(
209
+ "exp",
210
+ (_column_operand(value, "row.exp", "value"),),
211
+ {},
212
+ )
213
+ )
214
+
215
+ def sqrt(self, value: object, /) -> ColumnExpr:
216
+ return ColumnExpr(
217
+ build(
218
+ "sqrt",
219
+ (_column_operand(value, "row.sqrt", "value"),),
220
+ {},
221
+ )
222
+ )
223
+
224
+ def abs(self, value: object, /) -> ColumnExpr:
225
+ return ColumnExpr(
226
+ build(
227
+ "abs",
228
+ (_column_operand(value, "row.abs", "value"),),
229
+ {},
230
+ )
231
+ )
232
+
233
+ def clip(
234
+ self,
235
+ value: object,
236
+ /,
237
+ *,
238
+ lower: object,
239
+ upper: object,
240
+ ) -> ColumnExpr:
241
+ function = "row.clip"
242
+ lower_value = literal_value(lower)
243
+ upper_value = literal_value(upper)
244
+ numeric = (CInt, CFloat)
245
+ if (
246
+ isinstance(lower_value, numeric)
247
+ and isinstance(upper_value, numeric)
248
+ and lower_value.value > upper_value.value
249
+ ):
250
+ raise ValueError(
251
+ f"calc_flow.symbolic.{function}.lower: invalid_literal:"
252
+ " clip lower bound must not exceed the upper bound"
253
+ )
254
+ return ColumnExpr(
255
+ build(
256
+ "clip",
257
+ (_column_operand(value, function, "value"),),
258
+ {"lower": lower_value, "upper": upper_value},
259
+ )
260
+ )
261
+
262
+ def cast(self, value: object, data_type: str, /) -> ColumnExpr:
263
+ path = "calc_flow.symbolic.row.cast.data_type"
264
+ dtype = require_non_empty_str(data_type, path)
265
+ check_table_field_type(dtype, path)
266
+ return ColumnExpr(
267
+ build(
268
+ "cast",
269
+ (_column_operand(value, "row.cast", "value"),),
270
+ {"data_type": CDType(dtype)},
271
+ )
272
+ )
273
+
274
+
275
+ class TsNamespace:
276
+ """Row-preserving temporal primitives."""
277
+
278
+ __slots__ = ()
279
+
280
+ def lag(self, value: ColumnExpr, /, *, periods: int = 1) -> ColumnExpr:
281
+ function = "ts.lag"
282
+ return ColumnExpr(
283
+ build(
284
+ "lag",
285
+ (_column(value, function, "value")._node,),
286
+ {
287
+ "periods": CInt(
288
+ require_positive_int(
289
+ periods, f"calc_flow.symbolic.{function}.periods"
290
+ )
291
+ )
292
+ },
293
+ )
294
+ )
295
+
296
+ def delta(self, value: ColumnExpr, /, *, periods: int = 1) -> ColumnExpr:
297
+ function = "ts.delta"
298
+ return ColumnExpr(
299
+ build(
300
+ "delta",
301
+ (_column(value, function, "value")._node,),
302
+ {
303
+ "periods": CInt(
304
+ require_positive_int(
305
+ periods, f"calc_flow.symbolic.{function}.periods"
306
+ )
307
+ )
308
+ },
309
+ )
310
+ )
311
+
312
+ def ewma(
313
+ self,
314
+ value: ColumnExpr,
315
+ /,
316
+ *,
317
+ span: int,
318
+ min_periods: int = 1,
319
+ ) -> ColumnExpr:
320
+ """Declare an unadjusted exponentially weighted moving average."""
321
+
322
+ function = "ts.ewma"
323
+ return ColumnExpr(
324
+ build(
325
+ "ewma",
326
+ (_column(value, function, "value")._node,),
327
+ {
328
+ "span": CInt(
329
+ require_positive_int(
330
+ span, f"calc_flow.symbolic.{function}.span"
331
+ )
332
+ ),
333
+ "min_periods": CInt(
334
+ require_positive_int(
335
+ min_periods,
336
+ f"calc_flow.symbolic.{function}.min_periods",
337
+ )
338
+ ),
339
+ },
340
+ )
341
+ )
342
+
343
+ def ema(
344
+ self,
345
+ value: ColumnExpr,
346
+ /,
347
+ *,
348
+ span: int,
349
+ min_periods: int = 1,
350
+ ) -> ColumnExpr:
351
+ """Alias :meth:`ewma` without introducing a second primitive."""
352
+
353
+ return self.ewma(value, span=span, min_periods=min_periods)
354
+
355
+ def macd(
356
+ self,
357
+ value: ColumnExpr,
358
+ /,
359
+ *,
360
+ fast_span: int = 12,
361
+ slow_span: int = 26,
362
+ min_periods: int = 1,
363
+ ) -> ColumnExpr:
364
+ """Declare MACD as the difference of two shared EWMA nodes."""
365
+
366
+ function = "ts.macd"
367
+ column = _column(value, function, "value")
368
+ fast = require_positive_int(
369
+ fast_span, f"calc_flow.symbolic.{function}.fast_span"
370
+ )
371
+ slow = require_positive_int(
372
+ slow_span, f"calc_flow.symbolic.{function}.slow_span"
373
+ )
374
+ periods = require_positive_int(
375
+ min_periods, f"calc_flow.symbolic.{function}.min_periods"
376
+ )
377
+ if fast >= slow:
378
+ raise ValueError(
379
+ f"calc_flow.symbolic.{function}.fast_span: invalid_literal:"
380
+ " fast_span must be less than slow_span"
381
+ )
382
+ return self.ewma(column, span=fast, min_periods=periods) - self.ewma(
383
+ column, span=slow, min_periods=periods
384
+ )
385
+
386
+ def _rolling(
387
+ self,
388
+ primitive: str,
389
+ value: ColumnExpr,
390
+ frame_value: object,
391
+ min_periods: object,
392
+ /,
393
+ ) -> ColumnExpr:
394
+ function = f"ts.{primitive}"
395
+ column = _column(value, function, "value")
396
+ frame = _frame(frame_value, function)
397
+ periods = _min_periods(min_periods, frame_value, function)
398
+ return ColumnExpr(
399
+ build(
400
+ primitive,
401
+ (column._node,),
402
+ {"frame": frame, "min_periods": CInt(periods)},
403
+ )
404
+ )
405
+
406
+ def _rolling_ddof(
407
+ self,
408
+ primitive: str,
409
+ value: ColumnExpr,
410
+ frame_value: object,
411
+ min_periods: object,
412
+ ddof: object,
413
+ /,
414
+ ) -> ColumnExpr:
415
+ function = f"ts.{primitive}"
416
+ column = _column(value, function, "value")
417
+ frame = _frame(frame_value, function)
418
+ periods = _min_periods(min_periods, frame_value, function)
419
+ return ColumnExpr(
420
+ build(
421
+ primitive,
422
+ (column._node,),
423
+ {
424
+ "frame": frame,
425
+ "min_periods": CInt(periods),
426
+ "ddof": CInt(_ddof(ddof, function)),
427
+ },
428
+ )
429
+ )
430
+
431
+ def count(
432
+ self,
433
+ value: ColumnExpr,
434
+ /,
435
+ *,
436
+ window: RowFrame | DurationFrame,
437
+ min_periods: int = 1,
438
+ ) -> ColumnExpr:
439
+ return self._rolling("count", value, window, min_periods)
440
+
441
+ def sum(
442
+ self,
443
+ value: ColumnExpr,
444
+ /,
445
+ *,
446
+ window: RowFrame | DurationFrame,
447
+ min_periods: int = 1,
448
+ ) -> ColumnExpr:
449
+ return self._rolling("sum", value, window, min_periods)
450
+
451
+ def mean(
452
+ self,
453
+ value: ColumnExpr,
454
+ /,
455
+ *,
456
+ window: RowFrame | DurationFrame,
457
+ min_periods: int = 1,
458
+ ) -> ColumnExpr:
459
+ return self._rolling("mean", value, window, min_periods)
460
+
461
+ def min(
462
+ self,
463
+ value: ColumnExpr,
464
+ /,
465
+ *,
466
+ window: RowFrame | DurationFrame,
467
+ min_periods: int = 1,
468
+ ) -> ColumnExpr:
469
+ return self._rolling("min", value, window, min_periods)
470
+
471
+ def max(
472
+ self,
473
+ value: ColumnExpr,
474
+ /,
475
+ *,
476
+ window: RowFrame | DurationFrame,
477
+ min_periods: int = 1,
478
+ ) -> ColumnExpr:
479
+ return self._rolling("max", value, window, min_periods)
480
+
481
+ def variance(
482
+ self,
483
+ value: ColumnExpr,
484
+ /,
485
+ *,
486
+ window: RowFrame | DurationFrame,
487
+ min_periods: int = 1,
488
+ ddof: Literal[0, 1] = 1,
489
+ ) -> ColumnExpr:
490
+ return self._rolling_ddof("variance", value, window, min_periods, ddof)
491
+
492
+ def stddev(
493
+ self,
494
+ value: ColumnExpr,
495
+ /,
496
+ *,
497
+ window: RowFrame | DurationFrame,
498
+ min_periods: int = 1,
499
+ ddof: Literal[0, 1] = 1,
500
+ ) -> ColumnExpr:
501
+ return self._rolling_ddof("stddev", value, window, min_periods, ddof)
502
+
503
+ def covariance(
504
+ self,
505
+ left: ColumnExpr,
506
+ right: ColumnExpr,
507
+ /,
508
+ *,
509
+ window: RowFrame | DurationFrame,
510
+ min_periods: int = 1,
511
+ ddof: Literal[0, 1] = 1,
512
+ ) -> ColumnExpr:
513
+ function = "ts.covariance"
514
+ return ColumnExpr(
515
+ build(
516
+ "covariance",
517
+ (
518
+ _column(left, function, "left")._node,
519
+ _column(right, function, "right")._node,
520
+ ),
521
+ {
522
+ "frame": _frame(window, function),
523
+ "min_periods": CInt(_min_periods(min_periods, window, function)),
524
+ "ddof": CInt(_ddof(ddof, function)),
525
+ },
526
+ )
527
+ )
528
+
529
+ def correlation(
530
+ self,
531
+ left: ColumnExpr,
532
+ right: ColumnExpr,
533
+ /,
534
+ *,
535
+ window: RowFrame | DurationFrame,
536
+ min_periods: int = 1,
537
+ ddof: Literal[0, 1] = 1,
538
+ ) -> ColumnExpr:
539
+ function = "ts.correlation"
540
+ return ColumnExpr(
541
+ build(
542
+ "correlation",
543
+ (
544
+ _column(left, function, "left")._node,
545
+ _column(right, function, "right")._node,
546
+ ),
547
+ {
548
+ "frame": _frame(window, function),
549
+ "min_periods": CInt(_min_periods(min_periods, window, function)),
550
+ "ddof": CInt(_ddof(ddof, function)),
551
+ },
552
+ )
553
+ )
554
+
555
+
556
+ class CsNamespace:
557
+ """Cross-section features over explicit complete groups."""
558
+
559
+ __slots__ = ()
560
+
561
+ @staticmethod
562
+ def _group(value: object, function: str, /) -> CrossSectionGroup:
563
+ if not isinstance(value, CrossSectionGroup):
564
+ raise namespace_error(function, "group", "CrossSectionGroup", value)
565
+ return value
566
+
567
+ @staticmethod
568
+ def _grouping(group: CrossSectionGroup, /) -> CValue:
569
+ if group.bucket is None:
570
+ return CEnum("grouping", "exact_time")
571
+ return CMap.from_mapping(
572
+ {
573
+ "grouping": CEnum("grouping", "fixed_bucket"),
574
+ "width_micros": CInt(group.bucket.width_micros),
575
+ }
576
+ )
577
+
578
+ @staticmethod
579
+ def _group_args(group: CrossSectionGroup, /) -> tuple[Node, ...]:
580
+ return (
581
+ group.event_time._node,
582
+ *(column._node for column in group.partition_by),
583
+ )
584
+
585
+ def rank(
586
+ self,
587
+ value: ColumnExpr,
588
+ /,
589
+ *,
590
+ group: CrossSectionGroup,
591
+ direction: Literal["ascending", "descending"] = "ascending",
592
+ tie_method: Literal["average", "min", "max"] = "average",
593
+ null_placement: Literal["exclude", "first", "last"] = "exclude",
594
+ min_samples: int = 1,
595
+ ) -> ColumnExpr:
596
+ function = "cs.rank"
597
+ validated = self._group(group, function)
598
+ return ColumnExpr(
599
+ build(
600
+ "rank",
601
+ (
602
+ _column(value, function, "value")._node,
603
+ *self._group_args(validated),
604
+ ),
605
+ {
606
+ "grouping": self._grouping(validated),
607
+ "direction": _enum_value(
608
+ direction,
609
+ "direction",
610
+ ("ascending", "descending"),
611
+ function,
612
+ "direction",
613
+ ),
614
+ "tie_method": _enum_value(
615
+ tie_method,
616
+ "rank_tie_method",
617
+ ("average", "min", "max"),
618
+ function,
619
+ "tie_method",
620
+ ),
621
+ "null_placement": _enum_value(
622
+ null_placement,
623
+ "null_placement",
624
+ ("exclude", "first", "last"),
625
+ function,
626
+ "null_placement",
627
+ ),
628
+ "min_samples": CInt(_min_samples(min_samples, function)),
629
+ },
630
+ )
631
+ )
632
+
633
+ def percentile(
634
+ self,
635
+ value: ColumnExpr,
636
+ /,
637
+ *,
638
+ group: CrossSectionGroup,
639
+ direction: Literal["ascending", "descending"] = "ascending",
640
+ tie_method: Literal["average", "min", "max"] = "average",
641
+ null_placement: Literal["exclude", "first", "last"] = "exclude",
642
+ min_samples: int = 1,
643
+ ) -> ColumnExpr:
644
+ function = "cs.percentile"
645
+ validated = self._group(group, function)
646
+ return ColumnExpr(
647
+ build(
648
+ "percentile",
649
+ (
650
+ _column(value, function, "value")._node,
651
+ *self._group_args(validated),
652
+ ),
653
+ {
654
+ "grouping": self._grouping(validated),
655
+ "direction": _enum_value(
656
+ direction,
657
+ "direction",
658
+ ("ascending", "descending"),
659
+ function,
660
+ "direction",
661
+ ),
662
+ "tie_method": _enum_value(
663
+ tie_method,
664
+ "rank_tie_method",
665
+ ("average", "min", "max"),
666
+ function,
667
+ "tie_method",
668
+ ),
669
+ "null_placement": _enum_value(
670
+ null_placement,
671
+ "null_placement",
672
+ ("exclude", "first", "last"),
673
+ function,
674
+ "null_placement",
675
+ ),
676
+ "min_samples": CInt(_min_samples(min_samples, function)),
677
+ },
678
+ )
679
+ )
680
+
681
+ def demean(
682
+ self,
683
+ value: ColumnExpr,
684
+ /,
685
+ *,
686
+ group: CrossSectionGroup,
687
+ min_samples: int = 1,
688
+ ) -> ColumnExpr:
689
+ function = "cs.demean"
690
+ validated = self._group(group, function)
691
+ return ColumnExpr(
692
+ build(
693
+ "demean",
694
+ (
695
+ _column(value, function, "value")._node,
696
+ *self._group_args(validated),
697
+ ),
698
+ {
699
+ "grouping": self._grouping(validated),
700
+ "min_samples": CInt(_min_samples(min_samples, function)),
701
+ },
702
+ )
703
+ )
704
+
705
+ def zscore(
706
+ self,
707
+ value: ColumnExpr,
708
+ /,
709
+ *,
710
+ group: CrossSectionGroup,
711
+ min_samples: int = 1,
712
+ ddof: Literal[0, 1] = 0,
713
+ ) -> ColumnExpr:
714
+ function = "cs.zscore"
715
+ validated = self._group(group, function)
716
+ return ColumnExpr(
717
+ build(
718
+ "zscore",
719
+ (
720
+ _column(value, function, "value")._node,
721
+ *self._group_args(validated),
722
+ ),
723
+ {
724
+ "grouping": self._grouping(validated),
725
+ "min_samples": CInt(_min_samples(min_samples, function)),
726
+ "ddof": CInt(_ddof(ddof, function)),
727
+ },
728
+ )
729
+ )
730
+
731
+ def winsorize(
732
+ self,
733
+ value: ColumnExpr,
734
+ /,
735
+ *,
736
+ group: CrossSectionGroup,
737
+ lower: float,
738
+ upper: float,
739
+ min_samples: int = 1,
740
+ ) -> ColumnExpr:
741
+ function = "cs.winsorize"
742
+ validated = self._group(group, function)
743
+ lower_value = _finite_bound(lower, function, "lower")
744
+ upper_value = _finite_bound(upper, function, "upper")
745
+ if not 0 <= lower_value <= upper_value <= 1:
746
+ path = (
747
+ f"calc_flow.symbolic.{function}.lower"
748
+ if lower_value < 0 or lower_value > upper_value
749
+ else f"calc_flow.symbolic.{function}.upper"
750
+ )
751
+ raise ValueError(
752
+ f"{path}: invalid_literal: bounds must satisfy 0 <= lower <= upper <= 1"
753
+ )
754
+ return ColumnExpr(
755
+ build(
756
+ "winsorize",
757
+ (
758
+ _column(value, function, "value")._node,
759
+ *self._group_args(validated),
760
+ ),
761
+ {
762
+ "grouping": self._grouping(validated),
763
+ "min_samples": CInt(_min_samples(min_samples, function)),
764
+ "lower": _number_value(lower_value),
765
+ "upper": _number_value(upper_value),
766
+ },
767
+ )
768
+ )
769
+
770
+ def _selection(
771
+ self,
772
+ kind: Literal["top", "bottom"],
773
+ value: ColumnExpr,
774
+ group: CrossSectionGroup,
775
+ count: int,
776
+ include_ties: bool,
777
+ min_samples: int,
778
+ /,
779
+ ) -> ColumnExpr:
780
+ function = f"cs.{kind}"
781
+ validated = self._group(group, function)
782
+ return ColumnExpr(
783
+ build(
784
+ kind,
785
+ (
786
+ _column(value, function, "value")._node,
787
+ *self._group_args(validated),
788
+ ),
789
+ {
790
+ "grouping": self._grouping(validated),
791
+ "count": CInt(
792
+ require_positive_int(
793
+ count, f"calc_flow.symbolic.{function}.count"
794
+ )
795
+ ),
796
+ "include_ties": CBool(_include_ties(include_ties, function)),
797
+ "min_samples": CInt(_min_samples(min_samples, function)),
798
+ },
799
+ )
800
+ )
801
+
802
+ def top(
803
+ self,
804
+ value: ColumnExpr,
805
+ /,
806
+ *,
807
+ group: CrossSectionGroup,
808
+ count: int,
809
+ include_ties: bool = True,
810
+ min_samples: int = 1,
811
+ ) -> ColumnExpr:
812
+ """Select the largest valid values in each complete group."""
813
+
814
+ return self._selection("top", value, group, count, include_ties, min_samples)
815
+
816
+ def bottom(
817
+ self,
818
+ value: ColumnExpr,
819
+ /,
820
+ *,
821
+ group: CrossSectionGroup,
822
+ count: int,
823
+ include_ties: bool = True,
824
+ min_samples: int = 1,
825
+ ) -> ColumnExpr:
826
+ """Select the smallest valid values in each complete group."""
827
+
828
+ return self._selection("bottom", value, group, count, include_ties, min_samples)
829
+
830
+ def mean_fill(
831
+ self,
832
+ value: ColumnExpr,
833
+ /,
834
+ *,
835
+ group: CrossSectionGroup,
836
+ min_samples: int = 1,
837
+ ) -> ColumnExpr:
838
+ """Fill nulls with the complete group's mean while preserving NaN."""
839
+
840
+ function = "cs.mean_fill"
841
+ validated = self._group(group, function)
842
+ return ColumnExpr(
843
+ build(
844
+ "mean_fill",
845
+ (
846
+ _column(value, function, "value")._node,
847
+ *self._group_args(validated),
848
+ ),
849
+ {
850
+ "grouping": self._grouping(validated),
851
+ "min_samples": CInt(_min_samples(min_samples, function)),
852
+ },
853
+ )
854
+ )
855
+
856
+
857
+ def _finite_bound(value: object, function: str, parameter: str, /) -> int | float:
858
+ path = f"calc_flow.symbolic.{function}.{parameter}"
859
+ if type(value) not in (int, float):
860
+ raise TypeError(f"{path} must be a finite number; got {type_name(value)}")
861
+ if isinstance(value, float) and not math.isfinite(value):
862
+ raise ValueError(f"{path}: invalid_literal: must be finite")
863
+ return value
864
+
865
+
866
+ def _number_value(value: int | float, /) -> CValue:
867
+ return CInt(value) if type(value) is int else CFloat(value)
868
+
869
+
870
+ class TableNamespace:
871
+ """Table-scope bridging and structural operations."""
872
+
873
+ __slots__ = ()
874
+
875
+ def project(self, value: TableExpr, columns: Sequence[str], /) -> TableExpr:
876
+ function = "table.project"
877
+ return TableExpr(
878
+ build(
879
+ "project",
880
+ (_table(value, function, "value")._node,),
881
+ {"columns": _str_sequence(columns, function, "columns")},
882
+ )
883
+ )
884
+
885
+ def filter(self, value: TableExpr, predicate: ColumnExpr, /) -> TableExpr:
886
+ function = "table.filter"
887
+ return TableExpr(
888
+ build(
889
+ "filter",
890
+ (
891
+ _table(value, function, "value")._node,
892
+ _column(predicate, function, "predicate")._node,
893
+ ),
894
+ {},
895
+ )
896
+ )
897
+
898
+ def attach_columns(
899
+ self,
900
+ value: TableExpr,
901
+ array: ArrayExpr,
902
+ /,
903
+ *,
904
+ names: Sequence[str],
905
+ ) -> TableExpr:
906
+ function = "table.attach_columns"
907
+ return TableExpr(
908
+ build(
909
+ "attach_columns",
910
+ (
911
+ _table(value, function, "value")._node,
912
+ _array(array, function, "array")._node,
913
+ ),
914
+ {"names": _str_sequence(names, function, "names")},
915
+ )
916
+ )
917
+
918
+ def stream_join(
919
+ self,
920
+ left: TableExpr,
921
+ right: TableExpr,
922
+ /,
923
+ *,
924
+ left_keys: Sequence[str],
925
+ right_keys: Sequence[str],
926
+ left_event_time: str,
927
+ right_event_time: str,
928
+ bounds: JoinTimeBounds,
929
+ limits: JoinStateLimits,
930
+ left_prefix: str = "left",
931
+ right_prefix: str = "right",
932
+ output_entity_by: Sequence[str] = (),
933
+ output_event_time: str | None = None,
934
+ output_sequence_by: Sequence[str] = (),
935
+ ) -> TableExpr:
936
+ """Declare one bounded native inner join between two stream tables.
937
+
938
+ Output ordering is optional for a terminal row-local join. Declare all
939
+ three output ordering arguments when the result feeds another join or
940
+ a stateful symbolic stage.
941
+ """
942
+
943
+ from calc_flow.join_spec import (
944
+ require_distinct_prefixes,
945
+ require_equal_key_counts,
946
+ require_event_time_columns,
947
+ require_join_bounds,
948
+ require_join_limits,
949
+ timedelta_micros,
950
+ )
951
+
952
+ function = "table.stream_join"
953
+ left_value = _table(left, function, "left")
954
+ right_value = _table(right, function, "right")
955
+ left_key_values = _str_sequence(left_keys, function, "left_keys")
956
+ right_key_values = _str_sequence(right_keys, function, "right_keys")
957
+ if not left_key_values.items or not right_key_values.items:
958
+ raise ValueError(
959
+ "calc_flow.symbolic.table.stream_join.keys: invalid_literal:"
960
+ " key sequences must be non-empty"
961
+ )
962
+ require_equal_key_counts(
963
+ tuple(item.value for item in left_key_values.items),
964
+ tuple(item.value for item in right_key_values.items),
965
+ )
966
+ require_event_time_columns(left_event_time, right_event_time)
967
+ require_join_bounds(bounds)
968
+ require_join_limits(limits)
969
+ require_distinct_prefixes(left_prefix, right_prefix)
970
+ output_entities = _str_sequence(output_entity_by, function, "output_entity_by")
971
+ output_sequences = _str_sequence(
972
+ output_sequence_by, function, "output_sequence_by"
973
+ )
974
+ has_output_event_time = output_event_time is not None
975
+ has_output_entities = bool(output_entities.items)
976
+ has_output_sequences = bool(output_sequences.items)
977
+ if any(
978
+ (has_output_event_time, has_output_entities, has_output_sequences)
979
+ ) and not all(
980
+ (has_output_event_time, has_output_entities, has_output_sequences)
981
+ ):
982
+ raise ValueError(
983
+ "calc_flow.symbolic.table.stream_join.output_ordering:"
984
+ " invalid_literal: output ordering requires non-empty"
985
+ " output_entity_by, output_event_time, and output_sequence_by"
986
+ )
987
+ attrs = {
988
+ "left_keys": left_key_values,
989
+ "right_keys": right_key_values,
990
+ "left_event_time": CStr(left_event_time),
991
+ "right_event_time": CStr(right_event_time),
992
+ "before_micros": CInt(timedelta_micros(bounds.before, "before")),
993
+ "after_micros": CInt(timedelta_micros(bounds.after, "after")),
994
+ "max_state_rows_per_side": CInt(limits.max_state_rows_per_side),
995
+ "max_state_bytes_per_side": CInt(limits.max_state_bytes_per_side),
996
+ "max_matches_per_input_batch": CInt(limits.max_matches_per_input_batch),
997
+ "left_prefix": CStr(left_prefix),
998
+ "right_prefix": CStr(right_prefix),
999
+ }
1000
+ if has_output_event_time:
1001
+ attrs.update(
1002
+ {
1003
+ "output_entity_by": output_entities,
1004
+ "output_event_time": CStr(
1005
+ require_non_empty_str(
1006
+ output_event_time,
1007
+ f"calc_flow.symbolic.{function}.output_event_time",
1008
+ )
1009
+ ),
1010
+ "output_sequence_by": output_sequences,
1011
+ }
1012
+ )
1013
+ return TableExpr(
1014
+ build(
1015
+ "stream_join",
1016
+ (left_value._node, right_value._node),
1017
+ attrs,
1018
+ version=2 if has_output_event_time else 1,
1019
+ )
1020
+ )
1021
+
1022
+
1023
+ class LinalgNamespace:
1024
+ """Explicit table/array boundary operations."""
1025
+
1026
+ __slots__ = ()
1027
+
1028
+ def from_columns(
1029
+ self,
1030
+ value: TableExpr,
1031
+ /,
1032
+ *,
1033
+ columns: Sequence[str],
1034
+ backend: str,
1035
+ ) -> ArrayExpr:
1036
+ function = "linalg.from_columns"
1037
+ backend_value = require_non_empty_str(
1038
+ backend, f"calc_flow.symbolic.{function}.backend"
1039
+ )
1040
+ return ArrayExpr(
1041
+ build(
1042
+ "from_columns",
1043
+ (_table(value, function, "value")._node,),
1044
+ {
1045
+ "columns": _str_sequence(columns, function, "columns"),
1046
+ "backend": CStr(backend_value),
1047
+ },
1048
+ )
1049
+ )
1050
+
1051
+ def matmul(
1052
+ self,
1053
+ left: ArrayExpr,
1054
+ right: ArrayExpr | Parameter[ArrayExpr],
1055
+ /,
1056
+ ) -> ArrayExpr:
1057
+ function = "linalg.matmul"
1058
+ left_node = _array(left, function, "left")._node
1059
+ if isinstance(right, ArrayExpr) or (
1060
+ isinstance(right, Parameter) and right.kind == "array"
1061
+ ):
1062
+ right_node = right._node
1063
+ else:
1064
+ raise namespace_error(
1065
+ function,
1066
+ "right",
1067
+ "ArrayExpr | Parameter[ArrayExpr]",
1068
+ right,
1069
+ )
1070
+ return ArrayExpr(build("matmul", (left_node, right_node), {}))
1071
+
1072
+
1073
+ class WindowNamespace:
1074
+ """Cardinality-changing event-time windows."""
1075
+
1076
+ __slots__ = ()
1077
+
1078
+ def tumbling(
1079
+ self,
1080
+ value: TableExpr,
1081
+ /,
1082
+ *,
1083
+ event_time: str,
1084
+ size_micros: int,
1085
+ group_by: Sequence[str] = (),
1086
+ ) -> TableExpr:
1087
+ function = "window.tumbling"
1088
+ return TableExpr(
1089
+ build(
1090
+ "window_tumbling",
1091
+ (_table(value, function, "value")._node,),
1092
+ {
1093
+ "event_time": CStr(
1094
+ require_non_empty_str(
1095
+ event_time,
1096
+ f"calc_flow.symbolic.{function}.event_time",
1097
+ )
1098
+ ),
1099
+ "size_micros": CInt(
1100
+ require_positive_int(
1101
+ size_micros,
1102
+ f"calc_flow.symbolic.{function}.size_micros",
1103
+ )
1104
+ ),
1105
+ "group_by": _str_sequence(group_by, function, "group_by"),
1106
+ },
1107
+ )
1108
+ )
1109
+
1110
+ def hopping(
1111
+ self,
1112
+ value: TableExpr,
1113
+ /,
1114
+ *,
1115
+ event_time: str,
1116
+ size_micros: int,
1117
+ slide_micros: int,
1118
+ group_by: Sequence[str] = (),
1119
+ ) -> TableExpr:
1120
+ function = "window.hopping"
1121
+ return TableExpr(
1122
+ build(
1123
+ "window_hopping",
1124
+ (_table(value, function, "value")._node,),
1125
+ {
1126
+ "event_time": CStr(
1127
+ require_non_empty_str(
1128
+ event_time,
1129
+ f"calc_flow.symbolic.{function}.event_time",
1130
+ )
1131
+ ),
1132
+ "size_micros": CInt(
1133
+ require_positive_int(
1134
+ size_micros,
1135
+ f"calc_flow.symbolic.{function}.size_micros",
1136
+ )
1137
+ ),
1138
+ "slide_micros": CInt(
1139
+ require_positive_int(
1140
+ slide_micros,
1141
+ f"calc_flow.symbolic.{function}.slide_micros",
1142
+ )
1143
+ ),
1144
+ "group_by": _str_sequence(group_by, function, "group_by"),
1145
+ },
1146
+ )
1147
+ )
1148
+
1149
+
1150
+ row = RowNamespace()
1151
+ ts = TsNamespace()
1152
+ cs = CsNamespace()
1153
+ table = TableNamespace()
1154
+ linalg = LinalgNamespace()
1155
+ window = WindowNamespace()