fpstreams 2.2.1__tar.gz → 2.4.0__tar.gz

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.
Files changed (200) hide show
  1. {fpstreams-2.2.1 → fpstreams-2.4.0}/CHANGELOG.md +157 -0
  2. {fpstreams-2.2.1 → fpstreams-2.4.0}/PKG-INFO +34 -9
  3. {fpstreams-2.2.1 → fpstreams-2.4.0}/README.md +33 -8
  4. {fpstreams-2.2.1 → fpstreams-2.4.0}/pyproject.toml +1 -1
  5. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/Cargo.lock +1 -1
  6. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/Cargo.toml +1 -1
  7. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/__init__.py +7 -2
  8. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/collecting/aggregation.py +152 -9
  9. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/collecting/collector.py +47 -12
  10. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/collecting/program.py +13 -4
  11. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/collecting/reducer.py +23 -12
  12. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/_rows_fusion.py +50 -0
  13. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/arrow.py +180 -48
  14. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/async_iterators.py +78 -1
  15. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/async_ops.py +4 -0
  16. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/async_scheduler.py +11 -2
  17. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/relational/arrow_group.py +3 -1
  18. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/sorted_streams.py +22 -1
  19. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/sync_ops.py +97 -17
  20. fpstreams-2.4.0/src/fpstreams/execution/window_reduce.py +135 -0
  21. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/physical/async_plan.py +2 -1
  22. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/arrow.py +11 -16
  23. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/async_.py +20 -2
  24. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/explain.py +21 -1
  25. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/semantic_rules.py +25 -1
  26. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/sync.py +25 -2
  27. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/__init__.py +2 -1
  28. fpstreams-2.4.0/src/fpstreams/runtime/executor.py +118 -0
  29. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/limits.py +7 -2
  30. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/report.py +100 -5
  31. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/tasks.py +5 -2
  32. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/storage/__init__.py +3 -1
  33. fpstreams-2.4.0/src/fpstreams/storage/checkpoint.py +165 -0
  34. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/storage/codec.py +2 -0
  35. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/storage/spill_store.py +5 -1
  36. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/streams/async_flow.py +272 -11
  37. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/streams/async_terminals.py +25 -5
  38. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/streams/flow.py +226 -42
  39. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/streams/flow_terminals.py +31 -5
  40. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/streams/pairs.py +9 -5
  41. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/arrow.py +156 -55
  42. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/grouped.py +4 -6
  43. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/io.py +35 -10
  44. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/records.py +37 -1
  45. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/rows.py +16 -47
  46. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/testing.py +37 -22
  47. {fpstreams-2.2.1 → fpstreams-2.4.0}/LICENSE +0 -0
  48. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/build.rs +0 -0
  49. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/common.rs +0 -0
  50. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/float/affine_pair.rs +0 -0
  51. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/float/endpoints.rs +0 -0
  52. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/float.rs +0 -0
  53. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/integer/endpoints.rs +0 -0
  54. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/integer.rs +0 -0
  55. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/lib.rs +0 -0
  56. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/numeric_mean.rs +0 -0
  57. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/numpy_export.rs +0 -0
  58. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/numpy_group/buffer.rs +0 -0
  59. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/numpy_group.rs +0 -0
  60. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/pair/expr.rs +0 -0
  61. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/pair/prefix.rs +0 -0
  62. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/pair/row_filter.rs +0 -0
  63. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/pair/unique.rs +0 -0
  64. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/pair/value_filter.rs +0 -0
  65. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/pair/value_map.rs +0 -0
  66. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/pair.rs +0 -0
  67. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/pivot.rs +0 -0
  68. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/records.rs +0 -0
  69. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational/adapters/namedtuple.rs +0 -0
  70. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational/adapters.rs +0 -0
  71. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational/global_numeric.rs +0 -0
  72. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational/group_numeric.rs +0 -0
  73. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational/group_pair_expr.rs +0 -0
  74. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational/join_callable/many.rs +0 -0
  75. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational/join_callable/unique.rs +0 -0
  76. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational/join_callable.rs +0 -0
  77. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational/join_exact.rs +0 -0
  78. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational/join_i64.rs +0 -0
  79. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational.rs +0 -0
  80. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational_fixed/composite.rs +0 -0
  81. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational_fixed/global_multi/same_field.rs +0 -0
  82. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational_fixed/global_multi.rs +0 -0
  83. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational_fixed/group_multi.rs +0 -0
  84. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational_fixed/single.rs +0 -0
  85. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/relational_fixed.rs +0 -0
  86. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/scalar_sort.rs +0 -0
  87. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/scalar_unique.rs +0 -0
  88. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/select.rs +0 -0
  89. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/adapters.rs +0 -0
  90. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/group_exact/pairs.rs +0 -0
  91. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/group_exact/rows.rs +0 -0
  92. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/group_exact.rs +0 -0
  93. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/group_fixed.rs +0 -0
  94. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/join_callable/behavior.rs +0 -0
  95. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/join_callable/direct.rs +0 -0
  96. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/join_callable.rs +0 -0
  97. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/join_exact.rs +0 -0
  98. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/numeric/buffers.rs +0 -0
  99. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/numeric/kernels.rs +0 -0
  100. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/numeric/reductions.rs +0 -0
  101. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/numeric.rs +0 -0
  102. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests/numpy_group.rs +0 -0
  103. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/tests.rs +0 -0
  104. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/unnest.rs +0 -0
  105. {fpstreams-2.2.1 → fpstreams-2.4.0}/rust/src/unpivot.rs +0 -0
  106. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/_native.pyi +0 -0
  107. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/_provenance.py +0 -0
  108. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/aggregate.py +0 -0
  109. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/async_flow.py +0 -0
  110. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/collecting/__init__.py +0 -0
  111. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/collecting/_collector_base.py +0 -0
  112. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/collecting/aggregate_program.py +0 -0
  113. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/collecting/statistics.py +0 -0
  114. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/collectors.py +0 -0
  115. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/column.py +0 -0
  116. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/errors.py +0 -0
  117. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/exceptions.py +0 -0
  118. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/__init__.py +0 -0
  119. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/_pair_aggregate.py +0 -0
  120. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/_pair_dict.py +0 -0
  121. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/_pair_row_filter.py +0 -0
  122. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/_scalar_fusion.py +0 -0
  123. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/async_map.py +0 -0
  124. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/async_merge.py +0 -0
  125. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/async_prefetch.py +0 -0
  126. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/async_queue.py +0 -0
  127. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/async_timers.py +0 -0
  128. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/native.py +0 -0
  129. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/numpy_group.py +0 -0
  130. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/numpy_prefix.py +0 -0
  131. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/physical.py +0 -0
  132. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/relational/__init__.py +0 -0
  133. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/relational/arrow_global.py +0 -0
  134. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/relational/arrow_group_rows.py +0 -0
  135. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/relational/join.py +0 -0
  136. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/sorting.py +0 -0
  137. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/execution/sync.py +0 -0
  138. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/expr.py +0 -0
  139. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/expressions/__init__.py +0 -0
  140. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/expressions/_codegen.py +0 -0
  141. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/expressions/_row_codegen.py +0 -0
  142. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/expressions/program.py +0 -0
  143. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/expressions/row.py +0 -0
  144. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/expressions/row_eval.py +0 -0
  145. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/expressions/row_ir.py +0 -0
  146. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/expressions/scalar.py +0 -0
  147. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/expressions/selectors.py +0 -0
  148. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/expressions/typed_ir.py +0 -0
  149. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/functional.py +0 -0
  150. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/gather.py +0 -0
  151. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/io_safety.py +0 -0
  152. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/option.py +0 -0
  153. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/physical/__init__.py +0 -0
  154. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/physical/compiled.py +0 -0
  155. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/physical/kernel_cache.py +0 -0
  156. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/physical/plan.py +0 -0
  157. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/physical/relational.py +0 -0
  158. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/__init__.py +0 -0
  159. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/_pair_stages.py +0 -0
  160. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/arrow_source.py +0 -0
  161. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/async_utils.py +0 -0
  162. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/compiler.py +0 -0
  163. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/gather.py +0 -0
  164. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/logical.py +0 -0
  165. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/native.py +0 -0
  166. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/numpy.py +0 -0
  167. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/pair_i64_expression.py +0 -0
  168. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/plan_cache.py +0 -0
  169. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/semantic_analyzer.py +0 -0
  170. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/semantics.py +0 -0
  171. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/planning/source.py +0 -0
  172. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/primitives/__init__.py +0 -0
  173. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/primitives/option.py +0 -0
  174. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/primitives/result.py +0 -0
  175. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/py.typed +0 -0
  176. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/result.py +0 -0
  177. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/_distinct.py +0 -0
  178. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/failpoints.py +0 -0
  179. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/files.py +0 -0
  180. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/iterators.py +0 -0
  181. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/metrics.py +0 -0
  182. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/query.py +0 -0
  183. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/resources.py +0 -0
  184. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/runtime/spill.py +0 -0
  185. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/streams/__init__.py +0 -0
  186. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/streams/_async_identity.py +0 -0
  187. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/streams/_flow_structural_list.py +0 -0
  188. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/streams/_flow_unique_list.py +0 -0
  189. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/__init__.py +0 -0
  190. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/_text_sources.py +0 -0
  191. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/dataframe.py +0 -0
  192. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/factory.py +0 -0
  193. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/join.py +0 -0
  194. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/numpy.py +0 -0
  195. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/polars.py +0 -0
  196. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/spill.py +0 -0
  197. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/spill_io.py +0 -0
  198. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/spill_limits.py +0 -0
  199. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/sql.py +0 -0
  200. {fpstreams-2.2.1 → fpstreams-2.4.0}/src/fpstreams/tabular/sqlite_sink.py +0 -0
