Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -25,9 +25,10 @@
# Older Python versions safely ignore this variable.
__lazy_modules__: Set[str] = {
"google.api_core.gapic_v1.client_info",
"google.api_core.gapic_v1.request",
"google.api_core.gapic_v1.routing_header",
}
__all__ = ["client_info", "routing_header"]
__all__ = ["client_info", "request", "routing_header"]

if _has_grpc:
__lazy_modules__.update(
Expand All @@ -41,6 +42,7 @@

from google.api_core.gapic_v1 import ( # noqa: E402
client_info,
request,
routing_header,
)

Expand Down
72 changes: 72 additions & 0 deletions packages/google-api-core/google/api_core/gapic_v1/request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
Comment thread
hebaalazzeh marked this conversation as resolved.
# 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.
#

"""Helpers for preparing and structuring API requests.
This module provides utilities to preprocess request parameters and objects
before invoking API methods, such as automatically generating request IDs
if they are not already set.
"""

import uuid
from typing import Union

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.
Comment thread
hebaalazzeh marked this conversation as resolved.
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:
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()))
112 changes: 112 additions & 0 deletions packages/google-api-core/tests/unit/gapic/test_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
# 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 re

import pytest

from google.api_core.gapic_v1.request import setup_request_id


# --- Mock Request Helper Classes ---


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)


# --- Parameterized Test ---

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 cases
(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 cases
(MockProtoRequest(), True, "uuid"),
(MockProtoRequest(request_id="already_set"), True, "already_set"),
# ValueError case
(MockValueErrorRequest(), True, "uuid"),
# Dict cases
({}, True, "uuid"),
({"request_id": "already_set"}, True, "already_set"),
({"request_id": ""}, False, "uuid"),
({"request_id": "already_set"}, False, "already_set"),
# None case
(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_already_in_request",
"dict_non_proto3_optional_empty",
"dict_non_proto3_optional_already_set",
"none_request",
],
)
def test_setup_request_id(request_obj, is_proto3_optional, expected):
# Act
setup_request_id(request_obj, "request_id", is_proto3_optional)

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

# Extract the resulting value depending on container type
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
Loading