Skip to content

Commit 086cda3

Browse files
partheavchudnov-g
andauthored
feat: Automatically populate uuid4 fields (#1985)
Co-authored-by: Victor Chudnovsky <vchudnov@google.com>
1 parent 6c16db2 commit 086cda3

File tree

18 files changed

+5812
-242
lines changed

18 files changed

+5812
-242
lines changed

packages/gapic-generator/.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.31.0
17+
SHOWCASE_VERSION: 0.32.0
1818
PROTOC_VERSION: 3.20.2
1919

2020
jobs:

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ 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+
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|list %}
10+
import uuid
11+
{% endif %}
912
{% if service.any_deprecated %}
1013
import warnings
1114
{% endif %}
@@ -473,6 +476,27 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
473476
)
474477
{% endif %}
475478

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

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

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
{% block content %}
44

55
import os
6+
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|list %}
7+
import re
8+
{% endif %}
69
# try/except added for compatibility with python < 3.8
710
try:
811
from unittest import mock
@@ -67,6 +70,7 @@ from google.iam.v1 import policy_pb2 # type: ignore
6770
{% endif %}
6871
{% endfilter %}
6972

73+
{% with uuid4_re = "[a-f0-9]{8}-?[a-f0-9]{4}-?4[a-f0-9]{3}-?[89ab][a-f0-9]{3}-?[a-f0-9]{12}" %}
7074

