Skip to content

Commit 5282533

Browse files
authored
address review feedback
1 parent c73e4ec commit 5282533

10 files changed

Lines changed: 313 additions & 186 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor:
402402
logging.log(f"Received request: {request}")
403403
return request, metadata
404404

405-
{% if not method.void %}
405+
{% if not method.void and not method.is_resumable_upload %}
406406
{{ async_prefix }}def post_{{ method.name|snake_case }}(self, response):
407407
logging.log(f"Received response: {response}")
408408
return response
@@ -425,7 +425,7 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor:
425425
"""
426426
return request, metadata
427427

428-
{% if not method.void %}
428+
{% if not method.void and not method.is_resumable_upload %}
429429
{% if not method.server_streaming %}
430430
{{ async_prefix }}def post_{{ method.name|snake_case }}(self, response: {{method.output.ident}}) -> {{method.output.ident}}:
431431
{% else %}

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

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -375,30 +375,32 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport):
375375
)
376376
return _error()
377377
self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub()
378-
elif HAS_ASYNC_REST: # pragma: NO COVER
379-
# Lazily instantiate and cache the AsyncRestTransport stub on first
380-
# call rather than during __init__ (_prep_wrapped_messages), since
381-
# GrpcAsyncIOTransport may be initialized with synchronous
382-
# google.auth.credentials.Credentials that AsyncAuthorizedSession
383-
# rejects, and gRPC async stubs expose a synchronous __call__
384-
# returning an awaitable.
385-
class _AsyncRestStub: # pragma: NO COVER
386-
def __init__(_self): # pragma: NO COVER
387-
_self._rest_stub = None
388-
389-
def __call__(_self, *args, **kwargs): # pragma: NO COVER
390-
if _self._rest_stub is None:
378+
elif HAS_ASYNC_REST:
379+
# Defer instantiating Async{{ service.name }}RestTransport until the
380+
# resumable upload RPC is called so that initializing
381+
# {{ service.grpc_asyncio_transport_name }} with synchronous
382+
# google.auth.credentials.Credentials (e.g. via ADC or service
383+
# account helpers) for standard gRPC calls does not fail in
384+
# __init__ (_prep_wrapped_messages) when AsyncAuthorizedSession
385+
# validates for async credentials.
386+
transport = self
387+
class _AsyncRestStub:
388+
def __init__(self):
389+
self._rest_stub = None
390+
391+
def __call__(self, *args, **kwargs):
392+
if self._rest_stub is None:
391393
rest_transport = Async{{ service.name }}RestTransport(
392-
host=self._host,
393-
credentials=self._credentials,
394-
client_info=self._client_info,
394+
host=transport._host,
395+
credentials=transport._credentials,
396+
client_info=transport._client_info,
395397
)
396-
_self._rest_stub = rest_transport.{{ method.transport_safe_name|snake_case }}
397-
return _self._rest_stub(*args, **kwargs)
398+
self._rest_stub = rest_transport.{{ method.transport_safe_name|snake_case }}
399+
return self._rest_stub(*args, **kwargs)
398400
self._stubs['{{ method.transport_safe_name|snake_case }}'] = _AsyncRestStub()
399-
else: # pragma: NO COVER
400-
class _UnsupportedStub: # pragma: NO COVER
401-
def __call__(self, *args, **kwargs): # pragma: NO COVER
401+
else:
402+
class _UnsupportedStub:
403+
def __call__(self, *args, **kwargs):
402404
async def _unsupported():
403405
raise NotImplementedError("Async REST transport is required for async resumable upload methods.")
404406
return _unsupported()

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,10 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport):
277277

278278
uri = transcoded_request["uri"]
279279
upload_url = f"{self._host}{uri}"
280-
headers = dict(metadata)
280+
headers = {**dict(metadata), **dict((config.headers or {}) if config else {})}
281281
if config is None:
282282
config = resumable_transfer.ResumableUploadConfig(headers=headers)
283283
else:
284-
if config.headers:
285-
headers.update(dict(config.headers))
286284
config = dataclasses.replace(config, headers=headers)
287285

288286
return {{ method.client_output.ident }}(

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,10 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport):
236236

237237
uri = transcoded_request["uri"]
238238
upload_url = f"{self._host}{uri}"
239-
headers = dict(metadata)
239+
headers = {**dict(metadata), **dict((config.headers or {}) if config else {})}
240240
if config is None:
241241
config = resumable_transfer.ResumableUploadConfig(headers=headers)
242242
else:
243-
if config.headers:
244-
headers.update(dict(config.headers))
245243
config = dataclasses.replace(config, headers=headers)
246244

247245
return {{ method.client_output_async.ident }}(

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

Lines changed: 115 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,50 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a
15451545
with pytest.raises(core_exceptions.GoogleAPICallError) as exc_info:
15461546
await transport.{{ method.transport_safe_name|snake_case }}({{ method.input.ident }}())
15471547
assert "operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel" in str(exc_info.value)
1548+
1549+
1550+
@pytest.mark.asyncio
1551+
async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_rest_stub():
1552+
with mock.patch.object(transports.grpc_asyncio, "HAS_ASYNC_REST", True), mock.patch.object(
1553+
transports.grpc_asyncio,
1554+
"Async{{ service.name }}RestTransport",
1555+
create=True,
1556+
) as mock_rest_transport_cls:
1557+
mock_rest_transport = mock.MagicMock()
1558+
expected_session = {{ method.client_output_async.ident }}()
1559+
mock_rest_transport.{{ method.transport_safe_name|snake_case }} = mock.AsyncMock(
1560+
return_value=expected_session
1561+
)
1562+
mock_rest_transport_cls.return_value = mock_rest_transport
1563+
1564+
transport = transports.{{ service.name }}GrpcAsyncIOTransport(
1565+
credentials=async_anonymous_credentials(),
1566+
)
1567+
mock_rest_transport_cls.assert_not_called()
1568+
1569+
stub = transport.{{ method.transport_safe_name|snake_case }}
1570+
result1 = await stub({{ method.input.ident }}())
1571+
result2 = await stub({{ method.input.ident }}())
1572+
1573+
assert result1 is expected_session
1574+
assert result2 is expected_session
1575+
mock_rest_transport_cls.assert_called_once_with(
1576+
host=transport._host,
1577+
credentials=transport._credentials,
1578+
client_info=transport._client_info,
1579+
)
1580+
assert mock_rest_transport.{{ method.transport_safe_name|snake_case }}.call_count == 2
1581+
1582+
1583+
@pytest.mark.asyncio
1584+
async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_without_async_rest_error():
1585+
with mock.patch.object(transports.grpc_asyncio, "HAS_ASYNC_REST", False):
1586+
transport = transports.{{ service.name }}GrpcAsyncIOTransport(
1587+
credentials=async_anonymous_credentials(),
1588+
)
1589+
with pytest.raises(NotImplementedError) as exc_info:
1590+
await transport.{{ method.transport_safe_name|snake_case }}({{ method.input.ident }}())
1591+
assert "Async REST transport is required for async resumable upload methods." in str(exc_info.value)
15481592
{% endfor %}
15491593
{% endif %}
15501594

@@ -1556,13 +1600,40 @@ def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_start_e
15561600
transport="rest",
15571601
)
15581602
with mock.patch.object(type(client.transport._session), "request") as req:
1559-
response_value = mock.MagicMock()
1560-
response_value.status_code = 200
1561-
response_value.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"}
1562-
response_value.content = b""
1563-
req.return_value = response_value
1564-
response = client.{{ method.client_method_name|snake_case }}({{ method.input.ident }}())
1603+
start_response = mock.MagicMock()
1604+
start_response.ok = True
1605+
start_response.status_code = 200
1606+
start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"}
1607+
start_response.content = b""
1608+
1609+
expected_result = {{ method.output.ident }}(
1610+
{% for field in method.output.fields.values() | rejectattr('message') %}
1611+
{% if not field.oneof or field.proto3_optional %}
1612+
{{ field.name }}={{ field.mock_value }},
1613+
{% endif %}
1614+
{% endfor %}
1615+
)
1616+
{% if method.output.ident.is_proto_plus_type %}
1617+
json_return_value = {{ method.output.ident }}.to_json(expected_result)
1618+
{% else %}
1619+
json_return_value = json_format.MessageToJson(expected_result)
1620+
{% endif %}
1621+
upload_response = mock.MagicMock()
1622+
upload_response.ok = True
1623+
upload_response.status_code = 200
1624+
upload_response.headers = {"x-goog-upload-status": "final"}
1625+
upload_response.content = json_return_value.encode("UTF-8")
1626+
1627+
req.side_effect = [start_response, upload_response]
1628+
response = client.{{ method.client_method_name|snake_case }}(
1629+
{{ method.input.ident }}(),
1630+
config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}),
1631+
timeout=10.0,
1632+
)
15651633
assert isinstance(response, {{ method.client_output.ident }})
1634+
result = response.upload(b"test data")
1635+
assert isinstance(result, {{ method.output.ident }})
1636+
assert result == expected_result
15661637

15671638

15681639
{% if rest_async_io_enabled %}
@@ -1574,14 +1645,45 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_a
15741645
credentials=async_anonymous_credentials(),
15751646
transport="rest_asyncio",
15761647
)
1577-
with mock.patch.object(type(client.transport._session), "request", new_callable=mock.AsyncMock) as req:
1578-
response_value = mock.MagicMock()
1579-
response_value.status_code = 200
1580-
response_value.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"}
1581-
response_value.read = mock.AsyncMock(return_value=b"")
1582-
req.return_value = response_value
1583-
response = await client.{{ method.client_method_name|snake_case }}({{ method.input.ident }}())
1648+
with mock.patch.object(type(client.transport._session), "request", new_callable=mock.MagicMock) as req:
1649+
start_response = mock.MagicMock()
1650+
start_response.status = 200
1651+
start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"}
1652+
start_response.read = mock.AsyncMock(return_value=b"")
1653+
start_ctx = mock.MagicMock()
1654+
start_ctx.__aenter__ = mock.AsyncMock(return_value=start_response)
1655+
start_ctx.__aexit__ = mock.AsyncMock(return_value=None)
1656+
1657+
expected_result = {{ method.output.ident }}(
1658+
{% for field in method.output.fields.values() | rejectattr('message') %}
1659+
{% if not field.oneof or field.proto3_optional %}
1660+
{{ field.name }}={{ field.mock_value }},
1661+
{% endif %}
1662+
{% endfor %}
1663+
)
1664+
{% if method.output.ident.is_proto_plus_type %}
1665+
json_return_value = {{ method.output.ident }}.to_json(expected_result)
1666+
{% else %}
1667+
json_return_value = json_format.MessageToJson(expected_result)
1668+
{% endif %}
1669+
upload_response = mock.MagicMock()
1670+
upload_response.status = 200
1671+
upload_response.headers = {"x-goog-upload-status": "final"}
1672+
upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8"))
1673+
upload_ctx = mock.MagicMock()
1674+
upload_ctx.__aenter__ = mock.AsyncMock(return_value=upload_response)
1675+
upload_ctx.__aexit__ = mock.AsyncMock(return_value=None)
1676+
1677+
req.side_effect = [start_ctx, upload_ctx]
1678+
response = await client.{{ method.client_method_name|snake_case }}(
1679+
{{ method.input.ident }}(),
1680+
config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}),
1681+
timeout=10.0,
1682+
)
15841683
assert isinstance(response, {{ method.client_output_async.ident }})
1684+
result = await response.upload(b"test data")
1685+
assert isinstance(result, {{ method.output.ident }})
1686+
assert result == expected_result
15851687
{% endif %}
15861688
{% endfor %}
15871689
{% endif %}

‎packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2‎

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'):
4545
call.return_value = operations_pb2.Operation(name='operations/spam')
4646
{% elif method.server_streaming %}
4747
call.return_value = iter([{{ method.output.ident }}()])
48+
{% elif method.is_resumable_upload %}
49+
call.return_value = {{ method.client_output.ident }}()
4850
{% else %}
4951
call.return_value = {{ method.output.ident }}(
5052
{% for field in method.output.fields.values() | rejectattr('message')%}
@@ -315,6 +317,10 @@ async def test_{{ method_name }}_async(request_type, transport: str = 'grpc_asyn
315317
{% elif method.client_streaming and method.server_streaming %}
316318
call.return_value = mock.Mock(aio.StreamStreamCall, autospec=True)
317319
call.return_value.read = mock.AsyncMock(side_effect=[{{ method.output.ident }}()])
320+
{% elif method.is_resumable_upload %}
321+
call.return_value = grpc_helpers_async.FakeUnaryUnaryCall(
322+
{{ method.client_output_async.ident }}()
323+
)
318324
{% else %}
319325
call.return_value ={{ '' }}
320326
{%- if not method.client_streaming and not method.server_streaming -%}
@@ -1837,20 +1843,37 @@ def test_initialize_client_w_{{transport_name}}():
18371843
request = request_type(**request_init)
18381844

18391845
# Mock the http request call within the method and fake a BadRequest error.
1840-
with mock.patch.object({{mocked_session}}, 'request') as req, pytest.raises(core_exceptions.BadRequest):
1846+
with mock.patch.object({{mocked_session}}, 'request'{% if is_async and method.is_resumable_upload %}, new_callable=mock.MagicMock{% endif %}) as req, pytest.raises(core_exceptions.BadRequest):
18411847
# Wrap the value into a proper Response obj
1848+
{% if method.is_resumable_upload %}
1849+
response_value = mock.MagicMock()
1850+
{% else %}
18421851
response_value = mock.Mock()
1852+
{% endif %}
18431853
{% if is_async %}
18441854
response_value.read = mock.AsyncMock(return_value=b'{}')
1855+
{% if method.is_resumable_upload %}
1856+
response_value.status = 400
1857+
response_value.__aenter__ = mock.AsyncMock(return_value=response_value)
1858+
response_value.__aexit__ = mock.AsyncMock(return_value=None)
1859+
{% endif %}
18451860
{% else %}
18461861
json_return_value = ''
18471862
response_value.json = mock.Mock(return_value={})
1863+
{% if method.is_resumable_upload %}
1864+
response_value.ok = False
1865+
{% endif %}
18481866
{% endif %}{# if is_async #}
18491867
response_value.status_code = 400
18501868
response_value.request = mock.Mock()
18511869
req.return_value = response_value
18521870
req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"}
1871+
{% if method.is_resumable_upload %}
1872+
response = {{ await_prefix }}client.{{ method_name }}(request)
1873+
{{ await_prefix }}response.upload(b"")
1874+
{% else %}
18531875
{{ await_prefix }}client.{{ method_name }}(request)
1876+
{% endif %}
18541877

18551878
{% endif %}{# if 'grpc' in transport #}
18561879
{% endmacro %}
@@ -2272,13 +2295,13 @@ def test_initialize_client_w_{{transport_name}}():
22722295
{% if method.lro %}
22732296
mock.patch.object(operation.Operation, "_set_result_from_operation"), \
22742297
{% endif %}
2275-
{% if not method.void %}
2298+
{% if not method.void and not method.is_resumable_upload %}
22762299
mock.patch.object(transports.{{async_method_prefix}}{{ service.name }}RestInterceptor, "post_{{method.name|snake_case}}") as post, \
22772300
mock.patch.object(transports.{{async_method_prefix}}{{ service.name }}RestInterceptor, "post_{{method.name|snake_case}}_with_metadata") as post_with_metadata, \
22782301
{% endif %}
22792302
mock.patch.object(transports.{{async_method_prefix}}{{ service.name }}RestInterceptor, "pre_{{ method.name|snake_case }}") as pre:
22802303
pre.assert_not_called()
2281-
{% if not method.void %}
2304+
{% if not method.void and not method.is_resumable_upload %}
22822305
post.assert_not_called()
22832306
post_with_metadata.assert_not_called()
22842307
{% endif %}
@@ -2297,7 +2320,7 @@ def test_initialize_client_w_{{transport_name}}():
22972320
req.return_value = mock.Mock()
22982321
req.return_value.status_code = 200
22992322
req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"}
2300-
{% if not method.void %}
2323+
{% if not method.void and not method.is_resumable_upload %}
23012324
return_value = {% if method.output.ident.is_proto_plus_type %}{{ method.output.ident }}.to_json({{ method.output.ident }}()){% else %}json_format.MessageToJson({{ method.output.ident }}()){% endif %}
23022325

23032326
{% if method.server_streaming %}
@@ -2313,23 +2336,23 @@ def test_initialize_client_w_{{transport_name}}():
23132336
req.return_value.content = return_value
23142337
{% endif %}{# end is_async #}
23152338
{% endif %}{# end method.server_streaming #}
2316-
{% endif %}{# end not method.void #}
2339+
{% endif %}{# end not method.void and not method.is_resumable_upload #}
23172340

23182341
request = {{ method.input.ident }}()
23192342
metadata =[
23202343
("key", "val"),
23212344
("cephalopod", "squid"),
23222345
]
23232346
pre.return_value = request, metadata
2324-
{% if not method.void %}
2347+
{% if not method.void and not method.is_resumable_upload %}
23252348
post.return_value = {{ method.output.ident }}()
23262349
post_with_metadata.return_value = {{ method.output.ident }}(), metadata
23272350
{% endif %}
23282351

23292352
{{await_prefix}}client.{{ method_name }}(request, metadata=[("key", "val"), ("cephalopod", "squid"),])
23302353

23312354
pre.assert_called_once()
2332-
{% if not method.void %}
2355+
{% if not method.void and not method.is_resumable_upload %}
23332356
post.assert_called_once()
23342357
post_with_metadata.assert_called_once()
23352358
{% endif %}

0 commit comments

Comments
 (0)