From 2c3a593aff1ef2f494bddf2c648271108f310098 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Tue, 21 Jul 2026 16:18:17 +0000 Subject: [PATCH 01/10] feat(api-core): centralize rest transcoding helpers --- .../google/api_core/rest_helpers.py | 56 ++++++ packages/google-api-core/tests/conftest.py | 31 ++++ .../tests/unit/test_rest_helpers.py | 160 ++++++++++++++++++ 3 files changed, 247 insertions(+) create mode 100644 packages/google-api-core/tests/conftest.py diff --git a/packages/google-api-core/google/api_core/rest_helpers.py b/packages/google-api-core/google/api_core/rest_helpers.py index a78822f1572a..4d058fca33f7 100644 --- a/packages/google-api-core/google/api_core/rest_helpers.py +++ b/packages/google-api-core/google/api_core/rest_helpers.py @@ -16,6 +16,11 @@ import functools import operator +from typing import Any, Dict, List, Optional, Tuple + +from google.protobuf import json_format + +from google.api_core import path_template def flatten_query_params(obj, strict=False): @@ -107,3 +112,54 @@ def _canonicalize(obj, strict=False): value = value.lower() return value return obj + + +def transcode_request( + http_options: List[Dict[str, str]], + request: Any, + required_fields_default_values: Optional[Dict[str, Any]] = None, + rest_numeric_enums: bool = False, +) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: + """Transcodes a request into HTTP method, URI, body, and query parameters. + + Args: + http_options (List[Dict[str, str]]): List of HTTP transcoding rules. + request (Any): The protobuf or proto-plus request message. + required_fields_default_values (Optional[Dict[str, Any]]): Dictionary + of required fields default values to merge into query parameters if missing. + rest_numeric_enums (bool): Whether to encode enums as integers. + + Returns: + Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: A tuple containing: + - The raw transcoded request dictionary (containing keys like 'uri', 'method'). + - The serialized request body JSON string, or None if no body. + - The query parameters dictionary. + """ + # Convert proto-plus message to its underlying protobuf message if needed + pb_request = getattr(request, "_pb", request) + + transcoded_request = path_template.transcode(http_options, pb_request) + + body_json = None + if transcoded_request.get("body") is not None: + body_json = json_format.MessageToJson( + transcoded_request["body"], + use_integers_for_enums=rest_numeric_enums, + ) + + query_params_json = {} + if transcoded_request.get("query_params") is not None: + query_params_json = json_format.MessageToDict( + transcoded_request["query_params"], + use_integers_for_enums=rest_numeric_enums, + ) + + if required_fields_default_values: + for k, v in required_fields_default_values.items(): + if k not in query_params_json: + query_params_json[k] = v + + if rest_numeric_enums: + query_params_json["$alt"] = "json;enum-encoding=int" + + return transcoded_request, body_json, query_params_json diff --git a/packages/google-api-core/tests/conftest.py b/packages/google-api-core/tests/conftest.py new file mode 100644 index 000000000000..62a3c999f733 --- /dev/null +++ b/packages/google-api-core/tests/conftest.py @@ -0,0 +1,31 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +from unittest import mock + +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def mock_mtls_env(): + """Autouse session-scoped fixture to isolate unit tests from workstation mTLS environments.""" + with mock.patch.dict( + os.environ, + { + "GOOGLE_API_USE_CLIENT_CERTIFICATE": "false", + "CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE": "false", + }, + ): + yield diff --git a/packages/google-api-core/tests/unit/test_rest_helpers.py b/packages/google-api-core/tests/unit/test_rest_helpers.py index ff1a43f002aa..0d56e9c4a643 100644 --- a/packages/google-api-core/tests/unit/test_rest_helpers.py +++ b/packages/google-api-core/tests/unit/test_rest_helpers.py @@ -12,9 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json +from unittest import mock + import pytest +from google.protobuf import descriptor_pb2 from google.api_core import rest_helpers +from google.api_core.rest_helpers import transcode_request def test_flatten_simple_value(): @@ -92,3 +97,158 @@ def test_flatten_repeated_list(): with pytest.raises(ValueError): rest_helpers.flatten_query_params(obj) + + +def test_transcode_basic(): + # We use FieldDescriptorProto as it has standard primitive fields and nested messages. + http_options = [ + { + "method": "get", + "uri": "/v1/test/{name}", + } + ] + + request = descriptor_pb2.FieldDescriptorProto() + request.name = "my-field" + request.number = 123 + + transcoded, body, query_params = transcode_request(http_options, request) + + assert transcoded["method"] == "get" + assert transcoded["uri"] == "/v1/test/my-field" + assert body is None + # 'number' should be in query parameters + assert "number" in query_params + assert query_params["number"] == 123 + + +def test_transcode_with_nested_field(): + http_options = [ + { + "method": "get", + "uri": "/v1/test/{options.deprecated}/{name}", + } + ] + + request = descriptor_pb2.FieldDescriptorProto() + request.name = "my-field" + request.options.deprecated = True + request.number = 123 + + transcoded, body, query_params = transcode_request(http_options, request) + + assert transcoded["method"] == "get" + assert transcoded["uri"] == "/v1/test/True/my-field" + assert body is None + assert "number" in query_params + assert query_params["number"] == 123 + + +def test_transcode_with_body(): + http_options = [ + { + "method": "post", + "uri": "/v1/test/{name}", + "body": "options", + } + ] + + request = descriptor_pb2.FieldDescriptorProto() + request.name = "my-field" + request.options.deprecated = True + request.number = 123 + + transcoded, body, query_params = transcode_request(http_options, request) + + assert transcoded["method"] == "post" + assert transcoded["uri"] == "/v1/test/my-field" + assert body is not None + body_data = json.loads(body) + assert body_data["deprecated"] is True + # Query parameters should not contain 'options' (the body) + assert "number" in query_params + assert query_params["number"] == 123 + assert "options" not in query_params + + +def test_transcode_with_required_fields_default_values(): + http_options = [ + { + "method": "get", + "uri": "/v1/test/{name}", + } + ] + + request = descriptor_pb2.FieldDescriptorProto() + request.name = "my-field" + + required_defaults = {"requiredQueryParam": "default-val"} + + transcoded, body, query_params = transcode_request( + http_options, + request, + required_fields_default_values=required_defaults, + ) + + assert query_params["requiredQueryParam"] == "default-val" + + +def test_transcode_with_numeric_enums(): + http_options = [ + { + "method": "get", + "uri": "/v1/test/{name}", + } + ] + + request = descriptor_pb2.FieldDescriptorProto() + request.name = "my-field" + request.type = descriptor_pb2.FieldDescriptorProto.TYPE_STRING + + # Without numeric enums + _, _, query_params = transcode_request( + http_options, request, rest_numeric_enums=False + ) + assert query_params["type"] == "TYPE_STRING" + + # With numeric enums + _, _, query_params = transcode_request( + http_options, request, rest_numeric_enums=True + ) + # Type number for TYPE_STRING is 9 + assert query_params["type"] == 9 + assert query_params["$alt"] == "json;enum-encoding=int" + + +def test_transcode_no_query_params(): + http_options = [{"method": "get", "uri": "/v1/test"}] + request = descriptor_pb2.FieldDescriptorProto() + + with mock.patch( + "google.api_core.path_template.transcode", + return_value={"method": "get", "uri": "/v1/test"}, + ): + transcoded, body, query_params = transcode_request(http_options, request) + assert query_params == {} + + +def test_transcode_with_required_fields_existing_key(): + http_options = [ + { + "method": "get", + "uri": "/v1/test", + } + ] + + request = descriptor_pb2.FieldDescriptorProto() + request.name = "custom-name" + + required_defaults = {"name": "default-name"} + + transcoded, body, query_params = transcode_request( + http_options, + request, + required_fields_default_values=required_defaults, + ) + + assert query_params["name"] == "custom-name" From 7e47c49f66c17f3a41b1f0a8a25078c3d108d10f Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Wed, 22 Jul 2026 17:27:07 +0000 Subject: [PATCH 02/10] fix(api-core): add transcode alias, input validation, and gapic_v1 re-export --- .../google/api_core/gapic_v1/rest_helpers.py | 20 ++++++++++++ .../google/api_core/rest_helpers.py | 7 +++++ .../tests/unit/test_rest_helpers.py | 31 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 packages/google-api-core/google/api_core/gapic_v1/rest_helpers.py diff --git a/packages/google-api-core/google/api_core/gapic_v1/rest_helpers.py b/packages/google-api-core/google/api_core/gapic_v1/rest_helpers.py new file mode 100644 index 000000000000..511e1a3c25b4 --- /dev/null +++ b/packages/google-api-core/google/api_core/gapic_v1/rest_helpers.py @@ -0,0 +1,20 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from google.api_core.rest_helpers import ( + transcode, + transcode_request, +) + +__all__ = ["transcode", "transcode_request"] diff --git a/packages/google-api-core/google/api_core/rest_helpers.py b/packages/google-api-core/google/api_core/rest_helpers.py index 4d058fca33f7..cf6cdf5be5fd 100644 --- a/packages/google-api-core/google/api_core/rest_helpers.py +++ b/packages/google-api-core/google/api_core/rest_helpers.py @@ -135,6 +135,9 @@ def transcode_request( - The serialized request body JSON string, or None if no body. - The query parameters dictionary. """ + if request is None: + raise TypeError("request cannot be None") + # Convert proto-plus message to its underlying protobuf message if needed pb_request = getattr(request, "_pb", request) @@ -163,3 +166,7 @@ def transcode_request( query_params_json["$alt"] = "json;enum-encoding=int" return transcoded_request, body_json, query_params_json + + +transcode = transcode_request + diff --git a/packages/google-api-core/tests/unit/test_rest_helpers.py b/packages/google-api-core/tests/unit/test_rest_helpers.py index 0d56e9c4a643..84c198827af4 100644 --- a/packages/google-api-core/tests/unit/test_rest_helpers.py +++ b/packages/google-api-core/tests/unit/test_rest_helpers.py @@ -252,3 +252,34 @@ def test_transcode_with_required_fields_existing_key(): ) assert query_params["name"] == "custom-name" + + +def test_transcode_alias_and_gapic_v1_import(): + from google.api_core.gapic_v1.rest_helpers import ( + transcode as tr_gapic, + transcode_request as tr_req_gapic, + ) + from google.api_core.rest_helpers import transcode as tr_top + + assert tr_gapic is transcode_request + assert tr_req_gapic is transcode_request + assert tr_top is transcode_request + + +def test_transcode_request_invalid_request(): + http_options = [{"method": "get", "uri": "/v1/test"}] + with pytest.raises(TypeError, match="request cannot be None"): + transcode_request(http_options, None) + + +def test_transcode_request_proto_plus_wrapper(): + http_options = [{"method": "get", "uri": "/v1/test/{name}"}] + mock_pb = descriptor_pb2.FieldDescriptorProto() + mock_pb.name = "proto-plus-field" + + mock_proto_plus = mock.Mock() + mock_proto_plus._pb = mock_pb + + transcoded, _, _ = transcode_request(http_options, mock_proto_plus) + assert transcoded["uri"] == "/v1/test/proto-plus-field" + From 75894448b8caf345cf6e411366ed5c4f8890c0da Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Wed, 22 Jul 2026 17:28:45 +0000 Subject: [PATCH 03/10] refactor(api-core): consolidate rest_helpers into top-level google.api_core.rest_helpers --- .../google/api_core/gapic_v1/rest_helpers.py | 20 ------------------- .../tests/unit/test_rest_helpers.py | 8 +------- 2 files changed, 1 insertion(+), 27 deletions(-) delete mode 100644 packages/google-api-core/google/api_core/gapic_v1/rest_helpers.py diff --git a/packages/google-api-core/google/api_core/gapic_v1/rest_helpers.py b/packages/google-api-core/google/api_core/gapic_v1/rest_helpers.py deleted file mode 100644 index 511e1a3c25b4..000000000000 --- a/packages/google-api-core/google/api_core/gapic_v1/rest_helpers.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from google.api_core.rest_helpers import ( - transcode, - transcode_request, -) - -__all__ = ["transcode", "transcode_request"] diff --git a/packages/google-api-core/tests/unit/test_rest_helpers.py b/packages/google-api-core/tests/unit/test_rest_helpers.py index 84c198827af4..902e7bdf0026 100644 --- a/packages/google-api-core/tests/unit/test_rest_helpers.py +++ b/packages/google-api-core/tests/unit/test_rest_helpers.py @@ -254,15 +254,9 @@ def test_transcode_with_required_fields_existing_key(): assert query_params["name"] == "custom-name" -def test_transcode_alias_and_gapic_v1_import(): - from google.api_core.gapic_v1.rest_helpers import ( - transcode as tr_gapic, - transcode_request as tr_req_gapic, - ) +def test_transcode_alias(): from google.api_core.rest_helpers import transcode as tr_top - assert tr_gapic is transcode_request - assert tr_req_gapic is transcode_request assert tr_top is transcode_request From 25d41f0bbcf6ceebf3c8824982a60ce074c0b0a6 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Wed, 22 Jul 2026 17:46:03 +0000 Subject: [PATCH 04/10] chore(api-core): add __all__ export list to rest_helpers --- packages/google-api-core/google/api_core/rest_helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/google-api-core/google/api_core/rest_helpers.py b/packages/google-api-core/google/api_core/rest_helpers.py index cf6cdf5be5fd..a985299303aa 100644 --- a/packages/google-api-core/google/api_core/rest_helpers.py +++ b/packages/google-api-core/google/api_core/rest_helpers.py @@ -22,6 +22,8 @@ from google.api_core import path_template +__all__ = ["flatten_query_params", "transcode", "transcode_request"] + def flatten_query_params(obj, strict=False): """Flatten a dict into a list of (name,value) tuples. From 2d131099e8a44976a51ae7fd4f052e3515207ca4 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Wed, 22 Jul 2026 17:54:25 +0000 Subject: [PATCH 05/10] fix(lint): fix import ordering and trailing whitespace in rest_helpers and tests --- packages/google-api-core/google/api_core/rest_helpers.py | 4 +--- packages/google-api-core/tests/unit/test_rest_helpers.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/google-api-core/google/api_core/rest_helpers.py b/packages/google-api-core/google/api_core/rest_helpers.py index a985299303aa..91fef6275b78 100644 --- a/packages/google-api-core/google/api_core/rest_helpers.py +++ b/packages/google-api-core/google/api_core/rest_helpers.py @@ -18,9 +18,8 @@ import operator from typing import Any, Dict, List, Optional, Tuple -from google.protobuf import json_format - from google.api_core import path_template +from google.protobuf import json_format __all__ = ["flatten_query_params", "transcode", "transcode_request"] @@ -171,4 +170,3 @@ def transcode_request( transcode = transcode_request - diff --git a/packages/google-api-core/tests/unit/test_rest_helpers.py b/packages/google-api-core/tests/unit/test_rest_helpers.py index 902e7bdf0026..7aa8bea2a042 100644 --- a/packages/google-api-core/tests/unit/test_rest_helpers.py +++ b/packages/google-api-core/tests/unit/test_rest_helpers.py @@ -16,10 +16,10 @@ from unittest import mock import pytest -from google.protobuf import descriptor_pb2 from google.api_core import rest_helpers from google.api_core.rest_helpers import transcode_request +from google.protobuf import descriptor_pb2 def test_flatten_simple_value(): @@ -276,4 +276,3 @@ def test_transcode_request_proto_plus_wrapper(): transcoded, _, _ = transcode_request(http_options, mock_proto_plus) assert transcoded["uri"] == "/v1/test/proto-plus-field" - From d9f591bce68242946e0e516d7797440b39688fb8 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh <137334116+hebaalazzeh@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:09:55 -0700 Subject: [PATCH 06/10] Update packages/google-api-core/google/api_core/rest_helpers.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/google-api-core/google/api_core/rest_helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/google-api-core/google/api_core/rest_helpers.py b/packages/google-api-core/google/api_core/rest_helpers.py index 91fef6275b78..d82195e0bcbf 100644 --- a/packages/google-api-core/google/api_core/rest_helpers.py +++ b/packages/google-api-core/google/api_core/rest_helpers.py @@ -16,6 +16,7 @@ import functools import operator +import re from typing import Any, Dict, List, Optional, Tuple from google.api_core import path_template From fdb83f72353521593afe16dc5bf526f48960ab2f Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Wed, 22 Jul 2026 19:11:21 +0000 Subject: [PATCH 07/10] fix(api-core): filter bound path and body fields from required fields in query params --- .../google/api_core/rest_helpers.py | 30 +++++++++++++++++-- .../tests/unit/test_rest_helpers.py | 29 ++++++++++++++++++ 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/google-api-core/google/api_core/rest_helpers.py b/packages/google-api-core/google/api_core/rest_helpers.py index d82195e0bcbf..839f52443190 100644 --- a/packages/google-api-core/google/api_core/rest_helpers.py +++ b/packages/google-api-core/google/api_core/rest_helpers.py @@ -160,9 +160,33 @@ def transcode_request( ) if required_fields_default_values: - for k, v in required_fields_default_values.items(): - if k not in query_params_json: - query_params_json[k] = v + matched_option = None + for option in http_options: + if option.get("method", "").lower() == transcoded_request.get("method", "").lower(): + if path_template.validate(option.get("uri", ""), transcoded_request.get("uri", "")): + matched_option = option + break + + bound_fields = set() + if matched_option: + uri_template = matched_option.get("uri", "") + for m in path_template._VARIABLE_RE.finditer(uri_template): + name = m.group("name") + if name: + bound_fields.add(name.split(".")[0]) + body_param = matched_option.get("body") + if body_param: + if body_param == "*": + bound_fields = None + else: + bound_fields.add(body_param.split(".")[0]) + + if bound_fields is not None: + for k, v in required_fields_default_values.items(): + if k in bound_fields: + continue + if k not in query_params_json: + query_params_json[k] = v if rest_numeric_enums: query_params_json["$alt"] = "json;enum-encoding=int" diff --git a/packages/google-api-core/tests/unit/test_rest_helpers.py b/packages/google-api-core/tests/unit/test_rest_helpers.py index 7aa8bea2a042..b06b8cc0e3a7 100644 --- a/packages/google-api-core/tests/unit/test_rest_helpers.py +++ b/packages/google-api-core/tests/unit/test_rest_helpers.py @@ -276,3 +276,32 @@ def test_transcode_request_proto_plus_wrapper(): transcoded, _, _ = transcode_request(http_options, mock_proto_plus) assert transcoded["uri"] == "/v1/test/proto-plus-field" + + +def test_transcode_required_fields_bound_to_path(): + http_options = [{"method": "get", "uri": "/v1/test/{name}"}] + request = descriptor_pb2.FieldDescriptorProto() + request.name = "my-name" + + required_defaults = {"name": "default-name", "filter": "default-filter"} + transcoded, _, query_params = transcode_request( + http_options, request, required_fields_default_values=required_defaults + ) + + assert transcoded["uri"] == "/v1/test/my-name" + assert "name" not in query_params + assert query_params["filter"] == "default-filter" + + +def test_transcode_required_fields_bound_to_body_star(): + http_options = [{"method": "post", "uri": "/v1/test", "body": "*"}] + request = descriptor_pb2.FieldDescriptorProto() + request.name = "my-name" + + required_defaults = {"name": "default-name"} + _, body, query_params = transcode_request( + http_options, request, required_fields_default_values=required_defaults + ) + + assert body is not None + assert query_params == {} From c5c6806a5ff314bb49ba58473ffadd570bdc489f Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Wed, 22 Jul 2026 19:12:14 +0000 Subject: [PATCH 08/10] test(api-core): align bound field test function names with reviewer feedback --- packages/google-api-core/google/api_core/rest_helpers.py | 1 - packages/google-api-core/tests/unit/test_rest_helpers.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/google-api-core/google/api_core/rest_helpers.py b/packages/google-api-core/google/api_core/rest_helpers.py index 839f52443190..302ba4e188d3 100644 --- a/packages/google-api-core/google/api_core/rest_helpers.py +++ b/packages/google-api-core/google/api_core/rest_helpers.py @@ -16,7 +16,6 @@ import functools import operator -import re from typing import Any, Dict, List, Optional, Tuple from google.api_core import path_template diff --git a/packages/google-api-core/tests/unit/test_rest_helpers.py b/packages/google-api-core/tests/unit/test_rest_helpers.py index b06b8cc0e3a7..858333c3c616 100644 --- a/packages/google-api-core/tests/unit/test_rest_helpers.py +++ b/packages/google-api-core/tests/unit/test_rest_helpers.py @@ -278,7 +278,7 @@ def test_transcode_request_proto_plus_wrapper(): assert transcoded["uri"] == "/v1/test/proto-plus-field" -def test_transcode_required_fields_bound_to_path(): +def test_transcode_with_required_fields_in_path(): http_options = [{"method": "get", "uri": "/v1/test/{name}"}] request = descriptor_pb2.FieldDescriptorProto() request.name = "my-name" @@ -293,7 +293,7 @@ def test_transcode_required_fields_bound_to_path(): assert query_params["filter"] == "default-filter" -def test_transcode_required_fields_bound_to_body_star(): +def test_transcode_with_required_fields_in_body(): http_options = [{"method": "post", "uri": "/v1/test", "body": "*"}] request = descriptor_pb2.FieldDescriptorProto() request.name = "my-name" @@ -304,4 +304,5 @@ def test_transcode_required_fields_bound_to_body_star(): ) assert body is not None + assert "name" not in query_params assert query_params == {} From 0cf2c72295f99e1c341dca158b311125cc0e980f Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Wed, 22 Jul 2026 20:43:42 +0000 Subject: [PATCH 09/10] fix(api-core): resolve ruff lint, mypy type annotations, and 100% test coverage in rest_helpers --- .../google/api_core/rest_helpers.py | 32 +++++++++++-------- .../tests/unit/test_rest_helpers.py | 18 +++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/packages/google-api-core/google/api_core/rest_helpers.py b/packages/google-api-core/google/api_core/rest_helpers.py index 302ba4e188d3..d26a972fe456 100644 --- a/packages/google-api-core/google/api_core/rest_helpers.py +++ b/packages/google-api-core/google/api_core/rest_helpers.py @@ -16,7 +16,7 @@ import functools import operator -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Set, Tuple from google.api_core import path_template from google.protobuf import json_format @@ -161,23 +161,29 @@ def transcode_request( if required_fields_default_values: matched_option = None for option in http_options: - if option.get("method", "").lower() == transcoded_request.get("method", "").lower(): - if path_template.validate(option.get("uri", ""), transcoded_request.get("uri", "")): + if ( + option.get("method", "").lower() + == transcoded_request.get("method", "").lower() + ): + if path_template.validate( + option.get("uri", ""), transcoded_request.get("uri", "") + ): matched_option = option break - bound_fields = set() + bound_fields: Optional[Set[str]] = set() if matched_option: - uri_template = matched_option.get("uri", "") - for m in path_template._VARIABLE_RE.finditer(uri_template): - name = m.group("name") - if name: - bound_fields.add(name.split(".")[0]) body_param = matched_option.get("body") - if body_param: - if body_param == "*": - bound_fields = None - else: + if body_param == "*": + bound_fields = None + else: + assert bound_fields is not None + uri_template = matched_option.get("uri", "") + for m in path_template._VARIABLE_RE.finditer(uri_template): + name = m.group("name") + if name: + bound_fields.add(name.split(".")[0]) + if body_param: bound_fields.add(body_param.split(".")[0]) if bound_fields is not None: diff --git a/packages/google-api-core/tests/unit/test_rest_helpers.py b/packages/google-api-core/tests/unit/test_rest_helpers.py index 858333c3c616..fa3b58ed832d 100644 --- a/packages/google-api-core/tests/unit/test_rest_helpers.py +++ b/packages/google-api-core/tests/unit/test_rest_helpers.py @@ -306,3 +306,21 @@ def test_transcode_with_required_fields_in_body(): assert body is not None assert "name" not in query_params assert query_params == {} + + +def test_transcode_with_required_fields_in_body_param(): + http_options = [{"method": "post", "uri": "/v1/test/{name}", "body": "options"}] + request = descriptor_pb2.FieldDescriptorProto() + request.name = "my-name" + + required_defaults = { + "options": "default-options", + "filter": "default-filter", + } + transcoded, body, query_params = transcode_request( + http_options, request, required_fields_default_values=required_defaults + ) + + assert transcoded["uri"] == "/v1/test/my-name" + assert "options" not in query_params + assert query_params["filter"] == "default-filter" From f5e1acbfee28c3aeb9b1c0e2dd3b6ea823868ce6 Mon Sep 17 00:00:00 2001 From: Heba Alazzeh Date: Wed, 22 Jul 2026 20:50:01 +0000 Subject: [PATCH 10/10] test(api-core): add branch coverage tests for rest_helpers to achieve 100% total branch coverage --- .../google/api_core/rest_helpers.py | 4 +-- .../tests/unit/test_rest_helpers.py | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/google-api-core/google/api_core/rest_helpers.py b/packages/google-api-core/google/api_core/rest_helpers.py index d26a972fe456..b22b16565919 100644 --- a/packages/google-api-core/google/api_core/rest_helpers.py +++ b/packages/google-api-core/google/api_core/rest_helpers.py @@ -180,9 +180,7 @@ def transcode_request( assert bound_fields is not None uri_template = matched_option.get("uri", "") for m in path_template._VARIABLE_RE.finditer(uri_template): - name = m.group("name") - if name: - bound_fields.add(name.split(".")[0]) + bound_fields.add(m.group("name").split(".")[0]) if body_param: bound_fields.add(body_param.split(".")[0]) diff --git a/packages/google-api-core/tests/unit/test_rest_helpers.py b/packages/google-api-core/tests/unit/test_rest_helpers.py index fa3b58ed832d..d883c56c4d36 100644 --- a/packages/google-api-core/tests/unit/test_rest_helpers.py +++ b/packages/google-api-core/tests/unit/test_rest_helpers.py @@ -324,3 +324,38 @@ def test_transcode_with_required_fields_in_body_param(): assert transcoded["uri"] == "/v1/test/my-name" assert "options" not in query_params assert query_params["filter"] == "default-filter" + + +def test_transcode_required_fields_no_matching_option(): + http_options = [{"method": "get", "uri": "/v1/test"}] + request = descriptor_pb2.FieldDescriptorProto() + required_defaults = {"name": "default-name"} + + with mock.patch( + "google.api_core.path_template.transcode", + return_value={"method": "delete", "uri": "/v1/test"}, + ): + _, _, query_params = transcode_request( + http_options, request, required_fields_default_values=required_defaults + ) + assert query_params["name"] == "default-name" + + +def test_transcode_required_fields_uri_mismatch(): + http_options = [ + {"method": "get", "uri": "/v1/wrong_path/{name}"}, + {"method": "get", "uri": "/v1/test/{name}"}, + ] + request = descriptor_pb2.FieldDescriptorProto() + request.name = "my-name" + required_defaults = {"name": "default-name", "filter": "default-filter"} + + with mock.patch( + "google.api_core.path_template.transcode", + return_value={"method": "get", "uri": "/v1/test/my-name"}, + ): + transcoded, _, query_params = transcode_request( + http_options, request, required_fields_default_values=required_defaults + ) + assert "name" not in query_params + assert query_params["filter"] == "default-filter"