fpstreams 2.3.0__tar.gz → 2.4.1__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 (207) hide show
  1. {fpstreams-2.3.0 → fpstreams-2.4.1}/CHANGELOG.md +136 -0
  2. {fpstreams-2.3.0 → fpstreams-2.4.1}/PKG-INFO +29 -12
  3. {fpstreams-2.3.0 → fpstreams-2.4.1}/README.md +28 -11
  4. {fpstreams-2.3.0 → fpstreams-2.4.1}/pyproject.toml +1 -1
  5. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/Cargo.lock +1 -1
  6. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/Cargo.toml +1 -1
  7. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/__init__.py +7 -2
  8. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/collecting/aggregation.py +152 -9
  9. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/collecting/collector.py +47 -12
  10. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/collecting/program.py +13 -4
  11. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/collecting/reducer.py +23 -12
  12. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/arrow.py +180 -48
  13. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/async_iterators.py +78 -1
  14. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/async_ops.py +4 -0
  15. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/async_scheduler.py +11 -2
  16. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/relational/__init__.py +31 -553
  17. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/relational/arrow_group.py +3 -1
  18. fpstreams-2.4.1/src/fpstreams/execution/relational/collector_group.py +203 -0
  19. fpstreams-2.4.1/src/fpstreams/execution/relational/native_group.py +428 -0
  20. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/sync_ops.py +58 -1
  21. fpstreams-2.4.1/src/fpstreams/execution/window_reduce.py +135 -0
  22. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/physical/async_plan.py +2 -1
  23. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/arrow.py +11 -16
  24. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/async_.py +20 -2
  25. fpstreams-2.4.1/src/fpstreams/planning/compiler.py +424 -0
  26. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/explain.py +6 -0
  27. fpstreams-2.4.1/src/fpstreams/planning/relational.py +320 -0
  28. fpstreams-2.3.0/src/fpstreams/planning/compiler.py → fpstreams-2.4.1/src/fpstreams/planning/relational_specs.py +4 -652
  29. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/semantic_rules.py +25 -1
  30. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/sync.py +22 -2
  31. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/__init__.py +2 -1
  32. fpstreams-2.4.1/src/fpstreams/runtime/executor.py +118 -0
  33. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/limits.py +7 -2
  34. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/report.py +100 -5
  35. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/tasks.py +5 -2
  36. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/storage/__init__.py +3 -1
  37. fpstreams-2.4.1/src/fpstreams/storage/checkpoint.py +165 -0
  38. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/storage/codec.py +2 -0
  39. fpstreams-2.4.1/src/fpstreams/streams/_flow_frequencies.py +203 -0
  40. fpstreams-2.4.1/src/fpstreams/streams/_flow_materialize.py +406 -0
  41. fpstreams-2.4.1/src/fpstreams/streams/_flow_numeric.py +354 -0
  42. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/streams/async_flow.py +179 -10
  43. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/streams/async_terminals.py +25 -5
  44. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/streams/flow.py +142 -14
  45. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/streams/flow_terminals.py +206 -788
  46. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/streams/pairs.py +9 -5
  47. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/arrow.py +156 -55
  48. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/grouped.py +4 -6
  49. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/io.py +35 -10
  50. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/records.py +37 -1
  51. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/rows.py +16 -47
  52. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/testing.py +37 -22
  53. {fpstreams-2.3.0 → fpstreams-2.4.1}/LICENSE +0 -0
  54. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/build.rs +0 -0
  55. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/common.rs +0 -0
  56. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/float/affine_pair.rs +0 -0
  57. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/float/endpoints.rs +0 -0
  58. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/float.rs +0 -0
  59. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/integer/endpoints.rs +0 -0
  60. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/integer.rs +0 -0
  61. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/lib.rs +0 -0
  62. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/numeric_mean.rs +0 -0
  63. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/numpy_export.rs +0 -0
  64. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/numpy_group/buffer.rs +0 -0
  65. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/numpy_group.rs +0 -0
  66. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/pair/expr.rs +0 -0
  67. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/pair/prefix.rs +0 -0
  68. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/pair/row_filter.rs +0 -0
  69. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/pair/unique.rs +0 -0
  70. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/pair/value_filter.rs +0 -0
  71. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/pair/value_map.rs +0 -0
  72. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/pair.rs +0 -0
  73. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/pivot.rs +0 -0
  74. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/records.rs +0 -0
  75. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational/adapters/namedtuple.rs +0 -0
  76. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational/adapters.rs +0 -0
  77. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational/global_numeric.rs +0 -0
  78. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational/group_numeric.rs +0 -0
  79. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational/group_pair_expr.rs +0 -0
  80. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational/join_callable/many.rs +0 -0
  81. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational/join_callable/unique.rs +0 -0
  82. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational/join_callable.rs +0 -0
  83. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational/join_exact.rs +0 -0
  84. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational/join_i64.rs +0 -0
  85. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational.rs +0 -0
  86. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational_fixed/composite.rs +0 -0
  87. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational_fixed/global_multi/same_field.rs +0 -0
  88. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational_fixed/global_multi.rs +0 -0
  89. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational_fixed/group_multi.rs +0 -0
  90. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational_fixed/single.rs +0 -0
  91. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/relational_fixed.rs +0 -0
  92. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/scalar_sort.rs +0 -0
  93. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/scalar_unique.rs +0 -0
  94. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/select.rs +0 -0
  95. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/adapters.rs +0 -0
  96. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/group_exact/pairs.rs +0 -0
  97. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/group_exact/rows.rs +0 -0
  98. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/group_exact.rs +0 -0
  99. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/group_fixed.rs +0 -0
  100. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/join_callable/behavior.rs +0 -0
  101. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/join_callable/direct.rs +0 -0
  102. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/join_callable.rs +0 -0
  103. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/join_exact.rs +0 -0
  104. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/numeric/buffers.rs +0 -0
  105. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/numeric/kernels.rs +0 -0
  106. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/numeric/reductions.rs +0 -0
  107. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/numeric.rs +0 -0
  108. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests/numpy_group.rs +0 -0
  109. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/tests.rs +0 -0
  110. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/unnest.rs +0 -0
  111. {fpstreams-2.3.0 → fpstreams-2.4.1}/rust/src/unpivot.rs +0 -0
  112. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/_native.pyi +0 -0
  113. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/_provenance.py +0 -0
  114. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/aggregate.py +0 -0
  115. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/async_flow.py +0 -0
  116. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/collecting/__init__.py +0 -0
  117. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/collecting/_collector_base.py +0 -0
  118. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/collecting/aggregate_program.py +0 -0
  119. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/collecting/statistics.py +0 -0
  120. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/collectors.py +0 -0
  121. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/column.py +0 -0
  122. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/errors.py +0 -0
  123. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/exceptions.py +0 -0
  124. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/__init__.py +0 -0
  125. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/_pair_aggregate.py +0 -0
  126. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/_pair_dict.py +0 -0
  127. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/_pair_row_filter.py +0 -0
  128. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/_rows_fusion.py +0 -0
  129. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/_scalar_fusion.py +0 -0
  130. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/async_map.py +0 -0
  131. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/async_merge.py +0 -0
  132. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/async_prefetch.py +0 -0
  133. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/async_queue.py +0 -0
  134. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/async_timers.py +0 -0
  135. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/native.py +0 -0
  136. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/numpy_group.py +0 -0
  137. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/numpy_prefix.py +0 -0
  138. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/physical.py +0 -0
  139. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/relational/arrow_global.py +0 -0
  140. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/relational/arrow_group_rows.py +0 -0
  141. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/relational/join.py +0 -0
  142. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/sorted_streams.py +0 -0
  143. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/sorting.py +0 -0
  144. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/execution/sync.py +0 -0
  145. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/expr.py +0 -0
  146. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/expressions/__init__.py +0 -0
  147. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/expressions/_codegen.py +0 -0
  148. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/expressions/_row_codegen.py +0 -0
  149. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/expressions/program.py +0 -0
  150. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/expressions/row.py +0 -0
  151. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/expressions/row_eval.py +0 -0
  152. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/expressions/row_ir.py +0 -0
  153. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/expressions/scalar.py +0 -0
  154. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/expressions/selectors.py +0 -0
  155. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/expressions/typed_ir.py +0 -0
  156. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/functional.py +0 -0
  157. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/gather.py +0 -0
  158. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/io_safety.py +0 -0
  159. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/option.py +0 -0
  160. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/physical/__init__.py +0 -0
  161. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/physical/compiled.py +0 -0
  162. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/physical/kernel_cache.py +0 -0
  163. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/physical/plan.py +0 -0
  164. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/physical/relational.py +0 -0
  165. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/__init__.py +0 -0
  166. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/_pair_stages.py +0 -0
  167. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/arrow_source.py +0 -0
  168. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/async_utils.py +0 -0
  169. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/gather.py +0 -0
  170. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/logical.py +0 -0
  171. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/native.py +0 -0
  172. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/numpy.py +0 -0
  173. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/pair_i64_expression.py +0 -0
  174. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/plan_cache.py +0 -0
  175. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/semantic_analyzer.py +0 -0
  176. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/semantics.py +0 -0
  177. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/planning/source.py +0 -0
  178. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/primitives/__init__.py +0 -0
  179. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/primitives/option.py +0 -0
  180. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/primitives/result.py +0 -0
  181. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/py.typed +0 -0
  182. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/result.py +0 -0
  183. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/_distinct.py +0 -0
  184. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/failpoints.py +0 -0
  185. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/files.py +0 -0
  186. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/iterators.py +0 -0
  187. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/metrics.py +0 -0
  188. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/query.py +0 -0
  189. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/resources.py +0 -0
  190. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/runtime/spill.py +0 -0
  191. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/storage/spill_store.py +0 -0
  192. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/streams/__init__.py +0 -0
  193. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/streams/_async_identity.py +0 -0
  194. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/streams/_flow_structural_list.py +0 -0
  195. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/streams/_flow_unique_list.py +0 -0
  196. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/__init__.py +0 -0
  197. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/_text_sources.py +0 -0
  198. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/dataframe.py +0 -0
  199. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/factory.py +0 -0
  200. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/join.py +0 -0
  201. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/numpy.py +0 -0
  202. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/polars.py +0 -0
  203. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/spill.py +0 -0
  204. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/spill_io.py +0 -0
  205. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/spill_limits.py +0 -0
  206. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/sql.py +0 -0
  207. {fpstreams-2.3.0 → fpstreams-2.4.1}/src/fpstreams/tabular/sqlite_sink.py +0 -0
