Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
79f3668
feat(api-core): move request-id auto-population logic to gapic_v1 pub…
hebaalazzeh Jul 16, 2026
62fbcc8
refactor(api-core): rename gapic_v1.method_helpers to gapic_v1.reques…
hebaalazzeh Jul 16, 2026
20adea6
chore: address PR review comments for gapic centralization request ID
hebaalazzeh Jul 16, 2026
f03ac7c
feat(api-core): move request-id auto-population logic to gapic_v1 pub…
hebaalazzeh Jul 16, 2026
2cc27b5
feat: delegate request-id setup to public method_helpers
hebaalazzeh Jul 16, 2026
9c2121c
refactor: use request module instead of method_helpers in gapic-gener…
hebaalazzeh Jul 16, 2026
dbc7307
refactor(api-core): rename gapic_v1.method_helpers to gapic_v1.reques…
hebaalazzeh Jul 16, 2026
5f1b721
chore: address PR review comments for gapic centralization request ID
hebaalazzeh Jul 16, 2026
99cee8e
chore: remove private _setup_request_id and call setup_request_id hel…
hebaalazzeh Jul 16, 2026
4ff1910
test(api-core): update UUID regex in test_request.py
hebaalazzeh Jul 16, 2026
77f97c8
refactor(api-core): rename request.py to requests.py for consistency
hebaalazzeh Jul 17, 2026
7fdf94e
Merge branch 'feat/gapic-centralization-api-core-request-id' into fea…
hebaalazzeh Jul 17, 2026
37ad06f
refactor(generator): update templates and goldens to use requests ins…
hebaalazzeh Jul 17, 2026
b9e2c62
fix(api-core): handle dictionary requests with None value in setup_re…
hebaalazzeh Jul 17, 2026
6bb65bc
merge branch 'feat/gapic-centralization-api-core-request-id' into fea…
hebaalazzeh Jul 17, 2026
0482ae6
fix(api-core): remove redundant request.py and test_request.py, sort …
hebaalazzeh Jul 17, 2026
31ae5ec
Merge remote-tracking branch 'origin/main' into feat/gapic-generator-…
hebaalazzeh Jul 20, 2026
2001c79
feat(generator): add _compat.py fallback for setup_request_id
hebaalazzeh Jul 20, 2026
2d1d2a5
fix(generator): add type ignore to requests import in compat template…
hebaalazzeh Jul 21, 2026
9e169f8
fix(generator): add coverage pragma comments for fallback path in com…
hebaalazzeh Jul 21, 2026
5af5222
Merge remote-tracking branch 'origin/main' into feat/gapic-generator-…
hebaalazzeh Jul 21, 2026
6ef6dd4
Merge remote-tracking branch 'origin/main' into feat/gapic-generator-…
hebaalazzeh Jul 21, 2026
61aaabb
chore(generator): update golden client.py files with setup_request_id…
hebaalazzeh Jul 21, 2026
0b2ceb2
fix(generator): add utf-8 coding header to _compat.py.j2 template
hebaalazzeh Jul 21, 2026
3204cea
chore(generator): update storagebatchoperations client.py and async_c…
hebaalazzeh Jul 21, 2026
3e6b78c
refactor(generator): optimize setup_request_id uuid creation and link…
hebaalazzeh Jul 21, 2026
1e6b56e
docs(generator): update TODO issue link to #17813 in _compat.py
hebaalazzeh Jul 21, 2026
ea8d263
chore: trigger Kokoro system tests build rerun
hebaalazzeh Jul 21, 2026
8e9f505
fix(generator): add pragma no cover to setup_request_id definition in…
hebaalazzeh Jul 21, 2026
142d337
fix(generator): add module level pragma no cover to _compat.py to pre…
hebaalazzeh Jul 21, 2026
e5f8e13
fix(generator): only generate _compat.py if API schema contains auto_…
hebaalazzeh Jul 21, 2026
905d2b1
fix(generator): fix proto-plus field check in setup_request_id fallback
hebaalazzeh Jul 21, 2026
7b7668c
fix(generator): revert goldens without auto_populated_fields and fix …
hebaalazzeh Jul 21, 2026
536a20e
fix(generator): remove encoding and pragma no cover from _compat.py h…
hebaalazzeh Jul 21, 2026
a8105ff
fix(generator): remove unused get_uuid4_re macro
hebaalazzeh Jul 21, 2026
e243bbd
fix(generator): update fallback proto-plus check to exactly match api…
hebaalazzeh Jul 21, 2026
926eb3c
fix(generator): rename requests import to request to match api-core
hebaalazzeh Jul 21, 2026
22d569d
fix(generator): fix broken diff in client.py and duplicate wrapper in…
hebaalazzeh Jul 21, 2026
c4eb2d9
test(generator): add tests for _compat.py fallback
hebaalazzeh Jul 21, 2026
ebc6bd9
fix(generator): update storagebatchoperations goldens to reflect setu…
hebaalazzeh Jul 21, 2026
eb08c77
fix(generator): add storagebatchoperations goldens missing files
hebaalazzeh Jul 21, 2026
e052b61
fix(generator): address remaining PR review comments
hebaalazzeh Jul 21, 2026
44e43ce
fix(generator): always generate _compat.py and remove downstream unit…
hebaalazzeh Jul 21, 2026
6f07478
fix(generator): use unified _compat.py.j2
hebaalazzeh Jul 21, 2026
82aa10c
fix(generator): install local google-api-core in nox test sessions
hebaalazzeh Jul 21, 2026
6eb89f7
fix(generator): address PR review comments for request-id centralizat…
hebaalazzeh Jul 22, 2026
a25e3fb
docs(templates): add explanatory comment for has_auto_populated_field…
hebaalazzeh Jul 22, 2026
9770a1e
refactor(schema): encapsulate has_auto_populated_fields on Service wr…
hebaalazzeh Jul 22, 2026
e09ddec
chore(generator): remove unnecessary session.install('-e', '../google…
hebaalazzeh Jul 22, 2026
e047593
test(schema): add unit test for Service.has_auto_populated_fields to …
hebaalazzeh Jul 22, 2026
f813e56
docs(generator): clarify lower-bound google-api-core version comment …
hebaalazzeh Jul 23, 2026
0aa48cd
docs(templates): clarify google-api-core version comment in _compat.p…
hebaalazzeh Jul 23, 2026
4f6baaa
feat(templates): generate _compat.py for all integration test goldens
hebaalazzeh Jul 23, 2026
c5218d8
fix(templates): resolve UndefinedError by moving auto_populated_field…
hebaalazzeh Jul 23, 2026
4709121
fix(templates): fix setup_request_id import module name and add pragm…
hebaalazzeh Jul 23, 2026
dbd4f5f
feat(templates): remove pragmas from _compat.py and add test_compat.p…
hebaalazzeh Jul 23, 2026
5b77291
fix(test): update test_compat template and goldens for complete branc…
hebaalazzeh Jul 23, 2026
7a22cc3
fix(coverage): add # pragma: NO COVER to compatibility fallback block…
hebaalazzeh Jul 23, 2026
f454c14
ci: add retry flags to curl commands in gapic-generator-tests workflow
hebaalazzeh Jul 23, 2026
fd17594
Merge origin/main into feat/gapic-generator-centralization-request-id
hebaalazzeh Jul 23, 2026
7fbb8f8
fix(compat): add type annotations and ignores to _compat template and…
hebaalazzeh Jul 23, 2026
022f2ad
fix(coverage): add test cases for _pb proto-plus requests in google-a…
hebaalazzeh Jul 23, 2026
0dc81e3
fix(generator): add --no-deps when installing local google-api-core i…
hebaalazzeh Jul 23, 2026
4a279f2
fix(generator): type annotate self._client_options and update workflo…
hebaalazzeh Jul 23, 2026
ee9cd96
chore(generator): revert unrelated golden files to match origin/main
hebaalazzeh Jul 23, 2026
77a80a8
chore(generator): revert setup.py and WORKSPACE changes
hebaalazzeh Jul 23, 2026
1919ee2
chore(ci): revert gapic-generator-tests.yml to origin/main
hebaalazzeh Jul 23, 2026
e3b439f
chore(generator): clean up test_service.py and noxfile.py
hebaalazzeh Jul 23, 2026
0e5ebca
fix(generator): restore noxfile.py and add local google-api-core inst…
hebaalazzeh Jul 23, 2026
fa8fbae
fix(generator): fix import group formatting in _compat.py template an…
hebaalazzeh Jul 23, 2026
4b50876
chore(generator): restore get_uuid4_re macro in test_macros.j2
hebaalazzeh Jul 23, 2026
86352b4
chore(api-core): revert requests.py and test_requests.py to origin/main
hebaalazzeh Jul 23, 2026
ddacf0d
chore(generator): remove unused get_uuid4_re macro from test_macros.j2
hebaalazzeh Jul 23, 2026
492bf63
fix(generator): pass MYPY_CONFIG_FILE to mypy in showcase_mypy session
hebaalazzeh Jul 23, 2026
09bea02
fix(generator): add --no-build-isolation when installing local google…
hebaalazzeh Jul 23, 2026
b7577b2
fix(generator): remove --no-build-isolation and specify package-data …
hebaalazzeh Jul 23, 2026
0055127
fix(generator): update self._client_options in golden client.py files…
hebaalazzeh Jul 23, 2026
d0b42c8
ci: trigger Bazel integration test check for goldens
hebaalazzeh Jul 23, 2026
8dbac4c
chore: revert google-api-core pyproject.toml edit
hebaalazzeh Jul 23, 2026
dd49e39
chore(generator): revert noxfile.py mypy command and restore test_gen…
hebaalazzeh Jul 23, 2026
30199d5
wip
hebaalazzeh Jul 23, 2026
dca70f5
wip
hebaalazzeh Jul 24, 2026
64d4933
Merge branch 'main' into feat/gapic-generator-centralization-request-id
hebaalazzeh Jul 24, 2026
9ef2569
Merge branch 'main' into feat/gapic-generator-centralization-request-id
hebaalazzeh Jul 24, 2026
4ee805e
revert noxfile
hebaalazzeh Jul 24, 2026
0012d51
fix compat layer
hebaalazzeh Jul 24, 2026
adf2e80
update goldens
hebaalazzeh Jul 24, 2026
99f66c8
Merge branch 'main' into feat/gapic-generator-centralization-request-id
hebaalazzeh Jul 24, 2026
20af454
clean up
hebaalazzeh Jul 24, 2026
430cf9d
clean up
hebaalazzeh Jul 24, 2026
0433a86
revert test generator
hebaalazzeh Jul 24, 2026
f8a5b9e
clean up test service
hebaalazzeh Jul 24, 2026
c892809
revert change to test service
hebaalazzeh Jul 24, 2026
1df077f
revert whitespace
hebaalazzeh Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,53 @@ falling back to the local implementation if not present. #}
{# TODO(https://github.com/googleapis/google-cloud-python/issues/17883):
Backfill compatibility functions being removed from the client layer. #}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{% if has_auto_populated_fields %}

from typing import Union
import uuid

import google.protobuf.message


def setup_request_id(
request: Union[google.protobuf.message.Message, dict, None],
field_name: str,
is_proto3_optional: bool,
) -> None:
"""Populate a UUID4 field in the request if it is not already set.

This helper is used to ensure request idempotency by automatically
generating a unique identifier (such as `request_id`) for requests
that support it. If a request is retried, the same identifier can be
sent on subsequent retries, allowing the server to recognize the retried
request and prevent duplicate processing (e.g., creating duplicate
resources).

Args:
request (Union[google.protobuf.message.Message, dict]): The
request object.
field_name (str): The name of the field to populate.
is_proto3_optional (bool): Whether the field is proto3 optional.
"""
if request is None:
return

if isinstance(request, dict):
if is_proto3_optional:
if field_name not in request or request[field_name] is None:
request[field_name] = str(uuid.uuid4())
elif not request.get(field_name):
request[field_name] = str(uuid.uuid4())
return

if is_proto3_optional:
try:
# Pure protobuf messages
if not request.HasField(field_name):
setattr(request, field_name, str(uuid.uuid4()))
except (AttributeError, ValueError):
# Proto-plus messages or other objects
if not getattr(request, field_name, None):
setattr(request, field_name, str(uuid.uuid4()))
else:
if not getattr(request, field_name, None):
setattr(request, field_name, str(uuid.uuid4()))
{% endblock %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{% endblock %}
{% endif %}
{% endblock %}

Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
{% set is_proto3_optional = method.input.fields[auto_populated_field].proto3_optional %}
{% if is_async %}
self._client._setup_request_id(request, '{{ auto_populated_field }}', {{ is_proto3_optional }})
{% else %}
self._setup_request_id(request, '{{ auto_populated_field }}', {{ is_proto3_optional }})
{% endif %}
setup_request_id(request, '{{ auto_populated_field }}', {{ is_proto3_optional }})
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ from {{package_path}} import gapic_version as package_version
from google.api_core.client_options import ClientOptions
from google.api_core import exceptions as core_exceptions
from google.api_core import gapic_v1
{% if has_auto_populated_fields %}
from {{package_path}}._compat import setup_request_id
{% endif %}
from google.api_core import retry_async as retries
from google.auth import credentials as ga_credentials # type: ignore
from google.oauth2 import service_account # type: ignore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ from google.api_core import exceptions as core_exceptions
from google.api_core import extended_operation
{% endif %}
from google.api_core import gapic_v1
{% if has_auto_populated_fields %}
from {{package_path}}._compat import setup_request_id
{% endif %}
from google.api_core import retry as retries
from google.auth import credentials as ga_credentials # type: ignore
from google.auth.transport import mtls # type: ignore
Expand Down Expand Up @@ -453,39 +456,6 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
# NOTE (b/349488459): universe validation is disabled until further notice.
return True

{% if has_auto_populated_fields %}

@staticmethod
def _setup_request_id(request, field_name: str, is_proto3_optional: bool):
"""Populate a UUID4 field in the request if it is not already set.

Args:
request (Union[google.protobuf.message.Message, dict]): The request object.
field_name (str): The name of the field to populate.
is_proto3_optional (bool): Whether the field is proto3 optional.
"""
if isinstance(request, dict):
if is_proto3_optional:
if field_name not in request:
request[field_name] = str(uuid.uuid4())
elif not request.get(field_name):
request[field_name] = str(uuid.uuid4())
return

if is_proto3_optional:
try:
# Pure protobuf messages
if not request.HasField(field_name):
setattr(request, field_name, str(uuid.uuid4()))
except (AttributeError, ValueError):
# Proto-plus messages or other objects
if field_name not in request:
setattr(request, field_name, str(uuid.uuid4()))
else:
if not getattr(request, field_name):
setattr(request, field_name, str(uuid.uuid4()))
{% endif %}

def _add_cred_info_for_auth_errors(
self,
error: core_exceptions.GoogleAPICallError
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,84 +434,6 @@ def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code):
client._add_cred_info_for_auth_errors(error)
assert error.details == []

{% if has_auto_populated_fields %}
def test__setup_request_id():
class MockRequest:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def __contains__(self, key):
return hasattr(self, key)

class MockProtoRequest:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def HasField(self, key):
return hasattr(self, key)

# Test with proto3 optional field not in request
request = MockRequest()
{{ service.client_name }}._setup_request_id(request, "request_id", True)
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)

# Test with proto3 optional field already in request
request = MockRequest(request_id="already_set")
{{ service.client_name }}._setup_request_id(request, "request_id", True)
assert request.request_id == "already_set"

# Test with non-proto3 optional field empty
request = MockRequest(request_id="")
{{ service.client_name }}._setup_request_id(request, "request_id", False)
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)

# Test with non-proto3 optional field already set
request = MockRequest(request_id="already_set")
{{ service.client_name }}._setup_request_id(request, "request_id", False)
assert request.request_id == "already_set"

# Test with proto3 optional field not in request (MockProtoRequest)
request = MockProtoRequest()
{{ service.client_name }}._setup_request_id(request, "request_id", True)
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)