@@ -5,6 +5,163 @@ including changed defaults.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 2.4.0 - 2026-10-18
9
+
10
+ ### Added
11
+
12
+ - `ExecutionReport.to_dict()` and `ExecutionEvent.to_dict()` export plain
13
+ metadata. Report and plan dictionaries carry an independent schema version;
14
+ reports include metric units, query-owned resource scope, and event limits.
15
+
16
+ - `Flow.collapse(predicate, collector)` and its async equivalent reduce each
17
+ adjacent run with existing Collector state. Completion stops further steps
18
+ within a run while predicates continue to find its boundary. Collector hooks
19
+ remain synchronous, and awaitable finisher results are emitted as data.
20
+
21
+ - `Flow.window_reduce(size, combine, step=1)` and `AsyncFlow.window_reduce(...)`
22
+ reuse ordered partial aggregates across count-based windows. The synchronous
23
+ combine function must be associative and nonmutating. Cached entries are
24
+ bounded by `size + 1`; byte sizes and individual stack transfers are not.
25
+ Nonoverlapping windows use a running fold without retaining a raw window buffer.
26
+
27
+ - `flow.unfold(seed, function)` and `aflow.unfold(...)` generate values lazily
28
+ from `(value, next_state)` transitions until the callback returns `None`.
29
+ Class constructors expose the same operation; async transitions may be awaitable.
30
+ - `Flow.map_accum(initial, function)` and `AsyncFlow.map_accum(...)` thread
31
+ state through ordered callbacks returning `(next_state, output)`, emitting
32
+ one output per input through the existing gather/scan execution paths.
33
+ - `agg.min_by(selector)` and `agg.max_by(selector)` retain a whole winning
34
+ item in one pass. Ties keep the first item and empty input returns `None`;
35
+ the factories reuse existing grouped, async, and spill aggregation.
36
+ - `agg.top(count, key=...)` and `agg.bottom(count, key=...)` retain a bounded
37
+ list of whole items per group, ordered by key with stable ties. They compose
38
+ with the existing global, async, sorted, and spill aggregation paths.
39
+ - `run_with_report()` includes up to 64 immutable plan and route events in
40
+ `report.events`, with `events_truncated` marking further observations. Child
41
+ plans keep their own labels without replacing the outer route. Flow, Rows,
42
+ Pairs, and AsyncFlow use the same recorder.
43
+ - `BoundedExecutor(executor, max_pending=...)` shares one unfinished-task budget
44
+ across independent queries. It accepts existing thread or process pools,
45
+ releases capacity on completion or cancellation, and keeps the borrowed pool open.
46
+ - `FileCheckpoint(directory, version=...)` stores verified outputs for stable
47
+ file partitions. Each successful callback publishes an output file and its
48
+ metadata together; later runs verify input and output hashes before reuse.
49
+
50
+ ### Changed
51
+
52
+ - `filter_map()` avoids intermediate tuple adaptation in synchronous pipelines
53
+ and the extra predicate callback in async serial fusion. It calls the mapper
54
+ once per input and keeps every result except `None`.
55
+ - Proven primitive Arrow comparison chains can share a selection mask,
56
+ filtering payload once or counting directly from the mask. Offline Optuna
57
+ tuning favored nonempty batches with at least one column in the measured shapes.
58
+ - Spill decoding releases encoded buffers before yielding decoded records
59
+ and releases completed decoded frames before reading the next frame.
60
+
61
+ - `Collectors.head()` and `tail()` accept integer-index objects, with matching
62
+ `SupportsIndex` annotations. Partitioned reducer execution and reducer-law
63
+ layouts use the same integer protocol.
64
+ - `Collectors.teeing()` can combine partial states when both downstream
65
+ collectors provide combiners. It keeps their live callbacks and carries no
66
+ inferred reducer laws or native metadata.
67
+ - Named `aggregate()` and `Pairs.aggregate_values()` accept any `Collector`,
68
+ including composed `Collectors` and existing `agg` factory results. Global,
69
+ async, grouped, sorted, and spill paths share the existing collector lifecycle.
70
+ - Multi-collector Python aggregation reuses its state-list references within
71
+ each row while retaining live callbacks and their declaration order.
72
+ - Retained primitive Arrow tables and record batches can keep their native
73
+ buffers when `arrow_batches(schema=...)` receives the matching output schema,
74
+ including metadata. Safe direct-field projections and primitive filters can
75
+ execute columnarly. Casts, Python callbacks, and unsupported sources retain
76
+ the existing conversion path.
77
+ - Arrow execution accepts chains of safe direct comparisons followed by a
78
+ direct projection. Parquet keeps only the leading comparison as an automatic
79
+ scan hint; all filters remain in the ordered residual program.
80
+ - Default local Parquet scans with one fragment can omit safe unused fields
81
+ from validation scans, retaining query dependencies and fields whose Python
82
+ conversion can fail. Sources with possible scanner casts retain the full
83
+ caller-visible projection before automatic filtering and projection.
84
+
85
+ ### Fixed
86
+
87
+ - Query-wide task/file limits and task-scope limits reject noninteger values,
88
+ including NaN, before resource acquisition. Integer-index values are
89
+ normalized once during construction.
90
+
91
+ - `Collectors.first()` and `head(count)` preserve completed state when used
92
+ inside `grouping_by` or `partitioning_by`. Previously, `first()` returned the
93
+ last group item and `head()` could retain the whole group, even with a zero
94
+ count. Grouping adapters continue to read all input and step custom collectors.
95
+ - `Flow` and `AsyncFlow` validate `take`, `limit`, `drop`, and `skip` counts
96
+ through the integer index protocol before building the operation. Noninteger
97
+ values, including NaN and infinity, raise `TypeError` before opening the source.
98
+ This prevents async prefix operations from silently rounding counts or losing
99
+ their limit.
100
+ - `Flow.collect()` and `AsyncFlow.collect()` preserve a single streaming
101
+ collector's finished result type in type checkers. Named collectors return
102
+ a dictionary, while optional or dynamically forwarded arguments retain the
103
+ general result union. Runtime dispatch and validation are unchanged.
104
+ - `Collectors.head()` and `tail()` reject floats, including NaN and infinity,
105
+ at construction. Partitioned reducer execution validates `partition_size`
106
+ before creating state or opening input, preventing non-integer values from
107
+ silently removing partition boundaries.
108
+ - Partitioned reducer execution closes its input iterator on exhaustion and
109
+ failure, before merging states or finishing. Cleanup failures preserve an
110
+ active source or callback exception.
111
+ - Collector programs reject missing combiners before invoking any merge
112
+ callback, preventing avoidable partial mutations on invalid merge requests.
113
+ - Reducer law checks build independent states for each merge expression,
114
+ preventing in-place combiners from producing false identity or associativity
115
+ results. The input iterator closes before the examples are replayed.
116
+ - Unsafe Arrow batches evaluate filters in row order and stop when downstream
117
+ consumers stop, preserving Python exception order and early-exit behavior.
118
+ - Arrow prefix fallbacks reuse converted rows, so custom extension scalars
119
+ cross the Python conversion boundary once per input batch.
120
+ - Automatic Parquet hints preserve conversion errors in discarded fields and
121
+ rows, including casts across fragments and supplied dataset schemas.
122
+ Types whose Python conversion can fail are validated before automatic
123
+ filtering or projection; explicit source `columns` and `filter` still define
124
+ which values the source exposes.
125
+ - `scan_csv()` validates every caller-visible column before query projection,
126
+ preserving conversion errors in later CSV blocks. It opens the typed source
127
+ once; explicit source conversion options still control included columns.
128
+ - An explicit zero-column Arrow schema preserves the input row count in
129
+ `arrow_batches()` and `to_arrow()`, including partial final batches.
130
+ - Polars batch exports preserve the height of zero-column inputs. Parquet
131
+ writes reject nonempty rows without columns, preserving an existing target
132
+ instead of publishing a file that loses the input row count.
133
+
134
+ ## 2.3.0 - 2026-10-15
135
+
136
+ ### Fixed
137
+
138
+ - Async callback return types resolve correctly for `AsyncFlow.map()`,
139
+ `map_async()`, and `map_batches()`. Mixed immediate and awaitable returns
140
+ retain the resolved element type.
141
+
142
+ ### Changed
143
+
144
+ - Supported single-stage Python `with_columns()` and `select()` queries reuse a
145
+ loop template with separate callback bindings for each execution, reducing
146
+ fixed setup cost while retaining computed-column live selectors and projection
147
+ snapshots for each row.
148
+ - Explicit sorted grouping with one key avoids a temporary selector generator
149
+ and wrapper tuple per row while preserving live callbacks, key validation,
150
+ lookahead, and callback error behavior.
151
+
152
+ ### Added
153
+
154
+ - `Flow.map_batches(function, size=...)` and `AsyncFlow.map_batches(...)` call a
155
+ mapper once per tuple batch, including a shorter final batch. Each call emits
156
+ one result. Sync batches inherit `parallel()` settings; async batches support
157
+ concurrency, ordering, per-batch timeouts, and bounded buffering.
158
+ - `Flow.map_parallel()` and `Flow.parallel()` accept a caller-owned `executor`.
159
+ Queries cancel their queued work and wait for their own running mappers while
160
+ leaving the shared pool open. Existing worker/backend defaults remain in place
161
+ when no executor is supplied.
162
+ - Streaming analytics examples show generator input, valid weighted denominators,
163
+ parameterized SQLite selection, and atomic writes across multiple batches.
164
+
8
165
  ## 2.2.1 - 2026-10-07
