Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit 7f93c76

Browse files
committed
feat: Automatically populate uuid4 fields
1 parent 03a8f98 commit 7f93c76

11 files changed

Lines changed: 146 additions & 7 deletions

File tree

.github/workflows/tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ concurrency:
1414
cancel-in-progress: true
1515

1616
env:
17-
SHOWCASE_VERSION: 0.30.0
17+
SHOWCASE_VERSION: 0.31.0
1818
PROTOC_VERSION: 3.20.2
1919

2020
jobs:

gapic/ads-templates/%namespace/%name/%version/%sub/services/%service/client.py.j2

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ from collections import OrderedDict
66
import os
77
import re
88
from typing import Callable, Dict, Mapping, MutableMapping, MutableSequence, Optional, {% if service.any_server_streaming %}Iterable, {% endif %}{% if service.any_client_streaming %}Iterator, {% endif %}Sequence, Tuple, Type, Union, cast
9+
import uuid
910
{% if service.any_deprecated %}
1011
import warnings
1112
{% endif %}
@@ -473,6 +474,27 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
473474
)
474475
{% endif %}
475476

477+
{#
478+
Automatically populate uuid4 fields according to AIP-4235
479+
(https://google.aip.dev/client-libraries/4235) if the
480+
field satisfies either of:
481+
- The field supports explicit presence and has not been set by the user
482+
- The field doesn't support explicit presence, and its value is the empty
483+
string (i.e. the default value)
484+
#}
485+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
486+
{% if method_settings is not none %}
487+
{% for auto_populated_field in method_settings.auto_populated_fields %}
488+
{% if method.input.fields[auto_populated_field].proto3_optional %}
489+
if not request.HasField({{ auto_populated_field }}):
490+
{% else %}
491+
if not request.{{ auto_populated_field }}:
492+
{% endif %}
493+
request.{{ auto_populated_field }} = str(uuid.uuid4())
494+
{% endfor %}
495+
{% endif %}{# if method_settings is not none #}
496+
{% endwith %}{# method_settings #}
497+
476498
# Send the request.
477499
{%+ if not method.void %}response = {% endif %}rpc(
478500
{% if not method.client_streaming %}

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
{% block content %}
44

55
import os
6+
import re
67
# try/except added for compatibility with python < 3.8
78
try:
89
from unittest import mock
@@ -568,6 +569,16 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
568569
{% if method.client_streaming %}
569570
assert next(args[0]) == request
570571
{% else %}
572+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
573+
{% if method_settings is not none %}
574+
{% for auto_populated_field in method_settings.auto_populated_fields %}
575+
# Ensure that the uuid4 field is set according to AIP 4235
576+
re.match(r"[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z", args[0].{{ auto_populated_field }})
577+
# clear UUID field so that the check below succeeds
578+
args[0].{{ auto_populated_field }} = ""
579+
{% endfor %}
580+
{% endif %}{# if method_settings is not none #}
581+
{% endwith %}{# method_settings #}
571582
assert args[0] == {{ method.input.ident }}()
572583
{% endif %}
573584

@@ -629,6 +640,16 @@ def test_{{ method_name }}_empty_call():
629640
{% if method.client_streaming %}
630641
assert next(args[0]) == request
631642
{% else %}
643+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
644+
{% if method_settings is not none %}
645+
{% for auto_populated_field in method_settings.auto_populated_fields %}
646+
# Ensure that the uuid4 field is set according to AIP 4235
647+
re.match(r"[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z", args[0].{{ auto_populated_field }})
648+
# clear UUID field so that the check below succeeds
649+
args[0].{{ auto_populated_field }} = ""
650+
{% endfor %}
651+
{% endif %}{# if method_settings is not none #}
652+
{% endwith %}{# method_settings #}
632653
assert args[0] == {{ method.input.ident }}()
633654
{% endif %}
634655
{% endif %}

gapic/schema/api.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
TRANSPORT_REST = "rest"
6161

6262

63-
6463
class MethodSettingsError(ValueError):
6564
"""
6665
Raised when `google.api.client_pb2.MethodSettings` contains

gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@
183183
)
184184
{% endif %} {# method.explicit_routing #}
185185

186+
{{ auto_populate_uuid4_fields(api, method) }}
187+
186188
# Validate the universe domain.
187189
self._validate_universe_domain()
188190

@@ -265,3 +267,26 @@
265267

266268
{% macro define_extended_operation_subclass(extended_operation) %}
267269
{% endmacro %}
270+
271+
{% macro auto_populate_uuid4_fields(api, method) %}
272+
{#
273+
Automatically populate uuid4 fields according to AIP-4235
274+
(https://google.aip.dev/client-libraries/4235) if the
275+
field satisfies either of:
276+
- The field supports explicit presence and has not been set by the user
277+
- The field doesn't support explicit presence, and its value is the empty
278+
string (i.e. the default value)
279+
#}
280+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
281+
{% if method_settings is not none %}
282+
{% for auto_populated_field in method_settings.auto_populated_fields %}
283+
{% if method.input.fields[auto_populated_field].proto3_optional %}
284+
if not request.HasField({{ auto_populated_field }}):
285+
{% else %}
286+
if not request.{{ auto_populated_field }}:
287+
{% endif %}
288+
request.{{ auto_populated_field }} = str(uuid.uuid4())
289+
{% endfor %}
290+
{% endif %}{# if method_settings is not none #}
291+
{% endwith %}{# method_settings #}
292+
{% endmacro %}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
{% extends "_base.py.j2" %}
22

33
{% block content %}
4+
{% import "%namespace/%name_%version/%sub/services/%service/_client_macros.j2" as macros %}
45

56
from collections import OrderedDict
67
import functools
78
import re
89
from typing import Dict, Mapping, MutableMapping, MutableSequence, Optional, {% if service.any_server_streaming %}AsyncIterable, Awaitable, {% endif %}{% if service.any_client_streaming %}AsyncIterator, {% endif %}Sequence, Tuple, Type, Union
10+
import uuid
911
{% if service.any_deprecated %}
1012
import warnings
1113
{% endif %}
@@ -386,6 +388,8 @@ class {{ service.async_client_name }}:
386388
)
387389
{% endif %}
388390

391+
{{ macros.auto_populate_uuid4_fields(api, method) }}
392+
389393
# Validate the universe domain.
390394
self._client._validate_universe_domain()
391395

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import functools
1010
import os
1111
import re
1212
from typing import Dict, Mapping, MutableMapping, MutableSequence, Optional, {% if service.any_server_streaming %}Iterable, {% endif %}{% if service.any_client_streaming %}Iterator, {% endif %}Sequence, Tuple, Type, Union, cast
13+
import uuid
1314
import warnings
1415

1516
{% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
{% import "tests/unit/gapic/%name_%version/%sub/test_macros.j2" as test_macros %}
55

66
import os
7+
import re
78
# try/except added for compatibility with python < 3.8
89
try:
910
from unittest import mock
@@ -849,10 +850,10 @@ def test_{{ service.client_name|snake_case }}_create_channel_credentials_file(cl
849850

850851
{% for method in service.methods.values() if 'grpc' in opts.transport %}{# method_name #}
851852
{% if method.extended_lro %}
852-
{{ test_macros.grpc_required_tests(method, service, full_extended_lro=True) }}
853+
{{ test_macros.grpc_required_tests(method, service, api, full_extended_lro=True) }}
853854

854855
{% endif %}
855-
{{ test_macros.grpc_required_tests(method, service) }}
856+
{{ test_macros.grpc_required_tests(method, service, api) }}
856857
{% endfor %} {# method in methods for grpc #}
857858

858859
{% for method in service.methods.values() if 'rest' in opts.transport %}

gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{% macro grpc_required_tests(method, service, full_extended_lro=False) %}
1+
{% macro grpc_required_tests(method, service, api, full_extended_lro=False) %}
22
{% with method_name = method.safe_name|snake_case + "_unary" if method.extended_lro and not full_extended_lro else method.safe_name|snake_case, method_output = method.extended_lro.operation_type if method.extended_lro and not full_extended_lro else method.output %}
33
@pytest.mark.parametrize("request_type", [
44
{{ method.input.ident }},
@@ -58,7 +58,19 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
5858
{% if method.client_streaming %}
5959
assert next(args[0]) == request
6060
{% else %}
61-
assert args[0] == {{ method.input.ident }}()
61+
request = {{ method.input.ident }}()
62+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
63+
{% if method_settings is not none %}
64+
{% for auto_populated_field in method_settings.auto_populated_fields %}
65+
# Ensure that the uuid4 field is set according to AIP 4235
66+
re.match(r"[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z", args[0].{{ auto_populated_field }})
67+
# clear UUID field so that the check below succeeds
68+
args[0].{{ auto_populated_field }} = ""
69+
{% endfor %}
70+
{% endif %}{# if method_settings is not none #}
71+
{% endwith %}{# method_settings #}
72+
73+
assert args[0] == request
6274
{% endif %}
6375

6476
# Establish that the response is the type that we expect.
@@ -119,6 +131,16 @@ def test_{{ method_name }}_empty_call():
119131
{% if method.client_streaming %}
120132
assert next(args[0]) == request
121133
{% else %}
134+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
135+
{% if method_settings is not none %}
136+
{% for auto_populated_field in method_settings.auto_populated_fields %}
137+
# Ensure that the uuid4 field is set according to AIP 4235
138+
re.match(r"[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z", args[0].{{ auto_populated_field }})
139+
# clear UUID field so that the check below succeeds
140+
args[0].{{ auto_populated_field }} = ""
141+
{% endfor %}
142+
{% endif %}{# if method_settings is not none #}
143+
{% endwith %}{# method_settings #}
122144
assert args[0] == {{ method.input.ident }}()
123145
{% endif %}
124146
{% endif %}
@@ -182,6 +204,16 @@ async def test_{{ method_name }}_async(transport: str = 'grpc_asyncio', request_
182204
{% if method.client_streaming %}
183205
assert next(args[0]) == request
184206
{% else %}
207+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
208+
{% if method_settings is not none %}
209+
{% for auto_populated_field in method_settings.auto_populated_fields %}
210+
# Ensure that the uuid4 field is set according to AIP 4235
211+
re.match(r"[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z", args[0].{{ auto_populated_field }})
212+
# clear UUID field so that the check below succeeds
213+
args[0].{{ auto_populated_field }} = ""
214+
{% endfor %}
215+
{% endif %}{# if method_settings is not none #}
216+
{% endwith %}{# method_settings #}
185217
assert args[0] == {{ method.input.ident }}()
186218
{% endif %}
187219

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
nox.options.error_on_missing_interpreters = True
3030

3131

32-
showcase_version = os.environ.get("SHOWCASE_VERSION", "0.30.0")
32+
showcase_version = os.environ.get("SHOWCASE_VERSION", "0.31.0")
3333
ADS_TEMPLATES = path.join(path.dirname(__file__), "gapic", "ads-templates")
3434

3535

0 commit comments

Comments
 (0)