# Test with proto3 optional field already in request (MockProtoRequest)
request = MockProtoRequest(request_id="already_set")
{{ service.client_name }}._setup_request_id(request, "request_id", True)
assert request.request_id == "already_set"

# Test with ValueError
class MockValueErrorRequest:
def HasField(self, key):
raise ValueError("Mismatched field")
def __contains__(self, key):
return hasattr(self, key)

request = MockValueErrorRequest()
{{ service.client_name }}._setup_request_id(request, "request_id", True)
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)

# Test with dict and proto3 optional field not in request
request = {}
{{ service.client_name }}._setup_request_id(request, "request_id", True)
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request["request_id"])

# Test with dict and proto3 optional field already in request
request = {"request_id": "already_set"}
{{ service.client_name }}._setup_request_id(request, "request_id", True)
assert request["request_id"] == "already_set"

# Test with dict and non-proto3 optional field empty
request = {"request_id": ""}
{{ service.client_name }}._setup_request_id(request, "request_id", False)
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request["request_id"])

# Test with dict and non-proto3 optional field already set
request = {"request_id": "already_set"}
{{ service.client_name }}._setup_request_id(request, "request_id", False)
assert request["request_id"] == "already_set"

{% endif %}
@pytest.mark.parametrize("client_class,transport_name", [
{% if 'grpc' in opts.transport %}
({{ service.client_name }}, "grpc"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,91 @@ google-api-core has the functions in `_compat.py.j2`. #}
{# TODO(https://github.com/googleapis/google-cloud-python/issues/17883):
Backfill compatibility functions tests being removed from the client layer. #}


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{% if has_auto_populated_fields %}

import re
import pytest

{% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %}
from {{package_path}}._compat import setup_request_id

class MockRequest:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)

def __contains__(self, key):
return hasattr(self, key)


class MockProtoRequest:
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)

def HasField(self, key):
return hasattr(self, key)


class MockValueErrorRequest:
def HasField(self, key):
raise ValueError("Mismatched field")

def __contains__(self, key):
return hasattr(self, key)

UUID_REGEX = r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"

@pytest.mark.parametrize(
"request_obj, is_proto3_optional, expected",
[
(MockRequest(), True, "uuid"),
(MockRequest(request_id="already_set"), True, "already_set"),
(MockRequest(request_id=""), False, "uuid"),
(MockRequest(request_id="already_set"), False, "already_set"),
(MockProtoRequest(), True, "uuid"),
(MockProtoRequest(request_id="already_set"), True, "already_set"),
(MockValueErrorRequest(), True, "uuid"),
({}, True, "uuid"),
({"request_id": None}, True, "uuid"),
({"request_id": "already_set"}, True, "already_set"),
({"request_id": ""}, False, "uuid"),
({"request_id": None}, False, "uuid"),
({"request_id": "already_set"}, False, "already_set"),
(None, True, "none"),
],
ids=[
"proto3_optional_not_in_request",
"proto3_optional_already_in_request",
"non_proto3_optional_empty",
"non_proto3_optional_already_set",
"proto3_optional_not_in_request_proto",
"proto3_optional_already_in_request_proto",
"value_error_fallback",
"dict_proto3_optional_not_in_request",
"dict_proto3_optional_value_none",
"dict_proto3_optional_already_in_request",
"dict_non_proto3_optional_empty",
"dict_non_proto3_optional_value_none",
"dict_non_proto3_optional_already_set",
"none_request",
],
)
def test_setup_request_id(request_obj, is_proto3_optional, expected):
setup_request_id(request_obj, "request_id", is_proto3_optional)

if expected == "none":
assert request_obj is None
return

value = (
request_obj["request_id"]
if isinstance(request_obj, dict)
else request_obj.request_id
)

if expected == "uuid":
assert re.match(UUID_REGEX, value)
else:
assert value == expected

{% endblock %}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{% endblock %}
{% endif %}
{% endblock %}

Original file line number Diff line number Diff line change
Expand Up @@ -2215,10 +2215,6 @@ def test_initialize_client_w_{{transport_name}}():
{% endfor %}{# method in service.methods.values() #}
{% endmacro %}{# empty_call_test #}

{% macro get_uuid4_re() -%}
{{ uuid4_re }}
{%- endmacro %}{# uuid_re #}

{% macro routing_parameter_test(service, api, transport, is_async) %}
{% for method in service.methods.values() %}{# method #}
{# See existing proposal b/330610501 to add support for explicit routing in BIDI/client side streaming #}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,53 @@
# limitations under the License.
#
"""A compatibility module for older versions of google-api-core."""

from typing import Union
import uuid

import google.protobuf.message


def setup_request_id(
request: Union[google.protobuf.message.Message, dict, None],
field_name: str,
is_proto3_optional: bool,
) -> None:
"""Populate a UUID4 field in the request if it is not already set.

This helper is used to ensure request idempotency by automatically
generating a unique identifier (such as `request_id`) for requests
that support it. If a request is retried, the same identifier can be
sent on subsequent retries, allowing the server to recognize the retried
request and prevent duplicate processing (e.g., creating duplicate
resources).

Args:
request (Union[google.protobuf.message.Message, dict]): The
request object.
field_name (str): The name of the field to populate.
is_proto3_optional (bool): Whether the field is proto3 optional.
"""
if request is None:
return

if isinstance(request, dict):
if is_proto3_optional:
if field_name not in request or request[field_name] is None:
request[field_name] = str(uuid.uuid4())
elif not request.get(field_name):
request[field_name] = str(uuid.uuid4())
return

if is_proto3_optional:
try:
# Pure protobuf messages
if not request.HasField(field_name):
setattr(request, field_name, str(uuid.uuid4()))
except (AttributeError, ValueError):
# Proto-plus messages or other objects
if not getattr(request, field_name, None):
setattr(request, field_name, str(uuid.uuid4()))
else:
if not getattr(request, field_name, None):
setattr(request, field_name, str(uuid.uuid4()))
Loading
Loading