@@ -3,6 +3,142 @@
3
3
  This file records user-visible and compatibility-relevant changes in fpstreams 2,
4
4
  including changed defaults.
5
5
 
6
+ ## Unreleased
7
+
8
+ ## 2.4.1 - 2026-10-18
9
+
10
+ ### Changed
11
+
12
+ - Separate relational candidate recognition and tree compilation from the general
13
+ compiler. Native grouping, Collector grouping, and synchronous terminal helpers
14
+ now live in domain modules; public interfaces and execution rules stay the same.
15
+
16
+ ## 2.4.0 - 2026-10-18
17
+
18
+ ### Added
19
+
20
+ - `ExecutionReport.to_dict()` and `ExecutionEvent.to_dict()` export plain
21
+ metadata. Report and plan dictionaries carry an independent schema version;
22
+ reports include metric units, query-owned resource scope, and event limits.
23
+
24
+ - `Flow.collapse(predicate, collector)` and its async equivalent reduce each
25
+ adjacent run with existing Collector state. Completion stops further steps
26
+ within a run while predicates continue to find its boundary. Collector hooks
27
+ remain synchronous, and awaitable finisher results are emitted as data.
28
+
29
+ - `Flow.window_reduce(size, combine, step=1)` and `AsyncFlow.window_reduce(...)`
30
+ reuse ordered partial aggregates across count-based windows. The synchronous
31
+ combine function must be associative and nonmutating. Cached entries are
32
+ bounded by `size + 1`; byte sizes and individual stack transfers are not.
33
+ Nonoverlapping windows use a running fold without retaining a raw window buffer.
34
+
35
+ - `flow.unfold(seed, function)` and `aflow.unfold(...)` generate values lazily
36
+ from `(value, next_state)` transitions until the callback returns `None`.
37
+ Class constructors expose the same operation; async transitions may be awaitable.
38
+ - `Flow.map_accum(initial, function)` and `AsyncFlow.map_accum(...)` thread
39
+ state through ordered callbacks returning `(next_state, output)`, emitting
40
+ one output per input through the existing gather/scan execution paths.
41
+ - `agg.min_by(selector)` and `agg.max_by(selector)` retain a whole winning
42
+ item in one pass. Ties keep the first item and empty input returns `None`;
43
+ the factories reuse existing grouped, async, and spill aggregation.
44
+ - `agg.top(count, key=...)` and `agg.bottom(count, key=...)` retain a bounded
45
+ list of whole items per group, ordered by key with stable ties. They compose
46
+ with the existing global, async, sorted, and spill aggregation paths.
47
+ - `run_with_report()` includes up to 64 immutable plan and route events in
48
+ `report.events`, with `events_truncated` marking further observations. Child
49
+ plans keep their own labels without replacing the outer route. Flow, Rows,
50
+ Pairs, and AsyncFlow use the same recorder.
51
+ - `BoundedExecutor(executor, max_pending=...)` shares one unfinished-task budget
52
+ across independent queries. It accepts existing thread or process pools,
53
+ releases capacity on completion or cancellation, and keeps the borrowed pool open.
54
+ - `FileCheckpoint(directory, version=...)` stores verified outputs for stable
55
+ file partitions. Each successful callback publishes an output file and its
56
+ metadata together; later runs verify input and output hashes before reuse.
57
+
58
+ ### Changed
59
+
60
+ - `filter_map()` avoids intermediate tuple adaptation in synchronous pipelines
61
+ and the extra predicate callback in async serial fusion. It calls the mapper
62
+ once per input and keeps every result except `None`.
63
+ - Proven primitive Arrow comparison chains can share a selection mask,
64
+ filtering payload once or counting directly from the mask. Offline Optuna
65
+ tuning favored nonempty batches with at least one column in the measured shapes.
66
+ - Spill decoding releases encoded buffers before yielding decoded records
67
+ and releases completed decoded frames before reading the next frame.
68
+
69
+ - `Collectors.head()` and `tail()` accept integer-index objects, with matching
70
+ `SupportsIndex` annotations. Partitioned reducer execution and reducer-law
71
+ layouts use the same integer protocol.
72
+ - `Collectors.teeing()` can combine partial states when both downstream
73
+ collectors provide combiners. It keeps their live callbacks and carries no
74
+ inferred reducer laws or native metadata.
75
+ - Named `aggregate()` and `Pairs.aggregate_values()` accept any `Collector`,
76
+ including composed `Collectors` and existing `agg` factory results. Global,
77
+ async, grouped, sorted, and spill paths share the existing collector lifecycle.
78
+ - Multi-collector Python aggregation reuses its state-list references within
79
+ each row while retaining live callbacks and their declaration order.
80
+ - Retained primitive Arrow tables and record batches can keep their native
81
+ buffers when `arrow_batches(schema=...)` receives the matching output schema,
82
+ including metadata. Safe direct-field projections and primitive filters can
83
+ execute columnarly. Casts, Python callbacks, and unsupported sources retain
84
+ the existing conversion path.
85
+ - Arrow execution accepts chains of safe direct comparisons followed by a
86
+ direct projection. Parquet keeps only the leading comparison as an automatic
87
+ scan hint; all filters remain in the ordered residual program.
88
+ - Default local Parquet scans with one fragment can omit safe unused fields
89
+ from validation scans, retaining query dependencies and fields whose Python
90
+ conversion can fail. Sources with possible scanner casts retain the full
91
+ caller-visible projection before automatic filtering and projection.
92
+
93
+ ### Fixed
94
+
95
+ - Query-wide task/file limits and task-scope limits reject noninteger values,
96
+ including NaN, before resource acquisition. Integer-index values are
97
+ normalized once during construction.
98
+
99
+ - `Collectors.first()` and `head(count)` preserve completed state when used
100
+ inside `grouping_by` or `partitioning_by`. Previously, `first()` returned the
101
+ last group item and `head()` could retain the whole group, even with a zero
102
+ count. Grouping adapters continue to read all input and step custom collectors.
103
+ - `Flow` and `AsyncFlow` validate `take`, `limit`, `drop`, and `skip` counts
104
+ through the integer index protocol before building the operation. Noninteger
105
+ values, including NaN and infinity, raise `TypeError` before opening the source.
106
+ This prevents async prefix operations from silently rounding counts or losing
107
+ their limit.
108
+ - `Flow.collect()` and `AsyncFlow.collect()` preserve a single streaming
109
+ collector's finished result type in type checkers. Named collectors return
110
+ a dictionary, while optional or dynamically forwarded arguments retain the
111
+ general result union. Runtime dispatch and validation are unchanged.
112
+ - `Collectors.head()` and `tail()` reject floats, including NaN and infinity,
113
+ at construction. Partitioned reducer execution validates `partition_size`
114
+ before creating state or opening input, preventing non-integer values from
115
+ silently removing partition boundaries.
116
+ - Partitioned reducer execution closes its input iterator on exhaustion and
117
+ failure, before merging states or finishing. Cleanup failures preserve an
118
+ active source or callback exception.
119
+ - Collector programs reject missing combiners before invoking any merge
120
+ callback, preventing avoidable partial mutations on invalid merge requests.
121
+ - Reducer law checks build independent states for each merge expression,
122
+ preventing in-place combiners from producing false identity or associativity
123
+ results. The input iterator closes before the examples are replayed.
124
+ - Unsafe Arrow batches evaluate filters in row order and stop when downstream
125
+ consumers stop, preserving Python exception order and early-exit behavior.
126
+ - Arrow prefix fallbacks reuse converted rows, so custom extension scalars
127
+ cross the Python conversion boundary once per input batch.
128
+ - Automatic Parquet hints preserve conversion errors in discarded fields and
129
+ rows, including casts across fragments and supplied dataset schemas.
130
+ Types whose Python conversion can fail are validated before automatic
131
+ filtering or projection; explicit source `columns` and `filter` still define
132
+ which values the source exposes.
133
+ - `scan_csv()` validates every caller-visible column before query projection,
134
+ preserving conversion errors in later CSV blocks. It opens the typed source
135
+ once; explicit source conversion options still control included columns.
136
+ - An explicit zero-column Arrow schema preserves the input row count in
137
+ `arrow_batches()` and `to_arrow()`, including partial final batches.
138
+ - Polars batch exports preserve the height of zero-column inputs. Parquet
139
+ writes reject nonempty rows without columns, preserving an existing target
140
+ instead of publishing a file that loses the input row count.
141
+
6
142
  ## 2.3.0 - 2026-10-15
