This is an automated email from the ASF dual-hosted git repository.
claudevdm pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new d97899b7ab8 Add Sample.Any to the Python SDK to match Java's
Sample.any (#39442)
d97899b7ab8 is described below
commit d97899b7ab8998499d5ee56f4052faa371facd47
Author: SreeramaYeshwanthGowd <[email protected]>
AuthorDate: Mon Aug 10 17:09:31 2026 +0530
Add Sample.Any to the Python SDK to match Java's Sample.any (#39442)
* Add Sample.Any to the Python SDK to match Java's Sample.any
Python only had Sample.FixedSizeGlobally, which runs a uniform reservoir
sample and returns a single list. Add Sample.Any, the equivalent of Java's
Sample.any, which returns up to n arbitrary elements as a PCollection
without
the random sampling cost. If the input has fewer than n elements, all are
returned. Includes unit tests on the DirectRunner and a CHANGES.md entry.
Fixes #18552
* Reject negative n in Sample.Any
Match Java's Sample.any, which rejects a negative limit at construction
time. Add a regression test.
* Add explicit type hints to the FlatMap in Sample.Any
Address review feedback: declare with_input_types(list[T]) and
with_output_types(T) on the FlatMap that flattens the combiner's output.
---------
Co-authored-by: Jack McCluskey
<[email protected]>
---
CHANGES.md | 1 +
sdks/python/apache_beam/transforms/combiners.py | 58 ++++++++++++++++++++++
.../apache_beam/transforms/combiners_test.py | 54 ++++++++++++++++++++
3 files changed, 113 insertions(+)
diff --git a/CHANGES.md b/CHANGES.md
index 6f61ef415b4..e2c3f262536 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -123,6 +123,7 @@
* (Python) Added `Watch`, a transform that polls a growing set of outputs for
each input element, deduplicates outputs across poll rounds, and stops per a
user-supplied termination condition
([#21521](https://github.com/apache/beam/issues/21521)).
* (Python) Added support to analyze core dumps created after python worker
segmentation faults with `pystack` (or `gdb` if installed) using the
`--profiler_agent=coredump` pipeline option.
([#39484](https://github.com/apache/beam/issues/39484)).
+* (Python) Added `Sample.Any`, the Python equivalent of Java's `Sample.any`,
which returns up to n arbitrary elements from a PCollection
([#18552](https://github.com/apache/beam/issues/18552)).
* (Java) Added per-element OpenTelemetry trace propagation across stages in
the Dataflow Streaming Runner. Enable it with
`--experiments=enable_otel_defaults,element_metadata_supported,disable_portable_worker`.
Cloud Trace incurs additional cost.
([#33176](https://github.com/apache/beam/issues/33176))
* (Java) Added OpenTelemetry header propagation support for both reads and
writes in KafkaIO and PubSubIO.
([#33176](https://github.com/apache/beam/issues/33176))
* (Java) Added OpenTelemetry tracing support for SpannerIO change streams
([#33176](https://github.com/apache/beam/issues/33176))
diff --git a/sdks/python/apache_beam/transforms/combiners.py
b/sdks/python/apache_beam/transforms/combiners.py
index 8d35405f3ff..c45ba4e89b9 100644
--- a/sdks/python/apache_beam/transforms/combiners.py
+++ b/sdks/python/apache_beam/transforms/combiners.py
@@ -597,6 +597,35 @@ class Sample(object):
def default_label(self):
return 'FixedSizePerKey(%d)' % self._n
+ @with_input_types(T)
+ @with_output_types(T)
+ class Any(ptransform.PTransform):
+ """Returns up to n arbitrary elements from the input PCollection.
+
+ This is the Python equivalent of Java's ``Sample.any``. Unlike
+ ``FixedSizeGlobally`` it does not sample uniformly at random, and it
returns
+ the selected elements rather than a single list. If the input has fewer
than
+ n elements, all of them are returned.
+ """
+ def __init__(self, n):
+ if n < 0:
+ raise ValueError('Expected non-negative n, received %s.' % n)
+ self._n = n
+
+ def expand(self, pcoll):
+ return (
+ pcoll
+ | core.CombineGlobally(_SampleAnyCombineFn(
+ self._n)).without_defaults()
+ | core.FlatMap(lambda elements: elements).with_input_types(
+ list[T]).with_output_types(T))
+
+ def display_data(self):
+ return {'n': self._n}
+
+ def default_label(self):
+ return 'Any(%d)' % self._n
+
@with_input_types(T)
@with_output_types(list[T])
@@ -636,6 +665,35 @@ class SampleCombineFn(core.CombineFn):
self._top_combiner.teardown()
+@with_input_types(T)
+@with_output_types(list[T])
+class _SampleAnyCombineFn(core.CombineFn):
+ """CombineFn that keeps up to n arbitrary elements (no random sampling)."""
+ def __init__(self, n):
+ super().__init__()
+ self._n = n
+
+ def create_accumulator(self):
+ return []
+
+ def add_input(self, accumulator, element):
+ if len(accumulator) < self._n:
+ accumulator.append(element)
+ return accumulator
+
+ def merge_accumulators(self, accumulators):
+ result = []
+ for accumulator in accumulators:
+ for element in accumulator:
+ if len(result) >= self._n:
+ return result
+ result.append(element)
+ return result
+
+ def extract_output(self, accumulator):
+ return accumulator
+
+
class _TupleCombineFnBase(core.CombineFn):
def __init__(self, *combiners, merge_accumulators_batch_size=None):
self._combiners = [core.CombineFn.maybe_from_callable(c) for c in
combiners]
diff --git a/sdks/python/apache_beam/transforms/combiners_test.py
b/sdks/python/apache_beam/transforms/combiners_test.py
index a7f35771961..14348bb8ce7 100644
--- a/sdks/python/apache_beam/transforms/combiners_test.py
+++ b/sdks/python/apache_beam/transforms/combiners_test.py
@@ -253,6 +253,7 @@ class CombineTest(unittest.TestCase):
individual_test_per_key_dd(combine.Sample.FixedSizePerKey, 5)
individual_test_per_key_dd(combine.Sample.FixedSizeGlobally, 5)
+ individual_test_per_key_dd(combine.Sample.Any, 5)
def test_combine_globally_display_data(self):
transform = beam.CombineGlobally(combine.Smallest(5))
@@ -359,6 +360,59 @@ class CombineTest(unittest.TestCase):
assert_that(result, matcher())
+ def test_sample_any(self):
+ with TestPipeline() as pipeline:
+ pcoll = pipeline | 'start' >> Create([1, 2, 3, 4, 5])
+ result = pcoll | 'sample-any' >> combine.Sample.Any(3)
+
+ def check(actual):
+ assert len(actual) == 3, actual
+ for element in actual:
+ assert element in [1, 2, 3, 4, 5], element
+
+ assert_that(result, check)
+
+ def test_sample_any_at_most_input_size(self):
+ with TestPipeline() as pipeline:
+ pcoll = pipeline | 'start' >> Create([1, 2])
+ result = pcoll | 'sample-any' >> combine.Sample.Any(5)
+ assert_that(result, equal_to([1, 2]))
+
+ def test_sample_any_windowed(self):
+ with TestPipeline() as pipeline:
+ pcoll = (
+ pipeline
+ | 'start' >> Create([1, 2, 3, 4])
+ | 'timestamp' >> Map(lambda x: TimestampedValue(x, x * 10))
+ | 'window' >> WindowInto(FixedWindows(15)))
+ result = pcoll | 'sample-any' >> combine.Sample.Any(1)
+
+ def check(actual):
+ # Timestamps 10, 20, 30, 40 fall into fixed windows [0, 15), [15, 30)
+ # and [30, 45), holding {1}, {2} and {3, 4}. One element is sampled
from
+ # each window that has elements.
+ assert len(actual) == 3, actual
+ for element in actual:
+ assert element in [1, 2, 3, 4], element
+
+ assert_that(result, check)
+
+ def test_sample_any_empty(self):
+ with TestPipeline() as pipeline:
+ pcoll = pipeline | 'start' >> Create([])
+ result = pcoll | 'sample-any' >> combine.Sample.Any(3)
+ assert_that(result, equal_to([]))
+
+ def test_sample_any_zero(self):
+ with TestPipeline() as pipeline:
+ pcoll = pipeline | 'start' >> Create([1, 2, 3])
+ result = pcoll | 'sample-any' >> combine.Sample.Any(0)
+ assert_that(result, equal_to([]))
+
+ def test_sample_any_negative_n(self):
+ with self.assertRaises(ValueError):
+ combine.Sample.Any(-1)
+
def test_tuple_combine_fn(self):
with TestPipeline() as p:
result = (