Skip to content

Commit 3397d1b

Browse files
authored
chore: refactor setting of auto-populated field in request (#17004)
This PR addresses the feedback in #16944 (comment)
1 parent 1e6989d commit 3397d1b

7 files changed

Lines changed: 228 additions & 18 deletions

File tree

packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# limitations under the License.
1515
#}
1616

17-
{% macro auto_populate_uuid4_fields(api, method) %}
17+
{% macro auto_populate_uuid4_fields(api, method, is_async=False) %}
1818
{#
1919
Automatically populate UUID4 fields according to
2020
https://google.aip.dev/client-libraries/4235 when the
@@ -27,12 +27,12 @@
2727
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
2828
{% if method_settings is not none %}
2929
{% for auto_populated_field in method_settings.auto_populated_fields %}
30-
{% if method.input.fields[auto_populated_field].proto3_optional %}
31-
if '{{ auto_populated_field }}' not in request:
30+
{% set is_proto3_optional = method.input.fields[auto_populated_field].proto3_optional %}
31+
{% if is_async %}
32+
self._client._setup_request_id(request, '{{ auto_populated_field }}', {{ is_proto3_optional }})
3233
{% else %}
33-
if not request.{{ auto_populated_field }}:
34+
self._setup_request_id(request, '{{ auto_populated_field }}', {{ is_proto3_optional }})
3435
{% endif %}
35-
request.{{ auto_populated_field }} = str(uuid.uuid4())
3636
{% endfor %}
3737
{% endif %}{# if method_settings is not none #}
3838
{% endwith %}{# method_settings #}

packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ class {{ service.async_client_name }}:
395395

396396
{{ shared_macros.create_metadata(method) }}
397397
{{ shared_macros.add_api_version_header_to_metadata(service.version) }}
398-
{{ shared_macros.auto_populate_uuid4_fields(api, method) }}
398+
{{ shared_macros.auto_populate_uuid4_fields(api, method, is_async=True) }}
399399

400400
# Validate the universe domain.
401401
self._client._validate_universe_domain()

packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,38 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
453453
# NOTE (b/349488459): universe validation is disabled until further notice.
454454
return True
455455

456+
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|select|list %}
457+
@staticmethod
458+
def _setup_request_id(request, field_name: str, is_proto3_optional: bool):
459+
"""Populate a UUID4 field in the request if it is not already set.
460+
461+
Args:
462+
request (Union[google.protobuf.message.Message, dict]): The request object.
463+
field_name (str): The name of the field to populate.
464+
is_proto3_optional (bool): Whether the field is proto3 optional.
465+
"""
466+
if isinstance(request, dict):
467+
if is_proto3_optional:
468+
if field_name not in request:
469+
request[field_name] = str(uuid.uuid4())
470+
elif not request.get(field_name):
471+
request[field_name] = str(uuid.uuid4())
472+
return
473+
474+
if is_proto3_optional:
475+
try:
476+
# Pure protobuf messages
477+
if not request.HasField(field_name):
478+
setattr(request, field_name, str(uuid.uuid4()))
479+
except (AttributeError, ValueError):
480+
# Proto-plus messages or other objects
481+
if field_name not in request:
482+
setattr(request, field_name, str(uuid.uuid4()))
483+
else:
484+
if not getattr(request, field_name):
485+
setattr(request, field_name, str(uuid.uuid4()))
486+
{% endif %}
487+
456488
def _add_cred_info_for_auth_errors(
457489
self,
458490
error: core_exceptions.GoogleAPICallError

packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,84 @@ def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code):
415415
client._add_cred_info_for_auth_errors(error)
416416
assert error.details == []
417417

418+
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|select|list %}
419+
def test__setup_request_id():
420+
class MockRequest:
421+
def __init__(self, **kwargs):
422+
for k, v in kwargs.items():
423+
setattr(self, k, v)
424+
def __contains__(self, key):
425+
return hasattr(self, key)
426+
427+
class MockProtoRequest:
428+
def __init__(self, **kwargs):
429+
for k, v in kwargs.items():
430+
setattr(self, k, v)
431+
def HasField(self, key):
432+
return hasattr(self, key)
433+
434+
# Test with proto3 optional field not in request
435+
request = MockRequest()
436+
{{ service.client_name }}._setup_request_id(request, "request_id", True)
437+
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)
438+
439+
# Test with proto3 optional field already in request
440+
request = MockRequest(request_id="already_set")
441+
{{ service.client_name }}._setup_request_id(request, "request_id", True)
442+
assert request.request_id == "already_set"
443+
444+
# Test with non-proto3 optional field empty
445+
request = MockRequest(request_id="")
446+
{{ service.client_name }}._setup_request_id(request, "request_id", False)
447+
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)
448+
449+
# Test with non-proto3 optional field already set
450+
request = MockRequest(request_id="already_set")
451+
{{ service.client_name }}._setup_request_id(request, "request_id", False)
452+
assert request.request_id == "already_set"
453+
454+
# Test with proto3 optional field not in request (MockProtoRequest)
455+
request = MockProtoRequest()
456+
{{ service.client_name }}._setup_request_id(request, "request_id", True)
457+
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)
458+
459+
# Test with proto3 optional field already in request (MockProtoRequest)
460+
request = MockProtoRequest(request_id="already_set")
461+
{{ service.client_name }}._setup_request_id(request, "request_id", True)
462+
assert request.request_id == "already_set"
463+
464+
# Test with ValueError
465+
class MockValueErrorRequest:
466+
def HasField(self, key):
467+
raise ValueError("Mismatched field")
468+
def __contains__(self, key):
469+
return hasattr(self, key)
470+
471+
request = MockValueErrorRequest()
472+
{{ service.client_name }}._setup_request_id(request, "request_id", True)
473+
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request.request_id)
474+
475+
# Test with dict and proto3 optional field not in request
476+
request = {}
477+
{{ service.client_name }}._setup_request_id(request, "request_id", True)
478+
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request["request_id"])
479+
480+
# Test with dict and proto3 optional field already in request
481+
request = {"request_id": "already_set"}
482+
{{ service.client_name }}._setup_request_id(request, "request_id", True)
483+
assert request["request_id"] == "already_set"
484+
485+
# Test with dict and non-proto3 optional field empty
486+
request = {"request_id": ""}
487+
{{ service.client_name }}._setup_request_id(request, "request_id", False)
488+
assert re.match(r"{{ test_macros.get_uuid4_re() }}", request["request_id"])
489+
490+
# Test with dict and non-proto3 optional field already set
491+
request = {"request_id": "already_set"}
492+
{{ service.client_name }}._setup_request_id(request, "request_id", False)
493+
assert request["request_id"] == "already_set"
494+
495+
{% endif %}
418496
@pytest.mark.parametrize("client_class,transport_name", [
419497
{% if 'grpc' in opts.transport %}
420498
({{ service.client_name }}, "grpc"),

packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/async_client.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -621,8 +621,7 @@ async def sample_create_job():
621621
)),
622622
)
623623

