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

Commit f66182f

Browse files
committed
feat: add support for async rest streaming methods
1 parent 6a42d1c commit f66182f

12 files changed

Lines changed: 142 additions & 18 deletions

File tree

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ from google.api_core import exceptions as core_exceptions
1717
from google.api_core import gapic_v1
1818
from google.api_core import retry_async as retries
1919
from google.api_core import rest_helpers
20+
{# TODO(https://github.com/googleapis/gapic-generator-python/issues/2137): raise an import error if an older version of google.api.core is installed. #}
21+
from google.api_core import rest_streaming_async # type: ignore
2022

2123
from google.protobuf import json_format
2224

@@ -112,19 +114,19 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport):
112114
def __hash__(self):
113115
return hash("Async{{service.name}}RestTransport.{{method.name}}")
114116

115-
{% if method.http_options and not method.client_streaming and not method.server_streaming and not method.lro and not method.extended_lro and not method.paged_result_field %}
117+
{% if method.http_options and not method.client_streaming and not method.lro and not method.extended_lro and not method.paged_result_field %}
116118
{% set body_spec = method.http_options[0].body %}
117119
{{ shared_macros.response_method(body_spec, is_async=True)|indent(8) }}
118120

119-
{% endif %}{# method.http_options and not method.client_streaming and not method.server_streaming and not method.lro and not method.extended_lro and not method.paged_result_field #}
121+
{% endif %}{# method.http_options and not method.client_streaming and not method.lro and not method.extended_lro and not method.paged_result_field #}
120122
async def __call__(self,
121123
request: {{method.input.ident}}, *,
122124
retry: OptionalRetry=gapic_v1.method.DEFAULT,
123125
timeout: Optional[float]=None,
124126
metadata: Sequence[Tuple[str, str]]=(),
125127
{# TODO(b/362949446): Update the return type as we implement this for different method types. #}
126-
){% if not method.void %} -> {% if not method.server_streaming %}{{method.output.ident}}{% else %}None{% endif %}{% endif %}:
127-
{% if method.http_options and not method.client_streaming and not method.server_streaming and not method.lro and not method.extended_lro and not method.paged_result_field %}
128+
){% if not method.void %} -> {% if not method.server_streaming %}{{method.output.ident}}{% else %}rest_streaming_async.AsyncResponseIterator{% endif %}{% endif %}:
129+
{% if method.http_options and not method.client_streaming and not method.lro and not method.extended_lro and not method.paged_result_field %}
128130
r"""Call the {{- ' ' -}}
129131
{{ (method.name|snake_case).replace('_',' ')|wrap(
130132
width=70, offset=45, indent=8) }}
@@ -151,14 +153,18 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport):
151153

152154
{% if not method.void %}
153155
# Return the response
156+
{% if method.server_streaming %}
157+
resp = rest_streaming_async.AsyncResponseIterator(response, {{method.output.ident}})
158+
{% else %}
154159
resp = {{method.output.ident}}()
155160
{% if method.output.ident.is_proto_plus_type %}
156161
pb_resp = {{method.output.ident}}.pb(resp)
157162
{% else %}
158163
pb_resp = resp
159-
{% endif %}
164+
{% endif %}{# if method.output.ident.is_proto_plus_type #}
160165
content = await response.read()
161166
json_format.Parse(content, pb_resp, ignore_unknown_fields=True)
167+
{% endif %}{# if method.server_streaming #}
162168
return resp
163169

164170
{% endif %}{# method.void #}
@@ -167,7 +173,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport):
167173
raise NotImplementedError(
168174
"Method {{ method.name }} is not available over REST transport"
169175
)
170-
{% endif %}{# method.http_options and not method.client_streaming and not method.server_streaming and not method.lro and not method.extended_lro and not method.paged_result_field #}
176+
{% endif %}{# method.http_options and not method.client_streaming and not method.lro and not method.extended_lro and not method.paged_result_field #}
171177

172178
{% endfor %}
173179
{% for method in service.methods.values()|sort(attribute="name") %}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ except ImportError: # pragma: NO COVER
2020
import grpc
2121
from grpc.experimental import aio
2222
{% if "rest" in opts.transport %}
23-
from collections.abc import Iterable
23+
from collections.abc import Iterable, AsyncIterable
2424
from google.protobuf import json_format
2525
import json
2626
{% endif %}
@@ -95,6 +95,11 @@ from google.iam.v1 import policy_pb2 # type: ignore
9595
{% endfilter %}
9696
{{ shared_macros.add_google_api_core_version_header_import(service.version) }}
9797

98+
async def mock_async_gen(data, chunk_size=1):
99+
for i in range(0, len(data)): # pragma: NO COVER
100+
chunk = data[i : i + chunk_size]
101+
yield chunk.encode("utf-8")
102+
98103
def client_cert_source_callback():
99104
return b"cert bytes", b"key bytes"
100105

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1909,13 +1909,13 @@ def test_unsupported_parameter_rest_asyncio():
19091909
{% endmacro %}
19101910

19111911
{# is_rest_unsupported_method renders:
1912-
# 'True' if transport is async REST.
1913-
# 'True' if transport is sync REST and method is a client streaming method.
1912+
# 'True' if transport is async REST and method is one of [client_streaming, lro, extended_lro, paged_result_field].
1913+
# 'True' if transport is sync REST and method is a client_streaming method.
19141914
# 'False' otherwise.
19151915
#}
19161916
{# NOTE: We will keep updating this method as we add support for methods in async REST. #}
19171917
{% macro is_rest_unsupported_method(method, is_async) %}
1918-
{%- if method.client_streaming or (is_async and (method.server_streaming or method.lro or method.extended_lro or method.paged_result_field)) -%}
1918+
{%- if method.client_streaming or (is_async and (method.lro or method.extended_lro or method.paged_result_field)) -%}
19191919
{{'True'}}
19201920
{%- else -%}
19211921
{{'False'}}
@@ -2038,7 +2038,7 @@ def test_{{transport_name}}_initialize_client():
20382038
{# rest_method_call_success_test generates tests for rest methods
20392039
# when they make a successful request.
20402040
# NOTE: Currently, this macro does not support the following method
2041-
# types: [method.server_streaming, method.lro, method.extended_lro, method.paged_result_field].
2041+
# types: [method.lro, method.extended_lro, method.paged_result_field].
20422042
# As support is added for the above methods, the relevant guard can be removed from within the macro
20432043
# TODO(https://github.com/googleapis/gapic-generator-python/issues/2142): Clean up `rest_required_tests` once support for all the methods metioned above is added here.
20442044
#}
@@ -2051,7 +2051,7 @@ def test_{{transport_name}}_initialize_client():
20512051
{# TODO(https://github.com/googleapis/gapic-generator-python/issues/2143): Update the guard below as support we add for each method. Remove it once we have
20522052
# all the methods supported in async rest transport that are supported in sync rest transport.
20532053
#}
2054-
{% if not (method.server_streaming or method.lro or method.extended_lro or method.paged_result_field)%}
2054+
{% if not (method.lro or method.extended_lro or method.paged_result_field)%}
20552055
@pytest.mark.parametrize("request_type", [
20562056
{{ method.input.ident }},
20572057
dict,
@@ -2107,13 +2107,34 @@ def test_{{transport_name}}_initialize_client():
21072107
{% endif %}{# method.output.ident.is_proto_plus_type #}
21082108
json_return_value = json_format.MessageToJson(return_value)
21092109
{% endif %}{# method.void #}
2110+
2111+
{% if method.server_streaming %}
2112+
json_return_value = "[{}]".format(json_return_value)
2113+
{% if is_async %}
2114+
response_value.content.return_value = mock_async_gen(json_return_value)
2115+
{% else %}{# not is_async #}
2116+
response_value.iter_content = mock.Mock(return_value=iter(json_return_value))
2117+
{% endif %}{# is_async #}
2118+
{% else %}{# not method.streaming #}
21102119
{% if is_async %}
21112120
response_value.read = mock.AsyncMock(return_value=json_return_value.encode('UTF-8'))
2112-
{% else %}{# is_async #}
2121+
{% else %}{# not is_async #}
21132122
response_value.content = json_return_value.encode('UTF-8')
21142123
{% endif %}{# is_async #}
2124+
{% endif %}{# method.server_streaming #}
21152125
req.return_value = response_value
21162126
response = {{ await_prefix }}client.{{ method_name }}(request)
2127+
2128+
{% if method.server_streaming %}
2129+
{% if is_async %}
2130+
assert isinstance(response, AsyncIterable)
2131+
response = await response.__anext__()
2132+
{% else %}
2133+
assert isinstance(response, Iterable)
2134+
response = next(response)
2135+
{% endif %}
2136+
{% endif %}
2137+
21172138
# Establish that the response is the type that we expect.
21182139
{% if method.void %}
21192140
assert response is None

noxfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ def showcase_unit_w_rest_async(
442442
session.chdir(lib)
443443
# Note: google-api-core and google-auth are re-installed here to override the version installed in constraints.
444444
# TODO(https://github.com/googleapis/python-api-core/pull/694): Update the version of google-api-core once the linked PR is merged.
445-
session.install('--no-cache-dir', '--force-reinstall', "google-api-core[grpc]@git+https://github.com/googleapis/python-api-core.git@7dea20d73878eca93b61bb82ae6ddf335fb3a8ca")
445+
session.install('--no-cache-dir', '--force-reinstall', "google-api-core[grpc]@git+https://github.com/googleapis/python-api-core.git@16038182329055551a32acd0f9f505301be4bcc5")
446446
# TODO(https://github.com/googleapis/google-auth-library-python/pull/1577): Update the version of google-auth once the linked PR is merged.
447447
session.install('--no-cache-dir', '--force-reinstall', "google-auth@git+https://github.com/googleapis/google-auth-library-python.git@add-support-for-async-authorized-session-api")
448448
session.install("aiohttp")

tests/integration/goldens/asset/tests/unit/gapic/asset_v1/test_asset_service.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
import grpc
2525
from grpc.experimental import aio
26-
from collections.abc import Iterable
26+
from collections.abc import Iterable, AsyncIterable
2727
from google.protobuf import json_format
2828
import json
2929
import math
@@ -71,6 +71,11 @@
7171
import google.auth
7272

7373

74+
async def mock_async_gen(data, chunk_size=1):
75+
for i in range(0, len(data)): # pragma: NO COVER
76+
chunk = data[i : i + chunk_size]
77+
yield chunk.encode("utf-8")
78+
7479
def client_cert_source_callback():
7580
return b"cert bytes", b"key bytes"
7681

@@ -15286,9 +15291,11 @@ def test_batch_get_assets_history_rest_call_success(request_type):
1528615291
# Convert return value to protobuf type
1528715292
return_value = asset_service.BatchGetAssetsHistoryResponse.pb(return_value)
1528815293
json_return_value = json_format.MessageToJson(return_value)
15294+
1528915295
response_value.content = json_return_value.encode('UTF-8')
1529015296
req.return_value = response_value
1529115297
response = client.batch_get_assets_history(request)
15298+
1529215299
# Establish that the response is the type that we expect.
1529315300
assert isinstance(response, asset_service.BatchGetAssetsHistoryResponse)
1529415301

@@ -15346,9 +15353,11 @@ def test_create_feed_rest_call_success(request_type):
1534615353
# Convert return value to protobuf type
1534715354
return_value = asset_service.Feed.pb(return_value)
1534815355
json_return_value = json_format.MessageToJson(return_value)
15356+
1534915357
response_value.content = json_return_value.encode('UTF-8')
1535015358
req.return_value = response_value
1535115359
response = client.create_feed(request)
15360+
1535215361
# Establish that the response is the type that we expect.
1535315362
assert isinstance(response, asset_service.Feed)
1535415363
assert response.name == 'name_value'
@@ -15415,9 +15424,11 @@ def test_get_feed_rest_call_success(request_type):
1541515424
# Convert return value to protobuf type
1541615425
return_value = asset_service.Feed.pb(return_value)
1541715426
json_return_value = json_format.MessageToJson(return_value)
15427+
1541815428
response_value.content = json_return_value.encode('UTF-8')
1541915429
req.return_value = response_value
1542015430
response = client.get_feed(request)
15431+
1542115432
# Establish that the response is the type that we expect.
1542215433
assert isinstance(response, asset_service.Feed)
1542315434
assert response.name == 'name_value'
@@ -15479,9 +15490,11 @@ def test_list_feeds_rest_call_success(request_type):
1547915490
# Convert return value to protobuf type
1548015491
return_value = asset_service.ListFeedsResponse.pb(return_value)
1548115492
json_return_value = json_format.MessageToJson(return_value)
15493+
1548215494
response_value.content = json_return_value.encode('UTF-8')
1548315495
req.return_value = response_value
1548415496
response = client.list_feeds(request)
15497+
1548515498
# Establish that the response is the type that we expect.
1548615499
assert isinstance(response, asset_service.ListFeedsResponse)
1548715500

@@ -15539,9 +15552,11 @@ def test_update_feed_rest_call_success(request_type):
1553915552
# Convert return value to protobuf type
1554015553
return_value = asset_service.Feed.pb(return_value)
1554115554
json_return_value = json_format.MessageToJson(return_value)
15555+
1554215556
response_value.content = json_return_value.encode('UTF-8')
1554315557
req.return_value = response_value
1554415558
response = client.update_feed(request)
15559+
1554515560
# Establish that the response is the type that we expect.
1554615561
assert isinstance(response, asset_service.Feed)
1554715562
assert response.name == 'name_value'
@@ -15600,9 +15615,11 @@ def test_delete_feed_rest_call_success(request_type):
1560015615
response_value = mock.Mock()
1560115616
response_value.status_code = 200
1560215617
json_return_value = ''
15618+
1560315619
response_value.content = json_return_value.encode('UTF-8')
1560415620
req.return_value = response_value
1560515621
response = client.delete_feed(request)
15622+
1560615623
# Establish that the response is the type that we expect.
1560715624
assert response is None
1560815625

@@ -15698,9 +15715,11 @@ def test_analyze_iam_policy_rest_call_success(request_type):
1569815715
# Convert return value to protobuf type
1569915716
return_value = asset_service.AnalyzeIamPolicyResponse.pb(return_value)
1570015717
json_return_value = json_format.MessageToJson(return_value)
15718+
1570115719
response_value.content = json_return_value.encode('UTF-8')
1570215720
req.return_value = response_value
1570315721
response = client.analyze_iam_policy(request)
15722+
1570415723
# Establish that the response is the type that we expect.
1570515724
assert isinstance(response, asset_service.AnalyzeIamPolicyResponse)
1570615725
assert response.fully_explored is True
@@ -15775,9 +15794,11 @@ def test_analyze_move_rest_call_success(request_type):
1577515794
# Convert return value to protobuf type
1577615795
return_value = asset_service.AnalyzeMoveResponse.pb(return_value)
1577715796
json_return_value = json_format.MessageToJson(return_value)
15797+
1577815798
response_value.content = json_return_value.encode('UTF-8')
1577915799
req.return_value = response_value
1578015800
response = client.analyze_move(request)
15801+
1578115802
# Establish that the response is the type that we expect.
1578215803
assert isinstance(response, asset_service.AnalyzeMoveResponse)
1578315804

@@ -15832,9 +15853,11 @@ def test_query_assets_rest_call_success(request_type):
1583215853
# Convert return value to protobuf type
1583315854
return_value = asset_service.QueryAssetsResponse.pb(return_value)
1583415855
json_return_value = json_format.MessageToJson(return_value)
15856+
1583515857
response_value.content = json_return_value.encode('UTF-8')
1583615858
req.return_value = response_value
1583715859
response = client.query_assets(request)
15860+
1583815861
# Establish that the response is the type that we expect.
1583915862
assert isinstance(response, asset_service.QueryAssetsResponse)
1584015863
assert response.job_reference == 'job_reference_value'
@@ -15894,9 +15917,11 @@ def test_create_saved_query_rest_call_success(request_type):
1589415917
# Convert return value to protobuf type
1589515918
return_value = asset_service.SavedQuery.pb(return_value)
1589615919
json_return_value = json_format.MessageToJson(return_value)
15920+
1589715921
response_value.content = json_return_value.encode('UTF-8')
1589815922
req.return_value = response_value
1589915923
response = client.create_saved_query(request)
15924+
1590015925
# Establish that the response is the type that we expect.
1590115926
assert isinstance(response, asset_service.SavedQuery)
1590215927
assert response.name == 'name_value'
@@ -15960,9 +15985,11 @@ def test_get_saved_query_rest_call_success(request_type):
1596015985
# Convert return value to protobuf type
1596115986
return_value = asset_service.SavedQuery.pb(return_value)
1596215987
json_return_value = json_format.MessageToJson(return_value)
15988+
1596315989
response_value.content = json_return_value.encode('UTF-8')
1596415990
req.return_value = response_value
1596515991
response = client.get_saved_query(request)
15992+
1596615993
# Establish that the response is the type that we expect.
1596715994
assert isinstance(response, asset_service.SavedQuery)
1596815995
assert response.name == 'name_value'
@@ -16047,9 +16074,11 @@ def test_update_saved_query_rest_call_success(request_type):
1604716074
# Convert return value to protobuf type
1604816075
return_value = asset_service.SavedQuery.pb(return_value)
1604916076
json_return_value = json_format.MessageToJson(return_value)
16077+
1605016078
response_value.content = json_return_value.encode('UTF-8')
1605116079
req.return_value = response_value
1605216080
response = client.update_saved_query(request)
16081+
1605316082
# Establish that the response is the type that we expect.
1605416083
assert isinstance(response, asset_service.SavedQuery)
1605516084
assert response.name == 'name_value'
@@ -16106,9 +16135,11 @@ def test_delete_saved_query_rest_call_success(request_type):
1610616135
response_value = mock.Mock()
1610716136
response_value.status_code = 200
1610816137
json_return_value = ''
16138+
1610916139
response_value.content = json_return_value.encode('UTF-8')
1611016140
req.return_value = response_value
1611116141
response = client.delete_saved_query(request)
16142+
1611216143
# Establish that the response is the type that we expect.
1611316144
assert response is None
1611416145

@@ -16161,9 +16192,11 @@ def test_batch_get_effective_iam_policies_rest_call_success(request_type):
1616116192
# Convert return value to protobuf type
1616216193
return_value = asset_service.BatchGetEffectiveIamPoliciesResponse.pb(return_value)
1616316194
json_return_value = json_format.MessageToJson(return_value)
16195+
1616416196
response_value.content = json_return_value.encode('UTF-8')
1616516197
req.return_value = response_value
1616616198
response = client.batch_get_effective_iam_policies(request)
16199+
1616716200
# Establish that the response is the type that we expect.
1616816201
assert isinstance(response, asset_service.BatchGetEffectiveIamPoliciesResponse)
1616916202

0 commit comments

Comments
 (0)