Skip to content

Commit

Permalink
feat: Automatically populate uuid4 fields
Browse files Browse the repository at this point in the history
  • Loading branch information
parthea committed Mar 20, 2024
1 parent 24a23a1 commit 68a79a5
Show file tree
Hide file tree
Showing 15 changed files with 2,934 additions and 214 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ from collections import OrderedDict
import os
import re
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
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields")|list %}
import uuid
{% endif %}
{% if service.any_deprecated %}
import warnings
{% endif %}
Expand Down Expand Up @@ -473,6 +476,27 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta):
)
{% endif %}

{#
Automatically populate UUID4 fields according to AIP-4235
(https://google.aip.dev/client-libraries/4235) if the
field satisfies either of:
- The field supports explicit presence and has not been set by the user.
- The field doesn't support explicit presence, and its value is the empty
string (i.e. the default value).
#}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
{% if method.input.fields[auto_populated_field].proto3_optional %}
if '{{ auto_populated_field }}' not in request:
{% else %}
if not request.{{ auto_populated_field }}:
{% endif %}
request.{{ auto_populated_field }} = str(uuid.uuid4())
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}

# Send the request.
{%+ if not method.void %}response = {% endif %}rpc(
{% if not method.client_streaming %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
{% block content %}

import os
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields")|list %}
import re
{% endif %}
# try/except added for compatibility with python < 3.8
try:
from unittest import mock
Expand Down Expand Up @@ -521,6 +524,18 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
# Everything is optional in proto3 as far as the runtime is concerned,
# and we are mocking out the actual API, so just send an empty request.
request = request_type()

{# Set UUID4 fields so that they are not automatically popoulated. #}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
if isinstance(request, dict):
request['{{ auto_populated_field }}'] = "str_value"
else:
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{% if method.client_streaming %}
requests = [request]
{% endif %}
Expand Down Expand Up @@ -568,7 +583,15 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
{% if method.client_streaming %}
assert next(args[0]) == request
{% else %}
assert args[0] == {{ method.input.ident }}()
request = {{ method.input.ident }}()
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == request
{% endif %}

# Establish that the response is the type that we expect.
Expand Down Expand Up @@ -629,6 +652,16 @@ def test_{{ method_name }}_empty_call():
{% if method.client_streaming %}
assert next(args[0]) == request
{% else %}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
# Ensure that the uuid4 field is set according to AIP 4235
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}", args[0].{{ auto_populated_field }})
# clear UUID field so that the check below succeeds
args[0].{{ auto_populated_field }} = None
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == {{ method.input.ident }}()
{% endif %}
{% endif %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@
)
{% endif %} {# method.explicit_routing #}

{{ auto_populate_uuid4_fields(api, method) }}

# Validate the universe domain.
self._validate_universe_domain()

Expand Down Expand Up @@ -265,3 +267,26 @@

{% macro define_extended_operation_subclass(extended_operation) %}
{% endmacro %}

{% macro auto_populate_uuid4_fields(api, method) %}
{#
Automatically populate UUID4 fields according to AIP-4235
(https://google.aip.dev/client-libraries/4235) if the
field satisfies either of:
- The field supports explicit presence and has not been set by the user.
- The field doesn't support explicit presence, and its value is the empty
string (i.e. the default value).
#}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
{% if method.input.fields[auto_populated_field].proto3_optional %}
if '{{ auto_populated_field }}' not in request:
{% else %}
if not request.{{ auto_populated_field }}:
{% endif %}
request.{{ auto_populated_field }} = str(uuid.uuid4())
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{% endmacro %}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
{% extends "_base.py.j2" %}

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

from collections import OrderedDict
import functools
import re
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
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields")|list %}
import uuid
{% endif %}
{% if service.any_deprecated %}
import warnings
{% endif %}
Expand Down Expand Up @@ -386,6 +390,8 @@ class {{ service.async_client_name }}:
)
{% endif %}

{{ macros.auto_populate_uuid4_fields(api, method) }}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import functools
import os
import re
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
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields")|list %}
import uuid
{% endif %}
import warnings

{% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
{% import "tests/unit/gapic/%name_%version/%sub/test_macros.j2" as test_macros %}

import os
{% if api.all_method_settings.values()|map(attribute="auto_populated_fields")|list %}
import re
{% endif %}
# try/except added for compatibility with python < 3.8
try:
from unittest import mock
Expand Down Expand Up @@ -849,10 +852,10 @@ def test_{{ service.client_name|snake_case }}_create_channel_credentials_file(cl

{% for method in service.methods.values() if 'grpc' in opts.transport %}{# method_name #}
{% if method.extended_lro %}
{{ test_macros.grpc_required_tests(method, service, full_extended_lro=True) }}
{{ test_macros.grpc_required_tests(method, service, api, full_extended_lro=True) }}

{% endif %}
{{ test_macros.grpc_required_tests(method, service) }}
{{ test_macros.grpc_required_tests(method, service, api) }}
{% endfor %} {# method in methods for grpc #}

{% for method in service.methods.values() if 'rest' in opts.transport %}
Expand Down
110 changes: 107 additions & 3 deletions gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% macro grpc_required_tests(method, service, full_extended_lro=False) %}
{% macro grpc_required_tests(method, service, api, full_extended_lro=False) %}
{% 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 %}
@pytest.mark.parametrize("request_type", [
{{ method.input.ident }},
Expand All @@ -13,6 +13,17 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
# Everything is optional in proto3 as far as the runtime is concerned,
# and we are mocking out the actual API, so just send an empty request.
request = request_type()
{# Set UUID4 fields so that they are not automatically popoulated. #}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
if isinstance(request, dict):
request['{{ auto_populated_field }}'] = "str_value"
else:
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{% if method.client_streaming %}
requests = [request]
{% endif %}
Expand Down Expand Up @@ -58,7 +69,15 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
{% if method.client_streaming %}
assert next(args[0]) == request
{% else %}
assert args[0] == {{ method.input.ident }}()
request = {{ method.input.ident }}()
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == request
{% endif %}

# Establish that the response is the type that we expect.
Expand Down Expand Up @@ -119,11 +138,77 @@ def test_{{ method_name }}_empty_call():
{% if method.client_streaming %}
assert next(args[0]) == request
{% else %}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
# Ensure that the uuid4 field is set according to AIP 4235
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}", args[0].{{ auto_populated_field }})
# clear UUID field so that the check below succeeds
args[0].{{ auto_populated_field }} = None
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == {{ method.input.ident }}()
{% endif %}
{% endif %}

{% if not full_extended_lro %}
{% if not method.client_streaming %}
@pytest.mark.asyncio
async def test_{{ method_name }}_empty_call_async():
# This test is a coverage failsafe to make sure that totally empty calls,
# i.e. request == None and no flattened fields passed, work.
client = {{ service.async_client_name }}(
credentials=ga_credentials.AnonymousCredentials(),
transport='grpc_asyncio',
)

# Mock the actual call within the gRPC stub, and fake the request.
with mock.patch.object(
type(client.transport.{{ method.transport_safe_name|snake_case }}),
'__call__') as call:
# Designate an appropriate return value for the call.
{% if method.void %}
call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(None)
{% elif method.lro %}
call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(
operations_pb2.Operation(name='operations/spam')
)
{% elif not method.client_streaming and method.server_streaming %}
call.return_value = mock.Mock(aio.UnaryStreamCall, autospec=True)
call.return_value.read = mock.AsyncMock(side_effect=[{{ method.output.ident }}()])
{% elif method.client_streaming and method.server_streaming %}
call.return_value = mock.Mock(aio.StreamStreamCall, autospec=True)
call.return_value.read = mock.AsyncMock(side_effect=[{{ method.output.ident }}()])
{% else %}
call.return_value ={{ '' }}
{%- if not method.client_streaming and not method.server_streaming -%}
grpc_helpers_async.FakeUnaryUnaryCall
{%- else -%}
grpc_helpers_async.FakeStreamUnaryCall
{%- endif -%}({{ method.output.ident }}(
{% for field in method.output.fields.values() | rejectattr('message') %}{% if not field.oneof or field.proto3_optional %}
{{ field.name }}={{ field.mock_value }},
{% endif %}
{% endfor %}
))
{% endif %}
response = await client.{{ method_name }}()
call.assert_called()
_, args, _ = call.mock_calls[0]
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
# Ensure that the uuid4 field is set according to AIP 4235
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}", args[0].{{ auto_populated_field }})
# clear UUID field so that the check below succeeds
args[0].{{ auto_populated_field }} = None
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == {{ method.input.ident }}()
{% endif %}

@pytest.mark.asyncio
async def test_{{ method_name }}_async(transport: str = 'grpc_asyncio', request_type={{ method.input.ident }}):
client = {{ service.async_client_name }}(
Expand All @@ -134,6 +219,17 @@ async def test_{{ method_name }}_async(transport: str = 'grpc_asyncio', request_
# Everything is optional in proto3 as far as the runtime is concerned,
# and we are mocking out the actual API, so just send an empty request.
request = request_type()
{# Set UUID4 fields so that they are not automatically popoulated. #}
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
if isinstance(request, dict):
request['{{ auto_populated_field }}'] = "str_value"
else:
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
{% if method.client_streaming %}
requests = [request]
{% endif %}
Expand Down Expand Up @@ -182,7 +278,15 @@ async def test_{{ method_name }}_async(transport: str = 'grpc_asyncio', request_
{% if method.client_streaming %}
assert next(args[0]) == request
{% else %}
assert args[0] == {{ method.input.ident }}()
request = {{ method.input.ident }}()
{% with method_settings = api.all_method_settings.get(method.meta.address.proto) %}
{% if method_settings is not none %}
{% for auto_populated_field in method_settings.auto_populated_fields %}
request.{{ auto_populated_field }} = "str_value"
{% endfor %}
{% endif %}{# if method_settings is not none #}
{% endwith %}{# method_settings #}
assert args[0] == request
{% endif %}

# Establish that the response is the type that we expect.
Expand Down
Loading

0 comments on commit 68a79a5

Please sign in to comment.