624-
if not request.request_id:
625-
request.request_id = str(uuid.uuid4())
624+
self._client._setup_request_id(request, 'request_id', False)
626625

627626
# Validate the universe domain.
628627
self._client._validate_universe_domain()
@@ -728,8 +727,7 @@ async def sample_delete_job():
728727
)),
729728
)
730729

731-
if not request.request_id:
732-
request.request_id = str(uuid.uuid4())
730+
self._client._setup_request_id(request, 'request_id', False)
733731

734732
# Validate the universe domain.
735733
self._client._validate_universe_domain()
@@ -831,8 +829,7 @@ async def sample_cancel_job():
831829
)),
832830
)
833831

834-
if not request.request_id:
835-
request.request_id = str(uuid.uuid4())
832+
self._client._setup_request_id(request, 'request_id', False)
836833

837834
# Validate the universe domain.
838835
self._client._validate_universe_domain()

packages/gapic-generator/tests/integration/goldens/storagebatchoperations/google/cloud/storagebatchoperations_v1/services/storage_batch_operations/client.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,36 @@ def _validate_universe_domain(self):
471471
# NOTE (b/349488459): universe validation is disabled until further notice.
472472
return True
473473

474+
@staticmethod
475+
def _setup_request_id(request, field_name: str, is_proto3_optional: bool):
476+
"""Populate a UUID4 field in the request if it is not already set.
477+
478+
Args:
479+
request (Union[google.protobuf.message.Message, dict]): The request object.
480+
field_name (str): The name of the field to populate.
481+
is_proto3_optional (bool): Whether the field is proto3 optional.
482+
"""
483+
if isinstance(request, dict):
484+
if is_proto3_optional:
485+
if field_name not in request:
486+
request[field_name] = str(uuid.uuid4())
487+
elif not request.get(field_name):
488+
request[field_name] = str(uuid.uuid4())
489+
return
490+
491+
if is_proto3_optional:
492+
try:
493+
# Pure protobuf messages
494+
if not request.HasField(field_name):
495+
setattr(request, field_name, str(uuid.uuid4()))
496+
except (AttributeError, ValueError):
497+
# Proto-plus messages or other objects
498+
if field_name not in request:
499+
setattr(request, field_name, str(uuid.uuid4()))
500+
else:
501+
if not getattr(request, field_name):
502+
setattr(request, field_name, str(uuid.uuid4()))
503+
474504
def _add_cred_info_for_auth_errors(
475505
self,
476506
error: core_exceptions.GoogleAPICallError
@@ -1005,8 +1035,7 @@ def sample_create_job():
10051035
)),
10061036
)
10071037

