| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent a12aeef commit 2c3a593
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -16,6 +16,11 @@ | |||
| 16 | 16 | ||
| 17 | 17 | import functools | |
| 18 | 18 | import operator | |
| 19 | + from typing import Any, Dict, List, Optional, Tuple | ||
| 20 | + | ||
| 21 | + from google.protobuf import json_format | ||
| 22 | + | ||
| 23 | + from google.api_core import path_template | ||
| 19 | 24 | ||
| 20 | 25 | ||
| 21 | 26 | def flatten_query_params(obj, strict=False): | |
@@ -107,3 +112,54 @@ def _canonicalize(obj, strict=False): | |||
| 107 | 112 | value = value.lower() | |
| 108 | 113 | return value | |
| 109 | 114 | return obj | |
| 115 | + | ||
| 116 | + | ||
| 117 | + def transcode_request( | ||
| 118 | + http_options: List[Dict[str, str]], | ||
| 119 | + request: Any, | ||
| 120 | + required_fields_default_values: Optional[Dict[str, Any]] = None, | ||
| 121 | + rest_numeric_enums: bool = False, | ||
| 122 | + ) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: | ||
| 123 | + """Transcodes a request into HTTP method, URI, body, and query parameters. | ||
| 124 | + | ||
| 125 | + Args: | ||
| 126 | + http_options (List[Dict[str, str]]): List of HTTP transcoding rules. | ||
| 127 | + request (Any): The protobuf or proto-plus request message. | ||
| 128 | + required_fields_default_values (Optional[Dict[str, Any]]): Dictionary | ||
| 129 | + of required fields default values to merge into query parameters if missing. | ||
| 130 | + rest_numeric_enums (bool): Whether to encode enums as integers. | ||
| 131 | + | ||
| 132 | + Returns: | ||
| 133 | + Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: A tuple containing: | ||
| 134 | + - The raw transcoded request dictionary (containing keys like 'uri', 'method'). | ||
| 135 | + - The serialized request body JSON string, or None if no body. | ||
| 136 | + - The query parameters dictionary. | ||
| 137 | + """ | ||
| 138 | + # Convert proto-plus message to its underlying protobuf message if needed | ||
| 139 | + pb_request = getattr(request, "_pb", request) | ||
| 140 | + | ||
| 141 | + transcoded_request = path_template.transcode(http_options, pb_request) | ||
| 142 | + | ||
| 143 | + body_json = None | ||
| 144 | + if transcoded_request.get("body") is not None: | ||
| 145 | + body_json = json_format.MessageToJson( | ||
| 146 | + transcoded_request["body"], | ||
| 147 | + use_integers_for_enums=rest_numeric_enums, | ||
| 148 | + ) | ||
| 149 | + | ||
| 150 | + query_params_json = {} | ||
| 151 | + if transcoded_request.get("query_params") is not None: | ||
| 152 | + query_params_json = json_format.MessageToDict( | ||
| 153 | + transcoded_request["query_params"], | ||
| 154 | + use_integers_for_enums=rest_numeric_enums, | ||
| 155 | + ) | ||
| 156 | + | ||
| 157 | + if required_fields_default_values: | ||
| 158 | + for k, v in required_fields_default_values.items(): | ||
| 159 | + if k not in query_params_json: | ||
| 160 | + query_params_json[k] = v | ||
| 161 | + | ||
| 162 | + if rest_numeric_enums: | ||
| 163 | + query_params_json["$alt"] = "json;enum-encoding=int" | ||
| 164 | + | ||
| 165 | + return transcoded_request, body_json, query_params_json | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,31 @@ | |||
| 1 | + # Copyright 2026 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 | + import os | ||
| 16 | + from unittest import mock | ||
| 17 | + | ||
| 18 | + import pytest | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + @pytest.fixture(scope="session", autouse=True) | ||
| 22 | + def mock_mtls_env(): | ||
| 23 | + """Autouse session-scoped fixture to isolate unit tests from workstation mTLS environments.""" | ||
| 24 | + with mock.patch.dict( | ||
| 25 | + os.environ, | ||
| 26 | + { | ||
| 27 | + "GOOGLE_API_USE_CLIENT_CERTIFICATE": "false", | ||
| 28 | + "CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE": "false", | ||
| 29 | + }, | ||
| 30 | + ): | ||
| 31 | + yield | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -12,9 +12,14 @@ | |||
| 12 | 12 | # See the License for the specific language governing permissions and | |
| 13 | 13 | # limitations under the License. | |
| 14 | 14 | ||
| 15 | + import json | ||
| 16 | + from unittest import mock | ||
| 17 | + | ||
| 15 | 18 | import pytest | |
| 19 | + from google.protobuf import descriptor_pb2 | ||
| 16 | 20 | ||
| 17 | 21 | from google.api_core import rest_helpers | |
| 22 | + from google.api_core.rest_helpers import transcode_request | ||
| 18 | 23 | ||
| 19 | 24 | ||
| 20 | 25 | def test_flatten_simple_value(): | |
@@ -92,3 +97,158 @@ def test_flatten_repeated_list(): | |||
| 92 | 97 | ||
| 93 | 98 | with pytest.raises(ValueError): | |
| 94 | 99 | rest_helpers.flatten_query_params(obj) | |
| 100 | + | ||
| 101 | + | ||
| 102 | + def test_transcode_basic(): | ||
| 103 | + # We use FieldDescriptorProto as it has standard primitive fields and nested messages. | ||
| 104 | + http_options = [ | ||
| 105 | + { | ||
| 106 | + "method": "get", | ||
| 107 | + "uri": "/v1/test/{name}", | ||
| 108 | + } | ||
| 109 | + ] | ||
| 110 | + | ||
| 111 | + request = descriptor_pb2.FieldDescriptorProto() | ||
| 112 | + request.name = "my-field" | ||
| 113 | + request.number = 123 | ||
| 114 | + | ||
| 115 | + transcoded, body, query_params = transcode_request(http_options, request) | ||
| 116 | + | ||
| 117 | + assert transcoded["method"] == "get" | ||
| 118 | + assert transcoded["uri"] == "/v1/test/my-field" | ||
| 119 | + assert body is None | ||
| 120 | + # 'number' should be in query parameters | ||
| 121 | + assert "number" in query_params | ||
| 122 | + assert query_params["number"] == 123 | ||
| 123 | + | ||
| 124 | + | ||
| 125 | + def test_transcode_with_nested_field(): | ||
| 126 | + http_options = [ | ||
| 127 | + { | ||
| 128 | + "method": "get", | ||
| 129 | + "uri": "/v1/test/{options.deprecated}/{name}", | ||
| 130 | + } | ||
| 131 | + ] | ||
| 132 | + | ||
| 133 | + request = descriptor_pb2.FieldDescriptorProto() | ||
| 134 | + request.name = "my-field" | ||
| 135 | + request.options.deprecated = True | ||
| 136 | + request.number = 123 | ||
| 137 | + | ||
| 138 | + transcoded, body, query_params = transcode_request(http_options, request) | ||
| 139 | + | ||
| 140 | + assert transcoded["method"] == "get" | ||
| 141 | + assert transcoded["uri"] == "/v1/test/True/my-field" | ||
| 142 | + assert body is None | ||
| 143 | + assert "number" in query_params | ||
| 144 | + assert query_params["number"] == 123 | ||
| 145 | + | ||
| 146 | + | ||
| 147 | + def test_transcode_with_body(): | ||
| 148 | + http_options = [ | ||
| 149 | + { | ||
| 150 | + "method": "post", | ||
| 151 | + "uri": "/v1/test/{name}", | ||
| 152 | + "body": "options", | ||
| 153 | + } | ||
| 154 | + ] | ||
| 155 | + | ||
| 156 | + request = descriptor_pb2.FieldDescriptorProto() | ||
| 157 | + request.name = "my-field" | ||
| 158 | + request.options.deprecated = True | ||
| 159 | + request.number = 123 | ||
| 160 | + | ||
| 161 | + transcoded, body, query_params = transcode_request(http_options, request) | ||
| 162 | + | ||
| 163 | + assert transcoded["method"] == "post" | ||
| 164 | + assert transcoded["uri"] == "/v1/test/my-field" | ||
| 165 | + assert body is not None | ||
| 166 | + body_data = json.loads(body) | ||
| 167 | + assert body_data["deprecated"] is True | ||
| 168 | + # Query parameters should not contain 'options' (the body) | ||
| 169 | + assert "number" in query_params | ||
| 170 | + assert query_params["number"] == 123 | ||
| 171 | + assert "options" not in query_params | ||
| 172 | + | ||
| 173 | + | ||
| 174 | + def test_transcode_with_required_fields_default_values(): | ||
| 175 | + http_options = [ | ||
| 176 | + { | ||
| 177 | + "method": "get", | ||
| 178 | + "uri": "/v1/test/{name}", | ||
| 179 | + } | ||
| 180 | + ] | ||
| 181 | + | ||
| 182 | + request = descriptor_pb2.FieldDescriptorProto() | ||
| 183 | + request.name = "my-field" | ||
| 184 | + | ||
| 185 | + required_defaults = {"requiredQueryParam": "default-val"} | ||
| 186 | + | ||
| 187 | + transcoded, body, query_params = transcode_request( | ||
| 188 | + http_options, | ||
| 189 | + request, | ||
| 190 | + required_fields_default_values=required_defaults, | ||
| 191 | + ) | ||
| 192 | + | ||
| 193 | + assert query_params["requiredQueryParam"] == "default-val" | ||
| 194 | + | ||
| 195 | + | ||
| 196 | + def test_transcode_with_numeric_enums(): | ||
| 197 | + http_options = [ | ||
| 198 | + { | ||
| 199 | + "method": "get", | ||
| 200 | + "uri": "/v1/test/{name}", | ||
| 201 | + } | ||
| 202 | + ] | ||
| 203 | + | ||
| 204 | + request = descriptor_pb2.FieldDescriptorProto() | ||
| 205 | + request.name = "my-field" | ||
| 206 | + request.type = descriptor_pb2.FieldDescriptorProto.TYPE_STRING | ||
| 207 | + | ||
| 208 | + # Without numeric enums | ||
| 209 | + _, _, query_params = transcode_request( | ||
| 210 | + http_options, request, rest_numeric_enums=False | ||
| 211 | + ) | ||
| 212 | + assert query_params["type"] == "TYPE_STRING" | ||
| 213 | + | ||
| 214 | + # With numeric enums | ||
| 215 | + _, _, query_params = transcode_request( | ||
| 216 | + http_options, request, rest_numeric_enums=True | ||
| 217 | + ) | ||
| 218 | + # Type number for TYPE_STRING is 9 | ||
| 219 | + assert query_params["type"] == 9 | ||
| 220 | + assert query_params["$alt"] == "json;enum-encoding=int" | ||
| 221 | + | ||
| 222 | + | ||
| 223 | + def test_transcode_no_query_params(): | ||
| 224 | + http_options = [{"method": "get", "uri": "/v1/test"}] | ||
| 225 | + request = descriptor_pb2.FieldDescriptorProto() | ||
| 226 | + | ||
| 227 | + with mock.patch( | ||
| 228 | + "google.api_core.path_template.transcode", | ||
| 229 | + return_value={"method": "get", "uri": "/v1/test"}, | ||
| 230 | + ): | ||
| 231 | + transcoded, body, query_params = transcode_request(http_options, request) | ||
| 232 | + assert query_params == {} | ||
| 233 | + | ||
| 234 | + | ||
| 235 | + def test_transcode_with_required_fields_existing_key(): | ||
| 236 | + http_options = [ | ||
| 237 | + { | ||
| 238 | + "method": "get", | ||
| 239 | + "uri": "/v1/test", | ||
| 240 | + } | ||
| 241 | + ] | ||
| 242 | + | ||
| 243 | + request = descriptor_pb2.FieldDescriptorProto() | ||
| 244 | + request.name = "custom-name" | ||
| 245 | + | ||
| 246 | + required_defaults = {"name": "default-name"} | ||
| 247 | + | ||
| 248 | + transcoded, body, query_params = transcode_request( | ||
| 249 | + http_options, | ||
| 250 | + request, | ||
| 251 | + required_fields_default_values=required_defaults, | ||
| 252 | + ) | ||
| 253 | + | ||
| 254 | + assert query_params["name"] == "custom-name" | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments