Skip to content

Commit e02ee4b

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 1ef0340 commit e02ee4b

3 files changed

Lines changed: 174 additions & 1 deletion

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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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, Union
21+
import google.protobuf.message
22+
23+
24+
def setup_request_id(
25+
request: Union[google.protobuf.message.Message, dict],
26+
field_name: str,
27+
is_proto3_optional: bool,
28+
) -> None:
29+
"""Populate a UUID4 field in the request if it is not already set.
30+
31+
Args:
32+
request (Union[google.protobuf.message.Message, dict]): The
33+
request object.
34+
field_name (str): The name of the field to populate.
35+
is_proto3_optional (bool): Whether the field is proto3 optional.
36+
"""
37+
if isinstance(request, dict):
38+
if is_proto3_optional:
39+
if field_name not in request:
40+
request[field_name] = str(uuid.uuid4())
41+
elif not request.get(field_name):
42+
request[field_name] = str(uuid.uuid4())
43+
return
44+
45+
if is_proto3_optional:
46+
try:
47+
# Pure protobuf messages
48+
if not request.HasField(field_name):
49+
setattr(request, field_name, str(uuid.uuid4()))
50+
except (AttributeError, ValueError):
51+
# Proto-plus messages or other objects
52+
if not getattr(request, field_name, None):
53+
setattr(request, field_name, str(uuid.uuid4()))
54+
else:
55+
if not getattr(request, field_name):
56+
setattr(request, field_name, str(uuid.uuid4()))
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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 re
17+
from google.api_core.gapic_v1.method_helpers import setup_request_id
18+
19+
20+
def test_setup_request_id():
21+
class MockRequest:
22+
def __init__(self, **kwargs):
23+
for k, v in kwargs.items():
24+
setattr(self, k, v)
25+
26+
def __contains__(self, key):
27+
return hasattr(self, key)
28+
29+
class MockProtoRequest:
30+
def __init__(self, **kwargs):
31+
for k, v in kwargs.items():
32+
setattr(self, k, v)
33+
34+
def HasField(self, key):
35+
return hasattr(self, key)
36+
37+
# Test with proto3 optional field not in request
38+
request = MockRequest()
39+
setup_request_id(request, "request_id", True)
40+
assert re.match(
41+
r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
42+
request.request_id,
43+
)
44+
45+
# Test with proto3 optional field already in request
46+
request = MockRequest(request_id="already_set")
47+
setup_request_id(request, "request_id", True)
48+
assert request.request_id == "already_set"
49+
50+
# Test with non-proto3 optional field empty
51+
request = MockRequest(request_id="")
52+
setup_request_id(request, "request_id", False)
53+
assert re.match(
54+
r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
55+
request.request_id,
56+
)
57+
58+
# Test with non-proto3 optional field already set
59+
request = MockRequest(request_id="already_set")
60+
setup_request_id(request, "request_id", False)
61+
assert request.request_id == "already_set"
62+
63+
# Test with proto3 optional field not in request (MockProtoRequest)
64+
request = MockProtoRequest()
65+
setup_request_id(request, "request_id", True)
66+
assert re.match(
67+
r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
68+
request.request_id,
69+
)
70+
71+
# Test with proto3 optional field already in request (MockProtoRequest)
72+
request = MockProtoRequest(request_id="already_set")
73+
setup_request_id(request, "request_id", True)
74+
assert request.request_id == "already_set"
75+
76+
# Test with ValueError
77+
class MockValueErrorRequest:
78+
def HasField(self, key):
79+
raise ValueError("Mismatched field")
80+
81+
def __contains__(self, key):
82+
return hasattr(self, key)
83+
84+
request = MockValueErrorRequest()
85+
setup_request_id(request, "request_id", True)
86+
assert re.match(
87+
r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
88+
request.request_id,
89+
)
90+
91+
# Test with dict and proto3 optional field not in request
92+
request = {}
93+
setup_request_id(request, "request_id", True)
94+
assert re.match(
95+
r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
96+
request["request_id"],
97+
)
98+
99+
# Test with dict and proto3 optional field already in request
100+
request = {"request_id": "already_set"}
101+
setup_request_id(request, "request_id", True)
102+
assert request["request_id"] == "already_set"
103+
104+
# Test with dict and non-proto3 optional field empty
105+
request = {"request_id": ""}
106+
setup_request_id(request, "request_id", False)
107+
assert re.match(
108+
r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}",
109+
request["request_id"],
110+
)
111+
112+
# Test with dict and non-proto3 optional field already set
113+
request = {"request_id": "already_set"}
114+
setup_request_id(request, "request_id", False)
115+
assert request["request_id"] == "already_set"

0 commit comments

Comments
 (0)