Skip to content

Commit a0bff77

Browse files
committed
feat(api-core): move request-id auto-population logic to gapic_v1 public helpers
Introduces method_helpers module containing setup_request_id helper. Exposes the module as public in gapic_v1.
1 parent 75d30d5 commit a0bff77

6 files changed

Lines changed: 152 additions & 5 deletions

File tree

packages/google-api-core/google/api_core/gapic_v1/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@
2525
# Older Python versions safely ignore this variable.
2626
__lazy_modules__: Set[str] = {
2727
"google.api_core.gapic_v1.client_info",
28+
"google.api_core.gapic_v1.method_helpers",
2829
"google.api_core.gapic_v1.routing_header",
2930
}
30-
__all__ = ["client_info", "routing_header"]
31+
__all__ = ["client_info", "method_helpers", "routing_header"]
3132

3233
if _has_grpc:
3334
__lazy_modules__.update(
@@ -41,6 +42,7 @@
4142

4243
from google.api_core.gapic_v1 import ( # noqa: E402
4344
client_info,
45+
method_helpers,
4446
routing_header,
4547
)
4648

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2026 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
"""Helpers for method requests."""
18+
19+
import uuid
20+
from typing import Any
21+
22+
23+
def setup_request_id(request: Any, field_name: str, is_proto3_optional: bool) -> None:
24+
"""Populate a UUID4 field in the request if it is not already set.
25+
26+
Args:
27+
request (Union[google.protobuf.message.Message, dict]): The
28+
request object.
29+
field_name (str): The name of the field to populate.
30+
is_proto3_optional (bool): Whether the field is proto3 optional.
31+
"""
32+
if isinstance(request, dict):
33+
if is_proto3_optional:
34+
if field_name not in request:
35+
request[field_name] = str(uuid.uuid4())
36+
elif not request.get(field_name):
37+
request[field_name] = str(uuid.uuid4())
38+
return
39+
40+
if is_proto3_optional:
41+
try:
42+
# Pure protobuf messages
43+
if not request.HasField(field_name):
44+
setattr(request, field_name, str(uuid.uuid4()))
45+
except (AttributeError, ValueError):
46+
# Proto-plus messages or other objects
47+
if not getattr(request, field_name, None):
48+
setattr(request, field_name, str(uuid.uuid4()))
49+
else:
50+
if not getattr(request, field_name):
51+
setattr(request, field_name, str(uuid.uuid4()))

packages/google-api-core/noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ def prerelease_deps(session):
350350
@nox.session(python=DEFAULT_PYTHON_VERSION)
351351
def core_deps_from_source(session):
352352
"""Run the test suite installing dependencies from source."""
353-
default(session, prerelease=True)
353+
default(session, install_deps_from_source=True)
354354

355355

356356
@nox.session(python=DEFAULT_PYTHON_VERSION)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2026 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import os
17+
from unittest import mock
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
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2026 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
17+
from google.api_core.gapic_v1.method_helpers import setup_request_id
18+
19+
20+
def test_setup_request_id():
21+
import uuid
22+
23+
# test dict request
24+
req = {}
25+
setup_request_id(req, "request_id", True)
26+
assert "request_id" in req
27+
uuid_str = req["request_id"]
28+
uuid.UUID(uuid_str) # verify it is a valid UUID
29+
30+
# test dict request when already set
31+
req = {"request_id": "existing"}
32+
setup_request_id(req, "request_id", True)
33+
assert req["request_id"] == "existing"
34+
35+
class DummyRequest:
36+
def __init__(self):
37+
self.request_id = ""
38+
39+
def HasField(self, field_name):
40+
if not hasattr(self, field_name):
41+
raise ValueError()
42+
return bool(getattr(self, field_name))
43+
44+
# test object request proto3 optional true
45+
req_obj = DummyRequest()
46+
setup_request_id(req_obj, "request_id", True)
47+
assert req_obj.request_id != ""
48+
uuid.UUID(req_obj.request_id)
49+
50+
# test object request proto3 optional false
51+
req_obj2 = DummyRequest()
52+
setup_request_id(req_obj2, "request_id", False)
53+
assert req_obj2.request_id != ""
54+
uuid.UUID(req_obj2.request_id)
55+
56+
class CustomRequestWrapper:
57+
def __init__(self):
58+
self.request_id = ""
59+
60+
# test custom non-iterable object wrapper
61+
req_obj3 = CustomRequestWrapper()
62+
setup_request_id(req_obj3, "request_id", True)
63+
assert req_obj3.request_id != ""
64+
uuid.UUID(req_obj3.request_id)

packages/google-api-core/tests/unit/test_bidi.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@
3131
except ImportError: # pragma: NO COVER
3232
pytest.skip("No GRPC", allow_module_level=True)
3333

34-
from google.api_core import bidi
35-
from google.api_core import exceptions
34+
from google.api_core import bidi, exceptions
3635

3736

3837
class Test_RequestQueueGenerator(object):
@@ -195,7 +194,7 @@ def test_delays_entry_attempts_above_threshold(self):
195194
# (NOTE: not using assert all(...), b/c the coverage check would complain)
196195
for i, entry in enumerate(entries):
197196
if i != 3:
198-
assert entry["reported_wait"] == 0.0
197+
assert entry["reported_wait"] < 0.01
199198

200199
# The delayed entry is expected to have been delayed for a significant
201200
# chunk of the full second, and the actual and reported delay times

0 commit comments

Comments
 (0)