9
166
 
10
167
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fpstreams
3
- Version: 2.2.1
3
+ Version: 2.4.0
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.11
@@ -66,6 +66,15 @@ Lazy Python pipelines for iterables and records, with bounded async concurrency.
66
66
 
67
67
  Python 3.11 or newer is required.
68
68
 
69
+ The current stable release is [2.4.0](https://github.com/steventimes/fpstreams/releases/tag/v2.4.0).
70
+ It adds stateful pipeline operations, composed aggregation, shared execution
71
+ limits, file recovery, and versioned execution metadata. See the
72
+ [changelog](https://github.com/steventimes/fpstreams/blob/master/CHANGELOG.md) for its scope and the
73
+ [performance guide](https://steventimes.github.io/fpstreams/user-guide/performance/#process-mapping-and-explicit-batches)
74
+ for batch and executor examples. The
75
+ [roadmap](https://steventimes.github.io/fpstreams/roadmap/#next-development-steps)
76
+ lists the next optimization candidates and their acceptance criteria.
77
+
69
78
  ## Install
70
79
 
71
80
  Install the latest stable release:
@@ -360,23 +369,39 @@ python_result = pipeline.with_engine("python").to_list()
360
369
  native_result = pipeline.with_engine("native").to_list()
361
370
  ~~~
362
371
 
363
- Use `run_with_report()` to execute a terminal and inspect its recorded route.
364
- Version 2.2.0 adds Pairs terminals such as
365
- `flow([("a", 1)]).pairs().run_with_report("group_values")`. See the
372
+ Use `run_with_report()` to get a terminal's value, recorded route, and query-owned
373
+ resource counts in one execution. Pairs supports terminals such as
374
+ `flow([("a", 1)]).pairs().run_with_report("group_values")`. Version 2.4.0 also
375
+ includes up to 64 child plan and route observations in `report.events`. See the
366
376
  [execution report guide](https://steventimes.github.io/fpstreams/user-guide/execution-reports/)
367
377
  for supported terminals and the limits of route reporting.
368
378
 
379
+ In 2.4.0, `report.to_dict()` exports versioned metadata with metric units,
380
+ resource scope, and event limits. It excludes the terminal value and does not
381
+ rerun the query. See the
382
+ [metadata export guide](https://steventimes.github.io/fpstreams/user-guide/execution-reports/#metadata-export-unreleased)
383
+ for JSON storage and caller-managed OpenLineage integration. Report export
384
+ requires 2.4.0 or newer.
385
+
386
+ Version 2.4.0 also includes `BoundedExecutor` for a shared submission limit
387
+ across queries and `FileCheckpoint` for verified file-partition recovery. See
388
+ the [resource controls](https://steventimes.github.io/fpstreams/user-guide/performance/#share-a-submission-budget-unreleased)
389
+ and [file-partition recipe](https://steventimes.github.io/fpstreams/user-guide/analytics/#resume-file-partitions-unreleased)
390
+ for ownership, memory, and retry semantics. These additions require 2.4.0 or newer.
391
+
392
+ In 2.4.0, `flow.unfold` / `aflow.unfold` generate values from explicit state
393
+ transitions. `Flow.map_accum` and `AsyncFlow.map_accum` update state while mapping
394
+ each input to one output. See the
395
+ [state examples](https://steventimes.github.io/fpstreams/reference/flow/#state-driven-generation-and-mapping-unreleased)
396
+ for termination, tuple ordering, and mutable-state ownership. These APIs require
397
+ 2.4.0 or newer.
398
+
369
399
  A forced native plan raises `NativeUnsupportedError` if its complete types or
370
400
  operations cannot run natively. In particular, the presence of an internal
371
401
  native relational specialization does not make the complete relation eligible
372
402
  for `with_engine("native")`. An unsupported forced relational plan fails before
373
403
  claiming its one-shot sources; `auto` selects a legal fallback path.
374
404
 
375
- [`run_with_report()`](https://steventimes.github.io/fpstreams/user-guide/execution-reports/)
376
- returns a terminal's value, recorded route, and query-owned resource counts in
377
- one execution. It records the outer plan and some direct paths; it does not
378
- identify every internal kernel or runtime fallback.
379
-
380
405
  Identity list and tuple materialization stays in Python. Large, supported integer
381
406
  sums and numeric range reductions may use Rust in `auto` mode. `count()` uses a
382
407
  known exact size in O(1) when no operation changes cardinality and the source is
@@ -11,6 +11,15 @@ Lazy Python pipelines for iterables and records, with bounded async concurrency.
11
11
 
12
12
  Python 3.11 or newer is required.
13
13
 
14
+ The current stable release is [2.4.0](https://github.com/steventimes/fpstreams/releases/tag/v2.4.0).
15
+ It adds stateful pipeline operations, composed aggregation, shared execution
16
+ limits, file recovery, and versioned execution metadata. See the
17
+ [changelog](https://github.com/steventimes/fpstreams/blob/master/CHANGELOG.md) for its scope and the
18
+ [performance guide](https://steventimes.github.io/fpstreams/user-guide/performance/#process-mapping-and-explicit-batches)
19
+ for batch and executor examples. The
20
+ [roadmap](https://steventimes.github.io/fpstreams/roadmap/#next-development-steps)
21
+ lists the next optimization candidates and their acceptance criteria.
22
+
14
23
  ## Install
15
24
 
16
25
  Install the latest stable release:
@@ -305,23 +314,39 @@ python_result = pipeline.with_engine("python").to_list()
305
314
  native_result = pipeline.with_engine("native").to_list()
306
315
  ~~~
307
316
 
308
- Use `run_with_report()` to execute a terminal and inspect its recorded route.
309
- Version 2.2.0 adds Pairs terminals such as
310
- `flow([("a", 1)]).pairs().run_with_report("group_values")`. See the
317
+ Use `run_with_report()` to get a terminal's value, recorded route, and query-owned
318
+ resource counts in one execution. Pairs supports terminals such as
319
+ `flow([("a", 1)]).pairs().run_with_report("group_values")`. Version 2.4.0 also
320
+ includes up to 64 child plan and route observations in `report.events`. See the
311
321
  [execution report guide](https://steventimes.github.io/fpstreams/user-guide/execution-reports/)
312
322
  for supported terminals and the limits of route reporting.
313
323
 
324
+ In 2.4.0, `report.to_dict()` exports versioned metadata with metric units,
325
+ resource scope, and event limits. It excludes the terminal value and does not
326
+ rerun the query. See the
327
+ [metadata export guide](https://steventimes.github.io/fpstreams/user-guide/execution-reports/#metadata-export-unreleased)
328
+ for JSON storage and caller-managed OpenLineage integration. Report export
329
+ requires 2.4.0 or newer.
330
+
331
+ Version 2.4.0 also includes `BoundedExecutor` for a shared submission limit
332
+ across queries and `FileCheckpoint` for verified file-partition recovery. See
333
+ the [resource controls](https://steventimes.github.io/fpstreams/user-guide/performance/#share-a-submission-budget-unreleased)
334
+ and [file-partition recipe](https://steventimes.github.io/fpstreams/user-guide/analytics/#resume-file-partitions-unreleased)
335
+ for ownership, memory, and retry semantics. These additions require 2.4.0 or newer.
336
+
337
+ In 2.4.0, `flow.unfold` / `aflow.unfold` generate values from explicit state
338
+ transitions. `Flow.map_accum` and `AsyncFlow.map_accum` update state while mapping
339
+ each input to one output. See the
340
+ [state examples](https://steventimes.github.io/fpstreams/reference/flow/#state-driven-generation-and-mapping-unreleased)
341
+ for termination, tuple ordering, and mutable-state ownership. These APIs require
342
+ 2.4.0 or newer.
343
+
314
344
  A forced native plan raises `NativeUnsupportedError` if its complete types or
315
345
  operations cannot run natively. In particular, the presence of an internal
316
346
  native relational specialization does not make the complete relation eligible
317
347
  for `with_engine("native")`. An unsupported forced relational plan fails before
318
348
  claiming its one-shot sources; `auto` selects a legal fallback path.
319
349
 
320
- [`run_with_report()`](https://steventimes.github.io/fpstreams/user-guide/execution-reports/)
321
- returns a terminal's value, recorded route, and query-owned resource counts in
322
- one execution. It records the outer plan and some direct paths; it does not
323
- identify every internal kernel or runtime fallback.
324
-
325
350
  Identity list and tuple materialization stays in Python. Large, supported integer
326
351
  sums and numeric range reductions may use Rust in `auto` mode. `count()` uses a
327
352
  known exact size in O(1) when no operation changes cardinality and the source is
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "fpstreams"
7
- version = "2.2.1"
7
+ version = "2.4.0"
8
8
  description = "Lazy Python pipelines for iterables and records, with bounded async concurrency."
9
9
  readme = "README.md"
10
10
  authors = [
@@ -4,7 +4,7 @@ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "fpstreams-native"
7
- version = "2.2.1"
7
+ version = "2.4.0"
8
8
  dependencies = [
9
9
  "pyo3",
10
10
  "pyo3-build-config",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fpstreams-native"
3
- version = "2.2.1"
3
+ version = "2.4.0"
4
4
  edition = "2024"
5
5
  rust-version = "1.85"
6
6
  description = "Native fused execution kernels for fpstreams."
@@ -27,7 +27,9 @@ from .errors import (
27
27
  from .expressions import Expr, FExpr, RowExpr, coalesce, col, fitem, item, lit, when
28
28
  from .functional import curry, pipe, retry
29
29
  from .primitives import Err, Ok, Option, Result
30
- from .runtime.report import ExecutionReport, ExecutionResult
30
+ from .runtime.executor import BoundedExecutor
31
+ from .runtime.report import ExecutionEvent, ExecutionReport, ExecutionResult
32
+ from .storage.checkpoint import FileCheckpoint
31
33
  from .streams import (
32
34
  AsyncFlow,
33
35
  AsyncStream,
@@ -42,7 +44,7 @@ from .streams import (
42
44
  from .tabular import Rows, SpillLimits
43
45
  from .tabular.factory import rows as rows
44
46
 
45
- __version__ = "2.2.1"
47
+ __version__ = "2.4.0"
46
48
  Stream = Flow
47
49
  ParallelStream = Flow
48
50
 
@@ -50,6 +52,7 @@ __all__ = [
50
52
  "Aggregator",
51
53
  "AsyncFlow",
52
54
  "AsyncStream",
55
+ "BoundedExecutor",
53
56
  "BufferLimitError",
54
57
  "Collector",
55
58
  "Collectors",
@@ -58,10 +61,12 @@ __all__ = [
58
61
  "EmptyFlowError",
59
62
  "EmptyInputPolicy",
60
63
  "Err",
64
+ "ExecutionEvent",
61
65
  "ExecutionReport",
62
66
  "ExecutionResult",
63
67
  "Expr",
64
68
  "FExpr",
69
+ "FileCheckpoint",
65
70
  "Flow",
66
71
  "FlowConsumedError",
67
72
  "FlowError",
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import heapq
6
+ import operator
5
7
  from collections.abc import Callable, Iterable, Mapping
6
8
  from dataclasses import dataclass
7
9
  from types import CodeType, FunctionType, MappingProxyType
@@ -12,6 +14,8 @@ from ..runtime._distinct import DistinctKey, HashFailure
12
14
  from . import _collector_base
13
15
  from ._collector_base import Aggregator as Aggregator
14
16
  from .collector import (
17
+ Collector,
18
+ CollectorItems,
15
19
  finish_collectors,
16
20
  initialize_collectors,
17
21
  step_collectors,
@@ -561,7 +565,9 @@ _CANONICAL_GROUP_SELECTOR_MATCHES_STEP = _group_selector_matches_step
561
565
  _CANONICAL_GROUP_SELECTOR_MATCHES_STEP_CODE = _group_selector_matches_step.__code__
562
566
 
563
567
 
564
- def native_group_aggregation(aggregation: Aggregator) -> NativeGroupAggregation | None:
568
+ def native_group_aggregation(
569
+ aggregation: Collector[Any, Any, Any],
570
+ ) -> NativeGroupAggregation | None:
565
571
  """Recognize a factory result without trusting user-constructible native metadata."""
566
572
  if (
567
573
  _GROUP_STEP_KINDS is not _CANONICAL_GROUP_STEP_KINDS
@@ -620,7 +626,7 @@ def native_group_aggregation(aggregation: Aggregator) -> NativeGroupAggregation
620
626
  )
621
627
 
622
628
 
623
- def project_count_aggregation(aggregation: Aggregator) -> bool:
629
+ def project_count_aggregation(aggregation: Collector[Any, Any, Any]) -> bool:
624
630
  """Recognize only the constant-size count reducer built by :meth:`agg.count`.
625
631
 
626
632
  Native metadata and reducer laws are user-constructible, so neither is enough
@@ -647,7 +653,7 @@ def project_count_aggregation(aggregation: Aggregator) -> bool:
647
653
  )
648
654
 
649
655
 
650
- def native_aggregation_is_live(aggregation: Aggregator) -> bool:
656
+ def native_aggregation_is_live(aggregation: Collector[Any, Any, Any]) -> bool:
651
657
  """Recognize an unchanged project-created whole-value native aggregation."""
652
658
  if project_count_aggregation(aggregation):
653
659
  return True
@@ -709,14 +715,16 @@ class _DistinctState:
709
715
  return len(self.hashable) + len(self.unhashable)
710
716
 
711
717
 
712
- AggregationItems = tuple[tuple[str, Aggregator], ...]
718
+ AggregationItems = CollectorItems
713
719
 
714
720
 
715
- def prepare_aggregations(aggregations: Mapping[str, Aggregator]) -> AggregationItems:
721
+ def prepare_aggregations(
722
+ aggregations: Mapping[str, Collector[Any, Any, Any]],
723
+ ) -> AggregationItems:
716
724
  """Validate named aggregators and freeze mapping order as `(name, aggregator)` pairs.
717
725
 
718
- The mapping must be nonempty, names must be truthy, and every value must be an
719
- :class:`Aggregator` rather than an arbitrary collector.
726
+ The mapping must be nonempty, names must be truthy, and every value must be a
727
+ :class:`Collector`, including aggregators built by :data:`agg`.
720
728
  """
721
729
  if not aggregations:
722
730
  raise ValueError("aggregate requires at least one named aggregation")
@@ -724,8 +732,8 @@ def prepare_aggregations(aggregations: Mapping[str, Aggregator]) -> AggregationI
724
732
  for name, aggregation in items:
725
733
  if not name:
726
734
  raise ValueError("aggregate names cannot be empty")
727
- if not isinstance(aggregation, Aggregator):
728
- raise TypeError(f"aggregate {name!r} must be an Aggregator")
735
+ if not isinstance(aggregation, Collector):
736
+ raise TypeError(f"aggregate {name!r} must be an Aggregator or Collector")
729
737
  return items
730
738
 
731
739
 
@@ -799,6 +807,75 @@ def finish_native_aggregations(
799
807
  return result
800
808
 
801
809
 
810
+ def _extremum_by(selector: Selector, better: Callable[[Any, Any], bool]) -> Aggregator:
811
+ """Retain a winning item and its once-selected key without comparing payloads."""
812
+ select = compile_selector(selector)
813
+
814
+ def step(state: Any, row: Any) -> Any:
815
+ key = select(row)
816
+ return (key, row) if state is _MISSING or better(key, state[0]) else state
817
+
818
+ return Aggregator(
819
+ lambda: _MISSING,
820
+ step,
821
+ lambda state: None if state is _MISSING else state[1],
822
+ )
823
+
824
+
825
+ @dataclass(slots=True, eq=False)
826
+ class _RankedItem:
827
+ """Order heap entries by key and encounter index, never by their payload."""
828
+
829
+ key: Any
830
+ index: int
831
+ value: Any
832
+ largest: bool
833
+
834
+ def __lt__(self, other: _RankedItem) -> bool:
835
+ if self.key is other.key or self.key == other.key:
836
+ return self.index > other.index
837
+ return bool(self.key < other.key if self.largest else other.key < self.key)
838
+
839
+
840
+ @dataclass(slots=True)
841
+ class _RankedState:
842
+ """Retain only the requested winners and the next encounter index."""
843
+
844
+ heap: list[_RankedItem]
845
+ index: int = 0
846
+
847
+
848
+ def _ranked(count: int, key: Selector | None, *, largest: bool) -> Aggregator:
849
+ size = operator.index(count)
850
+ if size < 0:
851
+ raise ValueError("count must be non-negative")
852
+ select = _identity if key is None else compile_selector(key)
853
+ if size == 0:
854
+ return Aggregator(list, lambda state, _row: state, done=lambda _state: True)
855
+
856
+ def step(state: _RankedState, row: Any) -> _RankedState:
857
+ selected = select(row)
858
+ index = state.index
859
+ state.index += 1
860
+ if len(state.heap) < size:
861
+ heapq.heappush(state.heap, _RankedItem(selected, index, row, largest))
862
+ else:
863
+ worst = state.heap[0]
864
+ # Later ties cannot displace an earlier item. Avoid allocating an
865
+ # entry for every discarded row while keeping key comparisons live.
866
+ if worst.key is selected or worst.key == selected:
867
+ return state
868
+ if worst.key < selected if largest else selected < worst.key:
869
+ heapq.heapreplace(state.heap, _RankedItem(selected, index, row, largest))
870
+ return state
871
+
872
+ return Aggregator(
873
+ lambda: _RankedState([]),
874
+ step,
875
+ lambda state: [item.value for item in sorted(state.heap, reverse=True)],
876
+ )
877
+
878
+
802
879
  class _AggFactory:
803
880
  """Construct built-in aggregators for `Flow.aggregate` and tabular grouping."""
804
881
 
@@ -1066,6 +1143,72 @@ class _AggFactory:
1066
1143
  selector,
1067
1144
  )
1068
1145
 
1146
+ def min_by(self, selector: Selector) -> Aggregator:
1147
+ """Keep the first whole input item with the smallest selected key.
1148
+
1149
+ The selector runs once per item. Each subsequent key is compared with the
1150
+ retained key using `<`; ties keep the earlier item. One key and one item
1151
+ are retained per aggregation state. The source is fully consumed.
1152
+
1153
+ Args:
1154
+ selector: Required field, path, index, expression, or callable selecting a key.
1155
+
1156
+ Returns:
1157
+ An aggregator returning the original winning item, or `None` for empty input.
1158
+ Keys follow Python comparison rules, including errors and NaN behavior.
1159
+ """
1160
+ return _extremum_by(selector, operator.lt)
1161
+
1162
+ def max_by(self, selector: Selector) -> Aggregator:
1163
+ """Keep the first whole input item with the largest selected key.
1164
+
1165
+ The selector runs once per item. Each subsequent key is compared with the
1166
+ retained key using `>`; ties keep the earlier item. One key and one item
1167
+ are retained per aggregation state. The source is fully consumed.
1168
+
1169
+ Args:
1170
+ selector: Required field, path, index, expression, or callable selecting a key.
1171
+
1172
+ Returns:
1173
+ An aggregator returning the original winning item, or `None` for empty input.
1174
+ Keys follow Python comparison rules, including errors and NaN behavior.
1175
+ """
1176
+ return _extremum_by(selector, operator.gt)
1177
+
1178
+ def top(self, count: int, *, key: Selector | None = None) -> Aggregator:
1179
+ """Keep up to `count` whole items in descending key order.
1180
+
1181
+ Equal keys retain input order. The key runs once per item and payloads
1182
+ are never compared. Each state retains at most `count` items. Keys use
1183
+ Python heap and sorting comparisons; use consistently ordered keys.
1184
+
1185
+ Args:
1186
+ count: Non-negative integer number of items to retain.
1187
+ key: Optional selector; `None` compares whole items.
1188
+
1189
+ Returns:
1190
+ An aggregator returning a list, empty for empty input or zero count.
1191
+ Zero count does not invoke the key or pull an independent source.
1192
+ """
1193
+ return _ranked(count, key, largest=True)
1194
+
1195
+ def bottom(self, count: int, *, key: Selector | None = None) -> Aggregator:
1196
+ """Keep up to `count` whole items in ascending key order.
1197
+
1198
+ Equal keys retain input order. The key runs once per item and payloads
1199
+ are never compared. Each state retains at most `count` items. Keys use
1200
+ Python heap and sorting comparisons; use consistently ordered keys.
1201
+
1202
+ Args:
1203
+ count: Non-negative integer number of items to retain.
1204
+ key: Optional selector; `None` compares whole items.
1205
+
1206
+ Returns:
1207
+ An aggregator returning a list, empty for empty input or zero count.
1208
+ Zero count does not invoke the key or pull an independent source.
1209
+ """
1210
+ return _ranked(count, key, largest=False)
1211
+
1069
1212
  def first(self, selector: Selector | None = None) -> Aggregator:
1070
1213
  """Build an aggregator that stops after the first selected value.
1071
1214