7
143
 
8
144
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fpstreams
3
- Version: 2.3.0
3
+ Version: 2.4.1
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3.11
@@ -66,9 +66,10 @@ 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.3.0](https://github.com/steventimes/fpstreams/releases/tag/v2.3.0).
70
- It adds explicit `map_batches()` for sync and async pipelines, reusable mapping
71
- executors, and async callback typing fixes. See the
69
+ The current stable release is [2.4.1](https://github.com/steventimes/fpstreams/releases/tag/v2.4.1).
70
+ This maintenance release reorganizes planner and execution internals with no public API changes.
71
+ Version 2.4.0 added stateful pipeline operations, composed aggregation, shared execution
72
+ limits, file recovery, and versioned execution metadata. See the
72
73
  [changelog](https://github.com/steventimes/fpstreams/blob/master/CHANGELOG.md) for its scope and the
73
74
  [performance guide](https://steventimes.github.io/fpstreams/user-guide/performance/#process-mapping-and-explicit-batches)
74
75
  for batch and executor examples. The
@@ -369,23 +370,39 @@ python_result = pipeline.with_engine("python").to_list()
369
370
  native_result = pipeline.with_engine("native").to_list()
370
371
  ~~~
371
372
 
372
- Use `run_with_report()` to execute a terminal and inspect its recorded route.
373
- Version 2.2.0 adds Pairs terminals such as
374
- `flow([("a", 1)]).pairs().run_with_report("group_values")`. See the
373
+ Use `run_with_report()` to get a terminal's value, recorded route, and query-owned
374
+ resource counts in one execution. Pairs supports terminals such as
375
+ `flow([("a", 1)]).pairs().run_with_report("group_values")`. Version 2.4.0 also
376
+ includes up to 64 child plan and route observations in `report.events`. See the
375
377
  [execution report guide](https://steventimes.github.io/fpstreams/user-guide/execution-reports/)
376
378
  for supported terminals and the limits of route reporting.
377
379
 
380
+ In 2.4.0, `report.to_dict()` exports versioned metadata with metric units,
381
+ resource scope, and event limits. It excludes the terminal value and does not
382
+ rerun the query. See the
383
+ [metadata export guide](https://steventimes.github.io/fpstreams/user-guide/execution-reports/#metadata-export-unreleased)
384
+ for JSON storage and caller-managed OpenLineage integration. Report export
385
+ requires 2.4.0 or newer.
386
+
387
+ Version 2.4.0 also includes `BoundedExecutor` for a shared submission limit
388
+ across queries and `FileCheckpoint` for verified file-partition recovery. See
389
+ the [resource controls](https://steventimes.github.io/fpstreams/user-guide/performance/#share-a-submission-budget-unreleased)
390
+ and [file-partition recipe](https://steventimes.github.io/fpstreams/user-guide/analytics/#resume-file-partitions-unreleased)
391
+ for ownership, memory, and retry semantics. These additions require 2.4.0 or newer.
392
+
393
+ In 2.4.0, `flow.unfold` / `aflow.unfold` generate values from explicit state
394
+ transitions. `Flow.map_accum` and `AsyncFlow.map_accum` update state while mapping
395
+ each input to one output. See the
396
+ [state examples](https://steventimes.github.io/fpstreams/reference/flow/#state-driven-generation-and-mapping-unreleased)
397
+ for termination, tuple ordering, and mutable-state ownership. These APIs require
398
+ 2.4.0 or newer.
399
+
378
400
  A forced native plan raises `NativeUnsupportedError` if its complete types or
379
401
  operations cannot run natively. In particular, the presence of an internal
380
402
  native relational specialization does not make the complete relation eligible
381
403
  for `with_engine("native")`. An unsupported forced relational plan fails before
382
404
  claiming its one-shot sources; `auto` selects a legal fallback path.
383
405
 
384
- [`run_with_report()`](https://steventimes.github.io/fpstreams/user-guide/execution-reports/)
385
- returns a terminal's value, recorded route, and query-owned resource counts in
386
- one execution. It records the outer plan and some direct paths; it does not
387
- identify every internal kernel or runtime fallback.
388
-
389
406
  Identity list and tuple materialization stays in Python. Large, supported integer
390
407
  sums and numeric range reductions may use Rust in `auto` mode. `count()` uses a
391
408
  known exact size in O(1) when no operation changes cardinality and the source is
@@ -11,9 +11,10 @@ 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.3.0](https://github.com/steventimes/fpstreams/releases/tag/v2.3.0).
15
- It adds explicit `map_batches()` for sync and async pipelines, reusable mapping
16
- executors, and async callback typing fixes. See the
14
+ The current stable release is [2.4.1](https://github.com/steventimes/fpstreams/releases/tag/v2.4.1).
15
+ This maintenance release reorganizes planner and execution internals with no public API changes.
16
+ Version 2.4.0 added stateful pipeline operations, composed aggregation, shared execution
17
+ limits, file recovery, and versioned execution metadata. See the
17
18
  [changelog](https://github.com/steventimes/fpstreams/blob/master/CHANGELOG.md) for its scope and the
18
19
  [performance guide](https://steventimes.github.io/fpstreams/user-guide/performance/#process-mapping-and-explicit-batches)
19
20
  for batch and executor examples. The
@@ -314,23 +315,39 @@ python_result = pipeline.with_engine("python").to_list()
314
315
  native_result = pipeline.with_engine("native").to_list()
315
316
  ~~~
316
317
 
317
- Use `run_with_report()` to execute a terminal and inspect its recorded route.
318
- Version 2.2.0 adds Pairs terminals such as
319
- `flow([("a", 1)]).pairs().run_with_report("group_values")`. See the
318
+ Use `run_with_report()` to get a terminal's value, recorded route, and query-owned
319
+ resource counts in one execution. Pairs supports terminals such as
320
+ `flow([("a", 1)]).pairs().run_with_report("group_values")`. Version 2.4.0 also
321
+ includes up to 64 child plan and route observations in `report.events`. See the
320
322
  [execution report guide](https://steventimes.github.io/fpstreams/user-guide/execution-reports/)
321
323
  for supported terminals and the limits of route reporting.
322
324
 
325
+ In 2.4.0, `report.to_dict()` exports versioned metadata with metric units,
326
+ resource scope, and event limits. It excludes the terminal value and does not
327
+ rerun the query. See the
328
+ [metadata export guide](https://steventimes.github.io/fpstreams/user-guide/execution-reports/#metadata-export-unreleased)
329
+ for JSON storage and caller-managed OpenLineage integration. Report export
330
+ requires 2.4.0 or newer.
331
+
332
+ Version 2.4.0 also includes `BoundedExecutor` for a shared submission limit
333
+ across queries and `FileCheckpoint` for verified file-partition recovery. See
334
+ the [resource controls](https://steventimes.github.io/fpstreams/user-guide/performance/#share-a-submission-budget-unreleased)
335
+ and [file-partition recipe](https://steventimes.github.io/fpstreams/user-guide/analytics/#resume-file-partitions-unreleased)
336
+ for ownership, memory, and retry semantics. These additions require 2.4.0 or newer.
337
+
338
+ In 2.4.0, `flow.unfold` / `aflow.unfold` generate values from explicit state
339
+ transitions. `Flow.map_accum` and `AsyncFlow.map_accum` update state while mapping
340
+ each input to one output. See the
341
+ [state examples](https://steventimes.github.io/fpstreams/reference/flow/#state-driven-generation-and-mapping-unreleased)
342
+ for termination, tuple ordering, and mutable-state ownership. These APIs require
343
+ 2.4.0 or newer.
344
+
323
345
  A forced native plan raises `NativeUnsupportedError` if its complete types or
324
346
  operations cannot run natively. In particular, the presence of an internal
325
347
  native relational specialization does not make the complete relation eligible
326
348
  for `with_engine("native")`. An unsupported forced relational plan fails before
327
349
  claiming its one-shot sources; `auto` selects a legal fallback path.
328
350
 
329
- [`run_with_report()`](https://steventimes.github.io/fpstreams/user-guide/execution-reports/)
330
- returns a terminal's value, recorded route, and query-owned resource counts in
331
- one execution. It records the outer plan and some direct paths; it does not
332
- identify every internal kernel or runtime fallback.
333
-
334
351
  Identity list and tuple materialization stays in Python. Large, supported integer
335
352
  sums and numeric range reductions may use Rust in `auto` mode. `count()` uses a
336
353
  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.3.0"
7
+ version = "2.4.1"
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.3.0"
7
+ version = "2.4.1"
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.3.0"
3
+ version = "2.4.1"
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.3.0"
47
+ __version__ = "2.4.1"
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