Skip to content

Commit e3fee51

Browse files
authored
[YAML] Add Partition transform. (#30368)
1 parent 25805db commit e3fee51

7 files changed

Lines changed: 451 additions & 15 deletions

File tree

sdks/python/apache_beam/yaml/programming_guide_test.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,25 @@ def extract_timestamp(x):
404404
# [END setting_timestamp]
405405
''')
406406

407+
def test_partition(self):
408+
with beam.Pipeline(options=beam.options.pipeline_options.PipelineOptions(
409+
pickle_library='cloudpickle')) as p:
410+
elements = p | beam.Create([
411+
beam.Row(percentile=1),
412+
beam.Row(percentile=20),
413+
beam.Row(percentile=90),
414+
])
415+
_ = elements | YamlTransform(
416+
'''
417+
# [START model_multiple_pcollections_partition]
418+
type: Partition
419+
config:
420+
by: str(percentile // 10)
421+
language: python
422+
outputs: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
423+
# [END model_multiple_pcollections_partition]
424+
''')
425+
407426

408427
if __name__ == '__main__':
409428
logging.getLogger().setLevel(logging.INFO)

sdks/python/apache_beam/yaml/readme_test.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,25 @@ def expand(self, pcoll):
128128
lambda _: 1, sum, 'count')
129129

130130

131+
class _Fakes:
132+
fn = str
133+
134+
class SomeTransform(beam.PTransform):
135+
def __init__(*args, **kwargs):
136+
pass
137+
138+
def expand(self, pcoll):
139+
return pcoll
140+
141+
131142
RENDER_DIR = None
132143
TEST_TRANSFORMS = {
133144
'Sql': FakeSql,
134145
'ReadFromPubSub': FakeReadFromPubSub,
135146
'WriteToPubSub': FakeWriteToPubSub,
136147
'SomeGroupingTransform': FakeAggregation,
148+
'SomeTransform': _Fakes.SomeTransform,
149+
'AnotherTransform': _Fakes.SomeTransform,
137150
}
138151

139152

@@ -155,7 +168,7 @@ def input_file(self, name, content):
155168
return path
156169

157170
def input_csv(self):
158-
return self.input_file('input.csv', 'col1,col2,col3\nabc,1,2.5\n')
171+
return self.input_file('input.csv', 'col1,col2,col3\na,1,2.5\n')
159172

160173
def input_tsv(self):
161174
return self.input_file('input.tsv', 'col1\tcol2\tcol3\nabc\t1\t2.5\n')
@@ -250,13 +263,15 @@ def parse_test_methods(markdown_lines):
250263
else:
251264
if code_lines:
252265
if code_lines[0].startswith('- type:'):
266+
is_chain = not any('input:' in line for line in code_lines)
253267
# Treat this as a fragment of a larger pipeline.
254268
# pylint: disable=not-an-iterable
255269
code_lines = [
256270
'pipeline:',
257-
' type: chain',
271+
' type: chain' if is_chain else '',
258272
' transforms:',
259273
' - type: ReadFromCsv',
274+
' name: input',
260275
' config:',
261276
' path: whatever',
262277
] + [' ' + line for line in code_lines]
@@ -278,17 +293,6 @@ def createTestSuite(name, path):
278293
return type(name, (unittest.TestCase, ), dict(parse_test_methods(readme)))
279294

280295

281-
class _Fakes:
282-
fn = str
283-
284-
class SomeTransform(beam.PTransform):
285-
def __init__(*args, **kwargs):
286-
pass
287-
288-
def expand(self, pcoll):
289-
return pcoll
290-
291-
292296
# These are copied from $ROOT/website/www/site/content/en/documentation/sdks
293297
# at build time.
294298
YAML_DOCS_DIR = os.path.join(os.path.join(os.path.dirname(__file__), 'docs'))

sdks/python/apache_beam/yaml/yaml_mapping.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from typing import Callable
2525
from typing import Collection
2626
from typing import Dict
27+
from typing import List
2728
from typing import Mapping
2829
from typing import NamedTuple
2930
from typing import Optional
@@ -42,6 +43,7 @@
4243
from apache_beam.typehints import row_type
4344
from apache_beam.typehints import schemas
4445
from apache_beam.typehints import trivial_inference
46+
from apache_beam.typehints import typehints
4547
from apache_beam.typehints.row_type import RowTypeConstraint
4648
from apache_beam.typehints.schemas import named_fields_from_element_type
4749
from apache_beam.utils import python_callable
@@ -569,6 +571,86 @@ def extract_expr(name, v):
569571
return pcoll | sql_transform_constructor(query)
570572

571573

574+
@beam.ptransform.ptransform_fn
575+
def _Partition(
576+
pcoll,
577+
by: Union[str, Dict[str, str]],
578+
outputs: List[str],
579+
unknown_output: Optional[str] = None,
580+
error_handling: Optional[Mapping[str, Any]] = None,
581+
language: Optional[str] = 'generic'):
582+
"""Splits an input into several distinct outputs.
583+
584+
Each input element will go to a distinct output based on the field or
585+
function given in the `by` configuration parameter.
586+
587+
Args:
588+
by: A field, callable, or expression giving the destination output for
589+
this element. Should return a string that is a member of the `outputs`
590+
parameter. If `unknown_output` is also set, other returns values are
591+
accepted as well, otherwise an error will be raised.
592+
outputs: The set of outputs into which this input is being partitioned.
593+
unknown_output: (Optional) If set, indicates a destination output for any
594+
elements that are not assigned an output listed in the `outputs`
595+
parameter.
596+
error_handling: (Optional) Whether and how to handle errors during
597+
partitioning.
598+
language: (Optional) The language of the `by` expression.
599+
"""
600+
split_fn = _as_callable_for_pcoll(pcoll, by, 'by', language)
601+
try:
602+
split_fn_output_type = trivial_inference.infer_return_type(
603+
split_fn, [pcoll.element_type])
604+
except (TypeError, ValueError):
605+
pass
606+
else:
607+
if not typehints.is_consistent_with(split_fn_output_type,
608+
typehints.Optional[str]):
609+
raise ValueError(
610+
f'Partition function "{by}" must return a string type '
611+
f'not {split_fn_output_type}')
612+
error_output = error_handling['output'] if error_handling else None
613+
if error_output in outputs:
614+
raise ValueError(
615+
f'Error handling output "{error_output}" '
616+
f'cannot be among the listed outputs {outputs}')
617+
T = TypeVar('T')
618+
619+
def split(element):
620+
tag = split_fn(element)
621+
if tag is None:
622+
tag = unknown_output
623+
if not isinstance(tag, str):
624+
raise ValueError(
625+
f'Returned output name "{tag}" of type {type(tag)} '
626+
f'from "{by}" must be a string.')
627+
if tag not in outputs:
628+
if unknown_output:
629+
tag = unknown_output
630+
else:
631+
raise ValueError(f'Unknown output name "{tag}" from {by}')
632+
return beam.pvalue.TaggedOutput(tag, element)
633+
634+
output_set = set(outputs)
635+
if unknown_output:
636+
output_set.add(unknown_output)
637+
if error_output:
638+
output_set.add(error_output)
639+
mapping_transform = beam.Map(split)
640+
if error_output:
641+
mapping_transform = mapping_transform.with_exception_handling(
642+
**exception_handling_args(error_handling))
643+
else:
644+
mapping_transform = mapping_transform.with_outputs(*output_set)
645+
splits = pcoll | mapping_transform.with_input_types(T).with_output_types(T)
646+
result = {out: getattr(splits, out) for out in output_set}
647+
if error_output:
648+
result[
649+
error_output] = result[error_output] | _map_errors_to_standard_format(
650+
pcoll.element_type)
651+
return result
652+
653+
572654
@beam.ptransform.ptransform_fn
573655
@maybe_with_exception_handling_transform_fn
574656
def _AssignTimestamps(
@@ -588,7 +670,8 @@ def _AssignTimestamps(
588670
Args:
589671
timestamp: A field, callable, or expression giving the new timestamp.
590672
language: The language of the timestamp expression.
591-
error_handling: Whether and how to handle errors during iteration.
673+
error_handling: Whether and how to handle errors during timestamp
674+
evaluation.
592675
"""
593676
timestamp_fn = _as_callable_for_pcoll(pcoll, timestamp, 'timestamp', language)
594677
T = TypeVar('T')
@@ -611,6 +694,9 @@ def create_mapping_providers():
611694
'MapToFields-python': _PyJsMapToFields,
612695
'MapToFields-javascript': _PyJsMapToFields,
613696
'MapToFields-generic': _PyJsMapToFields,
697+
'Partition-python': _Partition,
698+
'Partition-javascript': _Partition,
699+
'Partition-generic': _Partition,
614700
}),
615701
yaml_provider.SqlBackedProvider({
616702
'Filter-sql': _SqlFilterTransform,

0 commit comments

Comments
 (0)