7175
def client_cert_source_callback():
7276
return b"cert bytes", b"key bytes"
@@ -513,6 +517,7 @@ def test_{{ service.client_name|snake_case }}_create_channel_credentials_file(cl
513517
dict,
514518
])
515519
def test_{{ method_name }}(request_type, transport: str = 'grpc'):
520+
{% with auto_populated_field_sample_value = "explicit value for autopopulate-able field" %}
516521
client = {{ service.client_name }}(
517522
credentials=ga_credentials.AnonymousCredentials(),
518523
transport=transport,
@@ -521,6 +526,18 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
521526
# Everything is optional in proto3 as far as the runtime is concerned,
522527
# and we are mocking out the actual API, so just send an empty request.
523528
request = request_type()
529+
530+
{# Set UUID4 fields so that they are not automatically populated. #}
531+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
532+
{% if method_settings is not none %}
533+
{% for auto_populated_field in method_settings.auto_populated_fields %}
534+
if isinstance(request, dict):
535+
request['{{ auto_populated_field }}'] = "{{ auto_populated_field_sample_value }}"
536+
else:
537+
request.{{ auto_populated_field }} = "{{ auto_populated_field_sample_value }}"
538+
{% endfor %}
539+
{% endif %}{# if method_settings is not none #}
540+
{% endwith %}{# method_settings #}
524541
{% if method.client_streaming %}
525542
requests = [request]
526543
{% endif %}
@@ -568,7 +585,15 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
568585
{% if method.client_streaming %}
569586
assert next(args[0]) == request
570587
{% else %}
571-
assert args[0] == {{ method.input.ident }}()
588+
request = {{ method.input.ident }}()
589+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
590+
{% if method_settings is not none %}
591+
{% for auto_populated_field in method_settings.auto_populated_fields %}
592+
request.{{ auto_populated_field }} = "{{ auto_populated_field_sample_value }}"
593+
{% endfor %}
594+
{% endif %}{# if method_settings is not none #}
595+
{% endwith %}{# method_settings #}
596+
assert args[0] == request
572597
{% endif %}
573598

574599
# Establish that the response is the type that we expect.
@@ -608,6 +633,7 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
608633
{% endif %}{# end oneof/optional #}
609634
{% endfor %}
610635
{% endif %}
636+
{% endwith %}{# auto_populated_field_sample_value #}
611637

612638

613639
{% if not method.client_streaming %}
@@ -629,8 +655,59 @@ def test_{{ method_name }}_empty_call():
629655
{% if method.client_streaming %}
630656
assert next(args[0]) == request
631657
{% else %}
658+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
659+
{% if method_settings is not none %}
660+
{% for auto_populated_field in method_settings.auto_populated_fields %}
661+
# Ensure that the uuid4 field is set according to AIP 4235
662+
assert re.match(r"{{ uuid4_re }}", args[0].{{ auto_populated_field }})
663+
# clear UUID field so that the check below succeeds
664+
args[0].{{ auto_populated_field }} = None
665+
{% endfor %}
666+
{% endif %}{# if method_settings is not none #}
667+
{% endwith %}{# method_settings #}
632668
assert args[0] == {{ method.input.ident }}()
633669
{% endif %}
670+
671+
672+
def test_{{ method_name }}_non_empty_request_with_auto_populated_field():
673+
# This test is a coverage failsafe to make sure that UUID4 fields are
674+
# automatically populated, according to AIP-4235, with non-empty requests.
675+
client = {{ service.client_name }}(
676+
credentials=ga_credentials.AnonymousCredentials(),
677+
transport='grpc',
678+
)
679+
680+
# Populate all string fields in the request which are not UUID4
681+
# since we want to check that UUID4 are populated automatically
682+
# if they meet the requirements of AIP 4235.
683+
request = {{ method.input.ident }}(
684+
{% for field in method.input.fields.values() if field.ident|string() == "str" and not field.uuid4 %}
685+
{{ field.name }}={{ field.mock_value }},
686+
{% endfor %}
687+
)
688+
689+
# Mock the actual call within the gRPC stub, and fake the request.
690+
with mock.patch.object(
691+
type(client.transport.{{ method.transport_safe_name|snake_case }}),
692+
'__call__') as call:
693+
client.{{ method_name }}(request=request)
694+
call.assert_called()
695+
_, args, _ = call.mock_calls[0]
696+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
697+
{% if method_settings is not none %}
698+
{% for auto_populated_field in method_settings.auto_populated_fields %}
699+
# Ensure that the uuid4 field is set according to AIP 4235
700+
assert re.match(r"{{ uuid4_re }}", args[0].{{ auto_populated_field }})
701+
# clear UUID field so that the check below succeeds
702+
args[0].{{ auto_populated_field }} = None
703+
{% endfor %}
704+
{% endif %}{# if method_settings is not none #}
705+
{% endwith %}{# method_settings #}
706+
assert args[0] == {{ method.input.ident }}(
707+
{% for field in method.input.fields.values() if field.ident|string() == "str" and not field.uuid4 %}
708+
{{ field.name }}={{ field.mock_value }},
709+
{% endfor %}
710+
)
634711
{% endif %}
635712

636713

@@ -2364,4 +2441,5 @@ def test_client_ctx():
23642441
pass
23652442
close.assert_called()
23662443

2444+
{% endwith %}{# uuid4_re #}
23672445
{% endblock %}

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

Lines changed: 26 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,27 @@
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
274+
https://google.aip.dev/client-libraries/4235 when 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+
When using this macro, ensure the calling template generates a line `import uuid`
280+
#}
281+
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
282+
{% if method_settings is not none %}
283+
{% for auto_populated_field in method_settings.auto_populated_fields %}
284+
{% if method.input.fields[auto_populated_field].proto3_optional %}
285+
if '{{ auto_populated_field }}' not in request:
286+
{% else %}
287+
if not request.{{ auto_populated_field }}:
288+
{% endif %}
289+
request.{{ auto_populated_field }} = str(uuid.uuid4())
290+
{% endfor %}
291+
{% endif %}{# if method_settings is not none #}
292+
{% endwith %}{# method_settings #}
293+
{% endmacro %}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
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+
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|list %}
11+
import uuid
12+
{% endif %}
913
{% if service.any_deprecated %}
1014
import warnings
1115
{% endif %}
@@ -386,6 +390,8 @@ class {{ service.async_client_name }}:
386390
)
387391
{% endif %}
388392

393+
{{ macros.auto_populate_uuid4_fields(api, method) }}
394+
389395
# Validate the universe domain.
390396
self._client._validate_universe_domain()
391397

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ 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+
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|list %}
14+
import uuid
15+
{% endif %}
1316
import warnings
1417

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

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

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

66
import os
7+
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|list %}
8+
import re
9+
{% endif %}
710
# try/except added for compatibility with python < 3.8
811
try:
912
from unittest import mock
@@ -849,10 +852,10 @@ def test_{{ service.client_name|snake_case }}_create_channel_credentials_file(cl
849852

850853
{% for method in service.methods.values() if 'grpc' in opts.transport %}{# method_name #}
851854
{% if method.extended_lro %}
852-
{{ test_macros.grpc_required_tests(method, service, full_extended_lro=True) }}
855+
{{ test_macros.grpc_required_tests(method, service, api, full_extended_lro=True) }}
853856

854857
{% endif %}
855-
{{ test_macros.grpc_required_tests(method, service) }}
858+
{{ test_macros.grpc_required_tests(method, service, api) }}
856859
{% endfor %} {# method in methods for grpc #}
857860

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

0 commit comments

Comments
 (0)