Skip to content

Commit 30cd1a4

Browse files
chalmerloweparthea
andauthored
feat: adds augmented pagination to account for BQ family of APIs (#2372)
Co-authored-by: Anthonios Partheniou <[email protected]>
1 parent 629cf19 commit 30cd1a4

File tree

3 files changed

+186
-6
lines changed

3 files changed

+186
-6
lines changed

gapic/schema/wrappers.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1830,13 +1830,34 @@ def ident(self) -> metadata.Address:
18301830
"""Return the identifier data to be used in templates."""
18311831
return self.meta.address
18321832

1833+
def _validate_paged_field_size_type(self, page_field_size) -> bool:
1834+
"""Validates allowed paged_field_size type(s).
1835+
1836+
Confirms whether the paged_field_size.type is an allowed wrapper type:
1837+
The norm is for type to be int, but an additional check is included to
1838+
account for BigQuery legacy APIs which allowed UInt32Value and
1839+
Int32Value.
1840+
"""
1841+
1842+
pb_type = page_field_size.type
1843+
1844+
return pb_type == int or (
1845+
isinstance(pb_type, MessageType)
1846+
and pb_type.message_pb.name in {"UInt32Value", "Int32Value"}
1847+
)
1848+
18331849
@utils.cached_property
18341850
def paged_result_field(self) -> Optional[Field]:
1835-
"""Return the response pagination field if the method is paginated."""
1836-
# If the request field lacks any of the expected pagination fields,
1837-
# then the method is not paginated.
1851+
"""Return the response pagination field if the method is paginated.
1852+
1853+
The request field must have a page_token field and a page_size field (or
1854+
for legacy APIs, a max_results field) and the response field
1855+
must have a next_token_field and a repeated field.
1856+
1857+
For the purposes of supporting legacy APIs, additional wrapper types are
1858+
allowed.
1859+
"""
18381860

1839-
# The request must have page_token and next_page_token as they keep track of pages
18401861
for source, source_type, name in (
18411862
(self.input, str, "page_token"),
18421863
(self.output, str, "next_page_token"),
@@ -1845,13 +1866,18 @@ def paged_result_field(self) -> Optional[Field]:
18451866
if not field or field.type != source_type:
18461867
return None
18471868

1848-
# The request must have max_results or page_size
1869+
# The request must have page_size (or max_results if legacy API)
18491870
page_fields = (
18501871
self.input.fields.get("max_results", None),
18511872
self.input.fields.get("page_size", None),
18521873
)
18531874
page_field_size = next((field for field in page_fields if field), None)
1854-
if not page_field_size or page_field_size.type != int:
1875+
1876+
if not page_field_size:
1877+
return None
1878+
1879+
# Confirm whether the paged_field_size is an allowed type.
1880+
if not self._validate_paged_field_size_type(page_field_size=page_field_size):
18551881
return None
18561882

18571883
# Return the first repeated field.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (C) 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
syntax = "proto3";
16+
17+
package google.fragment;
18+
19+
import "google/api/client.proto";
20+
import "google/protobuf/wrappers.proto";
21+
22+
service MaxResultsDatasetService {
23+
option (google.api.default_host) = "my.example.com";
24+
rpc ListMaxResultsDataset(ListMaxResultsDatasetRequest) returns (ListMaxResultsDatasetResponse) {
25+
}
26+
}
27+
28+
message ListMaxResultsDatasetRequest {
29+
google.protobuf.UInt32Value max_results = 2;
30+
string page_token = 3;
31+
}
32+
33+
message ListMaxResultsDatasetResponse {
34+
string next_page_token = 3;
35+
repeated string datasets = 4;
36+
}

tests/unit/schema/wrappers/test_method.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from google.api import routing_pb2
2424
from google.cloud import extended_operations_pb2 as ex_ops_pb2
2525
from google.protobuf import descriptor_pb2
26+
from google.protobuf import wrappers_pb2
2627

2728
from gapic.schema import metadata
2829
from gapic.schema import wrappers
@@ -189,6 +190,63 @@ def test_method_paged_result_field_no_page_field():
189190
assert method.paged_result_field is None
190191

191192

193+
def test_method_paged_result_field_invalid_wrapper_type():
194+
"""Validate paged_result_field() returns None if page_size/max_results wrappertypes
195+
are not allowed types.
196+
"""
197+
198+
# page_size is not allowed wrappertype
199+
parent = make_field(name="parent", type="TYPE_STRING")
200+
page_size = make_field(name="page_size", type="TYPE_DOUBLE") # not an allowed type
201+
page_token = make_field(name="page_token", type="TYPE_STRING")
202+
foos = make_field(name="foos", message=make_message("Foo"), repeated=True)
203+
next_page_token = make_field(name="next_page_token", type="TYPE_STRING")
204+
205+
input_msg = make_message(
206+
name="ListFoosRequest",
207+
fields=(
208+
parent,
209+
page_size,
210+
page_token,
211+
),
212+
)
213+
output_msg = make_message(
214+
name="ListFoosResponse",
215+
fields=(
216+
foos,
217+
next_page_token,
218+
),
219+
)
220+
method = make_method(
221+
"ListFoos",
222+
input_message=input_msg,
223+
output_message=output_msg,
224+
)
225+
assert method.paged_result_field is None
226+
227+
# max_results is not allowed wrappertype
228+
max_results = make_field(
229+
name="max_results", type="TYPE_STRING"
230+
) # not an allowed type
231+
232+
input_msg = make_message(
233+
name="ListFoosRequest",
234+
fields=(
235+
parent,
236+
max_results,
237+
page_token,
238+
),
239+
)
240+
241+
method = make_method(
242+
"ListFoos",
243+
input_message=input_msg,
244+
output_message=output_msg,
245+
)
246+
247+
assert method.paged_result_field is None
248+
249+
192250
def test_method_paged_result_ref_types():
193251
input_msg = make_message(
194252
name="ListSquidsRequest",
@@ -999,3 +1057,63 @@ def test_mixin_rule():
9991057
"city": {},
10001058
}
10011059
assert e == m.sample_request
1060+
1061+
1062+
@pytest.mark.parametrize(
1063+
"field_type, pb_type, expected",
1064+
[
1065+
# valid paged_result_field candidates
1066+
(int, "TYPE_INT32", True),
1067+
(wrappers_pb2.UInt32Value, "TYPE_MESSAGE", True),
1068+
(wrappers_pb2.Int32Value, "TYPE_MESSAGE", True),
1069+
# invalid paged_result_field candidates
1070+
(float, "TYPE_DOUBLE", False),
1071+
(wrappers_pb2.UInt32Value, "TYPE_DOUBLE", False),
1072+
(wrappers_pb2.Int32Value, "TYPE_DOUBLE", False),
1073+
],
1074+
)
1075+
def test__validate_paged_field_size_type(field_type, pb_type, expected):
1076+
"""Test _validate_paged_field_size_type with wrapper types and type indicators."""
1077+
1078+
# Setup
1079+
if pb_type in {"TYPE_INT32", "TYPE_DOUBLE"}:
1080+
page_size = make_field(name="page_size", type=pb_type)
1081+
else:
1082+
# expecting TYPE_MESSAGE which in this context is associated with
1083+
# *Int32Value in legacy APIs such as BigQuery.
1084+
# See: https://github.com/googleapis/gapic-generator-python/blob/c8b7229ba2865d6a2f5966aa151be121de81f92d/gapic/schema/wrappers.py#L378C1-L411C10
1085+
page_size = make_field(
1086+
name="max_results",
1087+
type=pb_type,
1088+
message=make_message(name=field_type.DESCRIPTOR.name),
1089+
)
1090+
1091+
parent = make_field(name="parent", type="TYPE_STRING")
1092+
page_token = make_field(name="page_token", type="TYPE_STRING")
1093+
next_page_token = make_field(name="next_page_token", type="TYPE_STRING")
1094+
1095+
input_msg = make_message(
1096+
name="ListFoosRequest",
1097+
fields=(
1098+
parent,
1099+
page_size,
1100+
page_token,
1101+
),
1102+
)
1103+
1104+
output_msg = make_message(
1105+
name="ListFoosResponse",
1106+
fields=(
1107+
make_field(name="foos", message=make_message("Foo"), repeated=True),
1108+
next_page_token,
1109+
),
1110+
)
1111+
1112+
method = make_method(
1113+
"ListFoos",
1114+
input_message=input_msg,
1115+
output_message=output_msg,
1116+
)
1117+
1118+
actual = method._validate_paged_field_size_type(page_field_size=page_size)
1119+
assert actual == expected

0 commit comments

Comments
 (0)