Skip to content

Commit cfa6c0e

Browse files
authored
fix: minor typo in ads template (#664)
CircleCI machinery now invokes the alternative (Ads) templates for the showcase_alternative_templates_* tests. Includes numerous fixes and additions to the Ads grpc transport, client class, and unit test templates. The showcase system tests now selectively enable async tests via an environment variable. The async client code has not yet been added to the Ads templates, and the corresponding system tests have been disabled for alternative templates.
1 parent 78d2979 commit cfa6c0e

File tree

17 files changed

+590
-357
lines changed

17 files changed

+590
-357
lines changed

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ class {{ service.name }}Transport(metaclass=abc.ABCMeta):
5555
credentials identify the application to the service; if none
5656
are specified, the client will attempt to ascertain the
5757
credentials from the environment.
58-
client_info (google.api_core.gapic_v1.client_info.ClientInfo):
59-
The client info used to send a user-agent string along with
60-
API requests. If ``None``, then default info will be used.
61-
Generally, you only need to set this if you're developing
58+
client_info (google.api_core.gapic_v1.client_info.ClientInfo):
59+
The client info used to send a user-agent string along with
60+
API requests. If ``None``, then default info will be used.
61+
Generally, you only need to set this if you're developing
6262
your own client library.
6363
"""
6464
# Save the hostname. Default to port 443 (HTTPS) if none is specified.
@@ -89,7 +89,7 @@ class {{ service.name }}Transport(metaclass=abc.ABCMeta):
8989
{% if method.retry.max_backoff %}maximum={{ method.retry.max_backoff }},{% endif %}
9090
{% if method.retry.backoff_multiplier %}multiplier={{ method.retry.backoff_multiplier }},{% endif %}
9191
predicate=retries.if_exception_type(
92-
{%- for ex in method.retry.retryable_exceptions|sort(attribute='__name__) %}
92+
{%- for ex in method.retry.retryable_exceptions|sort(attribute='__name__') %}
9393
exceptions.{{ ex.__name__ }},
9494
{%- endfor %}
9595
),

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

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{% extends '_base.py.j2' %}
22

33
{% block content %}
4-
from typing import Callable, Dict, Tuple
4+
import warnings
5+
from typing import Callable, Dict, Optional, Sequence, Tuple
56

67
from google.api_core import grpc_helpers # type: ignore
78
{%- if service.has_lro %}
@@ -10,7 +11,7 @@ from google.api_core import operations_v1 # type: ignore
1011
from google.api_core import gapic_v1 # type: ignore
1112
from google import auth # type: ignore
1213
from google.auth import credentials # type: ignore
13-
14+
from google.auth.transport.grpc import SslCredentials # type: ignore
1415

1516
import grpc # type: ignore
1617

@@ -38,8 +39,13 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
3839
def __init__(self, *,
3940
host: str{% if service.host %} = '{{ service.host }}'{% endif %},
4041
credentials: credentials.Credentials = None,
42+
credentials_file: str = None,
43+
scopes: Sequence[str] = None,
4144
channel: grpc.Channel = None,
45+
api_mtls_endpoint: str = None,
46+
client_cert_source: Callable[[], Tuple[bytes, bytes]] = None,
4247
ssl_channel_credentials: grpc.ChannelCredentials = None,
48+
quota_project_id: Optional[str] = None,
4349
client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO,
4450
) -> None:
4551
"""Instantiate the transport.
@@ -53,14 +59,29 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
5359
are specified, the client will attempt to ascertain the
5460
credentials from the environment.
5561
This argument is ignored if ``channel`` is provided.
62+
credentials_file (Optional[str]): A file with credentials that can
63+
be loaded with :func:`google.auth.load_credentials_from_file`.
64+
This argument is ignored if ``channel`` is provided.
65+
scopes (Optional(Sequence[str])): A list of scopes. This argument is
66+
ignored if ``channel`` is provided.
5667
channel (Optional[grpc.Channel]): A ``Channel`` instance through
5768
which to make calls.
69+
api_mtls_endpoint (Optional[str]): Deprecated. The mutual TLS endpoint.
70+
If provided, it overrides the ``host`` argument and tries to create
71+
a mutual TLS channel with client SSL credentials from
72+
``client_cert_source`` or applicatin default SSL credentials.
73+
client_cert_source (Optional[Callable[[], Tuple[bytes, bytes]]]):
74+
Deprecated. A callback to provide client SSL certificate bytes and
75+
private key bytes, both in PEM format. It is ignored if
76+
``api_mtls_endpoint`` is None.
5877
ssl_channel_credentials (grpc.ChannelCredentials): SSL credentials
5978
for grpc channel. It is ignored if ``channel`` is provided.
60-
client_info (google.api_core.gapic_v1.client_info.ClientInfo):
61-
The client info used to send a user-agent string along with
62-
API requests. If ``None``, then default info will be used.
63-
Generally, you only need to set this if you're developing
79+
quota_project_id (Optional[str]): An optional project to use for billing
80+
and quota.
81+
client_info (google.api_core.gapic_v1.client_info.ClientInfo):
82+
The client info used to send a user-agent string along with
83+
API requests. If ``None``, then default info will be used.
84+
Generally, you only need to set this if you're developing
6485
your own client library.
6586

6687
Raises:
@@ -74,14 +95,41 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
7495

7596
# If a channel was explicitly provided, set it.
7697
self._grpc_channel = channel
98+
elif api_mtls_endpoint:
99+
warnings.warn("api_mtls_endpoint and client_cert_source are deprecated", DeprecationWarning)
100+
101+
host = api_mtls_endpoint if ":" in api_mtls_endpoint else api_mtls_endpoint + ":443"
102+
103+
if credentials is None:
104+
credentials, _ = auth.default(scopes=self.AUTH_SCOPES, quota_project_id=quota_project_id)
105+
106+
# Create SSL credentials with client_cert_source or application
107+
# default SSL credentials.
108+
if client_cert_source:
109+
cert, key = client_cert_source()
110+
ssl_credentials = grpc.ssl_channel_credentials(
111+
certificate_chain=cert, private_key=key
112+
)
113+
else:
114+
ssl_credentials = SslCredentials().ssl_credentials
115+
116+
# create a new channel. The provided one is ignored.
117+
self._grpc_channel = type(self).create_channel(
118+
host,
119+
credentials=credentials,
120+
credentials_file=credentials_file,
121+
ssl_credentials=ssl_credentials,
122+
scopes=scopes or self.AUTH_SCOPES,
123+
quota_project_id=quota_project_id,
124+
)
77125
else:
78126
host = host if ":" in host else host + ":443"
79127

80128
if credentials is None:
81129
credentials, _ = auth.default(scopes=self.AUTH_SCOPES)
82130

83131
# create a new channel. The provided one is ignored.
84-
self._grpc_channel = grpc_helpers.create_channel(
132+
self._grpc_channel = type(self).create_channel(
85133
host,
86134
credentials=credentials,
87135
ssl_credentials=ssl_channel_credentials,
@@ -102,6 +150,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
102150
def create_channel(cls,
103151
host: str{% if service.host %} = '{{ service.host }}'{% endif %},
104152
credentials: credentials.Credentials = None,
153+
scopes: Optional[Sequence[str]] = None,
105154
**kwargs) -> grpc.Channel:
106155
"""Create and return a gRPC channel object.
107156
Args:
@@ -111,6 +160,9 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
111160
credentials identify this application to the service. If
112161
none are specified, the client will attempt to ascertain
113162
the credentials from the environment.
163+
scopes (Optional[Sequence[str]]): A optional list of scopes needed for this
164+
service. These are only used when credentials are not specified and
165+
are passed to :func:`google.auth.default`.
114166
kwargs (Optional[dict]): Keyword arguments, which are passed to the
115167
channel creation.
116168
Returns:
@@ -119,26 +171,14 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport):
119171
return grpc_helpers.create_channel(
120172
host,
121173
credentials=credentials,
122-
scopes=cls.AUTH_SCOPES,
174+
scopes=scopes or cls.AUTH_SCOPES,
123175
**kwargs
124176
)
125177

126178
@property
127179
def grpc_channel(self) -> grpc.Channel:
128-
"""Create the channel designed to connect to this service.
129-
130-
This property caches on the instance; repeated calls return
131-
the same channel.
180+
"""Return the channel designed to connect to this service.
132181
"""
133-
# Sanity check: Only create a new channel if we do not already
134-
# have one.
135-
if not hasattr(self, '_grpc_channel'):
136-
self._grpc_channel = self.create_channel(
137-
self._host,
138-
credentials=self._credentials,
139-
)
140-
141-
# Return the channel from cache.
142182
return self._grpc_channel
143183
{%- if service.has_lro %}
144184

packages/gapic-generator/gapic/ads-templates/noxfile.py.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def unit(session):
2020
'--cov-config=.coveragerc',
2121
'--cov-report=term',
2222
'--cov-report=html',
23-
os.path.join('tests', 'unit', '{{ api.naming.versioned_module_name }}'),
23+
os.path.join('tests', 'unit', 'gapic', '{{ api.naming.versioned_module_name }}'),
2424
)
2525

2626

packages/gapic-generator/gapic/ads-templates/setup.py.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ setuptools.setup(
2424
'grpc-google-iam-v1',
2525
{%- endif %}
2626
),
27-
python_requires='>={% if opts.lazy_import %}3.7{% else %}3.6{% endif %}',{# Lazy import requires module-level getattr #}
27+
python_requires='>=3.7',{# Lazy import requires module-level getattr #}
2828
setup_requires=[
2929
'libcst >= 0.2.5',
3030
],

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

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def test_{{ service.client_name|snake_case }}_client_options():
144144
# Check the case GOOGLE_API_USE_CLIENT_CERTIFICATE has unsupported value.
145145
with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "Unsupported"}):
146146
with pytest.raises(ValueError):
147-
client = client_class()
147+
client = {{ service.client_name }}()
148148

149149

150150
@mock.patch.object({{ service.client_name }}, "DEFAULT_ENDPOINT", modify_default_endpoint({{ service.client_name }}))
@@ -222,7 +222,7 @@ def test_{{ service.client_name|snake_case }}_mtls_env_auto(use_client_cert_env)
222222

223223

224224
def test_{{ service.client_name|snake_case }}_client_options_from_dict():
225-
with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}Transport.__init__') as grpc_transport:
225+
with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}GrpcTransport.__init__') as grpc_transport:
226226
grpc_transport.return_value = None
227227
client = {{ service.client_name }}(
228228
client_options={'api_endpoint': 'squid.clam.whelk'}
@@ -583,6 +583,15 @@ def test_transport_instance():
583583
assert client.transport is transport
584584

585585

586+
def test_transport_get_channel():
587+
# A client may be instantiated with a custom transport instance.
588+
transport = transports.{{ service.name }}GrpcTransport(
589+
credentials=credentials.AnonymousCredentials(),
590+
)
591+
channel = transport.grpc_channel
592+
assert channel
593+
594+
586595
def test_transport_grpc_default():
587596
# A client should use the gRPC transport by default.
588597
client = {{ service.client_name }}(
@@ -593,18 +602,20 @@ def test_transport_grpc_default():
593602
transports.{{ service.name }}GrpcTransport,
594603
)
595604

596-
597-
def test_transport_adc():
605+
@pytest.mark.parametrize("transport_class", [
606+
transports.{{ service.grpc_transport_name }},
607+
])
608+
def test_transport_adc(transport_class):
598609
# Test default credentials are used if not provided.
599610
with mock.patch.object(auth, 'default') as adc:
600611
adc.return_value = (credentials.AnonymousCredentials(), None)
601-
transports.{{ service.name }}Transport()
612+
transport_class()
602613
adc.assert_called_once()
603614

604615

605616
def test_{{ service.name|snake_case }}_base_transport():
606617
# Instantiate the base transport.
607-
with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}GrpcTransport.__init__') as Transport:
618+
with mock.patch('{{ (api.naming.module_namespace + (api.naming.versioned_module_name,) + service.meta.address.subpackage)|join(".") }}.services.{{ service.name|snake_case }}.transports.{{ service.name }}Transport.__init__') as Transport:
608619
Transport.return_value = None
609620
transport = transports.{{ service.name }}Transport(
610621
credentials=credentials.AnonymousCredentials(),
@@ -695,6 +706,85 @@ def test_{{ service.name|snake_case }}_grpc_transport_channel():
695706
assert transport._host == "squid.clam.whelk:443"
696707

697708

709+
@pytest.mark.parametrize("transport_class", [transports.{{ service.grpc_transport_name }}])
710+
def test_{{ service.name|snake_case }}_transport_channel_mtls_with_client_cert_source(
711+
transport_class
712+
):
713+
with mock.patch("grpc.ssl_channel_credentials", autospec=True) as grpc_ssl_channel_cred:
714+
with mock.patch.object(transport_class, "create_channel", autospec=True) as grpc_create_channel:
715+
mock_ssl_cred = mock.Mock()
716+
grpc_ssl_channel_cred.return_value = mock_ssl_cred
717+
718+
mock_grpc_channel = mock.Mock()
719+
grpc_create_channel.return_value = mock_grpc_channel
720+
721+
cred = credentials.AnonymousCredentials()
722+
with pytest.warns(DeprecationWarning):
723+
with mock.patch.object(auth, 'default') as adc:
724+
adc.return_value = (cred, None)
725+
transport = transport_class(
726+
host="squid.clam.whelk",
727+
api_mtls_endpoint="mtls.squid.clam.whelk",
728+
client_cert_source=client_cert_source_callback,
729+
)
730+
adc.assert_called_once()
731+
732+
grpc_ssl_channel_cred.assert_called_once_with(
733+
certificate_chain=b"cert bytes", private_key=b"key bytes"
734+
)
735+
grpc_create_channel.assert_called_once_with(
736+
"mtls.squid.clam.whelk:443",
737+
credentials=cred,
738+
credentials_file=None,
739+
scopes=(
740+
{%- for scope in service.oauth_scopes %}
741+
'{{ scope }}',
742+
{%- endfor %}
743+
),
744+
ssl_credentials=mock_ssl_cred,
745+
quota_project_id=None,
746+
)
747+
assert transport.grpc_channel == mock_grpc_channel
748+
749+
750+
@pytest.mark.parametrize("transport_class", [transports.{{ service.grpc_transport_name }},])
751+
def test_{{ service.name|snake_case }}_transport_channel_mtls_with_adc(
752+
transport_class
753+
):
754+
mock_ssl_cred = mock.Mock()
755+
with mock.patch.multiple(
756+
"google.auth.transport.grpc.SslCredentials",
757+
__init__=mock.Mock(return_value=None),
758+
ssl_credentials=mock.PropertyMock(return_value=mock_ssl_cred),
759+
):
760+
with mock.patch.object(transport_class, "create_channel", autospec=True) as grpc_create_channel:
761+
mock_grpc_channel = mock.Mock()
762+
grpc_create_channel.return_value = mock_grpc_channel
763+
mock_cred = mock.Mock()
764+
765+
with pytest.warns(DeprecationWarning):
766+
transport = transport_class(
767+
host="squid.clam.whelk",
768+
credentials=mock_cred,
769+
api_mtls_endpoint="mtls.squid.clam.whelk",
770+
client_cert_source=None,
771+
)
772+
773+
grpc_create_channel.assert_called_once_with(
774+
"mtls.squid.clam.whelk:443",
775+
credentials=mock_cred,
776+
credentials_file=None,
777+
scopes=(
778+
{%- for scope in service.oauth_scopes %}
779+
'{{ scope }}',
780+
{%- endfor %}
781+
),
782+
ssl_credentials=mock_ssl_cred,
783+
quota_project_id=None,
784+
)
785+
assert transport.grpc_channel == mock_grpc_channel
786+
787+
698788
{% if service.has_lro -%}
699789
def test_{{ service.name|snake_case }}_grpc_lro_client():
700790
client = {{ service.client_name }}(

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@ class {{ service.async_client_name }}:
5050
from_service_account_file = {{ service.client_name }}.from_service_account_file
5151
from_service_account_json = from_service_account_file
5252

53+
@property
54+
def transport(self) -> {{ service.name }}Transport:
55+
"""Return the transport used by the client instance.
56+
57+
Returns:
58+
{{ service.name }}Transport: The transport used by the client instance.
59+
"""
60+
return self._client.transport
61+
62+
5363
get_transport_class = functools.partial(type({{ service.client_name }}).get_transport_class, type({{ service.client_name }}))
5464

5565
def __init__(self, *,

0 commit comments

Comments
 (0)