1008-
if not request.request_id:
1009-
request.request_id = str(uuid.uuid4())
1038+
self._setup_request_id(request, 'request_id', False)
10101039

10111040
# Validate the universe domain.
10121041
self._validate_universe_domain()
@@ -1111,8 +1140,7 @@ def sample_delete_job():
11111140
)),
11121141
)
11131142

1114-
if not request.request_id:
1115-
request.request_id = str(uuid.uuid4())
1143+
self._setup_request_id(request, 'request_id', False)
11161144

11171145
# Validate the universe domain.
11181146
self._validate_universe_domain()
@@ -1213,8 +1241,7 @@ def sample_cancel_job():
12131241
)),
12141242
)
12151243

1216-
if not request.request_id:
1217-
request.request_id = str(uuid.uuid4())
1244+
self._setup_request_id(request, 'request_id', False)
12181245

12191246
# Validate the universe domain.
12201247
self._validate_universe_domain()

packages/gapic-generator/tests/integration/goldens/storagebatchoperations/tests/unit/gapic/storagebatchoperations_v1/test_storage_batch_operations.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,82 @@ def test__add_cred_info_for_auth_errors_no_get_cred_info(error_code):
334334
client._add_cred_info_for_auth_errors(error)
335335
assert error.details == []
336336

337+
def test__setup_request_id():
338+
class MockRequest:
339+
def __init__(self, **kwargs):
340+
for k, v in kwargs.items():
341+
setattr(self, k, v)
342+
def __contains__(self, key):
343+
return hasattr(self, key)
344+
345+
class MockProtoRequest:
346+
def __init__(self, **kwargs):
347+
for k, v in kwargs.items():
348+
setattr(self, k, v)
349+
def HasField(self, key):
350+
return hasattr(self, key)
351+
352+
# Test with proto3 optional field not in request
353+
request = MockRequest()
354+
StorageBatchOperationsClient._setup_request_id(request, "request_id", True)
355+
assert re.match(r"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}", request.request_id)
356+
357+
# Test with proto3 optional field already in request
358+
request = MockRequest(request_id="already_set")
359+
StorageBatchOperationsClient._setup_request_id(request, "request_id", True)
360+
assert request.request_id == "already_set"
361+
362+
# Test with non-proto3 optional field empty
363+
request = MockRequest(request_id="")
364+
StorageBatchOperationsClient._setup_request_id(request, "request_id", False)
365+
assert re.match(r"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}", request.request_id)
366+
367+
# Test with non-proto3 optional field already set
368+
request = MockRequest(request_id="already_set")
369+
StorageBatchOperationsClient._setup_request_id(request, "request_id", False)
370+
assert request.request_id == "already_set"
371+
372+
# Test with proto3 optional field not in request (MockProtoRequest)
373+
request = MockProtoRequest()
374+
StorageBatchOperationsClient._setup_request_id(request, "request_id", True)
375+
assert re.match(r"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}", request.request_id)
376+
377+
# Test with proto3 optional field already in request (MockProtoRequest)
378+
request = MockProtoRequest(request_id="already_set")
379+
StorageBatchOperationsClient._setup_request_id(request, "request_id", True)
380+
assert request.request_id == "already_set"
381+
382+
# Test with ValueError
383+
class MockValueErrorRequest:
384+
def HasField(self, key):
385+
raise ValueError("Mismatched field")
386+
def __contains__(self, key):
387+
return hasattr(self, key)
388+
389+
request = MockValueErrorRequest()
390+
StorageBatchOperationsClient._setup_request_id(request, "request_id", True)
391+
assert re.match(r"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}", request.request_id)
392+
393+
# Test with dict and proto3 optional field not in request
394+
request = {}
395+
StorageBatchOperationsClient._setup_request_id(request, "request_id", True)
396+
assert re.match(r"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}", request["request_id"])
397+
398+
# Test with dict and proto3 optional field already in request
399+
request = {"request_id": "already_set"}
400+
StorageBatchOperationsClient._setup_request_id(request, "request_id", True)
401+
assert request["request_id"] == "already_set"
402+
403+
# Test with dict and non-proto3 optional field empty
404+
request = {"request_id": ""}
405+
StorageBatchOperationsClient._setup_request_id(request, "request_id", False)
406+
assert re.match(r"[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}", request["request_id"])
407+
408+
# Test with dict and non-proto3 optional field already set
409+
request = {"request_id": "already_set"}
410+
StorageBatchOperationsClient._setup_request_id(request, "request_id", False)
411+
assert request["request_id"] == "already_set"
412+
337413
@pytest.mark.parametrize("client_class,transport_name", [
338414
(StorageBatchOperationsClient, "grpc"),
339415
(StorageBatchOperationsAsyncClient, "grpc_asyncio"),

0 commit comments

Comments
 (0)