Skip to content

Commit ec35441

Browse files
authored
CombinePerKey with gbek (Python) (#36382)
* [WIP] CombinePerKey with gbek * Run on dataflow postcommit * Run on all postcommit * Don't lift combinebykey * Lint
1 parent 9944acf commit ec35441

3 files changed

Lines changed: 117 additions & 2 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"comment": "Modify this file in a trivial way to cause this test suite to run.",
3-
"modification": 30
3+
"modification": 31
44
}
55

sdks/python/apache_beam/transforms/core.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3058,6 +3058,10 @@ def _process_argspec_fn(self):
30583058
return lambda element, *args, **kwargs: None
30593059

30603060
def expand(self, pcoll):
3061+
# When using gbek, don't allow overriding default implementation
3062+
gbek_option = (pcoll.pipeline._options.view_as(SetupOptions).gbek)
3063+
self._using_gbek = (gbek_option is not None and len(gbek_option) > 0)
3064+
30613065
args, kwargs = util.insert_values_in_args(
30623066
self.args, self.kwargs, self.side_inputs)
30633067
return pcoll | GroupByKey() | 'Combine' >> CombineValues(
@@ -3083,7 +3087,9 @@ def to_runner_api_parameter(
30833087
self,
30843088
context, # type: PipelineContext
30853089
):
3086-
# type: (...) -> typing.Tuple[str, beam_runner_api_pb2.CombinePayload]
3090+
# type: (...) -> tuple[str, typing.Optional[typing.Union[message.Message, bytes, str]]]
3091+
if getattr(self, '_using_gbek', False):
3092+
return super().to_runner_api_parameter(context)
30873093
if self.args or self.kwargs:
30883094
from apache_beam.transforms.combiners import curry_combine_fn
30893095
combine_fn = curry_combine_fn(self.fn, self.args, self.kwargs)
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to You under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
"""Integration tests for cross-language transform expansion."""
19+
20+
# pytype: skip-file
21+
22+
import random
23+
import string
24+
import unittest
25+
26+
import pytest
27+
28+
import apache_beam as beam
29+
from apache_beam.options.pipeline_options import SetupOptions
30+
from apache_beam.testing.test_pipeline import TestPipeline
31+
from apache_beam.testing.util import assert_that
32+
from apache_beam.testing.util import equal_to
33+
from apache_beam.transforms.util import GcpSecret
34+
from apache_beam.transforms.util import Secret
35+
36+
try:
37+
from google.cloud import secretmanager
38+
except ImportError:
39+
secretmanager = None # type: ignore[assignment]
40+
41+
42+
class GbekIT(unittest.TestCase):
43+
def setUp(self):
44+
if secretmanager is not None:
45+
self.project_id = 'apache-beam-testing'
46+
secret_postfix = ''.join(random.choice(string.digits) for _ in range(6))
47+
self.secret_id = 'gbek_secret_tests_' + secret_postfix
48+
self.client = secretmanager.SecretManagerServiceClient()
49+
self.project_path = f'projects/{self.project_id}'
50+
self.secret_path = f'{self.project_path}/secrets/{self.secret_id}'
51+
try:
52+
self.client.get_secret(request={'name': self.secret_path})
53+
except Exception:
54+
self.client.create_secret(
55+
request={
56+
'parent': self.project_path,
57+
'secret_id': self.secret_id,
58+
'secret': {
59+
'replication': {
60+
'automatic': {}
61+
}
62+
}
63+
})
64+
self.client.add_secret_version(
65+
request={
66+
'parent': self.secret_path,
67+
'payload': {
68+
'data': Secret.generate_secret_bytes()
69+
}
70+
})
71+
version_name = f'{self.secret_path}/versions/latest'
72+
self.gcp_secret = GcpSecret(version_name)
73+
self.secret_option = f'type:GcpSecret;version_name:{version_name}'
74+
75+
def tearDown(self):
76+
if secretmanager is not None:
77+
self.client.delete_secret(request={'name': self.secret_path})
78+
79+
@pytest.mark.it_postcommit
80+
@unittest.skipIf(secretmanager is None, 'GCP dependencies are not installed')
81+
def test_gbk_with_gbek_it(self):
82+
pipeline = TestPipeline(is_integration_test=True)
83+
pipeline.options.view_as(SetupOptions).gbek = self.secret_option
84+
85+
pcoll_1 = pipeline | 'Start 1' >> beam.Create([('a', 1), ('a', 2), ('b', 3),
86+
('c', 4)])
87+
result = (pcoll_1) | beam.GroupByKey()
88+
sorted_result = result | beam.Map(lambda x: (x[0], sorted(x[1])))
89+
assert_that(
90+
sorted_result, equal_to([('a', ([1, 2])), ('b', ([3])), ('c', ([4]))]))
91+
92+
pipeline.run().wait_until_finish()
93+
94+
@pytest.mark.it_postcommit
95+
@unittest.skipIf(secretmanager is None, 'GCP dependencies are not installed')
96+
def test_combineValues_with_gbek_it(self):
97+
pipeline = TestPipeline(is_integration_test=True)
98+
pipeline.options.view_as(SetupOptions).gbek = self.secret_option
99+
100+
pcoll_1 = pipeline | 'Start 1' >> beam.Create([('a', 1), ('a', 2), ('b', 3),
101+
('c', 4)])
102+
result = (pcoll_1) | beam.CombinePerKey(sum)
103+
assert_that(result, equal_to([('a', 3), ('b', 3), ('c', 4)]))
104+
105+
pipeline.run().wait_until_finish()
106+
107+
108+
if __name__ == '__main__':
109+
unittest.main()

0 commit comments

Comments
 (0)