From 43b0747346e205c62dcc3e5dc768e308385df0a2 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 29 Sep 2026 17:58:46 +0000 Subject: [PATCH 01/45] feat: add support for resumable uploads --- .../gapic/generator/generator.py | 2 + .../gapic-generator/gapic/schema/wrappers.py | 27 ++ .../%sub/services/%service/_client_macros.j2 | 14 + .../%sub/services/%service/_shared_macros.j2 | 29 +- .../%sub/services/%service/async_client.py.j2 | 17 + .../%sub/services/%service/client.py.j2 | 5 +- .../%service/transports/__init__.py.j2 | 2 +- .../services/%service/transports/base.py.j2 | 7 +- .../services/%service/transports/grpc.py.j2 | 23 ++ .../%service/transports/grpc_asyncio.py.j2 | 36 ++ .../services/%service/transports/rest.py.j2 | 6 + .../%service/transports/rest_asyncio.py.j2 | 18 +- .../gapic/templates/setup.py.j2 | 2 +- .../constraints-3.10-async-rest.txt.j2 | 2 +- .../%name_%version/%sub/test_%service.py.j2 | 67 +++- .../gapic/%name_%version/%sub/test_macros.j2 | 21 +- .../resumable_upload_service/async_client.py | 27 +- .../resumable_upload_service/client.py | 70 +++- .../transports/__init__.py | 15 +- .../transports/base.py | 5 +- .../transports/grpc.py | 21 +- .../transports/grpc_asyncio.py | 36 +- .../transports/rest.py | 4 +- .../transports/rest_asyncio.py | 370 ++++++++++++++++++ ...ppet_metadata_google.showcase.v1beta1.json | 4 +- .../integration/goldens/showcase/setup.py | 3 + .../testing/constraints-3.10-async-rest.txt | 13 + .../test_resumable_upload_service.py | 257 +++++++++++- .../tests/unit/generator/test_generator.py | 57 +++ .../tests/unit/schema/wrappers/test_method.py | 17 +- 30 files changed, 1121 insertions(+), 56 deletions(-) create mode 100755 packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py create mode 100755 packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt diff --git a/packages/gapic-generator/gapic/generator/generator.py b/packages/gapic-generator/gapic/generator/generator.py index 50a0bc649dcd..078c647eac77 100644 --- a/packages/gapic-generator/gapic/generator/generator.py +++ b/packages/gapic-generator/gapic/generator/generator.py @@ -352,12 +352,14 @@ def _render_template( and not api_schema.all_library_settings[ api_schema.naming.proto_package ].python_settings.experimental_features.rest_async_io_enabled + and not service.has_resumable_upload_methods ) or ( "rest_asyncio" in template_name and not api_schema.all_library_settings[ api_schema.naming.proto_package ].python_settings.experimental_features.rest_async_io_enabled + and not service.has_resumable_upload_methods ) or ("rest_base" in template_name and "rest" not in opts.transport) ): diff --git a/packages/gapic-generator/gapic/schema/wrappers.py b/packages/gapic-generator/gapic/schema/wrappers.py index a26f40ff4c45..778800b076a5 100644 --- a/packages/gapic-generator/gapic/schema/wrappers.py +++ b/packages/gapic-generator/gapic/schema/wrappers.py @@ -1640,6 +1640,28 @@ def _client_output(self, enable_asyncio: bool): ) ) + # If this method is a resumable upload, return a PythonType instance + # representing the resumable upload session (while self.output remains + # the underlying protobuf response message for final deserialization). + if self.is_resumable_upload: + return PythonType( + meta=metadata.Metadata( + address=metadata.Address( + name=( + "AsyncResumableUploadSession" + if enable_asyncio + else "ResumableUploadSession" + ), + module="resumable_transfer", + package=("google", "api_core"), + collisions=self.input.ident.collisions, + ), + documentation=utils.doc( + "An object representing a resumable upload session." + ), + ), + ) + # Return the usual output. return self.output @@ -1943,6 +1965,11 @@ def _ref_types(self, recursive: bool) -> Sequence[Union[MessageType, EnumType]]: if self.paged_result_field and self.paged_result_field.message: answer.append(self.paged_result_field.message) + # If this method is a resumable upload, client_output is ResumableUploadSession, + # so explicitly include self.output to ensure the underlying response message is imported. + if self.is_resumable_upload: + answer.append(self.output) + # Done; return the answer. return tuple(answer) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 index 9029ac8eca23..53726ab0a115 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 @@ -28,6 +28,9 @@ requests: Optional[Iterator[{{ method.input.ident }}]] = None, *, {% endif %} + {% if method.is_resumable_upload %} + config: Optional[ResumableUploadConfig] = None, + {% endif %} retry: OptionalRetry = gapic_v1.method.DEFAULT, timeout: Union[float, object] = gapic_v1.method.DEFAULT, {{ shared_macros.client_method_metadata_argument()|indent(8) }} = {{ shared_macros.client_method_metadata_default_value() }}, @@ -65,6 +68,10 @@ The request object iterator.{{ " " }} {{- method.input.meta.doc|rst(width=72, indent=16, nl=False) }} {% endif %} + {% if method.is_resumable_upload %} + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. + {% endif %} retry (google.api_core.retry.Retry): Designation of what errors, if any, should be retried. timeout (float): The timeout for this request. @@ -183,6 +190,13 @@ timeout=timeout, metadata=metadata, ) + {% elif method.is_resumable_upload %} + + # Wrap the response in a resumable upload session. + response = {{ method.client_output.ident }}( + config=config, + response_type={{ method.output.ident }}, + ) {% elif method.extended_lro and full_extended_lro %} {{ extended_operation_service_setup(api, method) }} {% endif %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 index e39425bb8117..aee755de31b5 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 @@ -53,6 +53,17 @@ except ImportError: # pragma: NO COVER {% endmacro %} {% macro create_metadata(method) %} + {% if method.is_resumable_upload %} + metadata = () if metadata is None else metadata + resumable_metadata = { + "x-goog-upload-protocol": "resumable", + "x-goog-upload-command": "start", + } + existing_keys = {k.lower() for k, _ in metadata} + metadata = tuple(metadata) + tuple( + (k, v) for k, v in resumable_metadata.items() if k not in existing_keys + ) + {% endif %} {% if method.explicit_routing %} header_params: dict[str, str] = {} {% if not method.client_streaming %} @@ -132,13 +143,13 @@ from google.longrunning import operations_pb2 # type: ignore {% endif %}{# import_ns.has_operations_mixin #} {% endmacro %} -{% macro http_options_method(rules) %} +{% macro http_options_method(rules, is_resumable_upload=False, resumable_upload_prefix="resumable/upload") %} @staticmethod def _get_http_options(): http_options: List[Dict[str, str]] = [ {%- for rule in rules %}{ 'method': '{{ rule.method }}', - 'uri': '{{ rule.uri }}', + 'uri': '{% if is_resumable_upload %}/{{ resumable_upload_prefix }}{% endif %}{{ rule.uri }}', {% if rule.body %} 'body': '{{ rule.body }}', {% endif %}{# rule.body #} @@ -299,11 +310,12 @@ def _get_http_options(): {%- endmacro %} -{% macro prep_wrapped_messages_async_method(api, service) %} +{% macro prep_wrapped_messages_async_method(api, service, is_rest_asyncio=False) %} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} def _prep_wrapped_messages(self, client_info): """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" self._wrapped_methods = { - {% for method in service.methods.values() %} + {% for method in service.methods.values() if not is_rest_asyncio or rest_async_io_enabled or method.is_resumable_upload %} self.{{ method.transport_safe_name|snake_case }}: self._wrap_method( self.{{ method.transport_safe_name|snake_case }}, {% if method.retry %} @@ -329,6 +341,7 @@ def _prep_wrapped_messages(self, client_info): client_info=client_info, ), {% endfor %}{# service.methods.values() #} + {% if not is_rest_asyncio or rest_async_io_enabled %} {% for method_name in api.mixin_api_methods.keys() %} {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2197): Use `transport_safe_name` similar # to what we do for non-mixin methods above. @@ -339,6 +352,7 @@ def _prep_wrapped_messages(self, client_info): client_info=client_info, ), {% endfor %}{# method_name in api.mixin_api_methods.keys() #} + {% endif %} } {% endmacro %} @@ -362,6 +376,7 @@ def _wrap_method(self, func, *args, **kwargs): # synchronous and asynchronous rest transports #} {% macro create_interceptor_class(api, service, method, is_async=False) %} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} {% set async_prefix = "async " if is_async else "" %} {% set async_method_name_prefix = "Async" if is_async else "" %} {% set async_docstring = "Asynchronous " if is_async else "" %} @@ -382,7 +397,7 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor: .. code-block:: python class MyCustom{{ service.name }}Interceptor({{ service.name }}RestInterceptor): - {% for _, method in service.methods|dictsort if not method.client_streaming %} + {% for _, method in service.methods|dictsort if not method.client_streaming and (not is_async or rest_async_io_enabled or method.is_resumable_upload) %} {{ async_prefix }}def pre_{{ method.name|snake_case }}(self, request, metadata): logging.log(f"Received request: {request}") return request, metadata @@ -400,7 +415,7 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor: """ - {% for method in service.methods.values()|sort(attribute="name") if not method.client_streaming and method.http_options %} + {% for method in service.methods.values()|sort(attribute="name") if not method.client_streaming and method.http_options and (not is_async or rest_async_io_enabled or method.is_resumable_upload) %} {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2147): Remove the condition below once async rest transport supports the guarded methods. #} {{ async_prefix }}def pre_{{ method.name|snake_case }}(self, request: {{method.input.ident}}, {{ client_method_metadata_argument() }}) -> Tuple[{{method.input.ident}}, {{ client_method_metadata_type() }}]: """Pre-rpc interceptor for {{ method.name|snake_case }} @@ -450,6 +465,7 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor: {% endif %}{# not method.void #} {% endfor %} + {% if not is_async or rest_async_io_enabled %} {% for name, signature in api.mixin_api_signatures.items() %} {{ async_prefix }}def pre_{{ name|snake_case }}( self, request: {{signature.request_type}}, {{ client_method_metadata_argument() }} @@ -473,6 +489,7 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor: return response {% endfor %} + {% endif %} {% endmacro %} {% macro generate_mixin_call_method(service, api, name, sig, is_async) %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 index a21ba6030a3d..3c880bdefc05 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 @@ -21,6 +21,9 @@ from {{package_path}} import gapic_version as package_version from google.api_core.client_options import ClientOptions from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +{% if service.has_resumable_upload_methods %} +from google.api_core.resumable_transfer import ResumableUploadConfig +{% endif %} {% if has_auto_populated_fields %} from {{package_path}}._compat import setup_request_id {% endif %} @@ -289,6 +292,9 @@ class {{ service.async_client_name }}: requests: Optional[AsyncIterator[{{ method.input.ident }}]] = None, *, {% endif %} + {% if method.is_resumable_upload %} + config: Optional[ResumableUploadConfig] = None, + {% endif %} retry: OptionalRetry = gapic_v1.method.DEFAULT, timeout: Union[float, object] = gapic_v1.method.DEFAULT, {{ shared_macros.client_method_metadata_argument()|indent(8) }} = {{ shared_macros.client_method_metadata_default_value() }}, @@ -324,6 +330,10 @@ class {{ service.async_client_name }}: The request object AsyncIterator.{{ " " }} {{- method.input.meta.doc|rst(width=72, indent=16, nl=False) }} {% endif %} + {% if method.is_resumable_upload %} + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. + {% endif %} retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, should be retried. timeout (float): The timeout for this request. @@ -436,6 +446,13 @@ class {{ service.async_client_name }}: timeout=timeout, metadata=metadata, ) + {% elif method.is_resumable_upload %} + + # Wrap the response in a resumable upload session. + response = {{ method.client_output_async.ident }}( + config=config, + response_type={{ method.output.ident }}, + ) {% endif %} {% if not method.void %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 index 01407a160d99..221eeca217f7 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 @@ -1,7 +1,7 @@ {% extends '_base.py.j2' %} {% block content %} {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2121): Remove the following variable (and the condition later in this file) for async rest transport once support for it is GA. #} -{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled or service.has_resumable_upload_methods %} {% set has_auto_populated_fields = api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|select|list %} {% import "%namespace/%name_%version/%sub/services/%service/_client_macros.j2" as macros %} {% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %} @@ -30,6 +30,9 @@ from google.api_core import exceptions as core_exceptions from google.api_core import extended_operation {% endif %} from google.api_core import gapic_v1 +{% if service.has_resumable_upload_methods %} +from google.api_core.resumable_transfer import ResumableUploadConfig +{% endif %} from {{package_path}}._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables {% if has_auto_populated_fields %} from {{package_path}}._compat import setup_request_id diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 index 9745b08d789c..b09426682c4d 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 @@ -1,5 +1,5 @@ {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2121): Remove the following variable (and the condition later in this file) for async rest transport once support for it is GA. #} -{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled or service.has_resumable_upload_methods %} {% extends '_base.py.j2' %} {% block content %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 index f0cf1178da69..0962df5dd3ed 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -3,7 +3,7 @@ {% block content %} import abc -from typing import {% if service.any_extended_operations_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union +from typing import {% if service.any_extended_operations_methods or service.has_resumable_upload_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} from {{package_path}} import gapic_version as package_version @@ -138,7 +138,12 @@ class {{ service.name }}Transport(abc.ABC): credentials = credentials.with_always_use_jwt_access(True) # Save the credentials. + {% if service.has_resumable_upload_methods %} + self._credentials: Any = credentials + self._client_info = client_info + {% else %} self._credentials = credentials + {% endif %} # Save the hostname. Default to port 443 (HTTPS) if none is specified. if ':' not in host: diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index e906c9d9ea71..b1f64934f89b 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -10,6 +10,9 @@ import pickle import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +{% if service.has_resumable_upload_methods %} +from google.api_core import exceptions as core_exceptions +{% endif %} from google.api_core import grpc_helpers {% if service.has_lro %} from google.api_core import operations_v1 @@ -49,6 +52,9 @@ from google.longrunning import operations_pb2 # type: ignore {% endif %} {% endfilter %} from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO +{% if service.has_resumable_upload_methods %} +from .rest import {{ service.name }}RestTransport +{% endif %} try: from google.api_core import client_logging # type: ignore @@ -353,11 +359,28 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): # gRPC handles serialization and deserialization, so we just need # to pass in the functions for each. if '{{ method.transport_safe_name|snake_case }}' not in self._stubs: + {% if method.is_resumable_upload %} + if not self._credentials: + class _ErrorStub: + def __call__(self, *args, **kwargs): + raise core_exceptions.GoogleAPICallError( + "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." + ) + self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub() + else: + rest_transport = {{ service.name }}RestTransport( + host=self._host, + credentials=self._credentials, + client_info=self._client_info, + ) + self._stubs['{{ method.transport_safe_name|snake_case }}'] = rest_transport.{{ method.transport_safe_name|snake_case }} + {% else %} self._stubs['{{ method.transport_safe_name|snake_case }}'] = self._logged_channel.{{ method.grpc_stub_type }}( '/{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}', request_serializer={{ method.input.ident }}.{% if method.input.ident.python_import.module.endswith('_pb2') %}SerializeToString{% else %}serialize{% endif %}, response_deserializer={{ method.output.ident }}.{% if method.output.ident.python_import.module.endswith('_pb2') %}FromString{% else %}deserialize{% endif %}, ) + {% endif %} return self._stubs['{{ method.transport_safe_name|snake_case }}'] {% endfor %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 7b8a885d227c..851827e762be 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -54,6 +54,13 @@ from google.longrunning import operations_pb2 # type: ignore {% endfilter %} from .base import {{ service.name }}Transport, DEFAULT_CLIENT_INFO from .grpc import {{ service.name }}GrpcTransport +{% if service.has_resumable_upload_methods %} +try: + from .rest_asyncio import Async{{ service.name }}RestTransport + HAS_ASYNC_REST = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + HAS_ASYNC_REST = False +{% endif %} try: from google.api_core import client_logging # type: ignore @@ -358,11 +365,40 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): # gRPC handles serialization and deserialization, so we just need # to pass in the functions for each. if '{{ method.transport_safe_name|snake_case }}' not in self._stubs: + {% if method.is_resumable_upload %} + if not self._credentials: + class _ErrorStub: + def __call__(self, *args, **kwargs): + async def _error(): + raise core_exceptions.GoogleAPICallError( + "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." + ) + return _error() + self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub() + elif HAS_ASYNC_REST: # pragma: NO COVER + class _AsyncRestStub: # pragma: NO COVER + def __call__(_self, *args, **kwargs): # pragma: NO COVER + rest_transport = Async{{ service.name }}RestTransport( + host=self._host, + credentials=self._credentials, + client_info=self._client_info, + ) + return rest_transport.{{ method.transport_safe_name|snake_case }}(*args, **kwargs) + self._stubs['{{ method.transport_safe_name|snake_case }}'] = _AsyncRestStub() + else: # pragma: NO COVER + class _UnsupportedStub: # pragma: NO COVER + def __call__(self, *args, **kwargs): # pragma: NO COVER + async def _unsupported(): + raise NotImplementedError("Async REST transport is required for async resumable upload methods.") + return _unsupported() + self._stubs['{{ method.transport_safe_name|snake_case }}'] = _UnsupportedStub() + {% else %} self._stubs['{{ method.transport_safe_name|snake_case }}'] = self._logged_channel.{{ method.grpc_stub_type }}( '/{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}', request_serializer={{ method.input.ident }}.{% if method.input.ident.python_import.module.endswith('_pb2') %}SerializeToString{% else %}serialize{% endif %}, response_deserializer={{ method.output.ident }}.{% if method.output.ident.python_import.module.endswith('_pb2') %}FromString{% else %}deserialize{% endif %}, ) + {% endif %} return self._stubs['{{ method.transport_safe_name|snake_case }}'] {% endfor %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index 1bc499c068ee..c07ca9b89bdb 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -264,7 +264,13 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): pb_resp = resp {% endif %} + {% if method.is_resumable_upload %} + # Resumable upload start responses return session metadata in headers with an empty body. + if response.content and response.content.strip(): + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + {% else %} json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + {% endif %} {% endif %}{# method.lro #} {#- TODO(https://github.com/googleapis/gapic-generator-python/issues/2274): Add debug log before intercepting a request #} resp = self._interceptor.post_{{ method.name|snake_case }}(resp) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index 0f79d6e1ffef..0d169caabb75 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -156,16 +156,16 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): self._interceptor = interceptor or Async{{ service.name }}RestInterceptor() self._wrap_with_kind = True self._prep_wrapped_messages(client_info) - {% if service.has_lro %} + {% if service.has_lro and rest_async_io_enabled %} self._operations_client: Optional[operations_v1.AsyncOperationsRestClient] = None {% endif %} - {{ shared_macros.prep_wrapped_messages_async_method(api, service)|indent(4) }} + {{ shared_macros.prep_wrapped_messages_async_method(api, service, is_rest_asyncio=True)|indent(4) }} {{ shared_macros.wrap_async_method_macro()|indent(4) }} - {% for method in service.methods.values()|sort(attribute="name") %} + {% for method in service.methods.values()|sort(attribute="name") if rest_async_io_enabled or method.is_resumable_upload %} class {{ method.name|make_private }}(_Base{{ service.name }}RestTransport._Base{{method.name}}, Async{{service.name}}RestStub): def __hash__(self): return hash("Async{{service.name}}RestTransport.{{method.name}}") @@ -221,7 +221,13 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): pb_resp = resp {% endif %}{# if method.output.ident.is_proto_plus_type #} content = await response.read() + {% if method.is_resumable_upload %} + # Resumable upload start responses return session metadata in headers with an empty body. + if content and content.strip(): + json_format.Parse(content, pb_resp, ignore_unknown_fields=True) + {% else %} json_format.Parse(content, pb_resp, ignore_unknown_fields=True) + {% endif %} {% endif %}{# if method.server_streaming #} resp = await self._interceptor.post_{{ method.name|snake_case }}(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] @@ -269,7 +275,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {% endfor %} - {% if service.has_lro %} + {% if service.has_lro and rest_async_io_enabled %} @property def operations_client(self) -> AsyncOperationsRestClient: @@ -316,7 +322,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): return self._operations_client {% endif %} - {% for method in service.methods.values()|sort(attribute="name") %} + {% for method in service.methods.values()|sort(attribute="name") if rest_async_io_enabled or method.is_resumable_upload %} {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2154): Remove `type: ignore`. #} @property def {{method.transport_safe_name|snake_case}}(self) -> Callable[ @@ -325,9 +331,11 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): return self._{{method.name}}(self._session, self._host, self._interceptor) # type: ignore {% endfor %} + {% if rest_async_io_enabled %} {% for name, sig in api.mixin_api_signatures.items() %} {{ shared_macros.generate_mixin_call_method(service, api, name, sig, is_async=True) | indent(4) }} {% endfor %} + {% endif %} @property def kind(self) -> str: diff --git a/packages/gapic-generator/gapic/templates/setup.py.j2 b/packages/gapic-generator/gapic/templates/setup.py.j2 index 610fdc35dc3f..e7e9f1549061 100644 --- a/packages/gapic-generator/gapic/templates/setup.py.j2 +++ b/packages/gapic-generator/gapic/templates/setup.py.j2 @@ -1,5 +1,5 @@ {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2121): Remove the following variable (and the condition later in this file) for async rest transport once support for it is GA. #} -{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled or api.has_resumable_upload_methods %} {% set unversioned_package_disabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.unversioned_package_disabled %} {% extends '_base.py.j2' %} {% from '_pypi_packages.j2' import pypi_packages %} diff --git a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 index 04e798f94d9a..aea4bb3a7d15 100644 --- a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 +++ b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 @@ -1,4 +1,4 @@ -{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled or api.has_resumable_upload_methods %} {% if rest_async_io_enabled %} # -*- coding: utf-8 -*- {% from '_pypi_packages.j2' import pypi_packages %} diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index d6880c73605a..d2402747ca28 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1,7 +1,7 @@ {% extends "_base.py.j2" %} {% block content %} {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2121): Remove the following variable (and the condition later in this file) for async rest transport once support for it is GA. #} -{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled or service.has_resumable_upload_methods %} {% set has_auto_populated_fields = api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|select|list %} {% import "tests/unit/gapic/%name_%version/%sub/test_macros.j2" as test_macros %} {% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %} @@ -1522,6 +1522,71 @@ def test_{{ service.name|snake_case }}_grpc_asyncio_transport_channel(): assert transport._ssl_channel_credentials == None +{% if service.has_resumable_upload_methods and 'grpc' in opts.transport %} +{% for method in service.methods.values() if method.is_resumable_upload %} +def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_channel_without_credentials_error(): + channel = grpc.secure_channel('http://localhost/', grpc.local_channel_credentials()) + transport = transports.{{ service.name }}GrpcTransport( + host="localhost:7469", + channel=channel, + ) + with pytest.raises(core_exceptions.GoogleAPICallError) as exc_info: + transport.{{ method.transport_safe_name|snake_case }}({{ method.input.ident }}()) + assert "operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_channel_without_credentials_error(): + channel = aio.secure_channel('http://localhost/', grpc.local_channel_credentials()) + transport = transports.{{ service.name }}GrpcAsyncIOTransport( + host="localhost:7469", + channel=channel, + ) + with pytest.raises(core_exceptions.GoogleAPICallError) as exc_info: + await transport.{{ method.transport_safe_name|snake_case }}({{ method.input.ident }}()) + assert "operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel" in str(exc_info.value) +{% endfor %} +{% endif %} + +{% if service.has_resumable_upload_methods and 'rest' in opts.transport %} +{% for method in service.methods.values() if method.is_resumable_upload %} +def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_start_empty_body(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + with mock.patch.object(type(client.transport._session), "request") as req: + response_value = mock.MagicMock() + response_value.status_code = 200 + response_value.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + response_value.content = b"" + req.return_value = response_value + response = client.{{ method.client_method_name|snake_case }}({{ method.input.ident }}()) + assert isinstance(response, {{ method.client_output.ident }}) + + +{% if rest_async_io_enabled %} +@pytest.mark.asyncio +async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_asyncio_start_empty_body(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip() + client = {{ service.async_client_name }}( + credentials=async_anonymous_credentials(), + transport="rest_asyncio", + ) + with mock.patch.object(type(client.transport._session), "request", new_callable=mock.AsyncMock) as req: + response_value = mock.MagicMock() + response_value.status_code = 200 + response_value.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + response_value.read = mock.AsyncMock(return_value=b"") + req.return_value = response_value + response = await client.{{ method.client_method_name|snake_case }}({{ method.input.ident }}()) + assert isinstance(response, {{ method.client_output_async.ident }}) +{% endif %} +{% endfor %} +{% endif %} + + # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are # removed from grpc/grpc_asyncio transport constructor. @pytest.mark.filterwarnings("ignore::FutureWarning") diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 index 05c60cbdc9c3..8acea0c5dc83 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 @@ -92,6 +92,8 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'): {% elif method.server_streaming %} for message in response: assert isinstance(message, {{ method.output.ident }}) + {% elif method.is_resumable_upload %} + assert isinstance(response, {{ method.client_output.ident }}) {% else %} {% if "next_page_token" in method.output.fields.values()|map(attribute='name', default="") and not method.paged_result_field %} {# Cheeser assertion to force code coverage for bad paginated methods #} @@ -357,6 +359,8 @@ async def test_{{ method_name }}_async(request_type, transport: str = 'grpc_asyn {% elif method.server_streaming %} message = await response.read() assert isinstance(message, {{ method.output.ident }}) + {% elif method.is_resumable_upload %} + assert isinstance(response, {{ method.client_output_async.ident }}) {% else %} assert isinstance(response, {{ method.client_output_async.ident }}) {% for field in method_output.fields.values() | rejectattr('message') %} @@ -1730,7 +1734,8 @@ def test_unsupported_parameter_rest_asyncio(): # into `run_transport_tests_for_config` and make any of the rest specific specific macros which are called within more generic. #} {% macro run_transport_tests_for_config(service, api, transport, is_async) %} -{% for method in service.methods.values() %} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} +{% for method in service.methods.values() if not (transport == 'rest' and is_async) or rest_async_io_enabled or method.is_resumable_upload %} {% if is_rest_unsupported_method(method, is_async) == 'True' or not method.http_options %} {{ rest_method_not_implemented_error(service, method, transport, is_async) }} {% else %} @@ -1741,12 +1746,14 @@ def test_unsupported_parameter_rest_asyncio(): {% endif %}{# if 'rest' in transport #} {% endif %}{# is_rest_unsupported_method(method, is_async) == 'False' and method.http_options #} {% endfor %}{# for method in service.methods.values() #} +{% if not (transport == 'rest' and is_async) or rest_async_io_enabled %} {% for name, sig in api.mixin_api_signatures.items() %} {% if 'rest' in transport %} {{ bad_request_mixins_test(service, api, name, sig, transport, is_async) }} {{ call_success_mixins_test(service, api, name, sig, transport, is_async) }} {% endif %}{# if 'rest' in transport #} {% endfor %} +{% endif %} {{ initialize_client_with_transport_test(service, transport, is_async) }} {% endmacro %} @@ -2102,6 +2109,12 @@ def test_initialize_client_w_{{transport_name}}(): assert response is None {% elif method.lro %} json_return_value = json_format.MessageToJson(return_value) + {% elif method.is_resumable_upload %} + {% if is_async %} + assert isinstance(response, {{ method.client_output_async.ident }}) + {% else %} + assert isinstance(response, {{ method.client_output.ident }}) + {% endif %} {% else %} {% if is_async %} assert isinstance(response, {{ method.client_output_async.ident }}) @@ -2202,7 +2215,8 @@ def test_initialize_client_w_{{transport_name}}(): {% endmacro %}{# call_success_mixins_test #} {% macro empty_call_test(service, api, transport, is_async) %} -{% for method in service.methods.values() %}{# method #} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} +{% for method in service.methods.values() if not (transport == 'rest' and is_async) or rest_async_io_enabled or method.is_resumable_upload %}{# method #} {% if not method.client_streaming %} # This test is a coverage failsafe to make sure that totally empty calls, # i.e. request == None and no flattened fields passed, work. @@ -2212,7 +2226,8 @@ def test_initialize_client_w_{{transport_name}}(): {% endmacro %}{# empty_call_test #} {% macro routing_parameter_test(service, api, transport, is_async) %} -{% for method in service.methods.values() %}{# method #} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} +{% for method in service.methods.values() if not (transport == 'rest' and is_async) or rest_async_io_enabled or method.is_resumable_upload %}{# method #} {# See existing proposal b/330610501 to add support for explicit routing in BIDI/client side streaming #} {% if method.explicit_routing and not method.client_streaming %} {# Any value that is part of the HTTP/1.1 URI should be sent as #} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py index 4a5769a6fafc..7f662558cc3d 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py @@ -24,6 +24,7 @@ from google.api_core.client_options import ClientOptions from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.api_core.resumable_transfer import ResumableUploadConfig from google.showcase_v1beta1._compat import setup_request_id from google.api_core import retry_async as retries from google.auth import credentials as ga_credentials # type: ignore @@ -36,6 +37,7 @@ except AttributeError: # pragma: NO COVER OptionalRetry = Union[retries.AsyncRetry, object, None] # type: ignore +from google.api_core import resumable_transfer from google.cloud.location import locations_pb2 # type: ignore from google.iam.v1 import iam_policy_pb2 # type: ignore from google.iam.v1 import policy_pb2 # type: ignore @@ -261,10 +263,11 @@ def __init__(self, *, async def upload_media(self, request: Optional[Union[resumable_upload.UploadMediaRequest, dict]] = None, *, + config: Optional[ResumableUploadConfig] = None, retry: OptionalRetry = gapic_v1.method.DEFAULT, timeout: Union[float, object] = gapic_v1.method.DEFAULT, metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), - ) -> resumable_upload.UploadMediaResponse: + ) -> resumable_transfer.AsyncResumableUploadSession: r"""A method with media_upload annotation enabled. .. code-block:: python @@ -295,6 +298,8 @@ async def sample_upload_media(): Args: request (Optional[Union[google.showcase_v1beta1.types.UploadMediaRequest, dict]]): The request object. + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, should be retried. timeout (float): The timeout for this request. @@ -304,7 +309,9 @@ async def sample_upload_media(): be of type `bytes`. Returns: - google.showcase_v1beta1.types.UploadMediaResponse: + google.api_core.resumable_transfer.AsyncResumableUploadSession: + An object representing a resumable + upload session. """ # Create or coerce a protobuf request object. @@ -317,6 +324,16 @@ async def sample_upload_media(): # and friendly error handling. rpc = self._client._transport._wrapped_methods[self._client._transport.upload_media] + metadata = () if metadata is None else metadata + resumable_metadata = { + "x-goog-upload-protocol": "resumable", + "x-goog-upload-command": "start", + } + existing_keys = {k.lower() for k, _ in metadata} + metadata = tuple(metadata) + tuple( + (k, v) for k, v in resumable_metadata.items() if k not in existing_keys + ) + # Validate the universe domain. self._client._validate_universe_domain() @@ -328,6 +345,12 @@ async def sample_upload_media(): metadata=metadata, ) + # Wrap the response in a resumable upload session. + response = resumable_transfer.AsyncResumableUploadSession( + config=config, + response_type=resumable_upload.UploadMediaResponse, + ) + # Done; return the response. return response diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py index 20f68773522a..812c86e07c84 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py @@ -28,6 +28,7 @@ from google.api_core import client_options as client_options_lib from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.api_core.resumable_transfer import ResumableUploadConfig from google.showcase_v1beta1._compat import get_universe_domain, get_api_endpoint, get_default_mtls_endpoint, should_use_client_cert, read_environment_variables from google.showcase_v1beta1._compat import setup_request_id from google.api_core import retry as retries @@ -51,6 +52,7 @@ _LOGGER = std_logging.getLogger(__name__) +from google.api_core import resumable_transfer from google.cloud.location import locations_pb2 # type: ignore from google.iam.v1 import iam_policy_pb2 # type: ignore from google.iam.v1 import policy_pb2 # type: ignore @@ -60,6 +62,13 @@ from .transports.grpc import ResumableUploadServiceGrpcTransport from .transports.grpc_asyncio import ResumableUploadServiceGrpcAsyncIOTransport from .transports.rest import ResumableUploadServiceRestTransport +ASYNC_REST_EXCEPTION = None +try: + from .transports.rest_asyncio import AsyncResumableUploadServiceRestTransport + HAS_ASYNC_REST_DEPENDENCIES = True +except ImportError as e: # pragma: NO COVER + HAS_ASYNC_REST_DEPENDENCIES = False + ASYNC_REST_EXCEPTION = e class ResumableUploadServiceClientMeta(type): @@ -73,6 +82,8 @@ class ResumableUploadServiceClientMeta(type): _transport_registry["grpc"] = ResumableUploadServiceGrpcTransport _transport_registry["grpc_asyncio"] = ResumableUploadServiceGrpcAsyncIOTransport _transport_registry["rest"] = ResumableUploadServiceRestTransport + if HAS_ASYNC_REST_DEPENDENCIES: # pragma: NO COVER + _transport_registry["rest_asyncio"] = AsyncResumableUploadServiceRestTransport def get_transport_class(cls, label: Optional[str] = None, @@ -87,6 +98,8 @@ def get_transport_class(cls, The transport class to use. """ # If a specific transport is requested, return that one. + if label == "rest_asyncio" and not HAS_ASYNC_REST_DEPENDENCIES: # pragma: NO COVER + raise ASYNC_REST_EXCEPTION if label: return cls._transport_registry[label] @@ -454,16 +467,38 @@ def __init__(self, *, )) if not transport_provided: - import google.auth._default # type: ignore - - if api_key_value and hasattr(google.auth._default, "get_api_key_credentials"): - credentials = google.auth._default.get_api_key_credentials(api_key_value) - transport_init: Union[Type[ResumableUploadServiceTransport], Callable[..., ResumableUploadServiceTransport]] = ( ResumableUploadServiceClient.get_transport_class(transport) if isinstance(transport, str) or transport is None else cast(Callable[..., ResumableUploadServiceTransport], transport) ) + + if "rest_asyncio" in str(transport_init): + unsupported_params = { + "google.api_core.client_options.ClientOptions.credentials_file": self._client_options.credentials_file, + "google.api_core.client_options.ClientOptions.scopes": self._client_options.scopes, + "google.api_core.client_options.ClientOptions.quota_project_id": self._client_options.quota_project_id, + "google.api_core.client_options.ClientOptions.client_cert_source": self._client_options.client_cert_source, + "google.api_core.client_options.ClientOptions.api_audience": self._client_options.api_audience, + + } + provided_unsupported_params = [name for name, value in unsupported_params.items() if value is not None] + if provided_unsupported_params: + raise core_exceptions.AsyncRestUnsupportedParameterError( # type: ignore + f"The following provided parameters are not supported for `transport=rest_asyncio`: {', '.join(provided_unsupported_params)}" + ) + self._transport = transport_init( + credentials=credentials, + host=self._api_endpoint, + client_info=client_info, + ) + return + + import google.auth._default # type: ignore + + if api_key_value and hasattr(google.auth._default, "get_api_key_credentials"): + credentials = google.auth._default.get_api_key_credentials(api_key_value) + # initialize with the provided callable or the passed in class self._transport = transport_init( credentials=credentials, @@ -495,10 +530,11 @@ def __init__(self, *, def upload_media(self, request: Optional[Union[resumable_upload.UploadMediaRequest, dict]] = None, *, + config: Optional[ResumableUploadConfig] = None, retry: OptionalRetry = gapic_v1.method.DEFAULT, timeout: Union[float, object] = gapic_v1.method.DEFAULT, metadata: Sequence[Tuple[str, Union[str, bytes]]] = (), - ) -> resumable_upload.UploadMediaResponse: + ) -> resumable_transfer.ResumableUploadSession: r"""A method with media_upload annotation enabled. .. code-block:: python @@ -529,6 +565,8 @@ def sample_upload_media(): Args: request (Union[google.showcase_v1beta1.types.UploadMediaRequest, dict]): The request object. + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. retry (google.api_core.retry.Retry): Designation of what errors, if any, should be retried. timeout (float): The timeout for this request. @@ -538,7 +576,9 @@ def sample_upload_media(): be of type `bytes`. Returns: - google.showcase_v1beta1.types.UploadMediaResponse: + google.api_core.resumable_transfer.ResumableUploadSession: + An object representing a resumable + upload session. """ # Create or coerce a protobuf request object. @@ -551,6 +591,16 @@ def sample_upload_media(): # and friendly error handling. rpc = self._transport._wrapped_methods[self._transport.upload_media] + metadata = () if metadata is None else metadata + resumable_metadata = { + "x-goog-upload-protocol": "resumable", + "x-goog-upload-command": "start", + } + existing_keys = {k.lower() for k, _ in metadata} + metadata = tuple(metadata) + tuple( + (k, v) for k, v in resumable_metadata.items() if k not in existing_keys + ) + # Validate the universe domain. self._validate_universe_domain() @@ -562,6 +612,12 @@ def sample_upload_media(): metadata=metadata, ) + # Wrap the response in a resumable upload session. + response = resumable_transfer.ResumableUploadSession( + config=config, + response_type=resumable_upload.UploadMediaResponse, + ) + # Done; return the response. return response diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py index 432a8bdbc168..ebe230e059ee 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py @@ -14,13 +14,22 @@ # limitations under the License. # from collections import OrderedDict -from typing import Dict, Type +from typing import Dict, Type, Tuple from .base import ResumableUploadServiceTransport from .grpc import ResumableUploadServiceGrpcTransport from .grpc_asyncio import ResumableUploadServiceGrpcAsyncIOTransport from .rest import ResumableUploadServiceRestTransport from .rest import ResumableUploadServiceRestInterceptor +ASYNC_REST_CLASSES: Tuple[str, ...] +try: + from .rest_asyncio import AsyncResumableUploadServiceRestTransport + from .rest_asyncio import AsyncResumableUploadServiceRestInterceptor + ASYNC_REST_CLASSES = ('AsyncResumableUploadServiceRestTransport', 'AsyncResumableUploadServiceRestInterceptor') + HAS_REST_ASYNC = True +except ImportError: # pragma: NO COVER + ASYNC_REST_CLASSES = () + HAS_REST_ASYNC = False # Compile a registry of transports. @@ -28,6 +37,8 @@ _transport_registry['grpc'] = ResumableUploadServiceGrpcTransport _transport_registry['grpc_asyncio'] = ResumableUploadServiceGrpcAsyncIOTransport _transport_registry['rest'] = ResumableUploadServiceRestTransport +if HAS_REST_ASYNC: # pragma: NO COVER + _transport_registry['rest_asyncio'] = AsyncResumableUploadServiceRestTransport __all__ = ( 'ResumableUploadServiceTransport', @@ -35,4 +46,4 @@ 'ResumableUploadServiceGrpcAsyncIOTransport', 'ResumableUploadServiceRestTransport', 'ResumableUploadServiceRestInterceptor', -) +) + ASYNC_REST_CLASSES diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py index 856b01756ab3..52b0d81ab4c2 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py @@ -14,7 +14,7 @@ # limitations under the License. # import abc -from typing import Awaitable, Callable, Dict, Optional, Sequence, Union +from typing import Any, Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version @@ -115,7 +115,8 @@ def __init__( credentials = credentials.with_always_use_jwt_access(True) # Save the credentials. - self._credentials = credentials + self._credentials: Any = credentials + self._client_info = client_info # Save the hostname. Default to port 443 (HTTPS) if none is specified. if ':' not in host: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py index e5d0324cf558..0df56c75d682 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py @@ -19,6 +19,7 @@ import warnings from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from google.api_core import exceptions as core_exceptions from google.api_core import grpc_helpers from google.api_core import gapic_v1 import google.auth # type: ignore @@ -36,6 +37,7 @@ from google.longrunning import operations_pb2 # type: ignore from google.showcase_v1beta1.types import resumable_upload from .base import ResumableUploadServiceTransport, DEFAULT_CLIENT_INFO +from .rest import ResumableUploadServiceRestTransport try: from google.api_core import client_logging # type: ignore @@ -337,11 +339,20 @@ def upload_media(self) -> Callable[ # gRPC handles serialization and deserialization, so we just need # to pass in the functions for each. if 'upload_media' not in self._stubs: - self._stubs['upload_media'] = self._logged_channel.unary_unary( - '/google.showcase.v1beta1.ResumableUploadService/UploadMedia', - request_serializer=resumable_upload.UploadMediaRequest.serialize, - response_deserializer=resumable_upload.UploadMediaResponse.deserialize, - ) + if not self._credentials: + class _ErrorStub: + def __call__(self, *args, **kwargs): + raise core_exceptions.GoogleAPICallError( + "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." + ) + self._stubs['upload_media'] = _ErrorStub() + else: + rest_transport = ResumableUploadServiceRestTransport( + host=self._host, + credentials=self._credentials, + client_info=self._client_info, + ) + self._stubs['upload_media'] = rest_transport.upload_media return self._stubs['upload_media'] def close(self): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 7bde6f9e3205..c2c554366192 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -40,6 +40,11 @@ from google.showcase_v1beta1.types import resumable_upload from .base import ResumableUploadServiceTransport, DEFAULT_CLIENT_INFO from .grpc import ResumableUploadServiceGrpcTransport +try: + from .rest_asyncio import AsyncResumableUploadServiceRestTransport + HAS_ASYNC_REST = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + HAS_ASYNC_REST = False try: from google.api_core import client_logging # type: ignore @@ -343,11 +348,32 @@ def upload_media(self) -> Callable[ # gRPC handles serialization and deserialization, so we just need # to pass in the functions for each. if 'upload_media' not in self._stubs: - self._stubs['upload_media'] = self._logged_channel.unary_unary( - '/google.showcase.v1beta1.ResumableUploadService/UploadMedia', - request_serializer=resumable_upload.UploadMediaRequest.serialize, - response_deserializer=resumable_upload.UploadMediaResponse.deserialize, - ) + if not self._credentials: + class _ErrorStub: + def __call__(self, *args, **kwargs): + async def _error(): + raise core_exceptions.GoogleAPICallError( + "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." + ) + return _error() + self._stubs['upload_media'] = _ErrorStub() + elif HAS_ASYNC_REST: # pragma: NO COVER + class _AsyncRestStub: # pragma: NO COVER + def __call__(_self, *args, **kwargs): # pragma: NO COVER + rest_transport = AsyncResumableUploadServiceRestTransport( + host=self._host, + credentials=self._credentials, + client_info=self._client_info, + ) + return rest_transport.upload_media(*args, **kwargs) + self._stubs['upload_media'] = _AsyncRestStub() + else: # pragma: NO COVER + class _UnsupportedStub: # pragma: NO COVER + def __call__(self, *args, **kwargs): # pragma: NO COVER + async def _unsupported(): + raise NotImplementedError("Async REST transport is required for async resumable upload methods.") + return _unsupported() + self._stubs['upload_media'] = _UnsupportedStub() return self._stubs['upload_media'] def _prep_wrapped_messages(self, client_info): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py index 3e29344fc715..8d0b8802319a 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py @@ -511,7 +511,9 @@ def __call__(self, resp = resumable_upload.UploadMediaResponse() pb_resp = resumable_upload.UploadMediaResponse.pb(resp) - json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) + # Resumable upload start responses return session metadata in headers with an empty body. + if response.content and response.content.strip(): + json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) resp = self._interceptor.post_upload_media(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py new file mode 100755 index 000000000000..8be728bf9375 --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import google.auth +try: + import aiohttp # type: ignore + from google.auth.aio.transport.sessions import AsyncAuthorizedSession # type: ignore + from google.api_core import rest_streaming_async # type: ignore + from google.api_core.operations_v1 import AsyncOperationsRestClient # type: ignore +except ImportError as e: # pragma: NO COVER + raise ImportError("`rest_asyncio` transport requires the library to be installed with the `async_rest` extra. Install the library with the `async_rest` extra using `pip install google-showcase[async_rest]`") from e + +from google.auth.aio import credentials as ga_credentials_async # type: ignore + +from google.api_core import exceptions as core_exceptions +from google.api_core import gapic_v1 +from google.iam.v1 import iam_policy_pb2 # type: ignore +from google.iam.v1 import policy_pb2 # type: ignore +from google.cloud.location import locations_pb2 # type: ignore +from google.api_core import retry_async as retries +from google.api_core import rest_helpers +from google.api_core import rest_streaming_async # type: ignore +from google.showcase_v1beta1._compat import transcode_request + +import google.protobuf + +from google.protobuf import json_format +from google.iam.v1 import iam_policy_pb2 # type: ignore +from google.iam.v1 import policy_pb2 # type: ignore +from google.cloud.location import locations_pb2 # type: ignore + +import json # type: ignore +import dataclasses +from typing import Any, Dict, List, Callable, Tuple, Optional, Sequence, Union + + +from google.showcase_v1beta1.types import resumable_upload +from google.longrunning import operations_pb2 # type: ignore + + +from .rest_base import _BaseResumableUploadServiceRestTransport + +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO + + +import logging + +try: + from google.api_core import client_logging # type: ignore + CLIENT_LOGGING_SUPPORTED = True # pragma: NO COVER +except ImportError: # pragma: NO COVER + CLIENT_LOGGING_SUPPORTED = False + +_LOGGER = logging.getLogger(__name__) + +try: + OptionalRetry = Union[retries.AsyncRetry, gapic_v1.method._MethodDefault, None] +except AttributeError: # pragma: NO COVER + OptionalRetry = Union[retries.AsyncRetry, object, None] # type: ignore + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=f"google-auth@{google.auth.__version__}", +) + +DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ + + +class AsyncResumableUploadServiceRestInterceptor: + """Asynchronous Interceptor for ResumableUploadService. + + Interceptors are used to manipulate requests, request metadata, and responses + in arbitrary ways. + Example use cases include: + * Logging + * Verifying requests according to service or custom semantics + * Stripping extraneous information from responses + + These use cases and more can be enabled by injecting an + instance of a custom subclass when constructing the AsyncResumableUploadServiceRestTransport. + + .. code-block:: python + class MyCustomResumableUploadServiceInterceptor(ResumableUploadServiceRestInterceptor): + async def pre_upload_media(self, request, metadata): + logging.log(f"Received request: {request}") + return request, metadata + + async def post_upload_media(self, response): + logging.log(f"Received response: {response}") + return response + + transport = AsyncResumableUploadServiceRestTransport(interceptor=MyCustomResumableUploadServiceInterceptor()) + client = async ResumableUploadServiceClient(transport=transport) + + + """ + async def pre_upload_media(self, request: resumable_upload.UploadMediaRequest, metadata: Sequence[Tuple[str, Union[str, bytes]]]) -> Tuple[resumable_upload.UploadMediaRequest, Sequence[Tuple[str, Union[str, bytes]]]]: + """Pre-rpc interceptor for upload_media + + Override in a subclass to manipulate the request or metadata + before they are sent to the ResumableUploadService server. + """ + return request, metadata + + async def post_upload_media(self, response: resumable_upload.UploadMediaResponse) -> resumable_upload.UploadMediaResponse: + """Post-rpc interceptor for upload_media + + DEPRECATED. Please use the `post_upload_media_with_metadata` + interceptor instead. + + Override in a subclass to read or manipulate the response + after it is returned by the ResumableUploadService server but before + it is returned to user code. This `post_upload_media` interceptor runs + before the `post_upload_media_with_metadata` interceptor. + """ + return response + + async def post_upload_media_with_metadata(self, response: resumable_upload.UploadMediaResponse, metadata: Sequence[Tuple[str, Union[str, bytes]]]) -> Tuple[resumable_upload.UploadMediaResponse, Sequence[Tuple[str, Union[str, bytes]]]]: + """Post-rpc interceptor for upload_media + + Override in a subclass to read or manipulate the response or metadata after it + is returned by the ResumableUploadService server but before it is returned to user code. + + We recommend only using this `post_upload_media_with_metadata` + interceptor in new development instead of the `post_upload_media` interceptor. + When both interceptors are used, this `post_upload_media_with_metadata` interceptor runs after the + `post_upload_media` interceptor. The (possibly modified) response returned by + `post_upload_media` will be passed to + `post_upload_media_with_metadata`. + """ + return response, metadata + + +@dataclasses.dataclass +class AsyncResumableUploadServiceRestStub: + _session: AsyncAuthorizedSession + _host: str + _interceptor: AsyncResumableUploadServiceRestInterceptor + +class AsyncResumableUploadServiceRestTransport(_BaseResumableUploadServiceRestTransport): + """Asynchronous REST backend transport for ResumableUploadService. + + A service showcasing universal resumable upload protocol + support. + + This class defines the same methods as the primary client, so the + primary client can load the underlying transport implementation + and call it. + + It sends JSON representations of protocol buffers over HTTP/1.1 + """ + def __init__(self, + *, + host: str = 'localhost:7469', + credentials: Optional[ga_credentials_async.Credentials] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + url_scheme: str = 'https', + interceptor: Optional[AsyncResumableUploadServiceRestInterceptor] = None, + ) -> None: + """Instantiate the transport. + + NOTE: This async REST transport functionality is currently in a beta + state (preview). We welcome your feedback via a GitHub issue in + this library's repository. Thank you! + + Args: + host (Optional[str]): + The hostname to connect to (default: 'localhost:7469'). + credentials (Optional[google.auth.aio.credentials.Credentials]): The + authorization credentials to attach to requests. These + credentials identify the application to the service; if none + are specified, the client will attempt to ascertain the + credentials from the environment. + client_info (google.api_core.gapic_v1.client_info.ClientInfo): + The client info used to send a user-agent string along with + API requests. If ``None``, then default info will be used. + Generally, you only need to set this if you are developing + your own client library. + url_scheme (str): the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + interceptor (Optional[AsyncResumableUploadServiceRestInterceptor]): Interceptor used + to manipulate requests, request metadata, and responses. + """ + # Run the base constructor + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=False, + url_scheme=url_scheme, + api_audience=None + ) + self._session = AsyncAuthorizedSession(self._credentials) # type: ignore + self._interceptor = interceptor or AsyncResumableUploadServiceRestInterceptor() + self._wrap_with_kind = True + self._prep_wrapped_messages(client_info) + + def _prep_wrapped_messages(self, client_info): + """ Precompute the wrapped methods, overriding the base class method to use async wrappers.""" + self._wrapped_methods = { + self.upload_media: self._wrap_method( + self.upload_media, + default_timeout=None, + client_info=client_info, + ), + } + + def _wrap_method(self, func, *args, **kwargs): + if self._wrap_with_kind: # pragma: NO COVER + kwargs["kind"] = self.kind + return gapic_v1.method_async.wrap_method(func, *args, **kwargs) + + class _UploadMedia(_BaseResumableUploadServiceRestTransport._BaseUploadMedia, AsyncResumableUploadServiceRestStub): + def __hash__(self): + return hash("AsyncResumableUploadServiceRestTransport.UploadMedia") + + @staticmethod + async def _get_response( + host, + metadata, + query_params, + session, + timeout, + transcoded_request, + body=None): + + uri = transcoded_request['uri'] + method = transcoded_request['method'] + headers = dict(metadata) + headers['Content-Type'] = 'application/json' + response = await getattr(session, method)( + "{host}{uri}".format(host=host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params, strict=True), + data=body, + ) + return response + + async def __call__(self, + request: resumable_upload.UploadMediaRequest, *, + retry: OptionalRetry=gapic_v1.method.DEFAULT, + timeout: Optional[float]=None, + metadata: Sequence[Tuple[str, Union[str, bytes]]]=(), + ) -> resumable_upload.UploadMediaResponse: + r"""Call the upload media method over HTTP. + + Args: + request (~.resumable_upload.UploadMediaRequest): + The request object. + retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be + sent along with the request as metadata. Normally, each value must be of type `str`, + but for metadata keys ending with the suffix `-bin`, the corresponding values must + be of type `bytes`. + + Returns: + ~.resumable_upload.UploadMediaResponse: + + """ + + http_options = _BaseResumableUploadServiceRestTransport._BaseUploadMedia._get_http_options() + request, metadata = await self._interceptor.pre_upload_media(request, metadata) + transcoded_request, body, query_params = transcode_request( + http_options, + request, + required_fields_default_values=getattr( + _BaseResumableUploadServiceRestTransport._BaseUploadMedia, + "_BaseUploadMedia__REQUIRED_FIELDS_DEFAULT_VALUES", + None, + ), + rest_numeric_enums=False, + ) + + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(logging.DEBUG): # pragma: NO COVER + request_url = "{host}{uri}".format(host=self._host, uri=transcoded_request['uri']) + method = transcoded_request['method'] + try: + request_payload = type(request).to_json(request) + except: + request_payload = None + http_request = { + "payload": request_payload, + "requestMethod": method, + "requestUrl": request_url, + "headers": dict(metadata), + } + _LOGGER.debug( + f"Sending request for google.showcase_v1beta1.ResumableUploadServiceClient.UploadMedia", + extra = { + "serviceName": "google.showcase.v1beta1.ResumableUploadService", + "rpcName": "UploadMedia", + "httpRequest": http_request, + "metadata": http_request["headers"], + }, + ) + + # Send the request + response = await AsyncResumableUploadServiceRestTransport._UploadMedia._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) + + # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception + # subclass. + if response.status_code >= 400: + content = await response.read() + payload = json.loads(content.decode('utf-8')) + request_url = "{host}{uri}".format(host=self._host, uri=transcoded_request['uri']) + method = transcoded_request['method'] + raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore + + # Return the response + resp = resumable_upload.UploadMediaResponse() + pb_resp = resumable_upload.UploadMediaResponse.pb(resp) + content = await response.read() + # Resumable upload start responses return session metadata in headers with an empty body. + if content and content.strip(): + json_format.Parse(content, pb_resp, ignore_unknown_fields=True) + resp = await self._interceptor.post_upload_media(resp) + response_metadata = [(k, str(v)) for k, v in response.headers.items()] + resp, _ = await self._interceptor.post_upload_media_with_metadata(resp, response_metadata) + if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(logging.DEBUG): # pragma: NO COVER + try: + response_payload = resumable_upload.UploadMediaResponse.to_json(response) + except: + response_payload = None + http_response = { + "payload": response_payload, + "headers": dict(response.headers), + "status": "OK", # need to obtain this properly + } + _LOGGER.debug( + "Received response for google.showcase_v1beta1.ResumableUploadServiceAsyncClient.upload_media", + extra = { + "serviceName": "google.showcase.v1beta1.ResumableUploadService", + "rpcName": "UploadMedia", + "metadata": http_response["headers"], + "httpResponse": http_response, + }, + ) + + return resp + + @property + def upload_media(self) -> Callable[ + [resumable_upload.UploadMediaRequest], + resumable_upload.UploadMediaResponse]: + return self._UploadMedia(self._session, self._host, self._interceptor) # type: ignore + + @property + def kind(self) -> str: + return "rest_asyncio" + + async def close(self): + await self._session.close() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/samples/generated_samples/snippet_metadata_google.showcase.v1beta1.json b/packages/gapic-generator/tests/integration/goldens/showcase/samples/generated_samples/snippet_metadata_google.showcase.v1beta1.json index 3d6373f36f36..6720f510f805 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/samples/generated_samples/snippet_metadata_google.showcase.v1beta1.json +++ b/packages/gapic-generator/tests/integration/goldens/showcase/samples/generated_samples/snippet_metadata_google.showcase.v1beta1.json @@ -4802,7 +4802,7 @@ "type": "Sequence[Tuple[str, Union[str, bytes]]]" } ], - "resultType": "google.showcase_v1beta1.types.UploadMediaResponse", + "resultType": "google.api_core.resumable_transfer.AsyncResumableUploadSession", "shortName": "upload_media" }, "description": "Sample for UploadMedia", @@ -4878,7 +4878,7 @@ "type": "Sequence[Tuple[str, Union[str, bytes]]]" } ], - "resultType": "google.showcase_v1beta1.types.UploadMediaResponse", + "resultType": "google.api_core.resumable_transfer.ResumableUploadSession", "shortName": "upload_media" }, "description": "Sample for UploadMedia", diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py index e369d99843c5..572e6df85c68 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py @@ -53,6 +53,9 @@ "grpc-google-iam-v1 >= 0.14.2, <1.0.0", ] extras = { + "async_rest": [ + "google-auth[aiohttp] >= 2.35.0, <3.0.0" + ], } url = "https://github.com/googleapis/google-cloud-python/tree/main/packages/google-showcase" diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt new file mode 100755 index 000000000000..68916cd2f245 --- /dev/null +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# This constraints file is used to check that lower bounds +# are correct in setup.py +# List all library dependencies and extras in this file, +# pinning their versions to their lower bounds. +# For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", +# then this file should have google-cloud-foo==1.14.0 +google-api-core==2.28.0 +google-auth==2.35.0 +grpcio==1.59.0 +proto-plus==1.26.1 +protobuf==6.33.5 +grpc-google-iam-v1==0.14.2 diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index dac0fc5053f7..8d31f0f5956c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -30,6 +30,13 @@ from google.api_core import api_core_version from proto.marshal.rules.dates import DurationRule, TimestampRule from proto.marshal.rules import wrappers +try: + import aiohttp # type: ignore + from google.auth.aio.transport.sessions import AsyncAuthorizedSession + from google.api_core.operations_v1 import AsyncOperationsRestClient + HAS_ASYNC_REST_EXTRA = True +except ImportError: # pragma: NO COVER + HAS_ASYNC_REST_EXTRA = False from requests import Response from requests import Request, PreparedRequest from requests.sessions import Session @@ -47,6 +54,7 @@ from google.api_core import grpc_helpers from google.api_core import grpc_helpers_async from google.api_core import path_template +from google.api_core import resumable_transfer from google.api_core import retry as retries from google.auth import credentials as ga_credentials from google.auth.exceptions import MutualTLSChannelError @@ -842,9 +850,7 @@ def test_upload_media(request_type, transport: str = 'grpc'): assert args[0] == request # Establish that the response is the type that we expect. - assert isinstance(response, resumable_upload.UploadMediaResponse) - assert response.name == 'name_value' - assert response.size == 443 + assert isinstance(response, resumable_transfer.ResumableUploadSession) def test_upload_media_non_empty_request_with_auto_populated_field(): @@ -974,9 +980,7 @@ async def test_upload_media_async(request_type, transport: str = 'grpc_asyncio') assert args[0] == request # Establish that the response is the type that we expect. - assert isinstance(response, resumable_upload.UploadMediaResponse) - assert response.name == 'name_value' - assert response.size == 443 + assert isinstance(response, resumable_transfer.AsyncResumableUploadSession) def test_upload_media_rest_use_cached_wrapped_rpc(): @@ -1243,9 +1247,7 @@ def test_upload_media_rest_call_success(request_type): response = client.upload_media(request) # Establish that the response is the type that we expect. - assert isinstance(response, resumable_upload.UploadMediaResponse) - assert response.name == 'name_value' - assert response.size == 443 + assert isinstance(response, resumable_transfer.ResumableUploadSession) @pytest.mark.parametrize("null_interceptor", [True, False]) @@ -1799,6 +1801,174 @@ def test_upload_media_empty_call_rest(): assert args[0] == request_msg +def test_transport_kind_rest_asyncio(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = ResumableUploadServiceAsyncClient.get_transport_class("rest_asyncio")( + credentials=async_anonymous_credentials() + ) + assert transport.kind == "rest_asyncio" + + +@pytest.mark.asyncio +async def test_upload_media_rest_asyncio_bad_request(request_type=resumable_upload.UploadMediaRequest): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + client = ResumableUploadServiceAsyncClient( + credentials=async_anonymous_credentials(), + transport="rest_asyncio" + ) + # send a request that will satisfy transcoding + request_init = {} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a BadRequest error. + with mock.patch.object(AsyncAuthorizedSession, 'request') as req, pytest.raises(core_exceptions.BadRequest): + # Wrap the value into a proper Response obj + response_value = mock.Mock() + response_value.read = mock.AsyncMock(return_value=b'{}') + response_value.status_code = 400 + response_value.request = mock.Mock() + req.return_value = response_value + req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} + await client.upload_media(request) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("request_type", [ + resumable_upload.UploadMediaRequest, + dict, +]) +async def test_upload_media_rest_asyncio_call_success(request_type): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + client = ResumableUploadServiceAsyncClient( + credentials=async_anonymous_credentials(), + transport="rest_asyncio" + ) + + # send a request that will satisfy transcoding + request_init = {} + request = request_type(**request_init) + + # Mock the http request call within the method and fake a response. + with mock.patch.object(type(client.transport._session), 'request') as req: + # Designate an appropriate value for the returned response. + return_value = resumable_upload.UploadMediaResponse( + name='name_value', + size=443, + ) + + # Wrap the value into a proper Response obj + response_value = mock.Mock() + response_value.status_code = 200 + + # Convert return value to protobuf type + return_value = resumable_upload.UploadMediaResponse.pb(return_value) + json_return_value = json_format.MessageToJson(return_value) + response_value.read = mock.AsyncMock(return_value=json_return_value.encode('UTF-8')) + req.return_value = response_value + req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} + response = await client.upload_media(request) + + # Establish that the response is the type that we expect. + assert isinstance(response, resumable_transfer.AsyncResumableUploadSession) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("null_interceptor", [True, False]) +async def test_upload_media_rest_asyncio_interceptors(null_interceptor): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + transport = transports.AsyncResumableUploadServiceRestTransport( + credentials=async_anonymous_credentials(), + interceptor=None if null_interceptor else transports.AsyncResumableUploadServiceRestInterceptor(), + ) + client = ResumableUploadServiceAsyncClient(transport=transport) + + with mock.patch.object(type(client.transport._session), "request") as req, \ + mock.patch.object(path_template, "transcode") as transcode, \ + mock.patch.object(transports.AsyncResumableUploadServiceRestInterceptor, "post_upload_media") as post, \ + mock.patch.object(transports.AsyncResumableUploadServiceRestInterceptor, "post_upload_media_with_metadata") as post_with_metadata, \ + mock.patch.object(transports.AsyncResumableUploadServiceRestInterceptor, "pre_upload_media") as pre: + pre.assert_not_called() + post.assert_not_called() + post_with_metadata.assert_not_called() + pb_message = resumable_upload.UploadMediaRequest.pb(resumable_upload.UploadMediaRequest()) + transcode.return_value = { + "method": "post", + "uri": "my_uri", + "body": pb_message, + "query_params": pb_message, + } + + req.return_value = mock.Mock() + req.return_value.status_code = 200 + req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} + return_value = resumable_upload.UploadMediaResponse.to_json(resumable_upload.UploadMediaResponse()) + req.return_value.read = mock.AsyncMock(return_value=return_value) + + request = resumable_upload.UploadMediaRequest() + metadata =[ + ("key", "val"), + ("cephalopod", "squid"), + ] + pre.return_value = request, metadata + post.return_value = resumable_upload.UploadMediaResponse() + post_with_metadata.return_value = resumable_upload.UploadMediaResponse(), metadata + + await client.upload_media(request, metadata=[("key", "val"), ("cephalopod", "squid"),]) + + pre.assert_called_once() + post.assert_called_once() + post_with_metadata.assert_called_once() + +def test_initialize_client_w_rest_asyncio(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + client = ResumableUploadServiceAsyncClient( + credentials=async_anonymous_credentials(), + transport="rest_asyncio" + ) + assert client is not None + + +# This test is a coverage failsafe to make sure that totally empty calls, +# i.e. request == None and no flattened fields passed, work. +@pytest.mark.asyncio +async def test_upload_media_empty_call_rest_asyncio(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + client = ResumableUploadServiceAsyncClient( + credentials=async_anonymous_credentials(), + transport="rest_asyncio", + ) + + # Mock the actual call, and fake the request. + with mock.patch.object( + type(client.transport.upload_media), + '__call__') as call: + await client.upload_media(request=None) + + # Establish that the underlying stub method was called. + call.assert_called() + _, args, _ = call.mock_calls[0] + request_msg = resumable_upload.UploadMediaRequest() + assert args[0] == request_msg + + +def test_unsupported_parameter_rest_asyncio(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + options = client_options.ClientOptions(quota_project_id="octopus") + with pytest.raises(core_exceptions.AsyncRestUnsupportedParameterError, match="google.api_core.client_options.ClientOptions.quota_project_id") as exc: # type: ignore + client = ResumableUploadServiceAsyncClient( + credentials=async_anonymous_credentials(), + transport="rest_asyncio", + client_options=options + ) + + def test_transport_grpc_default(): # A client should use the gRPC transport by default. client = ResumableUploadServiceClient( @@ -2103,6 +2273,61 @@ def test_resumable_upload_service_grpc_asyncio_transport_channel(): assert transport._ssl_channel_credentials == None +def test_resumable_upload_service_upload_media_grpc_channel_without_credentials_error(): + channel = grpc.secure_channel('http://localhost/', grpc.local_channel_credentials()) + transport = transports.ResumableUploadServiceGrpcTransport( + host="localhost:7469", + channel=channel, + ) + with pytest.raises(core_exceptions.GoogleAPICallError) as exc_info: + transport.upload_media(resumable_upload.UploadMediaRequest()) + assert "operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_resumable_upload_service_upload_media_grpc_asyncio_channel_without_credentials_error(): + channel = aio.secure_channel('http://localhost/', grpc.local_channel_credentials()) + transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( + host="localhost:7469", + channel=channel, + ) + with pytest.raises(core_exceptions.GoogleAPICallError) as exc_info: + await transport.upload_media(resumable_upload.UploadMediaRequest()) + assert "operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel" in str(exc_info.value) + +def test_resumable_upload_service_upload_media_rest_start_empty_body(): + client = ResumableUploadServiceClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + with mock.patch.object(type(client.transport._session), "request") as req: + response_value = mock.MagicMock() + response_value.status_code = 200 + response_value.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + response_value.content = b"" + req.return_value = response_value + response = client.upload_media(resumable_upload.UploadMediaRequest()) + assert isinstance(response, resumable_transfer.ResumableUploadSession) + + +@pytest.mark.asyncio +async def test_resumable_upload_service_upload_media_rest_asyncio_start_empty_body(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip() + client = ResumableUploadServiceAsyncClient( + credentials=async_anonymous_credentials(), + transport="rest_asyncio", + ) + with mock.patch.object(type(client.transport._session), "request", new_callable=mock.AsyncMock) as req: + response_value = mock.MagicMock() + response_value.status_code = 200 + response_value.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + response_value.read = mock.AsyncMock(return_value=b"") + req.return_value = response_value + response = await client.upload_media(resumable_upload.UploadMediaRequest()) + assert isinstance(response, resumable_transfer.AsyncResumableUploadSession) + + # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are # removed from grpc/grpc_asyncio transport constructor. @pytest.mark.filterwarnings("ignore::FutureWarning") @@ -3913,6 +4138,20 @@ def test_transport_close_rest(): close.assert_called_once() +@pytest.mark.asyncio +async def test_transport_close_rest_asyncio(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") + client = ResumableUploadServiceAsyncClient( + credentials=async_anonymous_credentials(), + transport="rest_asyncio" + ) + with mock.patch.object(type(getattr(client.transport, "_session")), "close") as close: + async with client: + close.assert_not_called() + close.assert_called_once() + + def test_client_ctx(): transports = [ 'rest', diff --git a/packages/gapic-generator/tests/unit/generator/test_generator.py b/packages/gapic-generator/tests/unit/generator/test_generator.py index 9d8545c4192f..96226a0f12e7 100644 --- a/packages/gapic-generator/tests/unit/generator/test_generator.py +++ b/packages/gapic-generator/tests/unit/generator/test_generator.py @@ -263,6 +263,63 @@ def test_get_response_ignores_unwanted_transports_and_clients(): } +def test_get_response_resumable_upload_generates_async_client_and_rest_asyncio(): + # Verify that async_client.py and transports/rest_asyncio.py are generated + # when a service has a resumable upload method, even if rest_async_io_enabled + # is False and grpc transport is not requested. + generator_obj = make_generator() + with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as list_templates: + list_templates.return_value = [ + "foo/%service/transports/rest.py.j2", + "foo/%service/transports/rest_asyncio.py.j2", + "foo/%service/transports/rest_base.py.j2", + "foo/%service/transports/__init__.py.j2", + "foo/%service/transports/base.py.j2", + "foo/%service/async_client.py.j2", + "foo/%service/client.py.j2", + ] + + with mock.patch.object(jinja2.Environment, "get_template") as get_template: + get_template.return_value = jinja2.Template("Service: {{ service.name }}") + api_schema = make_api( + make_proto( + descriptor_pb2.FileDescriptorProto( + name="resumable.proto", + package="foo.v1", + message_type=[ + descriptor_pb2.DescriptorProto(name="UploadMediaRequest"), + descriptor_pb2.DescriptorProto(name="UploadMediaResponse"), + ], + service=[ + descriptor_pb2.ServiceDescriptorProto( + name="ResumableUploadService", + method=[ + descriptor_pb2.MethodDescriptorProto( + name="UploadMedia", + input_type=".foo.v1.UploadMediaRequest", + output_type=".foo.v1.UploadMediaResponse", + ), + ], + ), + ], + ), + ) + ) + + cgr = generator_obj.get_response( + api_schema=api_schema, opts=Options.build("transport=rest") + ) + assert {i.name for i in cgr.file} == { + "foo/resumable_upload_service/transports/rest.py", + "foo/resumable_upload_service/transports/rest_asyncio.py", + "foo/resumable_upload_service/transports/rest_base.py", + "foo/resumable_upload_service/transports/__init__.py", + "foo/resumable_upload_service/transports/base.py", + "foo/resumable_upload_service/async_client.py", + "foo/resumable_upload_service/client.py", + } + + def test_get_response_enumerates_services(): generator_obj = make_generator() with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as list_templates: diff --git a/packages/gapic-generator/tests/unit/schema/wrappers/test_method.py b/packages/gapic-generator/tests/unit/schema/wrappers/test_method.py index 63a5adc0fcb4..d905c5696d4d 100644 --- a/packages/gapic-generator/tests/unit/schema/wrappers/test_method.py +++ b/packages/gapic-generator/tests/unit/schema/wrappers/test_method.py @@ -1124,9 +1124,24 @@ def test_method_is_resumable_upload(): # UploadMedia (and CreateYouTubeVideoUpload) are temporarily hardcoded as # resumable upload method names until resumable upload proto annotations # are adopted in the future. - # Verify that UploadMedia is identified as a resumable upload method. + # Verify that UploadMedia is identified as a resumable upload method and + # configures ResumableUploadSession / AsyncResumableUploadSession as the + # client output while retaining the raw output message in ref_types. method_upload = make_method("UploadMedia") assert method_upload.is_resumable_upload + assert method_upload.client_output.ident.name == "ResumableUploadSession" + assert ( + str(method_upload.client_output.ident) + == "resumable_transfer.ResumableUploadSession" + ) + assert ( + method_upload.client_output_async.ident.name == "AsyncResumableUploadSession" + ) + assert ( + str(method_upload.client_output_async.ident) + == "resumable_transfer.AsyncResumableUploadSession" + ) + assert method_upload.output in method_upload.ref_types # Verify that CreateYouTubeVideoUpload is also recognized as a resumable upload method. method_youtube_upload = make_method("CreateYouTubeVideoUpload") From 1977ce003e4bdea60a4bdf44358fd0a4ed48b3e5 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 29 Sep 2026 18:14:46 +0000 Subject: [PATCH 02/45] address review feedback --- .../%service/transports/grpc_asyncio.py.j2 | 23 ++++++++++++++----- .../transports/grpc_asyncio.py | 23 ++++++++++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 851827e762be..1f0f6bbf2e0d 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -376,14 +376,25 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): return _error() self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub() elif HAS_ASYNC_REST: # pragma: NO COVER + # Lazily instantiate and cache the AsyncRestTransport stub on first + # call rather than during __init__ (_prep_wrapped_messages), since + # GrpcAsyncIOTransport may be initialized with synchronous + # google.auth.credentials.Credentials that AsyncAuthorizedSession + # rejects, and gRPC async stubs expose a synchronous __call__ + # returning an awaitable. class _AsyncRestStub: # pragma: NO COVER + def __init__(_self): # pragma: NO COVER + _self._rest_stub = None + def __call__(_self, *args, **kwargs): # pragma: NO COVER - rest_transport = Async{{ service.name }}RestTransport( - host=self._host, - credentials=self._credentials, - client_info=self._client_info, - ) - return rest_transport.{{ method.transport_safe_name|snake_case }}(*args, **kwargs) + if _self._rest_stub is None: + rest_transport = Async{{ service.name }}RestTransport( + host=self._host, + credentials=self._credentials, + client_info=self._client_info, + ) + _self._rest_stub = rest_transport.{{ method.transport_safe_name|snake_case }} + return _self._rest_stub(*args, **kwargs) self._stubs['{{ method.transport_safe_name|snake_case }}'] = _AsyncRestStub() else: # pragma: NO COVER class _UnsupportedStub: # pragma: NO COVER diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index c2c554366192..8f1c60f022fc 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -358,14 +358,25 @@ async def _error(): return _error() self._stubs['upload_media'] = _ErrorStub() elif HAS_ASYNC_REST: # pragma: NO COVER + # Lazily instantiate and cache the AsyncRestTransport stub on first + # call rather than during __init__ (_prep_wrapped_messages), since + # GrpcAsyncIOTransport may be initialized with synchronous + # google.auth.credentials.Credentials that AsyncAuthorizedSession + # rejects, and gRPC async unit tests expect a non-coroutine __call__ + # returning an awaitable. class _AsyncRestStub: # pragma: NO COVER + def __init__(_self): # pragma: NO COVER + _self._rest_stub = None + def __call__(_self, *args, **kwargs): # pragma: NO COVER - rest_transport = AsyncResumableUploadServiceRestTransport( - host=self._host, - credentials=self._credentials, - client_info=self._client_info, - ) - return rest_transport.upload_media(*args, **kwargs) + if _self._rest_stub is None: + rest_transport = AsyncResumableUploadServiceRestTransport( + host=self._host, + credentials=self._credentials, + client_info=self._client_info, + ) + _self._rest_stub = rest_transport.upload_media + return _self._rest_stub(*args, **kwargs) self._stubs['upload_media'] = _AsyncRestStub() else: # pragma: NO COVER class _UnsupportedStub: # pragma: NO COVER From db71d9cb7cb9edffff6fd594b1dce166cb10f682 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 29 Sep 2026 18:37:56 +0000 Subject: [PATCH 03/45] update goldens --- .../resumable_upload_service/transports/grpc_asyncio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 8f1c60f022fc..e343bc4dbce7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -362,7 +362,7 @@ async def _error(): # call rather than during __init__ (_prep_wrapped_messages), since # GrpcAsyncIOTransport may be initialized with synchronous # google.auth.credentials.Credentials that AsyncAuthorizedSession - # rejects, and gRPC async unit tests expect a non-coroutine __call__ + # rejects, and gRPC async stubs expose a synchronous __call__ # returning an awaitable. class _AsyncRestStub: # pragma: NO COVER def __init__(_self): # pragma: NO COVER From a9aefa4d1c569a7fb72e6e015785925804a6e7e6 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 30 Sep 2026 00:06:29 +0000 Subject: [PATCH 04/45] address review feedback --- .../%sub/services/%service/_client_macros.j2 | 10 +- .../%sub/services/%service/async_client.py.j2 | 10 +- .../services/%service/transports/rest.py.j2 | 71 ++++++++-- .../%service/transports/rest_asyncio.py.j2 | 67 ++++++++-- .../cloud_redis/transports/rest_asyncio.py | 22 ++-- .../cloud_redis/transports/rest_asyncio.py | 10 +- .../resumable_upload_service/async_client.py | 5 - .../resumable_upload_service/client.py | 5 - .../transports/rest.py | 120 +++++------------ .../transports/rest_asyncio.py | 124 +++++------------- 10 files changed, 202 insertions(+), 242 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 index 53726ab0a115..57d452a29a95 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 @@ -168,6 +168,9 @@ retry=retry, timeout=timeout, metadata=metadata, + {% if method.is_resumable_upload %} + config=config, + {% endif %} ) {% if method.lro %} @@ -190,13 +193,6 @@ timeout=timeout, metadata=metadata, ) - {% elif method.is_resumable_upload %} - - # Wrap the response in a resumable upload session. - response = {{ method.client_output.ident }}( - config=config, - response_type={{ method.output.ident }}, - ) {% elif method.extended_lro and full_extended_lro %} {{ extended_operation_service_setup(api, method) }} {% endif %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 index 3c880bdefc05..548c5019c971 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 @@ -424,6 +424,9 @@ class {{ service.async_client_name }}: retry=retry, timeout=timeout, metadata=metadata, + {% if method.is_resumable_upload %} + config=config, + {% endif %} ) {% if method.lro %} @@ -446,13 +449,6 @@ class {{ service.async_client_name }}: timeout=timeout, metadata=metadata, ) - {% elif method.is_resumable_upload %} - - # Wrap the response in a resumable upload session. - response = {{ method.client_output_async.ident }}( - config=config, - response_type={{ method.output.ident }}, - ) {% endif %} {% if not method.void %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index c07ca9b89bdb..80765cbca5f1 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -13,6 +13,9 @@ from google.api_core import retry as retries from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 +{% if service.has_resumable_upload_methods %} +from google.api_core import resumable_transfer +{% endif %} {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} from {{package_path}}._compat import transcode_request import google.protobuf @@ -213,18 +216,21 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): class {{ method.name|make_private }}(_Base{{ service.name }}RestTransport._Base{{method.name}}, {{service.name}}RestStub): def __hash__(self): return hash("{{service.name}}RestTransport.{{method.name}}") - {% if method.http_options and not method.client_streaming %} + {% if method.http_options and not method.client_streaming and not method.is_resumable_upload %} {% set body_spec = method.http_options[0].body %} {{ shared_macros.response_method(body_spec, is_async=False, is_streaming_method=method.server_streaming)|indent(8) }} - {% endif %}{# method.http_options and not method.client_streaming #} + {% endif %}{# method.http_options and not method.client_streaming and not method.is_resumable_upload #} def __call__(self, request: {{method.input.ident}}, *, + {% if method.is_resumable_upload %} + config: Optional[resumable_transfer.ResumableUploadConfig]=None, + {% endif %} retry: OptionalRetry=gapic_v1.method.DEFAULT, timeout: Optional[float]=None, {{ shared_macros.client_method_metadata_argument()|indent(8) }}={{ shared_macros.client_method_metadata_default_value() }}, - ){% if not method.void %} -> {% if not method.server_streaming %}{{method.output.ident}}{% else %}rest_streaming.ResponseIterator{% endif %}{% endif %}: + ){% if not method.void %} -> {% if method.is_resumable_upload %}{{method.client_output.ident}}{% elif not method.server_streaming %}{{method.output.ident}}{% else %}rest_streaming.ResponseIterator{% endif %}{% endif %}: {% if method.http_options and not method.client_streaming %} r"""Call the {{- ' ' -}} {{ (method.name|snake_case).replace('_',' ')|wrap( @@ -235,6 +241,10 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): request (~.{{ method.input.ident }}): The request object.{{ ' ' }} {{- method.input.meta.doc|rst(width=72, indent=16, nl=False) }} + {% if method.is_resumable_upload %} + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. + {% endif %} retry (google.api_core.retry.Retry): Designation of what errors, if any, should be retried. timeout (float): The timeout for this request. @@ -242,10 +252,52 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {% if not method.void %} Returns: + {% if method.is_resumable_upload %} + ~.{{ method.client_output.ident }}: + {{ method.client_output.meta.doc|rst(width=72, indent=16) }} + {% else %} ~.{{ method.output.ident }}: {{ method.output.meta.doc|rst(width=72, indent=16) }} + {% endif %} {% endif %} """ + {% if method.is_resumable_upload %} + http_options = _Base{{ service.name }}RestTransport._Base{{ method.name }}._get_http_options() + request, metadata = self._interceptor.pre_{{ method.name|snake_case }}(request, metadata) + transcoded_request, body, query_params = transcode_request( + http_options, + request, + required_fields_default_values=getattr( + _Base{{ service.name }}RestTransport._Base{{ method.name }}, + "_Base{{ method.name }}__REQUIRED_FIELDS_DEFAULT_VALUES", + None, + ), + rest_numeric_enums={{ opts.rest_numeric_enums }}, + ) + + uri = transcoded_request["uri"] + upload_url = f"{self._host}{uri}" + headers = dict(metadata) + if config is None: + config = resumable_transfer.ResumableUploadConfig(headers=headers) + else: + if config.headers: + headers.update(dict(config.headers)) + config = dataclasses.replace(config, headers=headers) + + return {{ method.client_output.ident }}( + upload_url=upload_url, + config=config, + transport=self._session, + response_type={{ method.output.ident }}, + start_retry=retry if isinstance(retry, retries.Retry) else None, + **( + {"start_timeout": timeout} + if isinstance(timeout, (int, float)) + else {} + ), + ) + {% else %} {{ shared_macros.rest_call_method_common(body_spec, method.name, service, False, method.input.ident.is_proto_plus_type, opts.rest_numeric_enums)|indent(8) }} @@ -264,13 +316,7 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): pb_resp = resp {% endif %} - {% if method.is_resumable_upload %} - # Resumable upload start responses return session metadata in headers with an empty body. - if response.content and response.content.strip(): - json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - {% else %} json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - {% endif %} {% endif %}{# method.lro #} {#- TODO(https://github.com/googleapis/gapic-generator-python/issues/2274): Add debug log before intercepting a request #} resp = self._interceptor.post_{{ method.name|snake_case }}(resp) @@ -310,6 +356,7 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): return resp {% endif %}{# method.void #} + {% endif %}{# method.is_resumable_upload #} {% else %}{# method.http_options and not method.client_streaming #} raise NotImplementedError( "Method {{ method.name }} is not available over REST transport" @@ -321,7 +368,11 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): @property def {{method.transport_safe_name|snake_case}}(self) -> Callable[ [{{method.input.ident}}], - {{method.output.ident}}]: + {% if method.is_resumable_upload %} + {{ method.client_output.ident }}]: + {% else %} + {{ method.output.ident }}]: + {% endif %} # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast return self._{{method.name}}(self._session, self._host, self._interceptor) # type: ignore diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index 0d169caabb75..d20e790bd7a8 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -21,6 +21,9 @@ from google.auth.aio import credentials as ga_credentials_async # type: ignore from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +{% if service.has_resumable_upload_methods %} +from google.api_core import resumable_transfer +{% endif %} {% if service.has_lro %} from google.api_core import operations_v1 {% endif %}{# service.has_lro #} @@ -171,18 +174,21 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): return hash("Async{{service.name}}RestTransport.{{method.name}}") {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2169): Implement client streaming method. #} - {% if method.http_options and not method.client_streaming %} + {% if method.http_options and not method.client_streaming and not method.is_resumable_upload %} {% set body_spec = method.http_options[0].body %} {{ shared_macros.response_method(body_spec, is_async=True, is_streaming_method=None)|indent(8) }} - {% endif %}{# method.http_options and not method.client_streaming and not method.paged_result_field #} + {% endif %}{# method.http_options and not method.client_streaming and not method.is_resumable_upload #} async def __call__(self, request: {{method.input.ident}}, *, + {% if method.is_resumable_upload %} + config: Optional[resumable_transfer.ResumableUploadConfig]=None, + {% endif %} retry: OptionalRetry=gapic_v1.method.DEFAULT, timeout: Optional[float]=None, {{ shared_macros.client_method_metadata_argument()|indent(12) }}={{ shared_macros.client_method_metadata_default_value() }}, {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2169): Update return type for client streaming method. #} - ){% if not method.void %} -> {% if not method.server_streaming %}{{method.output.ident}}{% else %}rest_streaming_async.AsyncResponseIterator{% endif %}{% endif %}: + ){% if not method.void %} -> {% if method.is_resumable_upload %}{{method.client_output_async.ident}}{% elif not method.server_streaming %}{{method.output.ident}}{% else %}rest_streaming_async.AsyncResponseIterator{% endif %}{% endif %}: {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2169): Implement client streaming method. #} {% if method.http_options and not method.client_streaming %} r"""Call the {{- ' ' -}} @@ -194,6 +200,10 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): request (~.{{ method.input.ident }}): The request object.{{ ' ' }} {{- method.input.meta.doc|rst(width=72, indent=16, nl=False) }} + {% if method.is_resumable_upload %} + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. + {% endif %} retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, should be retried. timeout (float): The timeout for this request. @@ -201,10 +211,52 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {% if not method.void %} Returns: + {% if method.is_resumable_upload %} + ~.{{ method.client_output_async.ident }}: + {{ method.client_output_async.meta.doc|rst(width=72, indent=16) }} + {% else %} ~.{{ method.output.ident }}: {{ method.output.meta.doc|rst(width=72, indent=16) }} + {% endif %} {% endif %} """ + {% if method.is_resumable_upload %} + http_options = _Base{{ service.name }}RestTransport._Base{{ method.name }}._get_http_options() + request, metadata = await self._interceptor.pre_{{ method.name|snake_case }}(request, metadata) + transcoded_request, body, query_params = transcode_request( + http_options, + request, + required_fields_default_values=getattr( + _Base{{ service.name }}RestTransport._Base{{ method.name }}, + "_Base{{ method.name }}__REQUIRED_FIELDS_DEFAULT_VALUES", + None, + ), + rest_numeric_enums={{ opts.rest_numeric_enums }}, + ) + + uri = transcoded_request["uri"] + upload_url = f"{self._host}{uri}" + headers = dict(metadata) + if config is None: + config = resumable_transfer.ResumableUploadConfig(headers=headers) + else: + if config.headers: + headers.update(dict(config.headers)) + config = dataclasses.replace(config, headers=headers) + + return {{ method.client_output_async.ident }}( + upload_url=upload_url, + config=config, + transport=self._session, + response_type={{ method.output.ident }}, + start_retry=retry if isinstance(retry, retries.AsyncRetry) else None, + **( + {"start_timeout": timeout} + if isinstance(timeout, (int, float)) + else {} + ), + ) + {% else %} {{ shared_macros.rest_call_method_common(body_spec, method.name, service, True, method.input.ident.is_proto_plus_type, opts.rest_numeric_enums)|indent(8) }} @@ -221,13 +273,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): pb_resp = resp {% endif %}{# if method.output.ident.is_proto_plus_type #} content = await response.read() - {% if method.is_resumable_upload %} - # Resumable upload start responses return session metadata in headers with an empty body. - if content and content.strip(): - json_format.Parse(content, pb_resp, ignore_unknown_fields=True) - {% else %} json_format.Parse(content, pb_resp, ignore_unknown_fields=True) - {% endif %} {% endif %}{# if method.server_streaming #} resp = await self._interceptor.post_{{ method.name|snake_case }}(resp) response_metadata = [(k, str(v)) for k, v in response.headers.items()] @@ -266,6 +312,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): return resp {% endif %}{# method.void #} + {% endif %}{# method.is_resumable_upload #} {% else %} raise NotImplementedError( @@ -327,7 +374,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): @property def {{method.transport_safe_name|snake_case}}(self) -> Callable[ [{{method.input.ident}}], - {{method.output.ident}}]: + {% if method.is_resumable_upload %}{{method.client_output_async.ident}}{% else %}{{method.output.ident}}{% endif %}]: return self._{{method.name}}(self._session, self._host, self._interceptor) # type: ignore {% endfor %} diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py index 4d629a5a8443..548cc3fd858c 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py @@ -2426,67 +2426,67 @@ def operations_client(self) -> AsyncOperationsRestClient: @property def create_instance(self) -> Callable[ [cloud_redis.CreateInstanceRequest], - operations_pb2.Operation]: +operations_pb2.Operation]: return self._CreateInstance(self._session, self._host, self._interceptor) # type: ignore @property def delete_instance(self) -> Callable[ [cloud_redis.DeleteInstanceRequest], - operations_pb2.Operation]: +operations_pb2.Operation]: return self._DeleteInstance(self._session, self._host, self._interceptor) # type: ignore @property def export_instance(self) -> Callable[ [cloud_redis.ExportInstanceRequest], - operations_pb2.Operation]: +operations_pb2.Operation]: return self._ExportInstance(self._session, self._host, self._interceptor) # type: ignore @property def failover_instance(self) -> Callable[ [cloud_redis.FailoverInstanceRequest], - operations_pb2.Operation]: +operations_pb2.Operation]: return self._FailoverInstance(self._session, self._host, self._interceptor) # type: ignore @property def get_instance(self) -> Callable[ [cloud_redis.GetInstanceRequest], - cloud_redis.Instance]: +cloud_redis.Instance]: return self._GetInstance(self._session, self._host, self._interceptor) # type: ignore @property def get_instance_auth_string(self) -> Callable[ [cloud_redis.GetInstanceAuthStringRequest], - cloud_redis.InstanceAuthString]: +cloud_redis.InstanceAuthString]: return self._GetInstanceAuthString(self._session, self._host, self._interceptor) # type: ignore @property def import_instance(self) -> Callable[ [cloud_redis.ImportInstanceRequest], - operations_pb2.Operation]: +operations_pb2.Operation]: return self._ImportInstance(self._session, self._host, self._interceptor) # type: ignore @property def list_instances(self) -> Callable[ [cloud_redis.ListInstancesRequest], - cloud_redis.ListInstancesResponse]: +cloud_redis.ListInstancesResponse]: return self._ListInstances(self._session, self._host, self._interceptor) # type: ignore @property def reschedule_maintenance(self) -> Callable[ [cloud_redis.RescheduleMaintenanceRequest], - operations_pb2.Operation]: +operations_pb2.Operation]: return self._RescheduleMaintenance(self._session, self._host, self._interceptor) # type: ignore @property def update_instance(self) -> Callable[ [cloud_redis.UpdateInstanceRequest], - operations_pb2.Operation]: +operations_pb2.Operation]: return self._UpdateInstance(self._session, self._host, self._interceptor) # type: ignore @property def upgrade_instance(self) -> Callable[ [cloud_redis.UpgradeInstanceRequest], - operations_pb2.Operation]: +operations_pb2.Operation]: return self._UpgradeInstance(self._session, self._host, self._interceptor) # type: ignore @property diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py index 960d9639a214..7ae3f548fc7d 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py @@ -1338,31 +1338,31 @@ def operations_client(self) -> AsyncOperationsRestClient: @property def create_instance(self) -> Callable[ [cloud_redis.CreateInstanceRequest], - operations_pb2.Operation]: +operations_pb2.Operation]: return self._CreateInstance(self._session, self._host, self._interceptor) # type: ignore @property def delete_instance(self) -> Callable[ [cloud_redis.DeleteInstanceRequest], - operations_pb2.Operation]: +operations_pb2.Operation]: return self._DeleteInstance(self._session, self._host, self._interceptor) # type: ignore @property def get_instance(self) -> Callable[ [cloud_redis.GetInstanceRequest], - cloud_redis.Instance]: +cloud_redis.Instance]: return self._GetInstance(self._session, self._host, self._interceptor) # type: ignore @property def list_instances(self) -> Callable[ [cloud_redis.ListInstancesRequest], - cloud_redis.ListInstancesResponse]: +cloud_redis.ListInstancesResponse]: return self._ListInstances(self._session, self._host, self._interceptor) # type: ignore @property def update_instance(self) -> Callable[ [cloud_redis.UpdateInstanceRequest], - operations_pb2.Operation]: +operations_pb2.Operation]: return self._UpdateInstance(self._session, self._host, self._interceptor) # type: ignore @property diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py index 7f662558cc3d..7bd2e63a5ec1 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py @@ -343,12 +343,7 @@ async def sample_upload_media(): retry=retry, timeout=timeout, metadata=metadata, - ) - - # Wrap the response in a resumable upload session. - response = resumable_transfer.AsyncResumableUploadSession( config=config, - response_type=resumable_upload.UploadMediaResponse, ) # Done; return the response. diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py index 812c86e07c84..b68c78d76a56 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py @@ -610,12 +610,7 @@ def sample_upload_media(): retry=retry, timeout=timeout, metadata=metadata, - ) - - # Wrap the response in a resumable upload session. - response = resumable_transfer.ResumableUploadSession( config=config, - response_type=resumable_upload.UploadMediaResponse, ) # Done; return the response. diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py index 8d0b8802319a..7e48b9ccc62b 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py @@ -23,6 +23,7 @@ from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 +from google.api_core import resumable_transfer from google.showcase_v1beta1._compat import transcode_request import google.protobuf @@ -416,40 +417,20 @@ class _UploadMedia(_BaseResumableUploadServiceRestTransport._BaseUploadMedia, Re def __hash__(self): return hash("ResumableUploadServiceRestTransport.UploadMedia") - @staticmethod - def _get_response( - host, - metadata, - query_params, - session, - timeout, - transcoded_request, - body=None): - - uri = transcoded_request['uri'] - method = transcoded_request['method'] - headers = dict(metadata) - headers['Content-Type'] = 'application/json' - response = getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, - headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response - def __call__(self, request: resumable_upload.UploadMediaRequest, *, + config: Optional[resumable_transfer.ResumableUploadConfig]=None, retry: OptionalRetry=gapic_v1.method.DEFAULT, timeout: Optional[float]=None, metadata: Sequence[Tuple[str, Union[str, bytes]]]=(), - ) -> resumable_upload.UploadMediaResponse: + ) -> resumable_transfer.ResumableUploadSession: r"""Call the upload media method over HTTP. Args: request (~.resumable_upload.UploadMediaRequest): The request object. + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. retry (google.api_core.retry.Retry): Designation of what errors, if any, should be retried. timeout (float): The timeout for this request. @@ -459,10 +440,11 @@ def __call__(self, be of type `bytes`. Returns: - ~.resumable_upload.UploadMediaResponse: + ~.resumable_transfer.ResumableUploadSession: + An object representing a resumable + upload session. """ - http_options = _BaseResumableUploadServiceRestTransport._BaseUploadMedia._get_http_options() request, metadata = self._interceptor.pre_upload_media(request, metadata) transcoded_request, body, query_params = transcode_request( @@ -476,73 +458,33 @@ def __call__(self, rest_numeric_enums=True, ) - if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(logging.DEBUG): # pragma: NO COVER - request_url = "{host}{uri}".format(host=self._host, uri=transcoded_request['uri']) - method = transcoded_request['method'] - try: - request_payload = type(request).to_json(request) - except: - request_payload = None - http_request = { - "payload": request_payload, - "requestMethod": method, - "requestUrl": request_url, - "headers": dict(metadata), - } - _LOGGER.debug( - f"Sending request for google.showcase_v1beta1.ResumableUploadServiceClient.UploadMedia", - extra = { - "serviceName": "google.showcase.v1beta1.ResumableUploadService", - "rpcName": "UploadMedia", - "httpRequest": http_request, - "metadata": http_request["headers"], - }, - ) - - # Send the request - response = ResumableUploadServiceRestTransport._UploadMedia._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) - - # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception - # subclass. - if response.status_code >= 400: - raise core_exceptions.from_http_response(response) - - # Return the response - resp = resumable_upload.UploadMediaResponse() - pb_resp = resumable_upload.UploadMediaResponse.pb(resp) - - # Resumable upload start responses return session metadata in headers with an empty body. - if response.content and response.content.strip(): - json_format.Parse(response.content, pb_resp, ignore_unknown_fields=True) - - resp = self._interceptor.post_upload_media(resp) - response_metadata = [(k, str(v)) for k, v in response.headers.items()] - resp, _ = self._interceptor.post_upload_media_with_metadata(resp, response_metadata) - if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(logging.DEBUG): # pragma: NO COVER - try: - response_payload = resumable_upload.UploadMediaResponse.to_json(response) - except: - response_payload = None - http_response = { - "payload": response_payload, - "headers": dict(response.headers), - "status": response.status_code, - } - _LOGGER.debug( - "Received response for google.showcase_v1beta1.ResumableUploadServiceClient.upload_media", - extra = { - "serviceName": "google.showcase.v1beta1.ResumableUploadService", - "rpcName": "UploadMedia", - "metadata": http_response["headers"], - "httpResponse": http_response, - }, - ) - return resp + uri = transcoded_request["uri"] + upload_url = f"{self._host}{uri}" + headers = dict(metadata) + if config is None: + config = resumable_transfer.ResumableUploadConfig(headers=headers) + else: + if config.headers: + headers.update(dict(config.headers)) + config = dataclasses.replace(config, headers=headers) + + return resumable_transfer.ResumableUploadSession( + upload_url=upload_url, + config=config, + transport=self._session, + response_type=resumable_upload.UploadMediaResponse, + start_retry=retry if isinstance(retry, retries.Retry) else None, + **( + {"start_timeout": timeout} + if isinstance(timeout, (int, float)) + else {} + ), + ) @property def upload_media(self) -> Callable[ [resumable_upload.UploadMediaRequest], - resumable_upload.UploadMediaResponse]: + resumable_transfer.ResumableUploadSession]: # The return type is fine, but mypy isn't sophisticated enough to determine what's going on here. # In C++ this would require a dynamic_cast return self._UploadMedia(self._session, self._host, self._interceptor) # type: ignore diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py index 8be728bf9375..29f2d1372467 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -27,6 +27,7 @@ from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +from google.api_core import resumable_transfer from google.iam.v1 import iam_policy_pb2 # type: ignore from google.iam.v1 import policy_pb2 # type: ignore from google.cloud.location import locations_pb2 # type: ignore @@ -229,40 +230,20 @@ class _UploadMedia(_BaseResumableUploadServiceRestTransport._BaseUploadMedia, As def __hash__(self): return hash("AsyncResumableUploadServiceRestTransport.UploadMedia") - @staticmethod - async def _get_response( - host, - metadata, - query_params, - session, - timeout, - transcoded_request, - body=None): - - uri = transcoded_request['uri'] - method = transcoded_request['method'] - headers = dict(metadata) - headers['Content-Type'] = 'application/json' - response = await getattr(session, method)( - "{host}{uri}".format(host=host, uri=uri), - timeout=timeout, - headers=headers, - params=rest_helpers.flatten_query_params(query_params, strict=True), - data=body, - ) - return response - async def __call__(self, request: resumable_upload.UploadMediaRequest, *, + config: Optional[resumable_transfer.ResumableUploadConfig]=None, retry: OptionalRetry=gapic_v1.method.DEFAULT, timeout: Optional[float]=None, metadata: Sequence[Tuple[str, Union[str, bytes]]]=(), - ) -> resumable_upload.UploadMediaResponse: + ) -> resumable_transfer.AsyncResumableUploadSession: r"""Call the upload media method over HTTP. Args: request (~.resumable_upload.UploadMediaRequest): The request object. + config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): + Optional configuration for the resumable upload session. retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, should be retried. timeout (float): The timeout for this request. @@ -272,10 +253,11 @@ async def __call__(self, be of type `bytes`. Returns: - ~.resumable_upload.UploadMediaResponse: + ~.resumable_transfer.AsyncResumableUploadSession: + An object representing a resumable + upload session. """ - http_options = _BaseResumableUploadServiceRestTransport._BaseUploadMedia._get_http_options() request, metadata = await self._interceptor.pre_upload_media(request, metadata) transcoded_request, body, query_params = transcode_request( @@ -289,77 +271,33 @@ async def __call__(self, rest_numeric_enums=False, ) - if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(logging.DEBUG): # pragma: NO COVER - request_url = "{host}{uri}".format(host=self._host, uri=transcoded_request['uri']) - method = transcoded_request['method'] - try: - request_payload = type(request).to_json(request) - except: - request_payload = None - http_request = { - "payload": request_payload, - "requestMethod": method, - "requestUrl": request_url, - "headers": dict(metadata), - } - _LOGGER.debug( - f"Sending request for google.showcase_v1beta1.ResumableUploadServiceClient.UploadMedia", - extra = { - "serviceName": "google.showcase.v1beta1.ResumableUploadService", - "rpcName": "UploadMedia", - "httpRequest": http_request, - "metadata": http_request["headers"], - }, - ) - - # Send the request - response = await AsyncResumableUploadServiceRestTransport._UploadMedia._get_response(self._host, metadata, query_params, self._session, timeout, transcoded_request, body) - - # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception - # subclass. - if response.status_code >= 400: - content = await response.read() - payload = json.loads(content.decode('utf-8')) - request_url = "{host}{uri}".format(host=self._host, uri=transcoded_request['uri']) - method = transcoded_request['method'] - raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore - - # Return the response - resp = resumable_upload.UploadMediaResponse() - pb_resp = resumable_upload.UploadMediaResponse.pb(resp) - content = await response.read() - # Resumable upload start responses return session metadata in headers with an empty body. - if content and content.strip(): - json_format.Parse(content, pb_resp, ignore_unknown_fields=True) - resp = await self._interceptor.post_upload_media(resp) - response_metadata = [(k, str(v)) for k, v in response.headers.items()] - resp, _ = await self._interceptor.post_upload_media_with_metadata(resp, response_metadata) - if CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(logging.DEBUG): # pragma: NO COVER - try: - response_payload = resumable_upload.UploadMediaResponse.to_json(response) - except: - response_payload = None - http_response = { - "payload": response_payload, - "headers": dict(response.headers), - "status": "OK", # need to obtain this properly - } - _LOGGER.debug( - "Received response for google.showcase_v1beta1.ResumableUploadServiceAsyncClient.upload_media", - extra = { - "serviceName": "google.showcase.v1beta1.ResumableUploadService", - "rpcName": "UploadMedia", - "metadata": http_response["headers"], - "httpResponse": http_response, - }, - ) - - return resp + uri = transcoded_request["uri"] + upload_url = f"{self._host}{uri}" + headers = dict(metadata) + if config is None: + config = resumable_transfer.ResumableUploadConfig(headers=headers) + else: + if config.headers: + headers.update(dict(config.headers)) + config = dataclasses.replace(config, headers=headers) + + return resumable_transfer.AsyncResumableUploadSession( + upload_url=upload_url, + config=config, + transport=self._session, + response_type=resumable_upload.UploadMediaResponse, + start_retry=retry if isinstance(retry, retries.AsyncRetry) else None, + **( + {"start_timeout": timeout} + if isinstance(timeout, (int, float)) + else {} + ), + ) @property def upload_media(self) -> Callable[ [resumable_upload.UploadMediaRequest], - resumable_upload.UploadMediaResponse]: +resumable_transfer.AsyncResumableUploadSession]: return self._UploadMedia(self._session, self._host, self._interceptor) # type: ignore @property From 086f99ae0163cf92eb8b75d042bccd19c63d7527 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 30 Sep 2026 15:00:39 +0000 Subject: [PATCH 05/45] address review feedback --- .../%sub/services/%service/_shared_macros.j2 | 4 +- .../%service/transports/grpc_asyncio.py.j2 | 44 ++--- .../services/%service/transports/rest.py.j2 | 4 +- .../%service/transports/rest_asyncio.py.j2 | 4 +- .../%name_%version/%sub/test_%service.py.j2 | 128 ++++++++++++-- .../gapic/%name_%version/%sub/test_macros.j2 | 37 +++- .../transports/grpc_asyncio.py | 44 ++--- .../transports/rest.py | 36 +--- .../transports/rest_asyncio.py | 36 +--- .../test_resumable_upload_service.py | 162 +++++++++++++----- 10 files changed, 313 insertions(+), 186 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 index aee755de31b5..685e8505d496 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 @@ -402,7 +402,7 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor: logging.log(f"Received request: {request}") return request, metadata - {% if not method.void %} + {% if not method.void and not method.is_resumable_upload %} {{ async_prefix }}def post_{{ method.name|snake_case }}(self, response): logging.log(f"Received response: {response}") return response @@ -425,7 +425,7 @@ class {{ async_method_name_prefix }}{{ service.name }}RestInterceptor: """ return request, metadata - {% if not method.void %} + {% if not method.void and not method.is_resumable_upload %} {% if not method.server_streaming %} {{ async_prefix }}def post_{{ method.name|snake_case }}(self, response: {{method.output.ident}}) -> {{method.output.ident}}: {% else %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 1f0f6bbf2e0d..d1422e834f61 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -375,30 +375,32 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): ) return _error() self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub() - elif HAS_ASYNC_REST: # pragma: NO COVER - # Lazily instantiate and cache the AsyncRestTransport stub on first - # call rather than during __init__ (_prep_wrapped_messages), since - # GrpcAsyncIOTransport may be initialized with synchronous - # google.auth.credentials.Credentials that AsyncAuthorizedSession - # rejects, and gRPC async stubs expose a synchronous __call__ - # returning an awaitable. - class _AsyncRestStub: # pragma: NO COVER - def __init__(_self): # pragma: NO COVER - _self._rest_stub = None - - def __call__(_self, *args, **kwargs): # pragma: NO COVER - if _self._rest_stub is None: + elif HAS_ASYNC_REST: + # Defer instantiating Async{{ service.name }}RestTransport until the + # resumable upload RPC is called so that initializing + # {{ service.grpc_asyncio_transport_name }} with synchronous + # google.auth.credentials.Credentials (e.g. via ADC or service + # account helpers) for standard gRPC calls does not fail in + # __init__ (_prep_wrapped_messages) when AsyncAuthorizedSession + # validates for async credentials. + transport = self + class _AsyncRestStub: + def __init__(self): + self._rest_stub = None + + def __call__(self, *args, **kwargs): + if self._rest_stub is None: rest_transport = Async{{ service.name }}RestTransport( - host=self._host, - credentials=self._credentials, - client_info=self._client_info, + host=transport._host, + credentials=transport._credentials, + client_info=transport._client_info, ) - _self._rest_stub = rest_transport.{{ method.transport_safe_name|snake_case }} - return _self._rest_stub(*args, **kwargs) + self._rest_stub = rest_transport.{{ method.transport_safe_name|snake_case }} + return self._rest_stub(*args, **kwargs) self._stubs['{{ method.transport_safe_name|snake_case }}'] = _AsyncRestStub() - else: # pragma: NO COVER - class _UnsupportedStub: # pragma: NO COVER - def __call__(self, *args, **kwargs): # pragma: NO COVER + else: + class _UnsupportedStub: + def __call__(self, *args, **kwargs): async def _unsupported(): raise NotImplementedError("Async REST transport is required for async resumable upload methods.") return _unsupported() diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index 80765cbca5f1..ca19955d796c 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -277,12 +277,10 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): uri = transcoded_request["uri"] upload_url = f"{self._host}{uri}" - headers = dict(metadata) + headers = {**dict(metadata), **dict((config.headers or {}) if config else {})} if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) else: - if config.headers: - headers.update(dict(config.headers)) config = dataclasses.replace(config, headers=headers) return {{ method.client_output.ident }}( diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index d20e790bd7a8..c2ea111a2769 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -236,12 +236,10 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): uri = transcoded_request["uri"] upload_url = f"{self._host}{uri}" - headers = dict(metadata) + headers = {**dict(metadata), **dict((config.headers or {}) if config else {})} if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) else: - if config.headers: - headers.update(dict(config.headers)) config = dataclasses.replace(config, headers=headers) return {{ method.client_output_async.ident }}( diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index d2402747ca28..8d180dad0fac 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1545,6 +1545,50 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a with pytest.raises(core_exceptions.GoogleAPICallError) as exc_info: await transport.{{ method.transport_safe_name|snake_case }}({{ method.input.ident }}()) assert "operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel" in str(exc_info.value) + + +@pytest.mark.asyncio +async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_rest_stub(): + with mock.patch.object(transports.grpc_asyncio, "HAS_ASYNC_REST", True), mock.patch.object( + transports.grpc_asyncio, + "Async{{ service.name }}RestTransport", + create=True, + ) as mock_rest_transport_cls: + mock_rest_transport = mock.MagicMock() + expected_session = {{ method.client_output_async.ident }}() + mock_rest_transport.{{ method.transport_safe_name|snake_case }} = mock.AsyncMock( + return_value=expected_session + ) + mock_rest_transport_cls.return_value = mock_rest_transport + + transport = transports.{{ service.name }}GrpcAsyncIOTransport( + credentials=async_anonymous_credentials(), + ) + mock_rest_transport_cls.assert_not_called() + + stub = transport.{{ method.transport_safe_name|snake_case }} + result1 = await stub({{ method.input.ident }}()) + result2 = await stub({{ method.input.ident }}()) + + assert result1 is expected_session + assert result2 is expected_session + mock_rest_transport_cls.assert_called_once_with( + host=transport._host, + credentials=transport._credentials, + client_info=transport._client_info, + ) + assert mock_rest_transport.{{ method.transport_safe_name|snake_case }}.call_count == 2 + + +@pytest.mark.asyncio +async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_without_async_rest_error(): + with mock.patch.object(transports.grpc_asyncio, "HAS_ASYNC_REST", False): + transport = transports.{{ service.name }}GrpcAsyncIOTransport( + credentials=async_anonymous_credentials(), + ) + with pytest.raises(NotImplementedError) as exc_info: + await transport.{{ method.transport_safe_name|snake_case }}({{ method.input.ident }}()) + assert "Async REST transport is required for async resumable upload methods." in str(exc_info.value) {% endfor %} {% endif %} @@ -1556,13 +1600,40 @@ def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_start_e transport="rest", ) with mock.patch.object(type(client.transport._session), "request") as req: - response_value = mock.MagicMock() - response_value.status_code = 200 - response_value.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} - response_value.content = b"" - req.return_value = response_value - response = client.{{ method.client_method_name|snake_case }}({{ method.input.ident }}()) + start_response = mock.MagicMock() + start_response.ok = True + start_response.status_code = 200 + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + start_response.content = b"" + + expected_result = {{ 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 %} + ) + {% if method.output.ident.is_proto_plus_type %} + json_return_value = {{ method.output.ident }}.to_json(expected_result) + {% else %} + json_return_value = json_format.MessageToJson(expected_result) + {% endif %} + upload_response = mock.MagicMock() + upload_response.ok = True + upload_response.status_code = 200 + upload_response.headers = {"x-goog-upload-status": "final"} + upload_response.content = json_return_value.encode("UTF-8") + + req.side_effect = [start_response, upload_response] + response = client.{{ method.client_method_name|snake_case }}( + {{ method.input.ident }}(), + config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), + timeout=10.0, + ) assert isinstance(response, {{ method.client_output.ident }}) + result = response.upload(b"test data") + assert isinstance(result, {{ method.output.ident }}) + assert result == expected_result {% if rest_async_io_enabled %} @@ -1574,14 +1645,45 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_a credentials=async_anonymous_credentials(), transport="rest_asyncio", ) - with mock.patch.object(type(client.transport._session), "request", new_callable=mock.AsyncMock) as req: - response_value = mock.MagicMock() - response_value.status_code = 200 - response_value.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} - response_value.read = mock.AsyncMock(return_value=b"") - req.return_value = response_value - response = await client.{{ method.client_method_name|snake_case }}({{ method.input.ident }}()) + with mock.patch.object(type(client.transport._session), "request", new_callable=mock.MagicMock) as req: + start_response = mock.MagicMock() + start_response.status = 200 + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + start_response.read = mock.AsyncMock(return_value=b"") + start_ctx = mock.MagicMock() + start_ctx.__aenter__ = mock.AsyncMock(return_value=start_response) + start_ctx.__aexit__ = mock.AsyncMock(return_value=None) + + expected_result = {{ 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 %} + ) + {% if method.output.ident.is_proto_plus_type %} + json_return_value = {{ method.output.ident }}.to_json(expected_result) + {% else %} + json_return_value = json_format.MessageToJson(expected_result) + {% endif %} + upload_response = mock.MagicMock() + upload_response.status = 200 + upload_response.headers = {"x-goog-upload-status": "final"} + upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) + upload_ctx = mock.MagicMock() + upload_ctx.__aenter__ = mock.AsyncMock(return_value=upload_response) + upload_ctx.__aexit__ = mock.AsyncMock(return_value=None) + + req.side_effect = [start_ctx, upload_ctx] + response = await client.{{ method.client_method_name|snake_case }}( + {{ method.input.ident }}(), + config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), + timeout=10.0, + ) assert isinstance(response, {{ method.client_output_async.ident }}) + result = await response.upload(b"test data") + assert isinstance(result, {{ method.output.ident }}) + assert result == expected_result {% endif %} {% endfor %} {% endif %} diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 index 8acea0c5dc83..42ad0404a9bb 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 @@ -45,6 +45,8 @@ def test_{{ method_name }}(request_type, transport: str = 'grpc'): call.return_value = operations_pb2.Operation(name='operations/spam') {% elif method.server_streaming %} call.return_value = iter([{{ method.output.ident }}()]) + {% elif method.is_resumable_upload %} + call.return_value = {{ method.client_output.ident }}() {% else %} call.return_value = {{ method.output.ident }}( {% 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 {% 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 }}()]) + {% elif method.is_resumable_upload %} + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + {{ method.client_output_async.ident }}() + ) {% else %} call.return_value ={{ '' }} {%- if not method.client_streaming and not method.server_streaming -%} @@ -1837,20 +1843,37 @@ def test_initialize_client_w_{{transport_name}}(): request = request_type(**request_init) # Mock the http request call within the method and fake a BadRequest error. - with mock.patch.object({{mocked_session}}, 'request') as req, pytest.raises(core_exceptions.BadRequest): + 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): # Wrap the value into a proper Response obj + {% if method.is_resumable_upload %} + response_value = mock.MagicMock() + {% else %} response_value = mock.Mock() + {% endif %} {% if is_async %} response_value.read = mock.AsyncMock(return_value=b'{}') + {% if method.is_resumable_upload %} + response_value.status = 400 + response_value.__aenter__ = mock.AsyncMock(return_value=response_value) + response_value.__aexit__ = mock.AsyncMock(return_value=None) + {% endif %} {% else %} json_return_value = '' response_value.json = mock.Mock(return_value={}) + {% if method.is_resumable_upload %} + response_value.ok = False + {% endif %} {% endif %}{# if is_async #} response_value.status_code = 400 response_value.request = mock.Mock() req.return_value = response_value req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} + {% if method.is_resumable_upload %} + response = {{ await_prefix }}client.{{ method_name }}(request) + {{ await_prefix }}response.upload(b"") + {% else %} {{ await_prefix }}client.{{ method_name }}(request) + {% endif %} {% endif %}{# if 'grpc' in transport #} {% endmacro %} @@ -2272,13 +2295,13 @@ def test_initialize_client_w_{{transport_name}}(): {% if method.lro %} mock.patch.object(operation.Operation, "_set_result_from_operation"), \ {% endif %} - {% if not method.void %} + {% if not method.void and not method.is_resumable_upload %} mock.patch.object(transports.{{async_method_prefix}}{{ service.name }}RestInterceptor, "post_{{method.name|snake_case}}") as post, \ mock.patch.object(transports.{{async_method_prefix}}{{ service.name }}RestInterceptor, "post_{{method.name|snake_case}}_with_metadata") as post_with_metadata, \ {% endif %} mock.patch.object(transports.{{async_method_prefix}}{{ service.name }}RestInterceptor, "pre_{{ method.name|snake_case }}") as pre: pre.assert_not_called() - {% if not method.void %} + {% if not method.void and not method.is_resumable_upload %} post.assert_not_called() post_with_metadata.assert_not_called() {% endif %} @@ -2297,7 +2320,7 @@ def test_initialize_client_w_{{transport_name}}(): req.return_value = mock.Mock() req.return_value.status_code = 200 req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} - {% if not method.void %} + {% if not method.void and not method.is_resumable_upload %} 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 %} {% if method.server_streaming %} @@ -2313,7 +2336,7 @@ def test_initialize_client_w_{{transport_name}}(): req.return_value.content = return_value {% endif %}{# end is_async #} {% endif %}{# end method.server_streaming #} - {% endif %}{# end not method.void #} + {% endif %}{# end not method.void and not method.is_resumable_upload #} request = {{ method.input.ident }}() metadata =[ @@ -2321,7 +2344,7 @@ def test_initialize_client_w_{{transport_name}}(): ("cephalopod", "squid"), ] pre.return_value = request, metadata - {% if not method.void %} + {% if not method.void and not method.is_resumable_upload %} post.return_value = {{ method.output.ident }}() post_with_metadata.return_value = {{ method.output.ident }}(), metadata {% endif %} @@ -2329,7 +2352,7 @@ def test_initialize_client_w_{{transport_name}}(): {{await_prefix}}client.{{ method_name }}(request, metadata=[("key", "val"), ("cephalopod", "squid"),]) pre.assert_called_once() - {% if not method.void %} + {% if not method.void and not method.is_resumable_upload %} post.assert_called_once() post_with_metadata.assert_called_once() {% endif %} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index e343bc4dbce7..1f833109c151 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -357,30 +357,32 @@ async def _error(): ) return _error() self._stubs['upload_media'] = _ErrorStub() - elif HAS_ASYNC_REST: # pragma: NO COVER - # Lazily instantiate and cache the AsyncRestTransport stub on first - # call rather than during __init__ (_prep_wrapped_messages), since - # GrpcAsyncIOTransport may be initialized with synchronous - # google.auth.credentials.Credentials that AsyncAuthorizedSession - # rejects, and gRPC async stubs expose a synchronous __call__ - # returning an awaitable. - class _AsyncRestStub: # pragma: NO COVER - def __init__(_self): # pragma: NO COVER - _self._rest_stub = None - - def __call__(_self, *args, **kwargs): # pragma: NO COVER - if _self._rest_stub is None: + elif HAS_ASYNC_REST: + # Defer instantiating AsyncResumableUploadServiceRestTransport until the + # resumable upload RPC is called so that initializing + # ResumableUploadServiceGrpcAsyncIOTransport with synchronous + # google.auth.credentials.Credentials (e.g. via ADC or service + # account helpers) for standard gRPC calls does not fail in + # __init__ (_prep_wrapped_messages) when AsyncAuthorizedSession + # validates for async credentials. + transport = self + class _AsyncRestStub: + def __init__(self): + self._rest_stub = None + + def __call__(self, *args, **kwargs): + if self._rest_stub is None: rest_transport = AsyncResumableUploadServiceRestTransport( - host=self._host, - credentials=self._credentials, - client_info=self._client_info, + host=transport._host, + credentials=transport._credentials, + client_info=transport._client_info, ) - _self._rest_stub = rest_transport.upload_media - return _self._rest_stub(*args, **kwargs) + self._rest_stub = rest_transport.upload_media + return self._rest_stub(*args, **kwargs) self._stubs['upload_media'] = _AsyncRestStub() - else: # pragma: NO COVER - class _UnsupportedStub: # pragma: NO COVER - def __call__(self, *args, **kwargs): # pragma: NO COVER + else: + class _UnsupportedStub: + def __call__(self, *args, **kwargs): async def _unsupported(): raise NotImplementedError("Async REST transport is required for async resumable upload methods.") return _unsupported() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py index 7e48b9ccc62b..7d4a2df0c411 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py @@ -86,10 +86,6 @@ def pre_upload_media(self, request, metadata): logging.log(f"Received request: {request}") return request, metadata - def post_upload_media(self, response): - logging.log(f"Received response: {response}") - return response - transport = ResumableUploadServiceRestTransport(interceptor=MyCustomResumableUploadServiceInterceptor()) client = ResumableUploadServiceClient(transport=transport) @@ -103,34 +99,6 @@ def pre_upload_media(self, request: resumable_upload.UploadMediaRequest, metadat """ return request, metadata - def post_upload_media(self, response: resumable_upload.UploadMediaResponse) -> resumable_upload.UploadMediaResponse: - """Post-rpc interceptor for upload_media - - DEPRECATED. Please use the `post_upload_media_with_metadata` - interceptor instead. - - Override in a subclass to read or manipulate the response - after it is returned by the ResumableUploadService server but before - it is returned to user code. This `post_upload_media` interceptor runs - before the `post_upload_media_with_metadata` interceptor. - """ - return response - - def post_upload_media_with_metadata(self, response: resumable_upload.UploadMediaResponse, metadata: Sequence[Tuple[str, Union[str, bytes]]]) -> Tuple[resumable_upload.UploadMediaResponse, Sequence[Tuple[str, Union[str, bytes]]]]: - """Post-rpc interceptor for upload_media - - Override in a subclass to read or manipulate the response or metadata after it - is returned by the ResumableUploadService server but before it is returned to user code. - - We recommend only using this `post_upload_media_with_metadata` - interceptor in new development instead of the `post_upload_media` interceptor. - When both interceptors are used, this `post_upload_media_with_metadata` interceptor runs after the - `post_upload_media` interceptor. The (possibly modified) response returned by - `post_upload_media` will be passed to - `post_upload_media_with_metadata`. - """ - return response, metadata - def pre_list_locations( self, request: locations_pb2.ListLocationsRequest, metadata: Sequence[Tuple[str, Union[str, bytes]]] ) -> Tuple[locations_pb2.ListLocationsRequest, Sequence[Tuple[str, Union[str, bytes]]]]: @@ -460,12 +428,10 @@ def __call__(self, uri = transcoded_request["uri"] upload_url = f"{self._host}{uri}" - headers = dict(metadata) + headers = {**dict(metadata), **dict((config.headers or {}) if config else {})} if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) else: - if config.headers: - headers.update(dict(config.headers)) config = dataclasses.replace(config, headers=headers) return resumable_transfer.ResumableUploadSession( diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py index 29f2d1372467..d05e621a4245 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -100,10 +100,6 @@ async def pre_upload_media(self, request, metadata): logging.log(f"Received request: {request}") return request, metadata - async def post_upload_media(self, response): - logging.log(f"Received response: {response}") - return response - transport = AsyncResumableUploadServiceRestTransport(interceptor=MyCustomResumableUploadServiceInterceptor()) client = async ResumableUploadServiceClient(transport=transport) @@ -117,34 +113,6 @@ async def pre_upload_media(self, request: resumable_upload.UploadMediaRequest, m """ return request, metadata - async def post_upload_media(self, response: resumable_upload.UploadMediaResponse) -> resumable_upload.UploadMediaResponse: - """Post-rpc interceptor for upload_media - - DEPRECATED. Please use the `post_upload_media_with_metadata` - interceptor instead. - - Override in a subclass to read or manipulate the response - after it is returned by the ResumableUploadService server but before - it is returned to user code. This `post_upload_media` interceptor runs - before the `post_upload_media_with_metadata` interceptor. - """ - return response - - async def post_upload_media_with_metadata(self, response: resumable_upload.UploadMediaResponse, metadata: Sequence[Tuple[str, Union[str, bytes]]]) -> Tuple[resumable_upload.UploadMediaResponse, Sequence[Tuple[str, Union[str, bytes]]]]: - """Post-rpc interceptor for upload_media - - Override in a subclass to read or manipulate the response or metadata after it - is returned by the ResumableUploadService server but before it is returned to user code. - - We recommend only using this `post_upload_media_with_metadata` - interceptor in new development instead of the `post_upload_media` interceptor. - When both interceptors are used, this `post_upload_media_with_metadata` interceptor runs after the - `post_upload_media` interceptor. The (possibly modified) response returned by - `post_upload_media` will be passed to - `post_upload_media_with_metadata`. - """ - return response, metadata - @dataclasses.dataclass class AsyncResumableUploadServiceRestStub: @@ -273,12 +241,10 @@ async def __call__(self, uri = transcoded_request["uri"] upload_url = f"{self._host}{uri}" - headers = dict(metadata) + headers = {**dict(metadata), **dict((config.headers or {}) if config else {})} if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) else: - if config.headers: - headers.update(dict(config.headers)) config = dataclasses.replace(config, headers=headers) return resumable_transfer.AsyncResumableUploadSession( diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 8d31f0f5956c..ee936f5d2b2d 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -837,10 +837,7 @@ def test_upload_media(request_type, transport: str = 'grpc'): type(client.transport.upload_media), '__call__') as call: # Designate an appropriate return value for the call. - call.return_value = resumable_upload.UploadMediaResponse( - name='name_value', - size=443, - ) + call.return_value = resumable_transfer.ResumableUploadSession() response = client.upload_media(request) # Establish that the underlying gRPC stub method was called. @@ -967,10 +964,9 @@ async def test_upload_media_async(request_type, transport: str = 'grpc_asyncio') type(client.transport.upload_media), '__call__') as call: # Designate an appropriate return value for the call. - call.return_value =grpc_helpers_async.FakeUnaryUnaryCall(resumable_upload.UploadMediaResponse( - name='name_value', - size=443, - )) + call.return_value = grpc_helpers_async.FakeUnaryUnaryCall( + resumable_transfer.AsyncResumableUploadSession() + ) response = await client.upload_media(request) # Establish that the underlying gRPC stub method was called. @@ -1202,14 +1198,16 @@ def test_upload_media_rest_bad_request(request_type=resumable_upload.UploadMedia # Mock the http request call within the method and fake a BadRequest error. with mock.patch.object(Session, 'request') as req, pytest.raises(core_exceptions.BadRequest): # Wrap the value into a proper Response obj - response_value = mock.Mock() + response_value = mock.MagicMock() json_return_value = '' response_value.json = mock.Mock(return_value={}) + response_value.ok = False response_value.status_code = 400 response_value.request = mock.Mock() req.return_value = response_value req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} - client.upload_media(request) + response = client.upload_media(request) + response.upload(b"") @pytest.mark.parametrize("request_type", [ @@ -1260,12 +1258,8 @@ def test_upload_media_rest_interceptors(null_interceptor): with mock.patch.object(type(client.transport._session), "request") as req, \ mock.patch.object(path_template, "transcode") as transcode, \ - mock.patch.object(transports.ResumableUploadServiceRestInterceptor, "post_upload_media") as post, \ - mock.patch.object(transports.ResumableUploadServiceRestInterceptor, "post_upload_media_with_metadata") as post_with_metadata, \ mock.patch.object(transports.ResumableUploadServiceRestInterceptor, "pre_upload_media") as pre: pre.assert_not_called() - post.assert_not_called() - post_with_metadata.assert_not_called() pb_message = resumable_upload.UploadMediaRequest.pb(resumable_upload.UploadMediaRequest()) transcode.return_value = { "method": "post", @@ -1277,8 +1271,6 @@ def test_upload_media_rest_interceptors(null_interceptor): req.return_value = mock.Mock() req.return_value.status_code = 200 req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} - return_value = resumable_upload.UploadMediaResponse.to_json(resumable_upload.UploadMediaResponse()) - req.return_value.content = return_value request = resumable_upload.UploadMediaRequest() metadata =[ @@ -1286,14 +1278,10 @@ def test_upload_media_rest_interceptors(null_interceptor): ("cephalopod", "squid"), ] pre.return_value = request, metadata - post.return_value = resumable_upload.UploadMediaResponse() - post_with_metadata.return_value = resumable_upload.UploadMediaResponse(), metadata client.upload_media(request, metadata=[("key", "val"), ("cephalopod", "squid"),]) pre.assert_called_once() - post.assert_called_once() - post_with_metadata.assert_called_once() def test_list_locations_rest_bad_request(request_type=locations_pb2.ListLocationsRequest): @@ -1823,15 +1811,19 @@ async def test_upload_media_rest_asyncio_bad_request(request_type=resumable_uplo request = request_type(**request_init) # Mock the http request call within the method and fake a BadRequest error. - with mock.patch.object(AsyncAuthorizedSession, 'request') as req, pytest.raises(core_exceptions.BadRequest): + with mock.patch.object(AsyncAuthorizedSession, 'request', new_callable=mock.MagicMock) as req, pytest.raises(core_exceptions.BadRequest): # Wrap the value into a proper Response obj - response_value = mock.Mock() + response_value = mock.MagicMock() response_value.read = mock.AsyncMock(return_value=b'{}') + response_value.status = 400 + response_value.__aenter__ = mock.AsyncMock(return_value=response_value) + response_value.__aexit__ = mock.AsyncMock(return_value=None) response_value.status_code = 400 response_value.request = mock.Mock() req.return_value = response_value req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} - await client.upload_media(request) + response = await client.upload_media(request) + await response.upload(b"") @pytest.mark.asyncio @@ -1888,12 +1880,8 @@ async def test_upload_media_rest_asyncio_interceptors(null_interceptor): with mock.patch.object(type(client.transport._session), "request") as req, \ mock.patch.object(path_template, "transcode") as transcode, \ - mock.patch.object(transports.AsyncResumableUploadServiceRestInterceptor, "post_upload_media") as post, \ - mock.patch.object(transports.AsyncResumableUploadServiceRestInterceptor, "post_upload_media_with_metadata") as post_with_metadata, \ mock.patch.object(transports.AsyncResumableUploadServiceRestInterceptor, "pre_upload_media") as pre: pre.assert_not_called() - post.assert_not_called() - post_with_metadata.assert_not_called() pb_message = resumable_upload.UploadMediaRequest.pb(resumable_upload.UploadMediaRequest()) transcode.return_value = { "method": "post", @@ -1905,8 +1893,6 @@ async def test_upload_media_rest_asyncio_interceptors(null_interceptor): req.return_value = mock.Mock() req.return_value.status_code = 200 req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} - return_value = resumable_upload.UploadMediaResponse.to_json(resumable_upload.UploadMediaResponse()) - req.return_value.read = mock.AsyncMock(return_value=return_value) request = resumable_upload.UploadMediaRequest() metadata =[ @@ -1914,14 +1900,10 @@ async def test_upload_media_rest_asyncio_interceptors(null_interceptor): ("cephalopod", "squid"), ] pre.return_value = request, metadata - post.return_value = resumable_upload.UploadMediaResponse() - post_with_metadata.return_value = resumable_upload.UploadMediaResponse(), metadata await client.upload_media(request, metadata=[("key", "val"), ("cephalopod", "squid"),]) pre.assert_called_once() - post.assert_called_once() - post_with_metadata.assert_called_once() def test_initialize_client_w_rest_asyncio(): if not HAS_ASYNC_REST_EXTRA: @@ -2295,19 +2277,83 @@ async def test_resumable_upload_service_upload_media_grpc_asyncio_channel_withou await transport.upload_media(resumable_upload.UploadMediaRequest()) assert "operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel" in str(exc_info.value) + +@pytest.mark.asyncio +async def test_resumable_upload_service_upload_media_grpc_asyncio_rest_stub(): + with mock.patch.object(transports.grpc_asyncio, "HAS_ASYNC_REST", True), mock.patch.object( + transports.grpc_asyncio, + "AsyncResumableUploadServiceRestTransport", + create=True, + ) as mock_rest_transport_cls: + mock_rest_transport = mock.MagicMock() + expected_session = resumable_transfer.AsyncResumableUploadSession() + mock_rest_transport.upload_media = mock.AsyncMock( + return_value=expected_session + ) + mock_rest_transport_cls.return_value = mock_rest_transport + + transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( + credentials=async_anonymous_credentials(), + ) + mock_rest_transport_cls.assert_not_called() + + stub = transport.upload_media + result1 = await stub(resumable_upload.UploadMediaRequest()) + result2 = await stub(resumable_upload.UploadMediaRequest()) + + assert result1 is expected_session + assert result2 is expected_session + mock_rest_transport_cls.assert_called_once_with( + host=transport._host, + credentials=transport._credentials, + client_info=transport._client_info, + ) + assert mock_rest_transport.upload_media.call_count == 2 + + +@pytest.mark.asyncio +async def test_resumable_upload_service_upload_media_grpc_asyncio_without_async_rest_error(): + with mock.patch.object(transports.grpc_asyncio, "HAS_ASYNC_REST", False): + transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( + credentials=async_anonymous_credentials(), + ) + with pytest.raises(NotImplementedError) as exc_info: + await transport.upload_media(resumable_upload.UploadMediaRequest()) + assert "Async REST transport is required for async resumable upload methods." in str(exc_info.value) + def test_resumable_upload_service_upload_media_rest_start_empty_body(): client = ResumableUploadServiceClient( credentials=ga_credentials.AnonymousCredentials(), transport="rest", ) with mock.patch.object(type(client.transport._session), "request") as req: - response_value = mock.MagicMock() - response_value.status_code = 200 - response_value.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} - response_value.content = b"" - req.return_value = response_value - response = client.upload_media(resumable_upload.UploadMediaRequest()) + start_response = mock.MagicMock() + start_response.ok = True + start_response.status_code = 200 + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + start_response.content = b"" + + expected_result = resumable_upload.UploadMediaResponse( + name='name_value', + size=443, + ) + json_return_value = resumable_upload.UploadMediaResponse.to_json(expected_result) + upload_response = mock.MagicMock() + upload_response.ok = True + upload_response.status_code = 200 + upload_response.headers = {"x-goog-upload-status": "final"} + upload_response.content = json_return_value.encode("UTF-8") + + req.side_effect = [start_response, upload_response] + response = client.upload_media( + resumable_upload.UploadMediaRequest(), + config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), + timeout=10.0, + ) assert isinstance(response, resumable_transfer.ResumableUploadSession) + result = response.upload(b"test data") + assert isinstance(result, resumable_upload.UploadMediaResponse) + assert result == expected_result @pytest.mark.asyncio @@ -2318,14 +2364,38 @@ async def test_resumable_upload_service_upload_media_rest_asyncio_start_empty_bo credentials=async_anonymous_credentials(), transport="rest_asyncio", ) - with mock.patch.object(type(client.transport._session), "request", new_callable=mock.AsyncMock) as req: - response_value = mock.MagicMock() - response_value.status_code = 200 - response_value.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} - response_value.read = mock.AsyncMock(return_value=b"") - req.return_value = response_value - response = await client.upload_media(resumable_upload.UploadMediaRequest()) + with mock.patch.object(type(client.transport._session), "request", new_callable=mock.MagicMock) as req: + start_response = mock.MagicMock() + start_response.status = 200 + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + start_response.read = mock.AsyncMock(return_value=b"") + start_ctx = mock.MagicMock() + start_ctx.__aenter__ = mock.AsyncMock(return_value=start_response) + start_ctx.__aexit__ = mock.AsyncMock(return_value=None) + + expected_result = resumable_upload.UploadMediaResponse( + name='name_value', + size=443, + ) + json_return_value = resumable_upload.UploadMediaResponse.to_json(expected_result) + upload_response = mock.MagicMock() + upload_response.status = 200 + upload_response.headers = {"x-goog-upload-status": "final"} + upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) + upload_ctx = mock.MagicMock() + upload_ctx.__aenter__ = mock.AsyncMock(return_value=upload_response) + upload_ctx.__aexit__ = mock.AsyncMock(return_value=None) + + req.side_effect = [start_ctx, upload_ctx] + response = await client.upload_media( + resumable_upload.UploadMediaRequest(), + config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), + timeout=10.0, + ) assert isinstance(response, resumable_transfer.AsyncResumableUploadSession) + result = await response.upload(b"test data") + assert isinstance(result, resumable_upload.UploadMediaResponse) + assert result == expected_result # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are From 45f18d004587470ff1cb1ba8369e55159deccae6 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 30 Sep 2026 15:14:33 +0000 Subject: [PATCH 06/45] remove warnings related to rest numberic enums not set --- .../resumable_upload_service/transports/rest_asyncio.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py index d05e621a4245..aad862b335de 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -142,10 +142,6 @@ def __init__(self, ) -> None: """Instantiate the transport. - NOTE: This async REST transport functionality is currently in a beta - state (preview). We welcome your feedback via a GitHub issue in - this library's repository. Thank you! - Args: host (Optional[str]): The hostname to connect to (default: 'localhost:7469'). @@ -236,7 +232,7 @@ async def __call__(self, "_BaseUploadMedia__REQUIRED_FIELDS_DEFAULT_VALUES", None, ), - rest_numeric_enums=False, + rest_numeric_enums=True, ) uri = transcoded_request["uri"] From ee122614cdf0454a9a8b55a3bed4e094c10df588 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 30 Sep 2026 15:26:30 +0000 Subject: [PATCH 07/45] update test --- .../gapic/%name_%version/%sub/test_macros.j2 | 44 +++++++++++++++---- .../test_resumable_upload_service.py | 41 +++++++++++++---- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 index 42ad0404a9bb..7f4440e551c4 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 @@ -2041,7 +2041,7 @@ def test_initialize_client_w_{{transport_name}}(): request = request_type(**request_init) # Mock the http request call within the method and fake a response. - with mock.patch.object(type(client.transport._session), 'request') as req: + with mock.patch.object(type(client.transport._session), 'request'{% if is_async and method.is_resumable_upload %}, new_callable=mock.MagicMock{% endif %}) as req: # Designate an appropriate value for the returned response. {% if method.void %} return_value = None @@ -2080,7 +2080,11 @@ def test_initialize_client_w_{{transport_name}}(): {% endif %}{# method.void #} # Wrap the value into a proper Response obj + {% if method.is_resumable_upload %} + response_value = mock.MagicMock() + {% else %} response_value = mock.Mock() + {% endif %} response_value.status_code = 200 {% if method.void %} json_return_value = '' @@ -2106,9 +2110,37 @@ def test_initialize_client_w_{{transport_name}}(): response_value.content = json_return_value.encode('UTF-8') {% endif %}{# is_async #} {% endif %}{# method.server_streaming #} + {% if method.is_resumable_upload %} + start_response = mock.MagicMock() + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + response_value.headers = {"x-goog-upload-status": "final", "header-1": "value-1", "header-2": "value-2"} + {% if is_async %} + start_response.status = 200 + start_response.read = mock.AsyncMock(return_value=b"") + start_response.__aenter__ = mock.AsyncMock(return_value=start_response) + start_response.__aexit__ = mock.AsyncMock(return_value=None) + response_value.status = 200 + response_value.__aenter__ = mock.AsyncMock(return_value=response_value) + response_value.__aexit__ = mock.AsyncMock(return_value=None) + {% else %} + start_response.ok = True + start_response.status_code = 200 + start_response.content = b"" + response_value.ok = True + {% endif %} + req.side_effect = [start_response, response_value] + response = {{ await_prefix }}client.{{ method_name }}(request) + {% if is_async %} + assert isinstance(response, {{ method.client_output_async.ident }}) + {% else %} + assert isinstance(response, {{ method.client_output.ident }}) + {% endif %} + response = {{ await_prefix }}response.upload(b"test data") + {% else %} req.return_value = response_value req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} response = {{ await_prefix }}client.{{ method_name }}(request) + {% endif %} {% if "next_page_token" in method_output.fields.values()|map(attribute='name', default="") and not method.paged_result_field %} {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2199): The following assert statement is added to force # code coverage for bad paginated methods. Investigate what bad paginated methods are, and follow up on why this assertion is required. @@ -2132,14 +2164,10 @@ def test_initialize_client_w_{{transport_name}}(): assert response is None {% elif method.lro %} json_return_value = json_format.MessageToJson(return_value) - {% elif method.is_resumable_upload %} - {% if is_async %} - assert isinstance(response, {{ method.client_output_async.ident }}) - {% else %} - assert isinstance(response, {{ method.client_output.ident }}) - {% endif %} {% else %} - {% if is_async %} + {% if method.is_resumable_upload %} + assert isinstance(response, {{ method.output.ident }}) + {% elif is_async %} assert isinstance(response, {{ method.client_output_async.ident }}) {% else %} assert isinstance(response, {{ method.client_output.ident }}) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index ee936f5d2b2d..cf2d33bb9722 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -1233,19 +1233,29 @@ def test_upload_media_rest_call_success(request_type): ) # Wrap the value into a proper Response obj - response_value = mock.Mock() + response_value = mock.MagicMock() response_value.status_code = 200 # Convert return value to protobuf type return_value = resumable_upload.UploadMediaResponse.pb(return_value) json_return_value = json_format.MessageToJson(return_value) response_value.content = json_return_value.encode('UTF-8') - req.return_value = response_value - req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} + start_response = mock.MagicMock() + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + response_value.headers = {"x-goog-upload-status": "final", "header-1": "value-1", "header-2": "value-2"} + start_response.ok = True + start_response.status_code = 200 + start_response.content = b"" + response_value.ok = True + req.side_effect = [start_response, response_value] response = client.upload_media(request) + assert isinstance(response, resumable_transfer.ResumableUploadSession) + response = response.upload(b"test data") # Establish that the response is the type that we expect. - assert isinstance(response, resumable_transfer.ResumableUploadSession) + assert isinstance(response, resumable_upload.UploadMediaResponse) + assert response.name == 'name_value' + assert response.size == 443 @pytest.mark.parametrize("null_interceptor", [True, False]) @@ -1844,7 +1854,7 @@ async def test_upload_media_rest_asyncio_call_success(request_type): request = request_type(**request_init) # Mock the http request call within the method and fake a response. - with mock.patch.object(type(client.transport._session), 'request') as req: + with mock.patch.object(type(client.transport._session), 'request', new_callable=mock.MagicMock) as req: # Designate an appropriate value for the returned response. return_value = resumable_upload.UploadMediaResponse( name='name_value', @@ -1852,19 +1862,32 @@ async def test_upload_media_rest_asyncio_call_success(request_type): ) # Wrap the value into a proper Response obj - response_value = mock.Mock() + response_value = mock.MagicMock() response_value.status_code = 200 # Convert return value to protobuf type return_value = resumable_upload.UploadMediaResponse.pb(return_value) json_return_value = json_format.MessageToJson(return_value) response_value.read = mock.AsyncMock(return_value=json_return_value.encode('UTF-8')) - req.return_value = response_value - req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} + start_response = mock.MagicMock() + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + response_value.headers = {"x-goog-upload-status": "final", "header-1": "value-1", "header-2": "value-2"} + start_response.status = 200 + start_response.read = mock.AsyncMock(return_value=b"") + start_response.__aenter__ = mock.AsyncMock(return_value=start_response) + start_response.__aexit__ = mock.AsyncMock(return_value=None) + response_value.status = 200 + response_value.__aenter__ = mock.AsyncMock(return_value=response_value) + response_value.__aexit__ = mock.AsyncMock(return_value=None) + req.side_effect = [start_response, response_value] response = await client.upload_media(request) + assert isinstance(response, resumable_transfer.AsyncResumableUploadSession) + response = await response.upload(b"test data") # Establish that the response is the type that we expect. - assert isinstance(response, resumable_transfer.AsyncResumableUploadSession) + assert isinstance(response, resumable_upload.UploadMediaResponse) + assert response.name == 'name_value' + assert response.size == 443 @pytest.mark.asyncio From eac800b8920b8d13d08ca6c871e197dc7533297f Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 30 Sep 2026 15:55:56 +0000 Subject: [PATCH 08/45] address review feedback --- .../gapic-generator/gapic/templates/setup.py.j2 | 4 ++++ .../testing/constraints-3.10-async-rest.txt.j2 | 4 ++++ .../templates/testing/constraints-3.10.txt.j2 | 4 ++++ packages/gapic-generator/noxfile.py | 17 ++++++----------- .../tests/integration/goldens/showcase/setup.py | 2 +- .../testing/constraints-3.10-async-rest.txt | 2 +- .../showcase/testing/constraints-3.10.txt | 2 +- 7 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/setup.py.j2 b/packages/gapic-generator/gapic/templates/setup.py.j2 index e7e9f1549061..7c2d340e1170 100644 --- a/packages/gapic-generator/gapic/templates/setup.py.j2 +++ b/packages/gapic-generator/gapic/templates/setup.py.j2 @@ -36,7 +36,11 @@ else: release_status = "Development Status :: 5 - Production/Stable" dependencies = [ + {% if api.has_resumable_upload_methods %} + "google-api-core[grpc] >= 2.39.0, <3.0.0", + {% else %} "google-api-core[grpc] >= 2.28.0, <3.0.0", + {% endif %} # Exclude incompatible versions of `google-auth` # See https://github.com/googleapis/google-cloud-python/issues/12364 "google-auth >= 2.14.1, <3.0.0,!=2.24.0,!=2.25.0", diff --git a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 index aea4bb3a7d15..2232b4b694c6 100644 --- a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 +++ b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 @@ -8,7 +8,11 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 +{% if api.has_resumable_upload_methods %} +google-api-core==2.39.0 +{% else %} google-api-core==2.28.0 +{% endif %} google-auth==2.35.0 grpcio==1.59.0 proto-plus==1.26.1 diff --git a/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 b/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 index f8d078bfabb9..6db105c05ac0 100644 --- a/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 +++ b/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 @@ -5,7 +5,11 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 +{% if api.has_resumable_upload_methods %} +google-api-core==2.39.0 +{% else %} google-api-core==2.28.0 +{% endif %} google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index 2fe717cec79e..f3762a9327b3 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -193,10 +193,6 @@ def fragment(session, use_ads_templates=False): ) session.install("-e", ".") - # The specific failure is `Plugin output is unparseable` - if session.python == "3.10": - session.install("google-api-core<2.28") - frag_files = ( [Path(f) for f in session.posargs] if session.posargs else FRAGMENT_FILES ) @@ -260,13 +256,6 @@ def showcase_library( # Install grpcio-tools for protoc session.install("grpcio-tools") - # TODO(https://github.com/googleapis/gapic-generator-python/issues/2473): - # Warnings emitted from google-api-core starting in 2.28 - # appear to cause issues when running protoc. - # The specific failure is `Plugin output is unparseable` - if session.python == "3.10": - session.install("google-api-core<2.28") - # Install a client library for Showcase. with tempfile.TemporaryDirectory() as tmp_dir: # Download the Showcase descriptor. @@ -351,9 +340,15 @@ def showcase_library( f"--descriptor_set_in={tmp_dir}{path.sep}showcase.desc", opts, f"--python_gapic_out={tmp_dir}", + # TODO(https://github.com/googleapis/google-cloud-python/issues/16312): + # Add compliance.proto once this bug is fixed f"google/showcase/v1beta1/echo.proto", f"google/showcase/v1beta1/identity.proto", f"google/showcase/v1beta1/messaging.proto", + f"google/showcase/v1beta1/rest_error.proto", + f"google/showcase/v1beta1/resumable_upload.proto", + f"google/showcase/v1beta1/sequence.proto", + f"google/showcase/v1beta1/testing.proto", ) session.run( *cmd_tup, diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py index 572e6df85c68..4cf6e1d50d41 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py @@ -42,7 +42,7 @@ release_status = "Development Status :: 5 - Production/Stable" dependencies = [ - "google-api-core[grpc] >= 2.28.0, <3.0.0", + "google-api-core[grpc] >= 2.39.0, <3.0.0", # Exclude incompatible versions of `google-auth` # See https://github.com/googleapis/google-cloud-python/issues/12364 "google-auth >= 2.14.1, <3.0.0,!=2.24.0,!=2.25.0", diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt index 68916cd2f245..799c0fc1e176 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt @@ -5,7 +5,7 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.28.0 +google-api-core==2.39.0 google-auth==2.35.0 grpcio==1.59.0 proto-plus==1.26.1 diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt index 9c33fedfb264..99b79cca2c30 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt @@ -4,7 +4,7 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.28.0 +google-api-core==2.39.0 google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 From c0c6465ab8d22be35ccab374573e3a95908ac8ac Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 30 Sep 2026 16:02:39 +0000 Subject: [PATCH 09/45] fix build --- packages/gapic-generator/noxfile.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index f3762a9327b3..871a29c8302e 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -332,24 +332,30 @@ def showcase_library( "transport=grpc+rest", ) ) + # TODO(https://github.com/googleapis/google-cloud-python/issues/16312): + # Add compliance.proto once this bug is fixed + # We should use `"google/showcase/v1beta1/*.proto",` + protos = ( + "google/showcase/v1beta1/echo.proto", + "google/showcase/v1beta1/identity.proto", + "google/showcase/v1beta1/messaging.proto", + "google/showcase/v1beta1/rest_error.proto", + "google/showcase/v1beta1/sequence.proto", + "google/showcase/v1beta1/testing.proto", + ) + if templates == "DEFAULT": + protos += ("google/showcase/v1beta1/resumable_upload.proto",) cmd_tup = ( "python", "-m", "grpc_tools.protoc", - f"--experimental_allow_proto3_optional", + "--experimental_allow_proto3_optional", f"--descriptor_set_in={tmp_dir}{path.sep}showcase.desc", opts, f"--python_gapic_out={tmp_dir}", - # TODO(https://github.com/googleapis/google-cloud-python/issues/16312): - # Add compliance.proto once this bug is fixed - f"google/showcase/v1beta1/echo.proto", - f"google/showcase/v1beta1/identity.proto", - f"google/showcase/v1beta1/messaging.proto", - f"google/showcase/v1beta1/rest_error.proto", - f"google/showcase/v1beta1/resumable_upload.proto", - f"google/showcase/v1beta1/sequence.proto", - f"google/showcase/v1beta1/testing.proto", + *protos, ) + session.run( *cmd_tup, external=True, From 1c0ca3dd4ea371238d8d269c4ae76e43390dc75e Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 30 Sep 2026 16:21:11 +0000 Subject: [PATCH 10/45] fix build --- packages/gapic-generator/noxfile.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index 871a29c8302e..39b81d87f602 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -371,20 +371,19 @@ def showcase_library( constraints_path = str( f"{tmp_dir}/testing/constraints-{session.python}.txt" ) - extras = "" - if rest_async_io_enabled: - async_rest_constraints_path = str( - f"{tmp_dir}/testing/constraints-{session.python}-async-rest.txt" + async_rest_constraints_path = str( + f"{tmp_dir}/testing/constraints-{session.python}-async-rest.txt" + ) + if os.path.exists(async_rest_constraints_path): + # use async-rest constraints if available + constraints_path = async_rest_constraints_path + else: + session.log( + f"{async_rest_constraints_path} not found. Using base constraints file" ) - if os.path.exists(async_rest_constraints_path): - # use async-rest constraints if available - constraints_path = async_rest_constraints_path - else: - session.log( - f"{async_rest_constraints_path} not found. Using base constraints file" - ) - extras = "[async_rest]" - + # Showcase protos include `resumable_media.proto` + # Include the `async_rest` extra. + extras = "[async_rest]" session.install("-e", f"{tmp_dir}{extras}", "-r", constraints_path) else: # The ads templates do not have constraints files. From aa4df2667c2e81353f9088ef4b40ac3eccc270ce Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 30 Sep 2026 17:33:57 +0000 Subject: [PATCH 11/45] address review feedback --- .../%sub/services/%service/client.py.j2 | 8 ++--- .../services/%service/transports/base.py.j2 | 11 +++++++ .../services/%service/transports/grpc.py.j2 | 27 ++++++++++++----- .../%service/transports/grpc_asyncio.py.j2 | 29 ++++++++++++------- .../services/%service/transports/rest.py.j2 | 13 +++++---- .../%service/transports/rest_asyncio.py.j2 | 13 +++++---- .../%name_%version/%sub/test_%service.py.j2 | 19 ++++++++++++ .../resumable_upload_service/client.py | 8 ++--- .../transports/base.py | 6 ++-- .../transports/grpc.py | 21 +++++++++----- .../transports/grpc_asyncio.py | 24 +++++++-------- .../transports/rest.py | 13 +++++---- .../transports/rest_asyncio.py | 13 +++++---- .../test_resumable_upload_service.py | 19 ++++++++++++ 14 files changed, 153 insertions(+), 71 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 index 221eeca217f7..4da2011d35c8 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 @@ -85,14 +85,12 @@ from .transports.grpc_asyncio import {{ service.grpc_asyncio_transport_name }} from .transports.rest import {{ service.name }}RestTransport {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2121): Remove this condition when async rest is GA. #} {% if rest_async_io_enabled %} -ASYNC_REST_EXCEPTION = None try: from .transports.rest_asyncio import Async{{ service.name }}RestTransport HAS_ASYNC_REST_DEPENDENCIES = True {# NOTE: `pragma: NO COVER` is needed since the coverage for presubmits isn't combined. #} -except ImportError as e: # pragma: NO COVER +except ImportError: # pragma: NO COVER HAS_ASYNC_REST_DEPENDENCIES = False - ASYNC_REST_EXCEPTION = e {% endif %}{# if rest_async_io_enabled #} {% endif %} @@ -136,7 +134,9 @@ class {{ service.client_name }}Meta(type): {% if rest_async_io_enabled %} {# NOTE: `pragma: NO COVER` is needed since the coverage for presubmits isn't combined. #} if label == "rest_asyncio" and not HAS_ASYNC_REST_DEPENDENCIES: # pragma: NO COVER - raise ASYNC_REST_EXCEPTION + raise ImportError( + "`rest_asyncio` transport requires the library to be installed with the `async_rest` extra. Install the library with the `async_rest` extra using `pip install {{ api.naming.warehouse_package_name }}[async_rest]`" + ) {% endif %} if label: return cls._transport_registry[label] diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 index 0962df5dd3ed..dc873592aa2c 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -16,6 +16,9 @@ from google.api_core import retry as retries {% if service.has_lro %} from google.api_core import operations_v1 {% endif %} +{% if service.has_resumable_upload_methods %} +from google.api_core import resumable_transfer +{% endif %} from google.auth import credentials as ga_credentials # type: ignore from google.oauth2 import service_account # type: ignore import google.protobuf @@ -215,10 +218,18 @@ class {{ service.name }}Transport(abc.ABC): @property def {{ method.transport_safe_name|snake_case }}(self) -> Callable[ [{{ method.input.ident }}], + {% if method.is_resumable_upload %} + Union[ + {{ method.client_output.ident }}, + {{ method.client_output_async.ident }}, + Awaitable[{{ method.client_output_async.ident }}] + ]]: + {% else %} Union[ {{ method.output.ident }}, Awaitable[{{ method.output.ident }}] ]]: + {% endif %} raise NotImplementedError() {% endfor %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index b1f64934f89b..6dc44f887f56 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -12,6 +12,7 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union {% if service.has_resumable_upload_methods %} from google.api_core import exceptions as core_exceptions +from google.api_core import resumable_transfer {% endif %} from google.api_core import grpc_helpers {% if service.has_lro %} @@ -192,6 +193,9 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): {% if service.has_lro %} self._operations_client: Optional[operations_v1.OperationsClient] = None {% endif %} + {% if service.has_resumable_upload_methods %} + self._rest_transport: Optional[{{ service.name }}RestTransport] = None + {% endif %} if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -340,7 +344,11 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): @property def {{ method.transport_safe_name|snake_case }}(self) -> Callable[ [{{ method.input.ident }}], + {% if method.is_resumable_upload %} + {{ method.client_output.ident }}]: + {% else %} {{ method.output.ident }}]: + {% endif %} r"""Return a callable for the{{ ' ' }} {{- (method.name|snake_case).replace('_',' ')|wrap( width=70, offset=40, indent=8) -}} @@ -350,7 +358,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): Returns: Callable[[~.{{ method.input.name }}], - ~.{{ method.output.name }}]: + ~.{% if method.is_resumable_upload %}{{ method.client_output.name }}{% else %}{{ method.output.name }}{% endif %}]: A function that, when called, will call the underlying RPC on the server. """ @@ -368,12 +376,13 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): ) self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub() else: - rest_transport = {{ service.name }}RestTransport( - host=self._host, - credentials=self._credentials, - client_info=self._client_info, - ) - self._stubs['{{ method.transport_safe_name|snake_case }}'] = rest_transport.{{ method.transport_safe_name|snake_case }} + if self._rest_transport is None: + self._rest_transport = {{ service.name }}RestTransport( + host=self._host, + credentials=self._credentials, + client_info=self._client_info, + ) + self._stubs['{{ method.transport_safe_name|snake_case }}'] = self._rest_transport.{{ method.transport_safe_name|snake_case }} {% else %} self._stubs['{{ method.transport_safe_name|snake_case }}'] = self._logged_channel.{{ method.grpc_stub_type }}( '/{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}', @@ -468,6 +477,10 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): def close(self): self._logged_channel.close() + {% if service.has_resumable_upload_methods %} + if self._rest_transport is not None: + self._rest_transport.close() + {% endif %} {% include '%namespace/%name_%version/%sub/services/%service/transports/_mixins.py.j2' %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index d1422e834f61..5e800fe287f4 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -10,7 +10,7 @@ import json import pickle import logging as std_logging import warnings -from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import {% if service.has_resumable_upload_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async @@ -19,6 +19,9 @@ from google.api_core import retry_async as retries {% if service.has_lro %} from google.api_core import operations_v1 {% endif %} +{% if service.has_resumable_upload_methods %} +from google.api_core import resumable_transfer +{% endif %} from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -242,6 +245,9 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): {% if service.has_lro %} self._operations_client: Optional[operations_v1.OperationsAsyncClient] = None {% endif %} + {% if service.has_resumable_upload_methods %} + self._rest_transport: Optional[Any] = None + {% endif %} if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -346,7 +352,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): @property def {{ method.transport_safe_name|snake_case }}(self) -> Callable[ [{{ method.input.ident }}], - Awaitable[{{ method.output.ident }}]]: + Awaitable[{% if method.is_resumable_upload %}{{ method.client_output_async.ident }}{% else %}{{ method.output.ident }}{% endif %}]]: r"""Return a callable for the{{ ' ' }} {{- (method.name|snake_case).replace('_',' ')|wrap( width=70, offset=40, indent=8) -}} @@ -356,7 +362,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): Returns: Callable[[~.{{ method.input.name }}], - Awaitable[~.{{ method.output.name }}]]: + Awaitable[~.{% if method.is_resumable_upload %}{{ method.client_output_async.name }}{% else %}{{ method.output.name }}{% endif %}]]: A function that, when called, will call the underlying RPC on the server. """ @@ -385,18 +391,14 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): # validates for async credentials. transport = self class _AsyncRestStub: - def __init__(self): - self._rest_stub = None - def __call__(self, *args, **kwargs): - if self._rest_stub is None: - rest_transport = Async{{ service.name }}RestTransport( + if transport._rest_transport is None: + transport._rest_transport = Async{{ service.name }}RestTransport( host=transport._host, credentials=transport._credentials, client_info=transport._client_info, ) - self._rest_stub = rest_transport.{{ method.transport_safe_name|snake_case }} - return self._rest_stub(*args, **kwargs) + return transport._rest_transport.{{ method.transport_safe_name|snake_case }}(*args, **kwargs) self._stubs['{{ method.transport_safe_name|snake_case }}'] = _AsyncRestStub() else: class _UnsupportedStub: @@ -502,8 +504,15 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): {{ shared_macros.wrap_async_method_macro()|indent(4) }} + {% if service.has_resumable_upload_methods %} + async def close(self): + await self._logged_channel.close() + if self._rest_transport is not None: + await self._rest_transport.close() + {% else %} def close(self): return self._logged_channel.close() + {% endif %} @property def kind(self) -> str: diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index ca19955d796c..767db6c7c125 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -277,23 +277,24 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): uri = transcoded_request["uri"] upload_url = f"{self._host}{uri}" - headers = {**dict(metadata), **dict((config.headers or {}) if config else {})} + headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) else: config = dataclasses.replace(config, headers=headers) + session_kwargs: Dict[str, Any] = ( + {"start_timeout": timeout} + if isinstance(timeout, (int, float)) + else {} + ) return {{ method.client_output.ident }}( upload_url=upload_url, config=config, transport=self._session, response_type={{ method.output.ident }}, start_retry=retry if isinstance(retry, retries.Retry) else None, - **( - {"start_timeout": timeout} - if isinstance(timeout, (int, float)) - else {} - ), + **session_kwargs, ) {% else %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index c2ea111a2769..d9112d9f2f7f 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -236,23 +236,24 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): uri = transcoded_request["uri"] upload_url = f"{self._host}{uri}" - headers = {**dict(metadata), **dict((config.headers or {}) if config else {})} + headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) else: config = dataclasses.replace(config, headers=headers) + session_kwargs: Dict[str, Any] = ( + {"start_timeout": timeout} + if isinstance(timeout, (int, float)) + else {} + ) return {{ method.client_output_async.ident }}( upload_url=upload_url, config=config, transport=self._session, response_type={{ method.output.ident }}, start_retry=retry if isinstance(retry, retries.AsyncRetry) else None, - **( - {"start_timeout": timeout} - if isinstance(timeout, (int, float)) - else {} - ), + **session_kwargs, ) {% else %} diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 8d180dad0fac..2df348031d85 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1533,6 +1533,21 @@ def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_channel with pytest.raises(core_exceptions.GoogleAPICallError) as exc_info: transport.{{ method.transport_safe_name|snake_case }}({{ method.input.ident }}()) assert "operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel" in str(exc_info.value) + transport.close() + + +def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_rest_stub(): + transport = transports.{{ service.name }}GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + rest_transport = transport._rest_transport + assert rest_transport is not None + transport._stubs.pop("{{ method.transport_safe_name|snake_case }}", None) + _ = transport.{{ method.transport_safe_name|snake_case }} + assert transport._rest_transport is rest_transport + with mock.patch.object(rest_transport, "close") as mock_rest_close: + transport.close() + mock_rest_close.assert_called_once() @pytest.mark.asyncio @@ -1545,6 +1560,7 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a with pytest.raises(core_exceptions.GoogleAPICallError) as exc_info: await transport.{{ method.transport_safe_name|snake_case }}({{ method.input.ident }}()) assert "operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel" in str(exc_info.value) + await transport.close() @pytest.mark.asyncio @@ -1555,6 +1571,7 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a create=True, ) as mock_rest_transport_cls: mock_rest_transport = mock.MagicMock() + mock_rest_transport.close = mock.AsyncMock() expected_session = {{ method.client_output_async.ident }}() mock_rest_transport.{{ method.transport_safe_name|snake_case }} = mock.AsyncMock( return_value=expected_session @@ -1578,6 +1595,8 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a client_info=transport._client_info, ) assert mock_rest_transport.{{ method.transport_safe_name|snake_case }}.call_count == 2 + await transport.close() + mock_rest_transport.close.assert_called_once() @pytest.mark.asyncio diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py index b68c78d76a56..d7167ea9d354 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py @@ -62,13 +62,11 @@ from .transports.grpc import ResumableUploadServiceGrpcTransport from .transports.grpc_asyncio import ResumableUploadServiceGrpcAsyncIOTransport from .transports.rest import ResumableUploadServiceRestTransport -ASYNC_REST_EXCEPTION = None try: from .transports.rest_asyncio import AsyncResumableUploadServiceRestTransport HAS_ASYNC_REST_DEPENDENCIES = True -except ImportError as e: # pragma: NO COVER +except ImportError: # pragma: NO COVER HAS_ASYNC_REST_DEPENDENCIES = False - ASYNC_REST_EXCEPTION = e class ResumableUploadServiceClientMeta(type): @@ -99,7 +97,9 @@ def get_transport_class(cls, """ # If a specific transport is requested, return that one. if label == "rest_asyncio" and not HAS_ASYNC_REST_DEPENDENCIES: # pragma: NO COVER - raise ASYNC_REST_EXCEPTION + raise ImportError( + "`rest_asyncio` transport requires the library to be installed with the `async_rest` extra. Install the library with the `async_rest` extra using `pip install google-showcase[async_rest]`" + ) if label: return cls._transport_registry[label] diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py index 52b0d81ab4c2..01aee65c90a3 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py @@ -23,6 +23,7 @@ from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 from google.api_core import retry as retries +from google.api_core import resumable_transfer from google.auth import credentials as ga_credentials # type: ignore from google.oauth2 import service_account # type: ignore import google.protobuf @@ -197,8 +198,9 @@ def close(self): def upload_media(self) -> Callable[ [resumable_upload.UploadMediaRequest], Union[ - resumable_upload.UploadMediaResponse, - Awaitable[resumable_upload.UploadMediaResponse] + resumable_transfer.ResumableUploadSession, + resumable_transfer.AsyncResumableUploadSession, + Awaitable[resumable_transfer.AsyncResumableUploadSession] ]]: raise NotImplementedError() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py index 0df56c75d682..293255e7fba3 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py @@ -20,6 +20,7 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from google.api_core import exceptions as core_exceptions +from google.api_core import resumable_transfer from google.api_core import grpc_helpers from google.api_core import gapic_v1 import google.auth # type: ignore @@ -196,6 +197,7 @@ def __init__(self, *, self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials self._stubs: Dict[str, Callable] = {} + self._rest_transport: Optional[ResumableUploadServiceRestTransport] = None if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -323,14 +325,14 @@ def grpc_channel(self) -> grpc.Channel: @property def upload_media(self) -> Callable[ [resumable_upload.UploadMediaRequest], - resumable_upload.UploadMediaResponse]: + resumable_transfer.ResumableUploadSession]: r"""Return a callable for the upload media method over gRPC. A method with media_upload annotation enabled. Returns: Callable[[~.UploadMediaRequest], - ~.UploadMediaResponse]: + ~.ResumableUploadSession]: A function that, when called, will call the underlying RPC on the server. """ @@ -347,16 +349,19 @@ def __call__(self, *args, **kwargs): ) self._stubs['upload_media'] = _ErrorStub() else: - rest_transport = ResumableUploadServiceRestTransport( - host=self._host, - credentials=self._credentials, - client_info=self._client_info, - ) - self._stubs['upload_media'] = rest_transport.upload_media + if self._rest_transport is None: + self._rest_transport = ResumableUploadServiceRestTransport( + host=self._host, + credentials=self._credentials, + client_info=self._client_info, + ) + self._stubs['upload_media'] = self._rest_transport.upload_media return self._stubs['upload_media'] def close(self): self._logged_channel.close() + if self._rest_transport is not None: + self._rest_transport.close() @property def delete_operation( diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 1f833109c151..b0fd80b77f58 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -18,12 +18,13 @@ import pickle import logging as std_logging import warnings -from typing import Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Any, Awaitable, Callable, Dict, Optional, Sequence, Tuple, Union from google.api_core import gapic_v1 from google.api_core import grpc_helpers_async from google.api_core import exceptions as core_exceptions from google.api_core import retry_async as retries +from google.api_core import resumable_transfer from google.auth import credentials as ga_credentials # type: ignore from google.auth.transport.grpc import SslCredentials # type: ignore from google.protobuf.json_format import MessageToJson @@ -247,6 +248,7 @@ def __init__(self, *, self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials self._stubs: Dict[str, Callable] = {} + self._rest_transport: Optional[Any] = None if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -332,14 +334,14 @@ def grpc_channel(self) -> aio.Channel: @property def upload_media(self) -> Callable[ [resumable_upload.UploadMediaRequest], - Awaitable[resumable_upload.UploadMediaResponse]]: + Awaitable[resumable_transfer.AsyncResumableUploadSession]]: r"""Return a callable for the upload media method over gRPC. A method with media_upload annotation enabled. Returns: Callable[[~.UploadMediaRequest], - Awaitable[~.UploadMediaResponse]]: + Awaitable[~.AsyncResumableUploadSession]]: A function that, when called, will call the underlying RPC on the server. """ @@ -367,18 +369,14 @@ async def _error(): # validates for async credentials. transport = self class _AsyncRestStub: - def __init__(self): - self._rest_stub = None - def __call__(self, *args, **kwargs): - if self._rest_stub is None: - rest_transport = AsyncResumableUploadServiceRestTransport( + if transport._rest_transport is None: + transport._rest_transport = AsyncResumableUploadServiceRestTransport( host=transport._host, credentials=transport._credentials, client_info=transport._client_info, ) - self._rest_stub = rest_transport.upload_media - return self._rest_stub(*args, **kwargs) + return transport._rest_transport.upload_media(*args, **kwargs) self._stubs['upload_media'] = _AsyncRestStub() else: class _UnsupportedStub: @@ -449,8 +447,10 @@ def _wrap_method(self, func, *args, **kwargs): kwargs["kind"] = self.kind return gapic_v1.method_async.wrap_method(func, *args, **kwargs) - def close(self): - return self._logged_channel.close() + async def close(self): + await self._logged_channel.close() + if self._rest_transport is not None: + await self._rest_transport.close() @property def kind(self) -> str: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py index 7d4a2df0c411..bd073cf95131 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py @@ -428,23 +428,24 @@ def __call__(self, uri = transcoded_request["uri"] upload_url = f"{self._host}{uri}" - headers = {**dict(metadata), **dict((config.headers or {}) if config else {})} + headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) else: config = dataclasses.replace(config, headers=headers) + session_kwargs: Dict[str, Any] = ( + {"start_timeout": timeout} + if isinstance(timeout, (int, float)) + else {} + ) return resumable_transfer.ResumableUploadSession( upload_url=upload_url, config=config, transport=self._session, response_type=resumable_upload.UploadMediaResponse, start_retry=retry if isinstance(retry, retries.Retry) else None, - **( - {"start_timeout": timeout} - if isinstance(timeout, (int, float)) - else {} - ), + **session_kwargs, ) @property diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py index aad862b335de..406292ad60b0 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -237,23 +237,24 @@ async def __call__(self, uri = transcoded_request["uri"] upload_url = f"{self._host}{uri}" - headers = {**dict(metadata), **dict((config.headers or {}) if config else {})} + headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) else: config = dataclasses.replace(config, headers=headers) + session_kwargs: Dict[str, Any] = ( + {"start_timeout": timeout} + if isinstance(timeout, (int, float)) + else {} + ) return resumable_transfer.AsyncResumableUploadSession( upload_url=upload_url, config=config, transport=self._session, response_type=resumable_upload.UploadMediaResponse, start_retry=retry if isinstance(retry, retries.AsyncRetry) else None, - **( - {"start_timeout": timeout} - if isinstance(timeout, (int, float)) - else {} - ), + **session_kwargs, ) @property diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index cf2d33bb9722..fd46ebc24854 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -2287,6 +2287,21 @@ def test_resumable_upload_service_upload_media_grpc_channel_without_credentials_ with pytest.raises(core_exceptions.GoogleAPICallError) as exc_info: transport.upload_media(resumable_upload.UploadMediaRequest()) assert "operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel" in str(exc_info.value) + transport.close() + + +def test_resumable_upload_service_upload_media_grpc_rest_stub(): + transport = transports.ResumableUploadServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + rest_transport = transport._rest_transport + assert rest_transport is not None + transport._stubs.pop("upload_media", None) + _ = transport.upload_media + assert transport._rest_transport is rest_transport + with mock.patch.object(rest_transport, "close") as mock_rest_close: + transport.close() + mock_rest_close.assert_called_once() @pytest.mark.asyncio @@ -2299,6 +2314,7 @@ async def test_resumable_upload_service_upload_media_grpc_asyncio_channel_withou with pytest.raises(core_exceptions.GoogleAPICallError) as exc_info: await transport.upload_media(resumable_upload.UploadMediaRequest()) assert "operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel" in str(exc_info.value) + await transport.close() @pytest.mark.asyncio @@ -2309,6 +2325,7 @@ async def test_resumable_upload_service_upload_media_grpc_asyncio_rest_stub(): create=True, ) as mock_rest_transport_cls: mock_rest_transport = mock.MagicMock() + mock_rest_transport.close = mock.AsyncMock() expected_session = resumable_transfer.AsyncResumableUploadSession() mock_rest_transport.upload_media = mock.AsyncMock( return_value=expected_session @@ -2332,6 +2349,8 @@ async def test_resumable_upload_service_upload_media_grpc_asyncio_rest_stub(): client_info=transport._client_info, ) assert mock_rest_transport.upload_media.call_count == 2 + await transport.close() + mock_rest_transport.close.assert_called_once() @pytest.mark.asyncio From 8b13205218e99452763d34d23c36166747928e96 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 30 Sep 2026 18:03:29 +0000 Subject: [PATCH 12/45] address review feedback --- .../%sub/services/%service/transports/rest_base.py.j2 | 2 +- .../services/resumable_upload_service/transports/rest_base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_base.py.j2 index 08887c16eb51..005bdceaeff1 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_base.py.j2 @@ -120,7 +120,7 @@ class _Base{{ service.name }}RestTransport({{service.name}}Transport): {% set method_http_options = method.http_options %} - {{ shared_macros.http_options_method(method_http_options)|indent(8) }} + {{ shared_macros.http_options_method(method_http_options, method.is_resumable_upload, method.resumable_upload_prefix)|indent(8) }} {% endif %}{# method.http_options and not method.client_streaming #} {% endfor %} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_base.py index a02842ea30de..207171c714db 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_base.py @@ -97,7 +97,7 @@ def __hash__(self): # pragma: NO COVER def _get_http_options(): http_options: List[Dict[str, str]] = [{ 'method': 'post', - 'uri': '/v1beta1/files:upload', + 'uri': '/resumable/upload/v1beta1/files:upload', 'body': '*', }, ] From e786d2d69d1d5afe2f0b126e3e3f6f7fda93d26b Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 30 Sep 2026 20:29:22 +0000 Subject: [PATCH 13/45] update goldens --- .../google/cloud/redis_v1/services/cloud_redis/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py index 7b2e7759cd73..6246a9bf619c 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py @@ -62,13 +62,11 @@ from .transports.grpc import CloudRedisGrpcTransport from .transports.grpc_asyncio import CloudRedisGrpcAsyncIOTransport from .transports.rest import CloudRedisRestTransport -ASYNC_REST_EXCEPTION = None try: from .transports.rest_asyncio import AsyncCloudRedisRestTransport HAS_ASYNC_REST_DEPENDENCIES = True -except ImportError as e: # pragma: NO COVER +except ImportError: # pragma: NO COVER HAS_ASYNC_REST_DEPENDENCIES = False - ASYNC_REST_EXCEPTION = e class CloudRedisClientMeta(type): @@ -99,7 +97,9 @@ def get_transport_class(cls, """ # If a specific transport is requested, return that one. if label == "rest_asyncio" and not HAS_ASYNC_REST_DEPENDENCIES: # pragma: NO COVER - raise ASYNC_REST_EXCEPTION + raise ImportError( + "`rest_asyncio` transport requires the library to be installed with the `async_rest` extra. Install the library with the `async_rest` extra using `pip install google-cloud-redis[async_rest]`" + ) if label: return cls._transport_registry[label] From fc1aa349e84574551f0a4caf5c477b93d70c4de9 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 30 Sep 2026 20:44:07 +0000 Subject: [PATCH 14/45] update goldens --- .../google/cloud/redis_v1/services/cloud_redis/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py index 771b0baa9989..7ae8f6ebdff5 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py @@ -62,13 +62,11 @@ from .transports.grpc import CloudRedisGrpcTransport from .transports.grpc_asyncio import CloudRedisGrpcAsyncIOTransport from .transports.rest import CloudRedisRestTransport -ASYNC_REST_EXCEPTION = None try: from .transports.rest_asyncio import AsyncCloudRedisRestTransport HAS_ASYNC_REST_DEPENDENCIES = True -except ImportError as e: # pragma: NO COVER +except ImportError: # pragma: NO COVER HAS_ASYNC_REST_DEPENDENCIES = False - ASYNC_REST_EXCEPTION = e class CloudRedisClientMeta(type): @@ -99,7 +97,9 @@ def get_transport_class(cls, """ # If a specific transport is requested, return that one. if label == "rest_asyncio" and not HAS_ASYNC_REST_DEPENDENCIES: # pragma: NO COVER - raise ASYNC_REST_EXCEPTION + raise ImportError( + "`rest_asyncio` transport requires the library to be installed with the `async_rest` extra. Install the library with the `async_rest` extra using `pip install google-cloud-redis[async_rest]`" + ) if label: return cls._transport_registry[label] From e59e867e329b4482962806b8aa68b64b8614a7ab Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 15:23:57 +0000 Subject: [PATCH 15/45] address review feedback --- .../%sub/services/%service/transports/base.py.j2 | 3 ++- .../services/resumable_upload_service/transports/base.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 index dc873592aa2c..cd8d35c1bafc 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -142,7 +142,8 @@ class {{ service.name }}Transport(abc.ABC): # Save the credentials. {% if service.has_resumable_upload_methods %} - self._credentials: Any = credentials + #self._credentials: Any = credentials + self._credentials = credentials self._client_info = client_info {% else %} self._credentials = credentials diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py index 01aee65c90a3..4f46984f86fd 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py @@ -116,7 +116,8 @@ def __init__( credentials = credentials.with_always_use_jwt_access(True) # Save the credentials. - self._credentials: Any = credentials + #self._credentials: Any = credentials + self._credentials = credentials self._client_info = client_info # Save the hostname. Default to port 443 (HTTPS) if none is specified. From 03d618b0c5aa9c7c7d4260ede51110742c7f0563 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 15:53:11 +0000 Subject: [PATCH 16/45] address review feedback --- .../%sub/services/%service/transports/base.py.j2 | 7 ++----- .../%sub/services/%service/transports/grpc_asyncio.py.j2 | 6 +++++- .../services/resumable_upload_service/transports/base.py | 3 +-- .../resumable_upload_service/transports/grpc_asyncio.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 index cd8d35c1bafc..a19c8a242463 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/base.py.j2 @@ -3,7 +3,7 @@ {% block content %} import abc -from typing import {% if service.any_extended_operations_methods or service.has_resumable_upload_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union +from typing import {% if service.any_extended_operations_methods %}Any, {% endif %}Awaitable, Callable, Dict, Optional, Sequence, Union {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} from {{package_path}} import gapic_version as package_version @@ -141,12 +141,9 @@ class {{ service.name }}Transport(abc.ABC): credentials = credentials.with_always_use_jwt_access(True) # Save the credentials. - {% if service.has_resumable_upload_methods %} - #self._credentials: Any = credentials self._credentials = credentials + {% if service.has_resumable_upload_methods %} self._client_info = client_info - {% else %} - self._credentials = credentials {% endif %} # Save the hostname. Default to port 443 (HTTPS) if none is specified. diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 5e800fe287f4..62bfb9fdcbff 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -393,9 +393,13 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): class _AsyncRestStub: def __call__(self, *args, **kwargs): if transport._rest_transport is None: + {# Note: Type for creds is ignored due to the type hint mismatch for credentials in the base transport. + # TODO(https://github.com/googleapis/google-cloud-python/issues/16268): Remove `# type: ignore` once + # the type hints for credentials are updated to include asynchronous credentials. + #} transport._rest_transport = Async{{ service.name }}RestTransport( host=transport._host, - credentials=transport._credentials, + credentials=transport._credentials, # type: ignore client_info=transport._client_info, ) return transport._rest_transport.{{ method.transport_safe_name|snake_case }}(*args, **kwargs) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py index 4f46984f86fd..0009a9c91cbb 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/base.py @@ -14,7 +14,7 @@ # limitations under the License. # import abc -from typing import Any, Awaitable, Callable, Dict, Optional, Sequence, Union +from typing import Awaitable, Callable, Dict, Optional, Sequence, Union from google.showcase_v1beta1 import gapic_version as package_version @@ -116,7 +116,6 @@ def __init__( credentials = credentials.with_always_use_jwt_access(True) # Save the credentials. - #self._credentials: Any = credentials self._credentials = credentials self._client_info = client_info diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index b0fd80b77f58..632980091aa8 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -373,7 +373,7 @@ def __call__(self, *args, **kwargs): if transport._rest_transport is None: transport._rest_transport = AsyncResumableUploadServiceRestTransport( host=transport._host, - credentials=transport._credentials, + credentials=transport._credentials, # type: ignore client_info=transport._client_info, ) return transport._rest_transport.upload_media(*args, **kwargs) From b22f24f16d35ead6d0e5269d997f67ec62dd4a83 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 16:45:19 +0000 Subject: [PATCH 17/45] address review feedback --- .../gapic/generator/generator.py | 23 ++++++++++++++++--- .../tests/unit/generator/test_generator.py | 23 ++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/gapic-generator/gapic/generator/generator.py b/packages/gapic-generator/gapic/generator/generator.py index 078c647eac77..c1a90b98b34c 100644 --- a/packages/gapic-generator/gapic/generator/generator.py +++ b/packages/gapic-generator/gapic/generator/generator.py @@ -342,7 +342,9 @@ def _render_template( ) or ( "transport" in template_name - and not self._is_desired_transport(template_name, opts) + and not self._is_desired_transport( + template_name, opts, service=service + ) ) or # TODO(https://github.com/googleapis/gapic-generator-python/issues/2121): Remove this condition when async rest is GA. @@ -361,7 +363,11 @@ def _render_template( ].python_settings.experimental_features.rest_async_io_enabled and not service.has_resumable_upload_methods ) - or ("rest_base" in template_name and "rest" not in opts.transport) + or ( + "rest_base" in template_name + and "rest" not in opts.transport + and not service.has_resumable_upload_methods + ) ): continue @@ -388,9 +394,20 @@ def _render_template( ) return answer - def _is_desired_transport(self, template_name: str, opts: Options) -> bool: + def _is_desired_transport( + self, + template_name: str, + opts: Options, + service: Optional[Any] = None, + ) -> bool: """Returns true if template name contains a desired transport""" desired_transports = ["__init__", "base", "README"] + opts.transport + if ( + service is not None + and service.has_resumable_upload_methods + and "rest" not in desired_transports + ): + desired_transports.append("rest") return any(transport in template_name for transport in desired_transports) def _get_file( diff --git a/packages/gapic-generator/tests/unit/generator/test_generator.py b/packages/gapic-generator/tests/unit/generator/test_generator.py index 96226a0f12e7..335738896c2c 100644 --- a/packages/gapic-generator/tests/unit/generator/test_generator.py +++ b/packages/gapic-generator/tests/unit/generator/test_generator.py @@ -264,12 +264,14 @@ def test_get_response_ignores_unwanted_transports_and_clients(): def test_get_response_resumable_upload_generates_async_client_and_rest_asyncio(): - # Verify that async_client.py and transports/rest_asyncio.py are generated - # when a service has a resumable upload method, even if rest_async_io_enabled - # is False and grpc transport is not requested. + # Verify that async_client.py and REST transport files (rest.py, rest_base.py, + # rest_asyncio.py) are generated when a service has a resumable upload method, + # even if rest_async_io_enabled is False or only grpc transport is requested. generator_obj = make_generator() with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as list_templates: list_templates.return_value = [ + "foo/%service/transports/grpc.py.j2", + "foo/%service/transports/grpc_asyncio.py.j2", "foo/%service/transports/rest.py.j2", "foo/%service/transports/rest_asyncio.py.j2", "foo/%service/transports/rest_base.py.j2", @@ -319,6 +321,21 @@ def test_get_response_resumable_upload_generates_async_client_and_rest_asyncio() "foo/resumable_upload_service/client.py", } + cgr_grpc = generator_obj.get_response( + api_schema=api_schema, opts=Options.build("transport=grpc") + ) + assert {i.name for i in cgr_grpc.file} == { + "foo/resumable_upload_service/transports/grpc.py", + "foo/resumable_upload_service/transports/grpc_asyncio.py", + "foo/resumable_upload_service/transports/rest.py", + "foo/resumable_upload_service/transports/rest_asyncio.py", + "foo/resumable_upload_service/transports/rest_base.py", + "foo/resumable_upload_service/transports/__init__.py", + "foo/resumable_upload_service/transports/base.py", + "foo/resumable_upload_service/async_client.py", + "foo/resumable_upload_service/client.py", + } + def test_get_response_enumerates_services(): generator_obj = make_generator() From 00c2ce2b397548185ce2a3ecfdfd0b4ca2843b57 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 16:55:16 +0000 Subject: [PATCH 18/45] address review feedback --- .../%sub/services/%service/transports/grpc.py.j2 | 5 +++++ .../%sub/services/%service/transports/grpc_asyncio.py.j2 | 5 +++++ .../services/resumable_upload_service/transports/grpc.py | 5 +++++ .../resumable_upload_service/transports/grpc_asyncio.py | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index 6dc44f887f56..7eaae7e1ad4a 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -194,6 +194,11 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): self._operations_client: Optional[operations_v1.OperationsClient] = None {% endif %} {% if service.has_resumable_upload_methods %} + # Resumable upload RPCs operate over HTTP/REST rather than gRPC. + # Since gRPC is the default transport for GAPIC clients and services + # mix standard gRPC RPCs with resumable upload RPCs on the same client, + # gRPC transports delegate resumable upload calls to an internal REST + # transport instance. self._rest_transport: Optional[{{ service.name }}RestTransport] = None {% endif %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 62bfb9fdcbff..ef7741b3e3c3 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -246,6 +246,11 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): self._operations_client: Optional[operations_v1.OperationsAsyncClient] = None {% endif %} {% if service.has_resumable_upload_methods %} + # Resumable upload RPCs operate over HTTP/REST rather than gRPC. + # Since gRPC is the default transport for GAPIC clients and services + # mix standard gRPC RPCs with resumable upload RPCs on the same client, + # gRPC transports delegate resumable upload calls to an internal REST + # transport instance. self._rest_transport: Optional[Any] = None {% endif %} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py index 293255e7fba3..3e9f93e2afa1 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py @@ -197,6 +197,11 @@ def __init__(self, *, self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials self._stubs: Dict[str, Callable] = {} + # Resumable upload RPCs operate over HTTP/REST rather than gRPC. + # Since gRPC is the default transport for GAPIC clients and services + # mix standard gRPC RPCs with resumable upload RPCs on the same client, + # gRPC transports delegate resumable upload calls to an internal REST + # transport instance. self._rest_transport: Optional[ResumableUploadServiceRestTransport] = None if api_mtls_endpoint: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 632980091aa8..32dd68cf04d0 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -248,6 +248,11 @@ def __init__(self, *, self._grpc_channel = None self._ssl_channel_credentials = ssl_channel_credentials self._stubs: Dict[str, Callable] = {} + # Resumable upload RPCs operate over HTTP/REST rather than gRPC. + # Since gRPC is the default transport for GAPIC clients and services + # mix standard gRPC RPCs with resumable upload RPCs on the same client, + # gRPC transports delegate resumable upload calls to an internal REST + # transport instance. self._rest_transport: Optional[Any] = None if api_mtls_endpoint: From 28161a8f41d1e41804905488a77dd865bc67b88e Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 17:26:44 +0000 Subject: [PATCH 19/45] address review feedback --- .../%sub/services/%service/_shared_macros.j2 | 52 +++++++++++++++++++ .../services/%service/transports/rest.py.j2 | 35 +------------ .../%service/transports/rest_asyncio.py.j2 | 35 +------------ 3 files changed, 54 insertions(+), 68 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 index 685e8505d496..ea9dbec3e2dc 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 @@ -274,6 +274,58 @@ def _get_http_options(): {% endmacro %} +{# rest_resumable_upload_call_method_common includes the common code for a rest + resumable upload __call__ method to be re-used for sync and async REST + __call__ implementation. + + Args: + method: The method. + service: The service. + is_async (bool): Used to determine the code path i.e. whether for sync or async call. + rest_numeric_enums (bool): Used to determine whether to encode enums as numbers. #} +{% macro rest_resumable_upload_call_method_common(method, service, is_async=False, rest_numeric_enums=False) %} +{% set service_name = service.name %} +{% set method_name = method.name %} +{% set await_prefix = "await " if is_async else "" %} +{% set client_output_ident = method.client_output_async.ident if is_async else method.client_output.ident %} +{% set retry_class = "retries.AsyncRetry" if is_async else "retries.Retry" %} +http_options = _Base{{ service_name }}RestTransport._Base{{ method_name }}._get_http_options() +request, metadata = {{ await_prefix }}self._interceptor.pre_{{ method_name|snake_case }}(request, metadata) +transcoded_request, body, query_params = transcode_request( + http_options, + request, + required_fields_default_values=getattr( + _Base{{ service_name }}RestTransport._Base{{ method_name }}, + "_Base{{ method_name }}__REQUIRED_FIELDS_DEFAULT_VALUES", + None, + ), + rest_numeric_enums={{ rest_numeric_enums }}, +) + +uri = transcoded_request["uri"] +upload_url = f"{self._host}{uri}" +headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} +if config is None: + config = resumable_transfer.ResumableUploadConfig(headers=headers) +else: + config = dataclasses.replace(config, headers=headers) + +session_kwargs: Dict[str, Any] = ( + {"start_timeout": timeout} + if isinstance(timeout, (int, float)) + else {} +) +return {{ client_output_ident }}( + upload_url=upload_url, + config=config, + transport=self._session, + response_type={{ method.output.ident }}, + start_retry=retry if isinstance(retry, {{ retry_class }}) else None, + **session_kwargs, +) +{%- endmacro %} + + {% macro unary_request_interceptor_common(service) %} logging_enabled = CLIENT_LOGGING_SUPPORTED and _LOGGER.isEnabledFor(std_logging.DEBUG) if logging_enabled: # pragma: NO COVER diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index 767db6c7c125..ffc08c118995 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -262,40 +262,7 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {% endif %} """ {% if method.is_resumable_upload %} - http_options = _Base{{ service.name }}RestTransport._Base{{ method.name }}._get_http_options() - request, metadata = self._interceptor.pre_{{ method.name|snake_case }}(request, metadata) - transcoded_request, body, query_params = transcode_request( - http_options, - request, - required_fields_default_values=getattr( - _Base{{ service.name }}RestTransport._Base{{ method.name }}, - "_Base{{ method.name }}__REQUIRED_FIELDS_DEFAULT_VALUES", - None, - ), - rest_numeric_enums={{ opts.rest_numeric_enums }}, - ) - - uri = transcoded_request["uri"] - upload_url = f"{self._host}{uri}" - headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} - if config is None: - config = resumable_transfer.ResumableUploadConfig(headers=headers) - else: - config = dataclasses.replace(config, headers=headers) - - session_kwargs: Dict[str, Any] = ( - {"start_timeout": timeout} - if isinstance(timeout, (int, float)) - else {} - ) - return {{ method.client_output.ident }}( - upload_url=upload_url, - config=config, - transport=self._session, - response_type={{ method.output.ident }}, - start_retry=retry if isinstance(retry, retries.Retry) else None, - **session_kwargs, - ) + {{ shared_macros.rest_resumable_upload_call_method_common(method, service, False, opts.rest_numeric_enums)|indent(12) }} {% else %} {{ shared_macros.rest_call_method_common(body_spec, method.name, service, False, method.input.ident.is_proto_plus_type, opts.rest_numeric_enums)|indent(8) }} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index d9112d9f2f7f..2b5b20cfa4ab 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -221,40 +221,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {% endif %} """ {% if method.is_resumable_upload %} - http_options = _Base{{ service.name }}RestTransport._Base{{ method.name }}._get_http_options() - request, metadata = await self._interceptor.pre_{{ method.name|snake_case }}(request, metadata) - transcoded_request, body, query_params = transcode_request( - http_options, - request, - required_fields_default_values=getattr( - _Base{{ service.name }}RestTransport._Base{{ method.name }}, - "_Base{{ method.name }}__REQUIRED_FIELDS_DEFAULT_VALUES", - None, - ), - rest_numeric_enums={{ opts.rest_numeric_enums }}, - ) - - uri = transcoded_request["uri"] - upload_url = f"{self._host}{uri}" - headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} - if config is None: - config = resumable_transfer.ResumableUploadConfig(headers=headers) - else: - config = dataclasses.replace(config, headers=headers) - - session_kwargs: Dict[str, Any] = ( - {"start_timeout": timeout} - if isinstance(timeout, (int, float)) - else {} - ) - return {{ method.client_output_async.ident }}( - upload_url=upload_url, - config=config, - transport=self._session, - response_type={{ method.output.ident }}, - start_retry=retry if isinstance(retry, retries.AsyncRetry) else None, - **session_kwargs, - ) + {{ shared_macros.rest_resumable_upload_call_method_common(method, service, True, opts.rest_numeric_enums)|indent(12) }} {% else %} {{ shared_macros.rest_call_method_common(body_spec, method.name, service, True, method.input.ident.is_proto_plus_type, opts.rest_numeric_enums)|indent(8) }} From 94511d9642dfa393aed6a4bb4e8a98fb10a7b866 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 17:41:09 +0000 Subject: [PATCH 20/45] address review feedback --- .../%sub/services/%service/transports/grpc_asyncio.py.j2 | 6 ++++++ .../resumable_upload_service/transports/grpc_asyncio.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index ef7741b3e3c3..93b8bae7e8ae 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -387,6 +387,12 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): return _error() self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub() elif HAS_ASYNC_REST: + # TODO(https://github.com/googleapis/google-cloud-python/issues/16268): + # Resolve the credential type mismatch between {{ service.grpc_asyncio_transport_name }} + # (where grpc_helpers_async.create_channel and the base transport's + # google.auth.default() path use synchronous google.auth.credentials.Credentials) + # and Async{{ service.name }}RestTransport (where AsyncAuthorizedSession + # requires asynchronous google.auth.aio.credentials.Credentials). # Defer instantiating Async{{ service.name }}RestTransport until the # resumable upload RPC is called so that initializing # {{ service.grpc_asyncio_transport_name }} with synchronous diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 32dd68cf04d0..97ed2bef4154 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -365,6 +365,12 @@ async def _error(): return _error() self._stubs['upload_media'] = _ErrorStub() elif HAS_ASYNC_REST: + # TODO(https://github.com/googleapis/google-cloud-python/issues/16268): + # Resolve the credential type mismatch between ResumableUploadServiceGrpcAsyncIOTransport + # (where grpc_helpers_async.create_channel and the base transport's + # google.auth.default() path use synchronous google.auth.credentials.Credentials) + # and AsyncResumableUploadServiceRestTransport (where AsyncAuthorizedSession + # requires asynchronous google.auth.aio.credentials.Credentials). # Defer instantiating AsyncResumableUploadServiceRestTransport until the # resumable upload RPC is called so that initializing # ResumableUploadServiceGrpcAsyncIOTransport with synchronous From d51b71c1be404b0cf800ada8a46eee8ac41472aa Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 19:55:43 +0000 Subject: [PATCH 21/45] add test for bug where request body is not passed --- .../%name_%version/%sub/test_%service.py.j2 | 139 ++++++++++++++++++ .../test_resumable_upload_service.py | 101 +++++++++++++ 2 files changed, 240 insertions(+) diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 2df348031d85..cb9616f855ed 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1704,6 +1704,145 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_a assert isinstance(result, {{ method.output.ident }}) assert result == expected_result {% endif %} + + +def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_body_and_query_params(): + client = {{ service.client_name }}( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + with mock.patch.object(type(client.transport._session), "request") as req, mock.patch.object( + path_template, "transcode" + ) as transcode: + request = {{ method.input.ident }}( + {% for field in method.input.fields.values() | rejectattr('message') %} + {% if not field.oneof or field.proto3_optional %} + {{ field.name }}={{ field.mock_value }}, + {% endif %} + {% endfor %} + ) + {% if method.input.ident.is_proto_plus_type %} + pb_request = {{ method.input.ident }}.pb(request) + {% else %} + pb_request = request + {% endif %} + transcode.return_value = { + "uri": "v1/sample_method", + "method": "{{ method.http_options[0].method }}", + "body": pb_request, + "query_params": pb_request, + } + + start_response = mock.MagicMock() + start_response.ok = True + start_response.status_code = 200 + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + start_response.content = b"" + + expected_result = {{ method.output.ident }}() + {% if method.output.ident.is_proto_plus_type %} + json_return_value = {{ method.output.ident }}.to_json(expected_result) + {% else %} + json_return_value = json_format.MessageToJson(expected_result) + {% endif %} + upload_response = mock.MagicMock() + upload_response.ok = True + upload_response.status_code = 200 + upload_response.headers = {"x-goog-upload-status": "final"} + upload_response.content = json_return_value.encode("UTF-8") + + req.side_effect = [start_response, upload_response] + response = client.{{ method.client_method_name|snake_case }}(request) + result = response.upload(b"test data") + assert result == expected_result + + start_call = req.call_args_list[0] + {% for field in method.input.fields.values() | rejectattr('message') %} + {% if not field.oneof or field.proto3_optional %} + assert "{{ field.name | camel_case }}=" in start_call.args[1] + {% endif %} + {% endfor %} + {% if method.http_options[0].body %} + expected_body = json_format.MessageToJson( + pb_request, + use_integers_for_enums={{ opts.rest_numeric_enums }}, + ) + assert start_call.kwargs["data"] == expected_body.encode("UTF-8") + {% endif %} + + +{% if rest_async_io_enabled %} +@pytest.mark.asyncio +async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_asyncio_body_and_query_params(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip() + client = {{ service.async_client_name }}( + credentials=async_anonymous_credentials(), + transport="rest_asyncio", + ) + with mock.patch.object( + type(client.transport._session), "request", new_callable=mock.MagicMock + ) as req, mock.patch.object(path_template, "transcode") as transcode: + request = {{ method.input.ident }}( + {% for field in method.input.fields.values() | rejectattr('message') %} + {% if not field.oneof or field.proto3_optional %} + {{ field.name }}={{ field.mock_value }}, + {% endif %} + {% endfor %} + ) + {% if method.input.ident.is_proto_plus_type %} + pb_request = {{ method.input.ident }}.pb(request) + {% else %} + pb_request = request + {% endif %} + transcode.return_value = { + "uri": "v1/sample_method", + "method": "{{ method.http_options[0].method }}", + "body": pb_request, + "query_params": pb_request, + } + + start_response = mock.MagicMock() + start_response.status = 200 + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + start_response.read = mock.AsyncMock(return_value=b"") + start_ctx = mock.MagicMock() + start_ctx.__aenter__ = mock.AsyncMock(return_value=start_response) + start_ctx.__aexit__ = mock.AsyncMock(return_value=None) + + expected_result = {{ method.output.ident }}() + {% if method.output.ident.is_proto_plus_type %} + json_return_value = {{ method.output.ident }}.to_json(expected_result) + {% else %} + json_return_value = json_format.MessageToJson(expected_result) + {% endif %} + upload_response = mock.MagicMock() + upload_response.status = 200 + upload_response.headers = {"x-goog-upload-status": "final"} + upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) + upload_ctx = mock.MagicMock() + upload_ctx.__aenter__ = mock.AsyncMock(return_value=upload_response) + upload_ctx.__aexit__ = mock.AsyncMock(return_value=None) + + req.side_effect = [start_ctx, upload_ctx] + response = await client.{{ method.client_method_name|snake_case }}(request) + result = await response.upload(b"test data") + assert result == expected_result + + start_call = req.call_args_list[0] + {% for field in method.input.fields.values() | rejectattr('message') %} + {% if not field.oneof or field.proto3_optional %} + assert "{{ field.name | camel_case }}=" in start_call.args[1] + {% endif %} + {% endfor %} + {% if method.http_options[0].body %} + expected_body = json_format.MessageToJson( + pb_request, + use_integers_for_enums={{ opts.rest_numeric_enums }}, + ) + assert start_call.kwargs["data"] == expected_body.encode("UTF-8") + {% endif %} +{% endif %} {% endfor %} {% endif %} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index fd46ebc24854..86b3c6e209d6 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -2440,6 +2440,107 @@ async def test_resumable_upload_service_upload_media_rest_asyncio_start_empty_bo assert result == expected_result +def test_resumable_upload_service_upload_media_rest_body_and_query_params(): + client = ResumableUploadServiceClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + with mock.patch.object(type(client.transport._session), "request") as req, mock.patch.object( + path_template, "transcode" + ) as transcode: + request = resumable_upload.UploadMediaRequest( + name='name_value', + ) + pb_request = resumable_upload.UploadMediaRequest.pb(request) + transcode.return_value = { + "uri": "v1/sample_method", + "method": "post", + "body": pb_request, + "query_params": pb_request, + } + + start_response = mock.MagicMock() + start_response.ok = True + start_response.status_code = 200 + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + start_response.content = b"" + + expected_result = resumable_upload.UploadMediaResponse() + json_return_value = resumable_upload.UploadMediaResponse.to_json(expected_result) + upload_response = mock.MagicMock() + upload_response.ok = True + upload_response.status_code = 200 + upload_response.headers = {"x-goog-upload-status": "final"} + upload_response.content = json_return_value.encode("UTF-8") + + req.side_effect = [start_response, upload_response] + response = client.upload_media(request) + result = response.upload(b"test data") + assert result == expected_result + + start_call = req.call_args_list[0] + assert "name=" in start_call.args[1] + expected_body = json_format.MessageToJson( + pb_request, + use_integers_for_enums=True, + ) + assert start_call.kwargs["data"] == expected_body.encode("UTF-8") + + +@pytest.mark.asyncio +async def test_resumable_upload_service_upload_media_rest_asyncio_body_and_query_params(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip() + client = ResumableUploadServiceAsyncClient( + credentials=async_anonymous_credentials(), + transport="rest_asyncio", + ) + with mock.patch.object( + type(client.transport._session), "request", new_callable=mock.MagicMock + ) as req, mock.patch.object(path_template, "transcode") as transcode: + request = resumable_upload.UploadMediaRequest( + name='name_value', + ) + pb_request = resumable_upload.UploadMediaRequest.pb(request) + transcode.return_value = { + "uri": "v1/sample_method", + "method": "post", + "body": pb_request, + "query_params": pb_request, + } + + start_response = mock.MagicMock() + start_response.status = 200 + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + start_response.read = mock.AsyncMock(return_value=b"") + start_ctx = mock.MagicMock() + start_ctx.__aenter__ = mock.AsyncMock(return_value=start_response) + start_ctx.__aexit__ = mock.AsyncMock(return_value=None) + + expected_result = resumable_upload.UploadMediaResponse() + json_return_value = resumable_upload.UploadMediaResponse.to_json(expected_result) + upload_response = mock.MagicMock() + upload_response.status = 200 + upload_response.headers = {"x-goog-upload-status": "final"} + upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) + upload_ctx = mock.MagicMock() + upload_ctx.__aenter__ = mock.AsyncMock(return_value=upload_response) + upload_ctx.__aexit__ = mock.AsyncMock(return_value=None) + + req.side_effect = [start_ctx, upload_ctx] + response = await client.upload_media(request) + result = await response.upload(b"test data") + assert result == expected_result + + start_call = req.call_args_list[0] + assert "name=" in start_call.args[1] + expected_body = json_format.MessageToJson( + pb_request, + use_integers_for_enums=True, + ) + assert start_call.kwargs["data"] == expected_body.encode("UTF-8") + + # Remove this test when deprecated arguments (api_mtls_endpoint, client_cert_source) are # removed from grpc/grpc_asyncio transport constructor. @pytest.mark.filterwarnings("ignore::FutureWarning") From 485a92b89b4aa8820157afdd24cf229ad975cb24 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 20:02:37 +0000 Subject: [PATCH 22/45] fix: forward transcoded request body and query params in resumable uploads --- .../%sub/services/%service/_shared_macros.j2 | 14 ++++++++++++-- .../%sub/services/%service/transports/rest.py.j2 | 2 ++ .../%service/transports/rest_asyncio.py.j2 | 2 ++ .../resumable_upload_service/transports/rest.py | 11 +++++++++-- .../transports/rest_asyncio.py | 10 ++++++++-- 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 index ea9dbec3e2dc..9b70606d8960 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 @@ -286,6 +286,7 @@ def _get_http_options(): {% macro rest_resumable_upload_call_method_common(method, service, is_async=False, rest_numeric_enums=False) %} {% set service_name = service.name %} {% set method_name = method.name %} +{% set body_spec = method.http_options[0].body %} {% set await_prefix = "await " if is_async else "" %} {% set client_output_ident = method.client_output_async.ident if is_async else method.client_output.ident %} {% set retry_class = "retries.AsyncRetry" if is_async else "retries.Retry" %} @@ -303,7 +304,9 @@ transcoded_request, body, query_params = transcode_request( ) uri = transcoded_request["uri"] -upload_url = f"{self._host}{uri}" +params = rest_helpers.flatten_query_params(query_params, strict=True) +query_string = f"?{urllib.parse.urlencode(params)}" if params else "" +upload_url = f"{self._host}{uri}{query_string}" headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) @@ -315,7 +318,7 @@ session_kwargs: Dict[str, Any] = ( if isinstance(timeout, (int, float)) else {} ) -return {{ client_output_ident }}( +session = {{ client_output_ident }}( upload_url=upload_url, config=config, transport=self._session, @@ -323,6 +326,13 @@ return {{ client_output_ident }}( start_retry=retry if isinstance(retry, {{ retry_class }}) else None, **session_kwargs, ) +{% if body_spec %} +session.upload = functools.partial(session.upload, request_body=body or "") # type: ignore[method-assign] +{% if not is_async %} +session.iter_upload = functools.partial(session.iter_upload, request_body=body or "") # type: ignore[method-assign] +{% endif %} +{% endif %} +return session {%- endmacro %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index ffc08c118995..9c6c4c9375b8 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -14,6 +14,8 @@ from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 {% if service.has_resumable_upload_methods %} +import functools +import urllib.parse from google.api_core import resumable_transfer {% endif %} {% set package_path = api.naming.module_namespace|join('.') + "." + api.naming.versioned_module_name %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index 2b5b20cfa4ab..e3487b8ae54d 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -22,6 +22,8 @@ from google.auth.aio import credentials as ga_credentials_async # type: ignore from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 {% if service.has_resumable_upload_methods %} +import functools +import urllib.parse from google.api_core import resumable_transfer {% endif %} {% if service.has_lro %} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py index bd073cf95131..6ab112f03ae6 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py @@ -23,6 +23,8 @@ from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 +import functools +import urllib.parse from google.api_core import resumable_transfer from google.showcase_v1beta1._compat import transcode_request import google.protobuf @@ -427,7 +429,9 @@ def __call__(self, ) uri = transcoded_request["uri"] - upload_url = f"{self._host}{uri}" + params = rest_helpers.flatten_query_params(query_params, strict=True) + query_string = f"?{urllib.parse.urlencode(params)}" if params else "" + upload_url = f"{self._host}{uri}{query_string}" headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) @@ -439,7 +443,7 @@ def __call__(self, if isinstance(timeout, (int, float)) else {} ) - return resumable_transfer.ResumableUploadSession( + session = resumable_transfer.ResumableUploadSession( upload_url=upload_url, config=config, transport=self._session, @@ -447,6 +451,9 @@ def __call__(self, start_retry=retry if isinstance(retry, retries.Retry) else None, **session_kwargs, ) + session.upload = functools.partial(session.upload, request_body=body or "") # type: ignore[method-assign] + session.iter_upload = functools.partial(session.iter_upload, request_body=body or "") # type: ignore[method-assign] + return session @property def upload_media(self) -> Callable[ diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py index 406292ad60b0..235fd264de36 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -27,6 +27,8 @@ from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +import functools +import urllib.parse from google.api_core import resumable_transfer from google.iam.v1 import iam_policy_pb2 # type: ignore from google.iam.v1 import policy_pb2 # type: ignore @@ -236,7 +238,9 @@ async def __call__(self, ) uri = transcoded_request["uri"] - upload_url = f"{self._host}{uri}" + params = rest_helpers.flatten_query_params(query_params, strict=True) + query_string = f"?{urllib.parse.urlencode(params)}" if params else "" + upload_url = f"{self._host}{uri}{query_string}" headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) @@ -248,7 +252,7 @@ async def __call__(self, if isinstance(timeout, (int, float)) else {} ) - return resumable_transfer.AsyncResumableUploadSession( + session = resumable_transfer.AsyncResumableUploadSession( upload_url=upload_url, config=config, transport=self._session, @@ -256,6 +260,8 @@ async def __call__(self, start_retry=retry if isinstance(retry, retries.AsyncRetry) else None, **session_kwargs, ) + session.upload = functools.partial(session.upload, request_body=body or "") # type: ignore[method-assign] + return session @property def upload_media(self) -> Callable[ From bb533eed2c34754c3dced8ede2c17888dd053dad Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 20:10:13 +0000 Subject: [PATCH 23/45] address review feedback --- .../%sub/services/%service/_shared_macros.j2 | 11 ----------- .../services/resumable_upload_service/async_client.py | 10 ---------- .../services/resumable_upload_service/client.py | 10 ---------- 3 files changed, 31 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 index 9b70606d8960..42f4e94ef947 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 @@ -53,17 +53,6 @@ except ImportError: # pragma: NO COVER {% endmacro %} {% macro create_metadata(method) %} - {% if method.is_resumable_upload %} - metadata = () if metadata is None else metadata - resumable_metadata = { - "x-goog-upload-protocol": "resumable", - "x-goog-upload-command": "start", - } - existing_keys = {k.lower() for k, _ in metadata} - metadata = tuple(metadata) + tuple( - (k, v) for k, v in resumable_metadata.items() if k not in existing_keys - ) - {% endif %} {% if method.explicit_routing %} header_params: dict[str, str] = {} {% if not method.client_streaming %} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py index 7bd2e63a5ec1..60ecb78dfbe5 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py @@ -324,16 +324,6 @@ async def sample_upload_media(): # and friendly error handling. rpc = self._client._transport._wrapped_methods[self._client._transport.upload_media] - metadata = () if metadata is None else metadata - resumable_metadata = { - "x-goog-upload-protocol": "resumable", - "x-goog-upload-command": "start", - } - existing_keys = {k.lower() for k, _ in metadata} - metadata = tuple(metadata) + tuple( - (k, v) for k, v in resumable_metadata.items() if k not in existing_keys - ) - # Validate the universe domain. self._client._validate_universe_domain() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py index d7167ea9d354..c405df97fd44 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py @@ -591,16 +591,6 @@ def sample_upload_media(): # and friendly error handling. rpc = self._transport._wrapped_methods[self._transport.upload_media] - metadata = () if metadata is None else metadata - resumable_metadata = { - "x-goog-upload-protocol": "resumable", - "x-goog-upload-command": "start", - } - existing_keys = {k.lower() for k, _ in metadata} - metadata = tuple(metadata) + tuple( - (k, v) for k, v in resumable_metadata.items() if k not in existing_keys - ) - # Validate the universe domain. self._validate_universe_domain() From 776811e12494857d927b56e000b0012040957d4c Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 20:15:22 +0000 Subject: [PATCH 24/45] address review feedback --- .../services/%service/transports/grpc.py.j2 | 18 +++++++++++------- .../%name_%version/%sub/test_%service.py.j2 | 12 ++++++++++-- .../transports/grpc.py | 18 +++++++++++------- .../test_resumable_upload_service.py | 12 ++++++++++-- 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index 7eaae7e1ad4a..0e365bf4d60e 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -381,13 +381,17 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): ) self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub() else: - if self._rest_transport is None: - self._rest_transport = {{ service.name }}RestTransport( - host=self._host, - credentials=self._credentials, - client_info=self._client_info, - ) - self._stubs['{{ method.transport_safe_name|snake_case }}'] = self._rest_transport.{{ method.transport_safe_name|snake_case }} + transport = self + class _RestStub: + def __call__(self, *args, **kwargs): + if transport._rest_transport is None: + transport._rest_transport = {{ service.name }}RestTransport( + host=transport._host, + credentials=transport._credentials, + client_info=transport._client_info, + ) + return transport._rest_transport.{{ method.transport_safe_name|snake_case }}(*args, **kwargs) + self._stubs['{{ method.transport_safe_name|snake_case }}'] = _RestStub() {% else %} self._stubs['{{ method.transport_safe_name|snake_case }}'] = self._logged_channel.{{ method.grpc_stub_type }}( '/{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}', diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index cb9616f855ed..086f47d169bf 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1540,11 +1540,19 @@ def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_rest_st transport = transports.{{ service.name }}GrpcTransport( credentials=ga_credentials.AnonymousCredentials(), ) + assert transport._rest_transport is None + stub = transport.{{ method.transport_safe_name|snake_case }} + assert transport._rest_transport is None + + result1 = stub({{ method.input.ident }}()) rest_transport = transport._rest_transport assert rest_transport is not None - transport._stubs.pop("{{ method.transport_safe_name|snake_case }}", None) - _ = transport.{{ method.transport_safe_name|snake_case }} + assert isinstance(result1, {{ method.client_output.ident }}) + + result2 = stub({{ method.input.ident }}()) + assert isinstance(result2, {{ method.client_output.ident }}) assert transport._rest_transport is rest_transport + with mock.patch.object(rest_transport, "close") as mock_rest_close: transport.close() mock_rest_close.assert_called_once() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py index 3e9f93e2afa1..e75efd2004da 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py @@ -354,13 +354,17 @@ def __call__(self, *args, **kwargs): ) self._stubs['upload_media'] = _ErrorStub() else: - if self._rest_transport is None: - self._rest_transport = ResumableUploadServiceRestTransport( - host=self._host, - credentials=self._credentials, - client_info=self._client_info, - ) - self._stubs['upload_media'] = self._rest_transport.upload_media + transport = self + class _RestStub: + def __call__(self, *args, **kwargs): + if transport._rest_transport is None: + transport._rest_transport = ResumableUploadServiceRestTransport( + host=transport._host, + credentials=transport._credentials, + client_info=transport._client_info, + ) + return transport._rest_transport.upload_media(*args, **kwargs) + self._stubs['upload_media'] = _RestStub() return self._stubs['upload_media'] def close(self): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 86b3c6e209d6..e2f3b9ff9a8f 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -2294,11 +2294,19 @@ def test_resumable_upload_service_upload_media_grpc_rest_stub(): transport = transports.ResumableUploadServiceGrpcTransport( credentials=ga_credentials.AnonymousCredentials(), ) + assert transport._rest_transport is None + stub = transport.upload_media + assert transport._rest_transport is None + + result1 = stub(resumable_upload.UploadMediaRequest()) rest_transport = transport._rest_transport assert rest_transport is not None - transport._stubs.pop("upload_media", None) - _ = transport.upload_media + assert isinstance(result1, resumable_transfer.ResumableUploadSession) + + result2 = stub(resumable_upload.UploadMediaRequest()) + assert isinstance(result2, resumable_transfer.ResumableUploadSession) assert transport._rest_transport is rest_transport + with mock.patch.object(rest_transport, "close") as mock_rest_close: transport.close() mock_rest_close.assert_called_once() From b48cfebda610cc233a5a484fc34661253e38d7e9 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 20:51:26 +0000 Subject: [PATCH 25/45] address review feedback --- packages/gapic-generator/noxfile.py | 105 +++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 17 deletions(-) diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index 39b81d87f602..567d25aed5c0 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -242,8 +242,30 @@ def showcase_library( include_service_yaml=True, retry_config=True, rest_async_io_enabled=False, + install_async_rest_extra=False, ): - """Install the generated library into the session for showcase tests.""" + """Install the generated library into the session for showcase tests. + + Args: + session: The nox session object. + templates (str): The template directory to use for code generation. + Defaults to "DEFAULT". + other_opts (typing.Iterable[str]): Additional options passed to + `--python_gapic_opt` during code generation. + include_service_yaml (bool): Whether to download and pass + `showcase_v1beta1.yaml` to the generator. + retry_config (bool): Whether to download and pass + `showcase_grpc_service_config.json` to the generator. + rest_async_io_enabled (bool): Whether to enable the experimental + `rest_async_io_enabled` setting in `showcase_v1beta1.yaml` so + `rest_asyncio` transports are generated for all services. When True, + the `[async_rest]` extra is also installed. + install_async_rest_extra (bool): Whether to install the generated + library with the `[async_rest]` extra and its corresponding + `constraints-{python}-async-rest.txt` file even when + `rest_async_io_enabled` is False in `showcase_v1beta1.yaml` (e.g., + for services with resumable upload methods). + """ session.log("-" * 70) session.log("Note: Showcase must be running for these tests to work.") @@ -371,19 +393,19 @@ def showcase_library( constraints_path = str( f"{tmp_dir}/testing/constraints-{session.python}.txt" ) - async_rest_constraints_path = str( - f"{tmp_dir}/testing/constraints-{session.python}-async-rest.txt" - ) - if os.path.exists(async_rest_constraints_path): - # use async-rest constraints if available - constraints_path = async_rest_constraints_path - else: - session.log( - f"{async_rest_constraints_path} not found. Using base constraints file" + extras = "" + if rest_async_io_enabled or install_async_rest_extra: + async_rest_constraints_path = str( + f"{tmp_dir}/testing/constraints-{session.python}-async-rest.txt" ) - # Showcase protos include `resumable_media.proto` - # Include the `async_rest` extra. - extras = "[async_rest]" + if os.path.exists(async_rest_constraints_path): + # use async-rest constraints if available + constraints_path = async_rest_constraints_path + extras = "[async_rest]" + else: + session.log( + f"{async_rest_constraints_path} not found. Using base constraints file" + ) session.install("-e", f"{tmp_dir}{extras}", "-r", constraints_path) else: # The ads templates do not have constraints files. @@ -395,8 +417,10 @@ def showcase_library( @nox.session(python=ALL_PYTHON) +@nox.parametrize("install_async_rest_extra", [False, True]) def showcase( session, + install_async_rest_extra=False, templates="DEFAULT", other_opts: typing.Iterable[str] = (), env: typing.Optional[typing.Dict[str, str]] = {}, @@ -407,7 +431,12 @@ def showcase( (useful for local testing and canary validation). """ - with showcase_library(session, templates=templates, other_opts=other_opts): + with showcase_library( + session, + templates=templates, + other_opts=other_opts, + install_async_rest_extra=install_async_rest_extra, + ): # When opt-in environment variable is set (e.g. in canary CI or local testing), # install the local google-api-core package from source. if os.getenv("INSTALL_LOCAL_CORE") == "true": @@ -535,7 +564,26 @@ def showcase_pqc( ) -def run_showcase_unit_tests(session, fail_under=100, rest_async_io_enabled=False): +def run_showcase_unit_tests( + session, + fail_under=100, + rest_async_io_enabled=False, + install_async_rest_extra=False, +): + """Run the generated Showcase unit test suite with coverage verification. + + Args: + session: The nox session object. + fail_under (int): Minimum required test coverage percentage. + Defaults to 100. + rest_async_io_enabled (bool): Whether `rest_async_io_enabled` was enabled + in `showcase_v1beta1.yaml` during code generation. + install_async_rest_extra (bool): Whether the library was installed with + the `[async_rest]` extra. When both `rest_async_io_enabled` and + `install_async_rest_extra` are False, `**/rest_asyncio.py` is omitted + from coverage since optional `async_rest` dependencies are not + installed. + """ session.install( "coverage", "pytest", @@ -546,6 +594,20 @@ def run_showcase_unit_tests(session, fail_under=100, rest_async_io_enabled=False # Freeze and print python environment package versions session.run("python", "-m", "pip", "freeze") + if ( + not rest_async_io_enabled + and not install_async_rest_extra + and path.exists(".coveragerc") + ): + with open(".coveragerc", "r") as f: + coveragerc = f.read() + if "**/rest_asyncio.py" not in coveragerc: + coveragerc = coveragerc.replace( + "omit =\n", "omit =\n **/rest_asyncio.py\n" + ) + with open(".coveragerc", "w") as f: + f.write(coveragerc) + # Run the tests. session.run( "py.test", @@ -564,15 +626,24 @@ def run_showcase_unit_tests(session, fail_under=100, rest_async_io_enabled=False @nox.session(python=ALL_PYTHON) +@nox.parametrize("install_async_rest_extra", [False, True]) def showcase_unit( session, + install_async_rest_extra=False, templates="DEFAULT", other_opts: typing.Iterable[str] = (), ): """Run the generated unit tests against the Showcase library.""" - with showcase_library(session, templates=templates, other_opts=other_opts) as lib: + with showcase_library( + session, + templates=templates, + other_opts=other_opts, + install_async_rest_extra=install_async_rest_extra, + ) as lib: session.chdir(lib) - run_showcase_unit_tests(session) + run_showcase_unit_tests( + session, install_async_rest_extra=install_async_rest_extra + ) # TODO: `showcase_unit_w_rest_async` nox session runs showcase unit tests with the From 69cc533633437f700ec74f4fb801d26a3d1025a0 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 21:55:10 +0000 Subject: [PATCH 26/45] fix build --- .../%name_%version/%sub/services/%service/client.py.j2 | 4 ++-- .../%sub/services/%service/transports/__init__.py.j2 | 6 +++--- packages/gapic-generator/noxfile.py | 2 +- .../google/cloud/redis_v1/services/cloud_redis/client.py | 4 ++-- .../redis_v1/services/cloud_redis/transports/__init__.py | 6 +++--- .../google/cloud/redis_v1/services/cloud_redis/client.py | 4 ++-- .../redis_v1/services/cloud_redis/transports/__init__.py | 6 +++--- .../services/resumable_upload_service/client.py | 4 ++-- .../resumable_upload_service/transports/__init__.py | 6 +++--- 9 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 index 4da2011d35c8..c2f2cc4b6234 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 @@ -87,7 +87,7 @@ from .transports.rest import {{ service.name }}RestTransport {% if rest_async_io_enabled %} try: from .transports.rest_asyncio import Async{{ service.name }}RestTransport - HAS_ASYNC_REST_DEPENDENCIES = True + HAS_ASYNC_REST_DEPENDENCIES = True # pragma: NO COVER {# NOTE: `pragma: NO COVER` is needed since the coverage for presubmits isn't combined. #} except ImportError: # pragma: NO COVER HAS_ASYNC_REST_DEPENDENCIES = False @@ -497,7 +497,7 @@ class {{ service.client_name }}(metaclass={{ service.client_name }}Meta): else cast(Callable[..., {{ service.name }}Transport], transport) ) - if "rest_asyncio" in str(transport_init): + if "rest_asyncio" in str(transport_init): # pragma: NO COVER {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2136): Support the following parameters in async rest: #} unsupported_params = { "google.api_core.client_options.ClientOptions.credentials_file": self._client_options.credentials_file, diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 index b09426682c4d..bc7187742991 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 @@ -19,9 +19,9 @@ from .rest import {{ service.name }}RestInterceptor ASYNC_REST_CLASSES: Tuple[str, ...] try: from .rest_asyncio import Async{{ service.name }}RestTransport - from .rest_asyncio import Async{{ service.name }}RestInterceptor - ASYNC_REST_CLASSES = ('Async{{ service.name }}RestTransport', 'Async{{ service.name }}RestInterceptor') - HAS_REST_ASYNC = True + from .rest_asyncio import Async{{ service.name }}RestInterceptor # pragma: NO COVER + ASYNC_REST_CLASSES = ('Async{{ service.name }}RestTransport', 'Async{{ service.name }}RestInterceptor') # pragma: NO COVER + HAS_REST_ASYNC = True # pragma: NO COVER except ImportError: # pragma: NO COVER ASYNC_REST_CLASSES = () HAS_REST_ASYNC = False diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index 567d25aed5c0..b9d1870b9e21 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -395,13 +395,13 @@ def showcase_library( ) extras = "" if rest_async_io_enabled or install_async_rest_extra: + extras = "[async_rest]" async_rest_constraints_path = str( f"{tmp_dir}/testing/constraints-{session.python}-async-rest.txt" ) if os.path.exists(async_rest_constraints_path): # use async-rest constraints if available constraints_path = async_rest_constraints_path - extras = "[async_rest]" else: session.log( f"{async_rest_constraints_path} not found. Using base constraints file" diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py index 6246a9bf619c..bb62ed511a5c 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/client.py @@ -64,7 +64,7 @@ from .transports.rest import CloudRedisRestTransport try: from .transports.rest_asyncio import AsyncCloudRedisRestTransport - HAS_ASYNC_REST_DEPENDENCIES = True + HAS_ASYNC_REST_DEPENDENCIES = True # pragma: NO COVER except ImportError: # pragma: NO COVER HAS_ASYNC_REST_DEPENDENCIES = False @@ -506,7 +506,7 @@ def __init__(self, *, else cast(Callable[..., CloudRedisTransport], transport) ) - if "rest_asyncio" in str(transport_init): + if "rest_asyncio" in str(transport_init): # pragma: NO COVER unsupported_params = { "google.api_core.client_options.ClientOptions.credentials_file": self._client_options.credentials_file, "google.api_core.client_options.ClientOptions.scopes": self._client_options.scopes, diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py index 4b81d3f86f0b..4f1a5dd5f81f 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py @@ -24,9 +24,9 @@ ASYNC_REST_CLASSES: Tuple[str, ...] try: from .rest_asyncio import AsyncCloudRedisRestTransport - from .rest_asyncio import AsyncCloudRedisRestInterceptor - ASYNC_REST_CLASSES = ('AsyncCloudRedisRestTransport', 'AsyncCloudRedisRestInterceptor') - HAS_REST_ASYNC = True + from .rest_asyncio import AsyncCloudRedisRestInterceptor # pragma: NO COVER + ASYNC_REST_CLASSES = ('AsyncCloudRedisRestTransport', 'AsyncCloudRedisRestInterceptor') # pragma: NO COVER + HAS_REST_ASYNC = True # pragma: NO COVER except ImportError: # pragma: NO COVER ASYNC_REST_CLASSES = () HAS_REST_ASYNC = False diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py index 7ae8f6ebdff5..22043152a1c6 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/client.py @@ -64,7 +64,7 @@ from .transports.rest import CloudRedisRestTransport try: from .transports.rest_asyncio import AsyncCloudRedisRestTransport - HAS_ASYNC_REST_DEPENDENCIES = True + HAS_ASYNC_REST_DEPENDENCIES = True # pragma: NO COVER except ImportError: # pragma: NO COVER HAS_ASYNC_REST_DEPENDENCIES = False @@ -506,7 +506,7 @@ def __init__(self, *, else cast(Callable[..., CloudRedisTransport], transport) ) - if "rest_asyncio" in str(transport_init): + if "rest_asyncio" in str(transport_init): # pragma: NO COVER unsupported_params = { "google.api_core.client_options.ClientOptions.credentials_file": self._client_options.credentials_file, "google.api_core.client_options.ClientOptions.scopes": self._client_options.scopes, diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py index 4b81d3f86f0b..4f1a5dd5f81f 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/__init__.py @@ -24,9 +24,9 @@ ASYNC_REST_CLASSES: Tuple[str, ...] try: from .rest_asyncio import AsyncCloudRedisRestTransport - from .rest_asyncio import AsyncCloudRedisRestInterceptor - ASYNC_REST_CLASSES = ('AsyncCloudRedisRestTransport', 'AsyncCloudRedisRestInterceptor') - HAS_REST_ASYNC = True + from .rest_asyncio import AsyncCloudRedisRestInterceptor # pragma: NO COVER + ASYNC_REST_CLASSES = ('AsyncCloudRedisRestTransport', 'AsyncCloudRedisRestInterceptor') # pragma: NO COVER + HAS_REST_ASYNC = True # pragma: NO COVER except ImportError: # pragma: NO COVER ASYNC_REST_CLASSES = () HAS_REST_ASYNC = False diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py index c405df97fd44..073962795207 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py @@ -64,7 +64,7 @@ from .transports.rest import ResumableUploadServiceRestTransport try: from .transports.rest_asyncio import AsyncResumableUploadServiceRestTransport - HAS_ASYNC_REST_DEPENDENCIES = True + HAS_ASYNC_REST_DEPENDENCIES = True # pragma: NO COVER except ImportError: # pragma: NO COVER HAS_ASYNC_REST_DEPENDENCIES = False @@ -473,7 +473,7 @@ def __init__(self, *, else cast(Callable[..., ResumableUploadServiceTransport], transport) ) - if "rest_asyncio" in str(transport_init): + if "rest_asyncio" in str(transport_init): # pragma: NO COVER unsupported_params = { "google.api_core.client_options.ClientOptions.credentials_file": self._client_options.credentials_file, "google.api_core.client_options.ClientOptions.scopes": self._client_options.scopes, diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py index ebe230e059ee..6b0c61381549 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py @@ -24,9 +24,9 @@ ASYNC_REST_CLASSES: Tuple[str, ...] try: from .rest_asyncio import AsyncResumableUploadServiceRestTransport - from .rest_asyncio import AsyncResumableUploadServiceRestInterceptor - ASYNC_REST_CLASSES = ('AsyncResumableUploadServiceRestTransport', 'AsyncResumableUploadServiceRestInterceptor') - HAS_REST_ASYNC = True + from .rest_asyncio import AsyncResumableUploadServiceRestInterceptor # pragma: NO COVER + ASYNC_REST_CLASSES = ('AsyncResumableUploadServiceRestTransport', 'AsyncResumableUploadServiceRestInterceptor') # pragma: NO COVER + HAS_REST_ASYNC = True # pragma: NO COVER except ImportError: # pragma: NO COVER ASYNC_REST_CLASSES = () HAS_REST_ASYNC = False From 4e835664bfc694777e4e8fa0c031285736397758 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 22:08:10 +0000 Subject: [PATCH 27/45] update unit test --- .../%name_%version/%sub/test_%service.py.j2 | 32 +++++------- .../gapic/%name_%version/%sub/test_macros.j2 | 18 +++---- .../test_resumable_upload_service.py | 51 +++++++------------ 3 files changed, 37 insertions(+), 64 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 086f47d169bf..23f3ae35fc5f 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1672,14 +1672,12 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_a credentials=async_anonymous_credentials(), transport="rest_asyncio", ) - with mock.patch.object(type(client.transport._session), "request", new_callable=mock.MagicMock) as req: + with mock.patch.object(type(client.transport._session), "request") as req: start_response = mock.MagicMock() - start_response.status = 200 + start_response.status_code = 200 start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} start_response.read = mock.AsyncMock(return_value=b"") - start_ctx = mock.MagicMock() - start_ctx.__aenter__ = mock.AsyncMock(return_value=start_response) - start_ctx.__aexit__ = mock.AsyncMock(return_value=None) + start_response.close = mock.AsyncMock(return_value=None) expected_result = {{ method.output.ident }}( {% for field in method.output.fields.values() | rejectattr('message') %} @@ -1694,14 +1692,12 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_a json_return_value = json_format.MessageToJson(expected_result) {% endif %} upload_response = mock.MagicMock() - upload_response.status = 200 + upload_response.status_code = 200 upload_response.headers = {"x-goog-upload-status": "final"} upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) - upload_ctx = mock.MagicMock() - upload_ctx.__aenter__ = mock.AsyncMock(return_value=upload_response) - upload_ctx.__aexit__ = mock.AsyncMock(return_value=None) + upload_response.close = mock.AsyncMock(return_value=None) - req.side_effect = [start_ctx, upload_ctx] + req.side_effect = [start_response, upload_response] response = await client.{{ method.client_method_name|snake_case }}( {{ method.input.ident }}(), config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), @@ -1789,7 +1785,7 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_a transport="rest_asyncio", ) with mock.patch.object( - type(client.transport._session), "request", new_callable=mock.MagicMock + type(client.transport._session), "request" ) as req, mock.patch.object(path_template, "transcode") as transcode: request = {{ method.input.ident }}( {% for field in method.input.fields.values() | rejectattr('message') %} @@ -1811,12 +1807,10 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_a } start_response = mock.MagicMock() - start_response.status = 200 + start_response.status_code = 200 start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} start_response.read = mock.AsyncMock(return_value=b"") - start_ctx = mock.MagicMock() - start_ctx.__aenter__ = mock.AsyncMock(return_value=start_response) - start_ctx.__aexit__ = mock.AsyncMock(return_value=None) + start_response.close = mock.AsyncMock(return_value=None) expected_result = {{ method.output.ident }}() {% if method.output.ident.is_proto_plus_type %} @@ -1825,14 +1819,12 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_a json_return_value = json_format.MessageToJson(expected_result) {% endif %} upload_response = mock.MagicMock() - upload_response.status = 200 + upload_response.status_code = 200 upload_response.headers = {"x-goog-upload-status": "final"} upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) - upload_ctx = mock.MagicMock() - upload_ctx.__aenter__ = mock.AsyncMock(return_value=upload_response) - upload_ctx.__aexit__ = mock.AsyncMock(return_value=None) + upload_response.close = mock.AsyncMock(return_value=None) - req.side_effect = [start_ctx, upload_ctx] + req.side_effect = [start_response, upload_response] response = await client.{{ method.client_method_name|snake_case }}(request) result = await response.upload(b"test data") assert result == expected_result diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 index 7f4440e551c4..69c37a614fda 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 @@ -1843,7 +1843,7 @@ def test_initialize_client_w_{{transport_name}}(): request = request_type(**request_init) # Mock the http request call within the method and fake a BadRequest error. - 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): + with mock.patch.object({{mocked_session}}, 'request') as req, pytest.raises(core_exceptions.BadRequest): # Wrap the value into a proper Response obj {% if method.is_resumable_upload %} response_value = mock.MagicMock() @@ -1853,9 +1853,7 @@ def test_initialize_client_w_{{transport_name}}(): {% if is_async %} response_value.read = mock.AsyncMock(return_value=b'{}') {% if method.is_resumable_upload %} - response_value.status = 400 - response_value.__aenter__ = mock.AsyncMock(return_value=response_value) - response_value.__aexit__ = mock.AsyncMock(return_value=None) + response_value.close = mock.AsyncMock(return_value=None) {% endif %} {% else %} json_return_value = '' @@ -2041,7 +2039,7 @@ def test_initialize_client_w_{{transport_name}}(): request = request_type(**request_init) # Mock the http request call within the method and fake a response. - with mock.patch.object(type(client.transport._session), 'request'{% if is_async and method.is_resumable_upload %}, new_callable=mock.MagicMock{% endif %}) as req: + with mock.patch.object(type(client.transport._session), 'request') as req: # Designate an appropriate value for the returned response. {% if method.void %} return_value = None @@ -2112,19 +2110,15 @@ def test_initialize_client_w_{{transport_name}}(): {% endif %}{# method.server_streaming #} {% if method.is_resumable_upload %} start_response = mock.MagicMock() + start_response.status_code = 200 start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} response_value.headers = {"x-goog-upload-status": "final", "header-1": "value-1", "header-2": "value-2"} {% if is_async %} - start_response.status = 200 start_response.read = mock.AsyncMock(return_value=b"") - start_response.__aenter__ = mock.AsyncMock(return_value=start_response) - start_response.__aexit__ = mock.AsyncMock(return_value=None) - response_value.status = 200 - response_value.__aenter__ = mock.AsyncMock(return_value=response_value) - response_value.__aexit__ = mock.AsyncMock(return_value=None) + start_response.close = mock.AsyncMock(return_value=None) + response_value.close = mock.AsyncMock(return_value=None) {% else %} start_response.ok = True - start_response.status_code = 200 start_response.content = b"" response_value.ok = True {% endif %} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index e2f3b9ff9a8f..2c7c010955db 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -1241,10 +1241,10 @@ def test_upload_media_rest_call_success(request_type): json_return_value = json_format.MessageToJson(return_value) response_value.content = json_return_value.encode('UTF-8') start_response = mock.MagicMock() + start_response.status_code = 200 start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} response_value.headers = {"x-goog-upload-status": "final", "header-1": "value-1", "header-2": "value-2"} start_response.ok = True - start_response.status_code = 200 start_response.content = b"" response_value.ok = True req.side_effect = [start_response, response_value] @@ -1821,13 +1821,11 @@ async def test_upload_media_rest_asyncio_bad_request(request_type=resumable_uplo request = request_type(**request_init) # Mock the http request call within the method and fake a BadRequest error. - with mock.patch.object(AsyncAuthorizedSession, 'request', new_callable=mock.MagicMock) as req, pytest.raises(core_exceptions.BadRequest): + with mock.patch.object(AsyncAuthorizedSession, 'request') as req, pytest.raises(core_exceptions.BadRequest): # Wrap the value into a proper Response obj response_value = mock.MagicMock() response_value.read = mock.AsyncMock(return_value=b'{}') - response_value.status = 400 - response_value.__aenter__ = mock.AsyncMock(return_value=response_value) - response_value.__aexit__ = mock.AsyncMock(return_value=None) + response_value.close = mock.AsyncMock(return_value=None) response_value.status_code = 400 response_value.request = mock.Mock() req.return_value = response_value @@ -1854,7 +1852,7 @@ async def test_upload_media_rest_asyncio_call_success(request_type): request = request_type(**request_init) # Mock the http request call within the method and fake a response. - with mock.patch.object(type(client.transport._session), 'request', new_callable=mock.MagicMock) as req: + with mock.patch.object(type(client.transport._session), 'request') as req: # Designate an appropriate value for the returned response. return_value = resumable_upload.UploadMediaResponse( name='name_value', @@ -1870,15 +1868,12 @@ async def test_upload_media_rest_asyncio_call_success(request_type): json_return_value = json_format.MessageToJson(return_value) response_value.read = mock.AsyncMock(return_value=json_return_value.encode('UTF-8')) start_response = mock.MagicMock() + start_response.status_code = 200 start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} response_value.headers = {"x-goog-upload-status": "final", "header-1": "value-1", "header-2": "value-2"} - start_response.status = 200 start_response.read = mock.AsyncMock(return_value=b"") - start_response.__aenter__ = mock.AsyncMock(return_value=start_response) - start_response.__aexit__ = mock.AsyncMock(return_value=None) - response_value.status = 200 - response_value.__aenter__ = mock.AsyncMock(return_value=response_value) - response_value.__aexit__ = mock.AsyncMock(return_value=None) + start_response.close = mock.AsyncMock(return_value=None) + response_value.close = mock.AsyncMock(return_value=None) req.side_effect = [start_response, response_value] response = await client.upload_media(request) assert isinstance(response, resumable_transfer.AsyncResumableUploadSession) @@ -2414,14 +2409,12 @@ async def test_resumable_upload_service_upload_media_rest_asyncio_start_empty_bo credentials=async_anonymous_credentials(), transport="rest_asyncio", ) - with mock.patch.object(type(client.transport._session), "request", new_callable=mock.MagicMock) as req: + with mock.patch.object(type(client.transport._session), "request") as req: start_response = mock.MagicMock() - start_response.status = 200 + start_response.status_code = 200 start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} start_response.read = mock.AsyncMock(return_value=b"") - start_ctx = mock.MagicMock() - start_ctx.__aenter__ = mock.AsyncMock(return_value=start_response) - start_ctx.__aexit__ = mock.AsyncMock(return_value=None) + start_response.close = mock.AsyncMock(return_value=None) expected_result = resumable_upload.UploadMediaResponse( name='name_value', @@ -2429,14 +2422,12 @@ async def test_resumable_upload_service_upload_media_rest_asyncio_start_empty_bo ) json_return_value = resumable_upload.UploadMediaResponse.to_json(expected_result) upload_response = mock.MagicMock() - upload_response.status = 200 + upload_response.status_code = 200 upload_response.headers = {"x-goog-upload-status": "final"} upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) - upload_ctx = mock.MagicMock() - upload_ctx.__aenter__ = mock.AsyncMock(return_value=upload_response) - upload_ctx.__aexit__ = mock.AsyncMock(return_value=None) + upload_response.close = mock.AsyncMock(return_value=None) - req.side_effect = [start_ctx, upload_ctx] + req.side_effect = [start_response, upload_response] response = await client.upload_media( resumable_upload.UploadMediaRequest(), config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), @@ -2504,7 +2495,7 @@ async def test_resumable_upload_service_upload_media_rest_asyncio_body_and_query transport="rest_asyncio", ) with mock.patch.object( - type(client.transport._session), "request", new_callable=mock.MagicMock + type(client.transport._session), "request" ) as req, mock.patch.object(path_template, "transcode") as transcode: request = resumable_upload.UploadMediaRequest( name='name_value', @@ -2518,24 +2509,20 @@ async def test_resumable_upload_service_upload_media_rest_asyncio_body_and_query } start_response = mock.MagicMock() - start_response.status = 200 + start_response.status_code = 200 start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} start_response.read = mock.AsyncMock(return_value=b"") - start_ctx = mock.MagicMock() - start_ctx.__aenter__ = mock.AsyncMock(return_value=start_response) - start_ctx.__aexit__ = mock.AsyncMock(return_value=None) + start_response.close = mock.AsyncMock(return_value=None) expected_result = resumable_upload.UploadMediaResponse() json_return_value = resumable_upload.UploadMediaResponse.to_json(expected_result) upload_response = mock.MagicMock() - upload_response.status = 200 + upload_response.status_code = 200 upload_response.headers = {"x-goog-upload-status": "final"} upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) - upload_ctx = mock.MagicMock() - upload_ctx.__aenter__ = mock.AsyncMock(return_value=upload_response) - upload_ctx.__aexit__ = mock.AsyncMock(return_value=None) + upload_response.close = mock.AsyncMock(return_value=None) - req.side_effect = [start_ctx, upload_ctx] + req.side_effect = [start_response, upload_response] response = await client.upload_media(request) result = await response.upload(b"test data") assert result == expected_result From dafc8debfa5fd5454ddd621aa50f6d28404f73bf Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Thu, 1 Oct 2026 22:31:55 +0000 Subject: [PATCH 28/45] fix build --- .../%service/transports/rest_asyncio.py.j2 | 40 ++++++++++++++++++- .../transports/rest_asyncio.py | 36 ++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index e3487b8ae54d..7527708eea71 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -22,6 +22,7 @@ from google.auth.aio import credentials as ga_credentials_async # type: ignore from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 {% if service.has_resumable_upload_methods %} +import contextlib import functools import urllib.parse from google.api_core import resumable_transfer @@ -92,9 +93,46 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ {{ shared_macros.create_interceptor_class(api, service, method, is_async=True) }} +{% if service.has_resumable_upload_methods %} +@dataclasses.dataclass +class _AsyncResumableUploadSessionAdapter: + """Adapts AsyncAuthorizedSession to the aiohttp-like request context manager interface expected by AsyncResumableUploadSession.""" + + _session: AsyncAuthorizedSession + + @contextlib.asynccontextmanager + async def request( + self, + method: str, + url: str, + *, + data: Optional[bytes] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Any = None, + **kwargs, + ): + response = await self._session.request( + method, + url, + data=data, + headers=headers, + timeout=getattr(timeout, "total", timeout), + **kwargs, + ) + response.status = response.status_code # type: ignore[attr-defined] + try: + yield response + finally: + await response.close() +{% endif %} + @dataclasses.dataclass class Async{{service.name}}RestStub: + {% if service.has_resumable_upload_methods %} + _session: Union[AsyncAuthorizedSession, _AsyncResumableUploadSessionAdapter] + {% else %} _session: AsyncAuthorizedSession + {% endif %} _host: str _interceptor: Async{{service.name}}RestInterceptor @@ -343,7 +381,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): def {{method.transport_safe_name|snake_case}}(self) -> Callable[ [{{method.input.ident}}], {% if method.is_resumable_upload %}{{method.client_output_async.ident}}{% else %}{{method.output.ident}}{% endif %}]: - return self._{{method.name}}(self._session, self._host, self._interceptor) # type: ignore + return self._{{method.name}}({% if method.is_resumable_upload %}_AsyncResumableUploadSessionAdapter(self._session){% else %}self._session{% endif %}, self._host, self._interceptor) # type: ignore {% endfor %} {% if rest_async_io_enabled %} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py index 235fd264de36..fadd0f670d23 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -27,6 +27,7 @@ from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 +import contextlib import functools import urllib.parse from google.api_core import resumable_transfer @@ -117,8 +118,39 @@ async def pre_upload_media(self, request: resumable_upload.UploadMediaRequest, m @dataclasses.dataclass -class AsyncResumableUploadServiceRestStub: +class _AsyncResumableUploadSessionAdapter: + """Adapts AsyncAuthorizedSession to the aiohttp-like request context manager interface expected by AsyncResumableUploadSession.""" + _session: AsyncAuthorizedSession + + @contextlib.asynccontextmanager + async def request( + self, + method: str, + url: str, + *, + data: Optional[bytes] = None, + headers: Optional[Dict[str, str]] = None, + timeout: Any = None, + **kwargs, + ): + response = await self._session.request( + method, + url, + data=data, + headers=headers, + timeout=getattr(timeout, "total", timeout), + **kwargs, + ) + response.status = response.status_code # type: ignore[attr-defined] + try: + yield response + finally: + await response.close() + +@dataclasses.dataclass +class AsyncResumableUploadServiceRestStub: + _session: Union[AsyncAuthorizedSession, _AsyncResumableUploadSessionAdapter] _host: str _interceptor: AsyncResumableUploadServiceRestInterceptor @@ -267,7 +299,7 @@ async def __call__(self, def upload_media(self) -> Callable[ [resumable_upload.UploadMediaRequest], resumable_transfer.AsyncResumableUploadSession]: - return self._UploadMedia(self._session, self._host, self._interceptor) # type: ignore + return self._UploadMedia(_AsyncResumableUploadSessionAdapter(self._session), self._host, self._interceptor) # type: ignore @property def kind(self) -> str: From 50b0c1832c65a8294fb5df99601c0354cec224f2 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 2 Oct 2026 20:32:15 +0000 Subject: [PATCH 29/45] address review feedback --- packages/gapic-generator/gapic/templates/setup.py.j2 | 3 ++- .../gapic-generator/tests/integration/goldens/redis/setup.py | 3 ++- .../tests/integration/goldens/redis_selective/setup.py | 3 ++- .../tests/integration/goldens/showcase/setup.py | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/setup.py.j2 b/packages/gapic-generator/gapic/templates/setup.py.j2 index 7c2d340e1170..291c9ac9b03f 100644 --- a/packages/gapic-generator/gapic/templates/setup.py.j2 +++ b/packages/gapic-generator/gapic/templates/setup.py.j2 @@ -61,7 +61,8 @@ dependencies = [ extras = { {% if rest_async_io_enabled %} "async_rest": [ - "google-auth[aiohttp] >= 2.35.0, <3.0.0" + "google-auth[aiohttp] >= 2.60.0, <3.0.0", + "google-api-core >= 2.41.0, <3.0.0", ], {% endif %} } diff --git a/packages/gapic-generator/tests/integration/goldens/redis/setup.py b/packages/gapic-generator/tests/integration/goldens/redis/setup.py index 29dcfeccbc8b..fdadb0f8c9a8 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/setup.py @@ -53,7 +53,8 @@ ] extras = { "async_rest": [ - "google-auth[aiohttp] >= 2.35.0, <3.0.0" + "google-auth[aiohttp] >= 2.60.0, <3.0.0", + "google-api-core >= 2.41.0, <3.0.0", ], } url = "https://github.com/googleapis/google-cloud-python/tree/main/packages/google-cloud-redis" diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py index 29dcfeccbc8b..fdadb0f8c9a8 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py @@ -53,7 +53,8 @@ ] extras = { "async_rest": [ - "google-auth[aiohttp] >= 2.35.0, <3.0.0" + "google-auth[aiohttp] >= 2.60.0, <3.0.0", + "google-api-core >= 2.41.0, <3.0.0", ], } url = "https://github.com/googleapis/google-cloud-python/tree/main/packages/google-cloud-redis" diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py index 4cf6e1d50d41..0a21a411bfe2 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py @@ -54,7 +54,8 @@ ] extras = { "async_rest": [ - "google-auth[aiohttp] >= 2.35.0, <3.0.0" + "google-auth[aiohttp] >= 2.60.0, <3.0.0", + "google-api-core >= 2.41.0, <3.0.0", ], } url = "https://github.com/googleapis/google-cloud-python/tree/main/packages/google-showcase" From 809a0a7e9d9905f6b6c2005f866e48ae57e11788 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 2 Oct 2026 20:40:29 +0000 Subject: [PATCH 30/45] sync minimum version of google-api-core for resumable uploads --- packages/gapic-generator/gapic/templates/setup.py.j2 | 2 +- .../gapic/templates/testing/constraints-3.10-async-rest.txt.j2 | 2 +- .../gapic/templates/testing/constraints-3.10.txt.j2 | 2 +- .../gapic-generator/tests/integration/goldens/showcase/setup.py | 2 +- .../goldens/showcase/testing/constraints-3.10-async-rest.txt | 2 +- .../integration/goldens/showcase/testing/constraints-3.10.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/setup.py.j2 b/packages/gapic-generator/gapic/templates/setup.py.j2 index 291c9ac9b03f..8e0ccbcfdb8d 100644 --- a/packages/gapic-generator/gapic/templates/setup.py.j2 +++ b/packages/gapic-generator/gapic/templates/setup.py.j2 @@ -37,7 +37,7 @@ else: dependencies = [ {% if api.has_resumable_upload_methods %} - "google-api-core[grpc] >= 2.39.0, <3.0.0", + "google-api-core[grpc] >= 2.41.0, <3.0.0", {% else %} "google-api-core[grpc] >= 2.28.0, <3.0.0", {% endif %} diff --git a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 index 2232b4b694c6..332973808bb6 100644 --- a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 +++ b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 @@ -9,7 +9,7 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 {% if api.has_resumable_upload_methods %} -google-api-core==2.39.0 +google-api-core==2.41.0 {% else %} google-api-core==2.28.0 {% endif %} diff --git a/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 b/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 index 6db105c05ac0..85af16c110fd 100644 --- a/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 +++ b/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 @@ -6,7 +6,7 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 {% if api.has_resumable_upload_methods %} -google-api-core==2.39.0 +google-api-core==2.41.0 {% else %} google-api-core==2.28.0 {% endif %} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py index 0a21a411bfe2..263074e55d01 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py @@ -42,7 +42,7 @@ release_status = "Development Status :: 5 - Production/Stable" dependencies = [ - "google-api-core[grpc] >= 2.39.0, <3.0.0", + "google-api-core[grpc] >= 2.41.0, <3.0.0", # Exclude incompatible versions of `google-auth` # See https://github.com/googleapis/google-cloud-python/issues/12364 "google-auth >= 2.14.1, <3.0.0,!=2.24.0,!=2.25.0", diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt index 799c0fc1e176..85cef31887ec 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt @@ -5,7 +5,7 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.39.0 +google-api-core==2.41.0 google-auth==2.35.0 grpcio==1.59.0 proto-plus==1.26.1 diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt index 99b79cca2c30..a75b92057fdb 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt @@ -4,7 +4,7 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.39.0 +google-api-core==2.41.0 google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 From 59fa610d518dc9c7802956453681e34ab1eab074 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 3 Oct 2026 00:52:16 +0000 Subject: [PATCH 31/45] temporary commit to be reverted --- .github/workflows/docs.yml | 12 ++- .github/workflows/gapic-generator-tests.yml | 10 +-- .github/workflows/lint.yml | 14 +++- .github/workflows/unittest.yml | 10 ++- ci/run_single_test.sh | 7 ++ .../gapic/templates/setup.py.j2 | 21 +++++- .../templates/testing/_default_constraints.j2 | 6 +- .../constraints-3.10-async-rest.txt.j2 | 6 +- .../templates/testing/constraints-3.10.txt.j2 | 9 ++- packages/gapic-generator/noxfile.py | 73 ++++++++++++++++--- packages/gapic-generator/requirements.in | 7 +- packages/gapic-generator/requirements.txt | 12 ++- .../asset/testing/constraints-3.10.txt | 6 +- .../asset/testing/constraints-3.11.txt | 6 +- .../asset/testing/constraints-3.12.txt | 6 +- .../credentials/testing/constraints-3.10.txt | 6 +- .../credentials/testing/constraints-3.11.txt | 6 +- .../credentials/testing/constraints-3.12.txt | 6 +- .../eventarc/testing/constraints-3.10.txt | 6 +- .../eventarc/testing/constraints-3.11.txt | 6 +- .../eventarc/testing/constraints-3.12.txt | 6 +- .../logging/testing/constraints-3.10.txt | 6 +- .../logging/testing/constraints-3.11.txt | 6 +- .../logging/testing/constraints-3.12.txt | 6 +- .../testing/constraints-3.10.txt | 6 +- .../testing/constraints-3.11.txt | 6 +- .../testing/constraints-3.12.txt | 6 +- .../tests/integration/goldens/redis/setup.py | 10 ++- .../testing/constraints-3.10-async-rest.txt | 3 +- .../redis/testing/constraints-3.10.txt | 6 +- .../redis/testing/constraints-3.11.txt | 6 +- .../redis/testing/constraints-3.12.txt | 6 +- .../goldens/redis_selective/setup.py | 10 ++- .../testing/constraints-3.10-async-rest.txt | 3 +- .../testing/constraints-3.10.txt | 6 +- .../testing/constraints-3.11.txt | 6 +- .../testing/constraints-3.12.txt | 6 +- .../integration/goldens/showcase/setup.py | 22 ++++-- .../testing/constraints-3.10-async-rest.txt | 6 +- .../showcase/testing/constraints-3.10.txt | 6 +- .../showcase/testing/constraints-3.11.txt | 6 +- .../showcase/testing/constraints-3.12.txt | 6 +- .../testing/constraints-3.10.txt | 6 +- .../testing/constraints-3.11.txt | 6 +- .../testing/constraints-3.12.txt | 6 +- 45 files changed, 290 insertions(+), 113 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e8fe1a06f6f2..dd8d3393ed20 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -105,7 +105,10 @@ jobs: python-version: "3.10" - name: Install nox run: | - python -m pip install --upgrade setuptools pip wheel + # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition + # where setuptools deletes the temporary '/bdist' directory mid-build during + # highly parallelized 'uv pip install -e .' editable installations. + python -m pip install --upgrade "setuptools<70.0.0" pip wheel python -m pip install nox uv - name: Run docs for ${{ matrix.package_shard.description }} env: @@ -154,8 +157,11 @@ jobs: python-version: "3.10" - name: Install nox run: | - python -m pip install --upgrade setuptools pip wheel - python -m pip install nox uv + python -m pip install --upgrade pip wheel + # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition + # where setuptools deletes the temporary '/bdist' directory mid-build during + # highly parallelized 'uv pip install -e .' editable installations. + python -m pip install nox uv "setuptools<70" - name: Run docfx env: BUILD_TYPE: presubmit diff --git a/.github/workflows/gapic-generator-tests.yml b/.github/workflows/gapic-generator-tests.yml index c9fa162db280..9a9fe2d1482f 100644 --- a/.github/workflows/gapic-generator-tests.yml +++ b/.github/workflows/gapic-generator-tests.yml @@ -88,7 +88,7 @@ jobs: GOOGLE_SDK_PYTHON_LOGGING_SCOPE: ${{ matrix.logging_scope }} MATRIX_PYTHON: ${{ matrix.python }} run: | - pip install nox + pip install nox uv cd packages/gapic-generator nox -s showcase_unit${{ matrix.variant }}-${MATRIX_PYTHON} @@ -125,7 +125,7 @@ jobs: ./gapic-showcase run & cd - - name: Install nox. - run: python -m pip install nox + run: python -m pip install nox uv - name: Install protoc run: | sudo mkdir -p /usr/src/protoc/ @@ -181,7 +181,7 @@ jobs: ./gapic-showcase run & cd - - name: Install nox. - run: python -m pip install nox + run: python -m pip install nox uv - name: Install protoc run: | sudo mkdir -p /usr/src/protoc/ @@ -220,7 +220,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y pandoc - name: Run Mypy run: | - pip install nox + pip install nox uv cd packages/gapic-generator nox -s showcase_mypy @@ -318,7 +318,7 @@ jobs: sudo ln -s /usr/src/protoc/bin/protoc /usr/local/bin/protoc - name: Run Tests run: | - pip install nox + pip install nox uv cd packages/gapic-generator # Run fragment for current matrix python nox -s fragment-${MATRIX_PYTHON} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 702487b9d0fe..f0cf848d835b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -88,8 +88,11 @@ jobs: python-version: "3.14" - name: Install nox run: | - python -m pip install --upgrade setuptools pip wheel build twine - python -m pip install nox uv + python -m pip install --upgrade pip wheel build twine + # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition + # where setuptools deletes the temporary '/bdist' directory mid-build during + # highly parallelized 'uv pip install -e .' editable installations. + python -m pip install nox uv "setuptools<70" - name: Run lint env: BUILD_TYPE: presubmit @@ -145,8 +148,11 @@ jobs: python-version: "3.14" - name: Install nox run: | - python -m pip install --upgrade setuptools pip wheel - python -m pip install nox uv + python -m pip install --upgrade pip wheel + # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition + # where setuptools deletes the temporary '/bdist' directory mid-build during + # highly parallelized 'uv pip install -e .' editable installations. + python -m pip install nox uv "setuptools<70" - name: Run mypy for ${{ matrix.package_shard.description }} env: BUILD_TYPE: presubmit diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index a7006861b623..36a9addd29d5 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -131,7 +131,10 @@ jobs: cache-dependency-glob: 'packages/**/testing/constraints*.txt' - name: Install nox and nox-uv run: | - uv pip install --system nox nox-uv + # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition + # where setuptools deletes the temporary '/bdist' directory mid-build during + # highly parallelized 'uv pip install -e .' editable installations. + uv pip install --system nox nox-uv "setuptools<70.0.0" # TODO(https://github.com/googleapis/google-cloud-python/issues/18532): # Remove UV_PRERELEASE once Python 3.15 is officially released and upstream binary wheels are available. - name: Enable prereleases for Python 3.15 @@ -239,7 +242,10 @@ jobs: - name: Install coverage if: ${{ needs.initialize.outputs.packages != '' }} run: | - python -m pip install --upgrade setuptools pip wheel + # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition + # where setuptools deletes the temporary '/bdist' directory mid-build during + # highly parallelized 'uv pip install -e .' editable installations. + python -m pip install --upgrade "setuptools<70" pip wheel python -m pip install coverage - name: Download coverage results if: ${{ needs.initialize.outputs.packages != '' }} diff --git a/ci/run_single_test.sh b/ci/run_single_test.sh index ced52a121eae..942b1ce02203 100755 --- a/ci/run_single_test.sh +++ b/ci/run_single_test.sh @@ -26,6 +26,13 @@ set -e +# 1. Force UV to pin setuptools inside Nox environments, resolving the /bdist race condition. +# 2. Redirect TMPDIR to a unique path so parallel workers never cross-contaminate. +export UV_CONSTRAINT="/tmp/constraints.txt" +export TMPDIR=$(mktemp -d) +echo "setuptools<70.0.0" > "${UV_CONSTRAINT}" +export UV_CACHE_DIR="${TMPDIR}/uv_cache" + if [ -z "${TEST_TYPE}" ]; then echo "missing TEST_TYPE env var" exit 1 diff --git a/packages/gapic-generator/gapic/templates/setup.py.j2 b/packages/gapic-generator/gapic/templates/setup.py.j2 index 8e0ccbcfdb8d..22f722f72675 100644 --- a/packages/gapic-generator/gapic/templates/setup.py.j2 +++ b/packages/gapic-generator/gapic/templates/setup.py.j2 @@ -37,13 +37,20 @@ else: dependencies = [ {% if api.has_resumable_upload_methods %} - "google-api-core[grpc] >= 2.41.0, <3.0.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged + "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", + # TODO: Uncomment the line below + # "google-api-core[grpc] >= 2.41.0, <3.0.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged + "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", + # TODO: Uncomment the line below + # "google-auth[aiohttp] >= 2.60.0, <3.0.0", {% else %} "google-api-core[grpc] >= 2.28.0, <3.0.0", - {% endif %} # Exclude incompatible versions of `google-auth` # See https://github.com/googleapis/google-cloud-python/issues/12364 "google-auth >= 2.14.1, <3.0.0,!=2.24.0,!=2.25.0", + {% endif %} "grpcio >= 1.59.0, < 2.0.0", "grpcio >= 1.75.1, < 2.0.0; python_version >= '3.14'", "proto-plus >= 1.26.1, <2.0.0", @@ -61,8 +68,14 @@ dependencies = [ extras = { {% if rest_async_io_enabled %} "async_rest": [ - "google-auth[aiohttp] >= 2.60.0, <3.0.0", - "google-api-core >= 2.41.0, <3.0.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged + "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", + # TODO: Uncomment the line below + # "google-auth[aiohttp] >= 2.60.0, <3.0.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged + "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", + # TODO: Uncomment the line below + # "google-api-core >= 2.41.0, <3.0.0", ], {% endif %} } diff --git a/packages/gapic-generator/gapic/templates/testing/_default_constraints.j2 b/packages/gapic-generator/gapic/templates/testing/_default_constraints.j2 index 85d819aaff23..ce93530e06c8 100644 --- a/packages/gapic-generator/gapic/templates/testing/_default_constraints.j2 +++ b/packages/gapic-generator/gapic/templates/testing/_default_constraints.j2 @@ -1,8 +1,10 @@ {% from '_pypi_packages.j2' import pypi_packages %} # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 index 332973808bb6..9a416d2a8d04 100644 --- a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 +++ b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 @@ -9,11 +9,13 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 {% if api.has_resumable_upload_methods %} -google-api-core==2.41.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.41.0 {% else %} google-api-core==2.28.0 {% endif %} -google-auth==2.35.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.35.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 b/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 index 85af16c110fd..d8d846220727 100644 --- a/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 +++ b/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 @@ -6,11 +6,14 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 {% if api.has_resumable_upload_methods %} -google-api-core==2.41.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.41.0 {% else %} -google-api-core==2.28.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.28.0 {% endif %} -google-auth==2.14.1 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index b9d1870b9e21..a595355c24d8 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -82,7 +82,15 @@ def unit(session): "grpcio-status", "proto-plus", ) - session.install("-e", ".") + session.install("setuptools<70.0.0", "wheel") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") + session.run( + "uv", "pip", "install", + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + ) + session.run("uv", "pip", "install", ".") session.run( "py.test", *( @@ -98,6 +106,9 @@ def unit(session): path.join("tests", "unit"), ] ), + # `test_utils` is not part of the installed package (only `gapic` is + # included in setup.py), but it is needed for running tests. + env={"PYTHONPATH": "."}, ) @@ -150,7 +161,7 @@ def __call__(self, frag): # Install the generated fragment library. if self.use_ads_templates: - self.session.install(tmp_dir, "-e", ".", "-qqq") + self.session.install(tmp_dir, ".", "-qqq") else: # Use the constraints file for the specific python runtime version. # We do this to make sure that we're testing against the lowest @@ -161,7 +172,7 @@ def __call__(self, frag): constraints_path = str( f"{tmp_dir}/testing/constraints-{self.session.python}.txt" ) - self.session.install(tmp_dir, "-e", ".", "-qqq", "-r", constraints_path) + self.session.install(tmp_dir, ".", "-qqq", "-r", constraints_path) # Run the fragment's generated unit tests. # Don't bother parallelizing them: we already parallelize @@ -191,7 +202,15 @@ def fragment(session, use_ads_templates=False): "pytest-asyncio", "grpcio-tools", ) - session.install("-e", ".") + session.install("setuptools<70.0.0", "wheel") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") + session.run( + "uv", "pip", "install", + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + ) + session.run("uv", "pip", "install", ".") frag_files = ( [Path(f) for f in session.posargs] if session.posargs else FRAGMENT_FILES @@ -273,7 +292,15 @@ def showcase_library( session.log("-" * 70) # Install gapic-generator-python - session.install("-e", ".") + session.install("setuptools<70.0.0", "wheel") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") + session.run( + "uv", "pip", "install", + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + ) + session.run("uv", "pip", "install", ".") # Install grpcio-tools for protoc session.install("grpcio-tools") @@ -445,7 +472,7 @@ def showcase( session.error( f"INSTALL_LOCAL_CORE is set to 'true' but {local_core} does not exist." ) - session.install("-e", str(local_core)) + session.install(str(local_core)) # Use pytest-asyncio<1.0.0 while we investigate the recent failure described in # https://github.com/googleapis/gapic-generator-python/issues/2399 @@ -491,7 +518,7 @@ def showcase_w_rest_async( session.error( f"INSTALL_LOCAL_CORE is set to 'true' but {local_core} does not exist." ) - session.install("-e", str(local_core)) + session.install(str(local_core)) # Use pytest-asyncio<1.0.0 while we investigate the recent failure described in # https://github.com/googleapis/gapic-generator-python/issues/2399 @@ -743,7 +770,15 @@ def snippetgen(session): ) # Install gapic-generator-python - session.install("-e", ".") + session.install("setuptools<70.0.0", "wheel") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") + session.run( + "uv", "pip", "install", + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + ) + session.run("uv", "pip", "install", ".") session.install("grpcio-tools", "pytest", "pytest-asyncio") @@ -754,8 +789,15 @@ def snippetgen(session): def docs(session): """Build the docs for this generator.""" - session.install("-e", ".") - + session.install("setuptools<70.0.0", "wheel") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") + session.run( + "uv", "pip", "install", + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + ) + session.run("uv", "pip", "install", ".") session.install( # We need to pin to specific versions of the `sphinxcontrib-*` packages # which still support sphinx 4.x. @@ -790,7 +832,7 @@ def docs(session): def docfx(session): """Build the docfx yaml files for this library.""" - session.install("-e", ".") + session.install(".") session.install( # We need to pin to specific versions of the `sphinxcontrib-*` packages # which still support sphinx 4.x. @@ -843,7 +885,14 @@ def mypy(session): "types-dataclasses", "click==8.1.3", ) - session.install(".") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") + session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") + session.run( + "uv", "pip", "install", + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + ) + session.run("uv", "pip", "install", ".") session.run("mypy", f"--config-file={MYPY_CONFIG_FILE}", "-p", "gapic") diff --git a/packages/gapic-generator/requirements.in b/packages/gapic-generator/requirements.in index 2776bb919bb8..05185f1c9201 100644 --- a/packages/gapic-generator/requirements.in +++ b/packages/gapic-generator/requirements.in @@ -1,5 +1,10 @@ click -google-api-core +# TODO Revert this change once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +google-api-core @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core +# TODO uncomment the line below +# google-api-core +# TODO Revert this change once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +google-auth @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth googleapis-common-protos jinja2 MarkupSafe diff --git a/packages/gapic-generator/requirements.txt b/packages/gapic-generator/requirements.txt index ff61f2e8a44e..6e5efea886eb 100644 --- a/packages/gapic-generator/requirements.txt +++ b/packages/gapic-generator/requirements.txt @@ -611,14 +611,12 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -google-api-core==2.40.0 \ - --hash=sha256:4b9e0a80024c269ae173136d5439f4ed284651d6e5e2773ba5c694f481f9c0f4 \ - --hash=sha256:ebee7d1b138b5362beecec260e6e8988ac97346562c7382ccb6f0ad8435c599f +google-api-core @ git+https://github.com/googleapis/google-cloud-python.git@9b96af76254c33b9c3e0ceb390bbaeab2e33bbea#subdirectory=packages/google-api-core # via -r requirements.in -google-auth==2.59.0 \ - --hash=sha256:43e47b1d3067fb90d77bd9ba21f139bb0577b3f2b9f3e2516fc00f37435efcad \ - --hash=sha256:eb32f44f89f6b577947ebee5887c1db46e6b1a278889ba369a88179643f32240 - # via google-api-core +google-auth @ git+https://github.com/googleapis/google-cloud-python.git@9b96af76254c33b9c3e0ceb390bbaeab2e33bbea#subdirectory=packages/google-auth + # via + # -r requirements.in + # google-api-core googleapis-common-protos==1.75.5 \ --hash=sha256:c7a866fc34ed29a3b10af627a4b9b1dc2433313ca6e959f0ae4feb132047ed72 \ --hash=sha256:d7285525c23039db98f2463e6d5a4f9b958b94d497f03a844ece3259c4e72d5d diff --git a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.10.txt index 71d3c4c8d058..6fd63a4545b0 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.10.txt @@ -4,8 +4,10 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.28.0 -google-auth==2.14.1 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.28.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.11.txt index c5602c5029bf..083b31dbe2b4 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.11.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.12.txt index c5602c5029bf..083b31dbe2b4 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.12.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.10.txt index 0dadf6857ff5..1e69a37f1459 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.10.txt @@ -4,8 +4,10 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.28.0 -google-auth==2.14.1 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.28.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.11.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.11.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.12.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.12.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.10.txt index 9c33fedfb264..034c13d55698 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.10.txt @@ -4,8 +4,10 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.28.0 -google-auth==2.14.1 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.28.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.11.txt index 1cd0c5a2c3d4..6f8057b0c797 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.11.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.12.txt index 1cd0c5a2c3d4..6f8057b0c797 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.12.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.10.txt index 0dadf6857ff5..1e69a37f1459 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.10.txt @@ -4,8 +4,10 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.28.0 -google-auth==2.14.1 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.28.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.11.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.11.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.12.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.12.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.10.txt index 0dadf6857ff5..1e69a37f1459 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.10.txt @@ -4,8 +4,10 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.28.0 -google-auth==2.14.1 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.28.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.11.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.11.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.12.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.12.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/redis/setup.py b/packages/gapic-generator/tests/integration/goldens/redis/setup.py index fdadb0f8c9a8..58f6a4edb6fc 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/setup.py @@ -53,8 +53,14 @@ ] extras = { "async_rest": [ - "google-auth[aiohttp] >= 2.60.0, <3.0.0", - "google-api-core >= 2.41.0, <3.0.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged + "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", + # TODO: Uncomment the line below + # "google-auth[aiohttp] >= 2.60.0, <3.0.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged + "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", + # TODO: Uncomment the line below + # "google-api-core >= 2.41.0, <3.0.0", ], } url = "https://github.com/googleapis/google-cloud-python/tree/main/packages/google-cloud-redis" diff --git a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt index c0c1ba32f46f..78f9f3578bfa 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt @@ -6,7 +6,8 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 google-api-core==2.28.0 -google-auth==2.35.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.35.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10.txt index 0dadf6857ff5..1e69a37f1459 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10.txt @@ -4,8 +4,10 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.28.0 -google-auth==2.14.1 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.28.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.11.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.11.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.12.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.12.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py index fdadb0f8c9a8..58f6a4edb6fc 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py @@ -53,8 +53,14 @@ ] extras = { "async_rest": [ - "google-auth[aiohttp] >= 2.60.0, <3.0.0", - "google-api-core >= 2.41.0, <3.0.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged + "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", + # TODO: Uncomment the line below + # "google-auth[aiohttp] >= 2.60.0, <3.0.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged + "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", + # TODO: Uncomment the line below + # "google-api-core >= 2.41.0, <3.0.0", ], } url = "https://github.com/googleapis/google-cloud-python/tree/main/packages/google-cloud-redis" diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt index c0c1ba32f46f..78f9f3578bfa 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt @@ -6,7 +6,8 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 google-api-core==2.28.0 -google-auth==2.35.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.35.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10.txt index 0dadf6857ff5..1e69a37f1459 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10.txt @@ -4,8 +4,10 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.28.0 -google-auth==2.14.1 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.28.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.11.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.11.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.12.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.12.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py index 263074e55d01..adb5903ec5f5 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py @@ -42,10 +42,14 @@ release_status = "Development Status :: 5 - Production/Stable" dependencies = [ - "google-api-core[grpc] >= 2.41.0, <3.0.0", - # Exclude incompatible versions of `google-auth` - # See https://github.com/googleapis/google-cloud-python/issues/12364 - "google-auth >= 2.14.1, <3.0.0,!=2.24.0,!=2.25.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged + "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", + # TODO: Uncomment the line below + # "google-api-core[grpc] >= 2.41.0, <3.0.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged + "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", + # TODO: Uncomment the line below + # "google-auth[aiohttp] >= 2.60.0, <3.0.0", "grpcio >= 1.59.0, < 2.0.0", "grpcio >= 1.75.1, < 2.0.0; python_version >= '3.14'", "proto-plus >= 1.26.1, <2.0.0", @@ -54,8 +58,14 @@ ] extras = { "async_rest": [ - "google-auth[aiohttp] >= 2.60.0, <3.0.0", - "google-api-core >= 2.41.0, <3.0.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged + "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", + # TODO: Uncomment the line below + # "google-auth[aiohttp] >= 2.60.0, <3.0.0", + # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged + "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", + # TODO: Uncomment the line below + # "google-api-core >= 2.41.0, <3.0.0", ], } url = "https://github.com/googleapis/google-cloud-python/tree/main/packages/google-showcase" diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt index 85cef31887ec..e519a640decd 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt @@ -5,8 +5,10 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.41.0 -google-auth==2.35.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.41.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.35.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt index a75b92057fdb..cd2f9dae7f91 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt @@ -4,8 +4,10 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.41.0 -google-auth==2.14.1 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.41.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.11.txt index 1cd0c5a2c3d4..6f8057b0c797 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.11.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.12.txt index 1cd0c5a2c3d4..6f8057b0c797 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.12.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.10.txt index 0dadf6857ff5..1e69a37f1459 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.10.txt @@ -4,8 +4,10 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -google-api-core==2.28.0 -google-auth==2.14.1 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core==2.28.0 +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.11.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.11.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.12.txt index 7599dea499ed..9f35b36d441e 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.12.txt @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -google-api-core -google-auth +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged +# google-api-core +# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged +# google-auth grpcio proto-plus protobuf From 86e88a3e51f4db6f6ce6f2646ebbba01572a56e3 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 3 Oct 2026 15:01:29 +0000 Subject: [PATCH 32/45] clean up --- .../%sub/services/%service/_shared_macros.j2 | 12 ++--- .../%service/transports/grpc_asyncio.py.j2 | 13 ----- .../services/%service/transports/rest.py.j2 | 1 - .../%service/transports/rest_asyncio.py.j2 | 47 +++---------------- .../%name_%version/%sub/test_%service.py.j2 | 33 ++++++++++++- .../cloud_redis/transports/rest_asyncio.py | 22 ++++----- .../cloud_redis/transports/rest_asyncio.py | 10 ++-- .../transports/grpc_asyncio.py | 13 ----- .../transports/rest.py | 7 +-- .../transports/rest_asyncio.py | 44 ++--------------- .../test_resumable_upload_service.py | 31 +++++++++++- 11 files changed, 93 insertions(+), 140 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 index 42f4e94ef947..0098ac53fa72 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 @@ -307,21 +307,17 @@ session_kwargs: Dict[str, Any] = ( if isinstance(timeout, (int, float)) else {} ) -session = {{ client_output_ident }}( +return {{ client_output_ident }}( upload_url=upload_url, config=config, transport=self._session, response_type={{ method.output.ident }}, start_retry=retry if isinstance(retry, {{ retry_class }}) else None, + {% if body_spec %} + request_body=body, + {% endif %} **session_kwargs, ) -{% if body_spec %} -session.upload = functools.partial(session.upload, request_body=body or "") # type: ignore[method-assign] -{% if not is_async %} -session.iter_upload = functools.partial(session.iter_upload, request_body=body or "") # type: ignore[method-assign] -{% endif %} -{% endif %} -return session {%- endmacro %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 93b8bae7e8ae..73b36467e519 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -387,19 +387,6 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): return _error() self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub() elif HAS_ASYNC_REST: - # TODO(https://github.com/googleapis/google-cloud-python/issues/16268): - # Resolve the credential type mismatch between {{ service.grpc_asyncio_transport_name }} - # (where grpc_helpers_async.create_channel and the base transport's - # google.auth.default() path use synchronous google.auth.credentials.Credentials) - # and Async{{ service.name }}RestTransport (where AsyncAuthorizedSession - # requires asynchronous google.auth.aio.credentials.Credentials). - # Defer instantiating Async{{ service.name }}RestTransport until the - # resumable upload RPC is called so that initializing - # {{ service.grpc_asyncio_transport_name }} with synchronous - # google.auth.credentials.Credentials (e.g. via ADC or service - # account helpers) for standard gRPC calls does not fail in - # __init__ (_prep_wrapped_messages) when AsyncAuthorizedSession - # validates for async credentials. transport = self class _AsyncRestStub: def __call__(self, *args, **kwargs): diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index 9c6c4c9375b8..ae015e11571c 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -14,7 +14,6 @@ from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 {% if service.has_resumable_upload_methods %} -import functools import urllib.parse from google.api_core import resumable_transfer {% endif %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index 7527708eea71..2bbe5aa82129 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -22,8 +22,6 @@ from google.auth.aio import credentials as ga_credentials_async # type: ignore from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 {% if service.has_resumable_upload_methods %} -import contextlib -import functools import urllib.parse from google.api_core import resumable_transfer {% endif %} @@ -93,46 +91,9 @@ DEFAULT_CLIENT_INFO.protobuf_runtime_version = google.protobuf.__version__ {{ shared_macros.create_interceptor_class(api, service, method, is_async=True) }} -{% if service.has_resumable_upload_methods %} -@dataclasses.dataclass -class _AsyncResumableUploadSessionAdapter: - """Adapts AsyncAuthorizedSession to the aiohttp-like request context manager interface expected by AsyncResumableUploadSession.""" - - _session: AsyncAuthorizedSession - - @contextlib.asynccontextmanager - async def request( - self, - method: str, - url: str, - *, - data: Optional[bytes] = None, - headers: Optional[Dict[str, str]] = None, - timeout: Any = None, - **kwargs, - ): - response = await self._session.request( - method, - url, - data=data, - headers=headers, - timeout=getattr(timeout, "total", timeout), - **kwargs, - ) - response.status = response.status_code # type: ignore[attr-defined] - try: - yield response - finally: - await response.close() -{% endif %} - @dataclasses.dataclass class Async{{service.name}}RestStub: - {% if service.has_resumable_upload_methods %} - _session: Union[AsyncAuthorizedSession, _AsyncResumableUploadSessionAdapter] - {% else %} _session: AsyncAuthorizedSession - {% endif %} _host: str _interceptor: Async{{service.name}}RestInterceptor @@ -380,8 +341,12 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): @property def {{method.transport_safe_name|snake_case}}(self) -> Callable[ [{{method.input.ident}}], - {% if method.is_resumable_upload %}{{method.client_output_async.ident}}{% else %}{{method.output.ident}}{% endif %}]: - return self._{{method.name}}({% if method.is_resumable_upload %}_AsyncResumableUploadSessionAdapter(self._session){% else %}self._session{% endif %}, self._host, self._interceptor) # type: ignore + {% if method.is_resumable_upload %} + {{method.client_output_async.ident}}]: + {% else %} + {{method.output.ident}}]: + {% endif %} + return self._{{method.name}}(self._session, self._host, self._interceptor) # type: ignore {% endfor %} {% if rest_async_io_enabled %} diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 23f3ae35fc5f..36b2f707ed6b 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1587,7 +1587,7 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a mock_rest_transport_cls.return_value = mock_rest_transport transport = transports.{{ service.name }}GrpcAsyncIOTransport( - credentials=async_anonymous_credentials(), + credentials=ga_credentials.AnonymousCredentials(), ) mock_rest_transport_cls.assert_not_called() @@ -1607,11 +1607,40 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a mock_rest_transport.close.assert_called_once() +{% if rest_async_io_enabled %} +@pytest.mark.asyncio +async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_rest_stub_sync_credentials(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip() + transport = transports.{{ service.name }}GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + assert transport._rest_transport is None + stub = transport.{{ method.transport_safe_name|snake_case }} + assert transport._rest_transport is None + + result1 = await stub({{ method.input.ident }}()) + rest_transport = transport._rest_transport + assert rest_transport is not None + assert isinstance(result1, {{ method.client_output_async.ident }}) + + result2 = await stub({{ method.input.ident }}()) + assert isinstance(result2, {{ method.client_output_async.ident }}) + assert transport._rest_transport is rest_transport + + with mock.patch.object( + rest_transport, "close", wraps=rest_transport.close + ) as mock_rest_close: + await transport.close() + mock_rest_close.assert_called_once() +{% endif %} + + @pytest.mark.asyncio async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_without_async_rest_error(): with mock.patch.object(transports.grpc_asyncio, "HAS_ASYNC_REST", False): transport = transports.{{ service.name }}GrpcAsyncIOTransport( - credentials=async_anonymous_credentials(), + credentials=ga_credentials.AnonymousCredentials(), ) with pytest.raises(NotImplementedError) as exc_info: await transport.{{ method.transport_safe_name|snake_case }}({{ method.input.ident }}()) diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py index 548cc3fd858c..4d629a5a8443 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py @@ -2426,67 +2426,67 @@ def operations_client(self) -> AsyncOperationsRestClient: @property def create_instance(self) -> Callable[ [cloud_redis.CreateInstanceRequest], -operations_pb2.Operation]: + operations_pb2.Operation]: return self._CreateInstance(self._session, self._host, self._interceptor) # type: ignore @property def delete_instance(self) -> Callable[ [cloud_redis.DeleteInstanceRequest], -operations_pb2.Operation]: + operations_pb2.Operation]: return self._DeleteInstance(self._session, self._host, self._interceptor) # type: ignore @property def export_instance(self) -> Callable[ [cloud_redis.ExportInstanceRequest], -operations_pb2.Operation]: + operations_pb2.Operation]: return self._ExportInstance(self._session, self._host, self._interceptor) # type: ignore @property def failover_instance(self) -> Callable[ [cloud_redis.FailoverInstanceRequest], -operations_pb2.Operation]: + operations_pb2.Operation]: return self._FailoverInstance(self._session, self._host, self._interceptor) # type: ignore @property def get_instance(self) -> Callable[ [cloud_redis.GetInstanceRequest], -cloud_redis.Instance]: + cloud_redis.Instance]: return self._GetInstance(self._session, self._host, self._interceptor) # type: ignore @property def get_instance_auth_string(self) -> Callable[ [cloud_redis.GetInstanceAuthStringRequest], -cloud_redis.InstanceAuthString]: + cloud_redis.InstanceAuthString]: return self._GetInstanceAuthString(self._session, self._host, self._interceptor) # type: ignore @property def import_instance(self) -> Callable[ [cloud_redis.ImportInstanceRequest], -operations_pb2.Operation]: + operations_pb2.Operation]: return self._ImportInstance(self._session, self._host, self._interceptor) # type: ignore @property def list_instances(self) -> Callable[ [cloud_redis.ListInstancesRequest], -cloud_redis.ListInstancesResponse]: + cloud_redis.ListInstancesResponse]: return self._ListInstances(self._session, self._host, self._interceptor) # type: ignore @property def reschedule_maintenance(self) -> Callable[ [cloud_redis.RescheduleMaintenanceRequest], -operations_pb2.Operation]: + operations_pb2.Operation]: return self._RescheduleMaintenance(self._session, self._host, self._interceptor) # type: ignore @property def update_instance(self) -> Callable[ [cloud_redis.UpdateInstanceRequest], -operations_pb2.Operation]: + operations_pb2.Operation]: return self._UpdateInstance(self._session, self._host, self._interceptor) # type: ignore @property def upgrade_instance(self) -> Callable[ [cloud_redis.UpgradeInstanceRequest], -operations_pb2.Operation]: + operations_pb2.Operation]: return self._UpgradeInstance(self._session, self._host, self._interceptor) # type: ignore @property diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py index 7ae3f548fc7d..960d9639a214 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py @@ -1338,31 +1338,31 @@ def operations_client(self) -> AsyncOperationsRestClient: @property def create_instance(self) -> Callable[ [cloud_redis.CreateInstanceRequest], -operations_pb2.Operation]: + operations_pb2.Operation]: return self._CreateInstance(self._session, self._host, self._interceptor) # type: ignore @property def delete_instance(self) -> Callable[ [cloud_redis.DeleteInstanceRequest], -operations_pb2.Operation]: + operations_pb2.Operation]: return self._DeleteInstance(self._session, self._host, self._interceptor) # type: ignore @property def get_instance(self) -> Callable[ [cloud_redis.GetInstanceRequest], -cloud_redis.Instance]: + cloud_redis.Instance]: return self._GetInstance(self._session, self._host, self._interceptor) # type: ignore @property def list_instances(self) -> Callable[ [cloud_redis.ListInstancesRequest], -cloud_redis.ListInstancesResponse]: + cloud_redis.ListInstancesResponse]: return self._ListInstances(self._session, self._host, self._interceptor) # type: ignore @property def update_instance(self) -> Callable[ [cloud_redis.UpdateInstanceRequest], -operations_pb2.Operation]: + operations_pb2.Operation]: return self._UpdateInstance(self._session, self._host, self._interceptor) # type: ignore @property diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 97ed2bef4154..fb87091d2357 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -365,19 +365,6 @@ async def _error(): return _error() self._stubs['upload_media'] = _ErrorStub() elif HAS_ASYNC_REST: - # TODO(https://github.com/googleapis/google-cloud-python/issues/16268): - # Resolve the credential type mismatch between ResumableUploadServiceGrpcAsyncIOTransport - # (where grpc_helpers_async.create_channel and the base transport's - # google.auth.default() path use synchronous google.auth.credentials.Credentials) - # and AsyncResumableUploadServiceRestTransport (where AsyncAuthorizedSession - # requires asynchronous google.auth.aio.credentials.Credentials). - # Defer instantiating AsyncResumableUploadServiceRestTransport until the - # resumable upload RPC is called so that initializing - # ResumableUploadServiceGrpcAsyncIOTransport with synchronous - # google.auth.credentials.Credentials (e.g. via ADC or service - # account helpers) for standard gRPC calls does not fail in - # __init__ (_prep_wrapped_messages) when AsyncAuthorizedSession - # validates for async credentials. transport = self class _AsyncRestStub: def __call__(self, *args, **kwargs): diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py index 6ab112f03ae6..8014d3cc8894 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py @@ -23,7 +23,6 @@ from google.api_core import rest_helpers from google.api_core import rest_streaming from google.api_core import gapic_v1 -import functools import urllib.parse from google.api_core import resumable_transfer from google.showcase_v1beta1._compat import transcode_request @@ -443,17 +442,15 @@ def __call__(self, if isinstance(timeout, (int, float)) else {} ) - session = resumable_transfer.ResumableUploadSession( + return resumable_transfer.ResumableUploadSession( upload_url=upload_url, config=config, transport=self._session, response_type=resumable_upload.UploadMediaResponse, start_retry=retry if isinstance(retry, retries.Retry) else None, + request_body=body, **session_kwargs, ) - session.upload = functools.partial(session.upload, request_body=body or "") # type: ignore[method-assign] - session.iter_upload = functools.partial(session.iter_upload, request_body=body or "") # type: ignore[method-assign] - return session @property def upload_media(self) -> Callable[ diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py index fadd0f670d23..6930b8148752 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -27,8 +27,6 @@ from google.api_core import exceptions as core_exceptions from google.api_core import gapic_v1 -import contextlib -import functools import urllib.parse from google.api_core import resumable_transfer from google.iam.v1 import iam_policy_pb2 # type: ignore @@ -117,40 +115,9 @@ async def pre_upload_media(self, request: resumable_upload.UploadMediaRequest, m return request, metadata -@dataclasses.dataclass -class _AsyncResumableUploadSessionAdapter: - """Adapts AsyncAuthorizedSession to the aiohttp-like request context manager interface expected by AsyncResumableUploadSession.""" - - _session: AsyncAuthorizedSession - - @contextlib.asynccontextmanager - async def request( - self, - method: str, - url: str, - *, - data: Optional[bytes] = None, - headers: Optional[Dict[str, str]] = None, - timeout: Any = None, - **kwargs, - ): - response = await self._session.request( - method, - url, - data=data, - headers=headers, - timeout=getattr(timeout, "total", timeout), - **kwargs, - ) - response.status = response.status_code # type: ignore[attr-defined] - try: - yield response - finally: - await response.close() - @dataclasses.dataclass class AsyncResumableUploadServiceRestStub: - _session: Union[AsyncAuthorizedSession, _AsyncResumableUploadSessionAdapter] + _session: AsyncAuthorizedSession _host: str _interceptor: AsyncResumableUploadServiceRestInterceptor @@ -284,22 +251,21 @@ async def __call__(self, if isinstance(timeout, (int, float)) else {} ) - session = resumable_transfer.AsyncResumableUploadSession( + return resumable_transfer.AsyncResumableUploadSession( upload_url=upload_url, config=config, transport=self._session, response_type=resumable_upload.UploadMediaResponse, start_retry=retry if isinstance(retry, retries.AsyncRetry) else None, + request_body=body, **session_kwargs, ) - session.upload = functools.partial(session.upload, request_body=body or "") # type: ignore[method-assign] - return session @property def upload_media(self) -> Callable[ [resumable_upload.UploadMediaRequest], -resumable_transfer.AsyncResumableUploadSession]: - return self._UploadMedia(_AsyncResumableUploadSessionAdapter(self._session), self._host, self._interceptor) # type: ignore + resumable_transfer.AsyncResumableUploadSession]: + return self._UploadMedia(self._session, self._host, self._interceptor) # type: ignore @property def kind(self) -> str: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 2c7c010955db..78f220bd634c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -2336,7 +2336,7 @@ async def test_resumable_upload_service_upload_media_grpc_asyncio_rest_stub(): mock_rest_transport_cls.return_value = mock_rest_transport transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( - credentials=async_anonymous_credentials(), + credentials=ga_credentials.AnonymousCredentials(), ) mock_rest_transport_cls.assert_not_called() @@ -2356,11 +2356,38 @@ async def test_resumable_upload_service_upload_media_grpc_asyncio_rest_stub(): mock_rest_transport.close.assert_called_once() +@pytest.mark.asyncio +async def test_resumable_upload_service_upload_media_grpc_asyncio_rest_stub_sync_credentials(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip() + transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + assert transport._rest_transport is None + stub = transport.upload_media + assert transport._rest_transport is None + + result1 = await stub(resumable_upload.UploadMediaRequest()) + rest_transport = transport._rest_transport + assert rest_transport is not None + assert isinstance(result1, resumable_transfer.AsyncResumableUploadSession) + + result2 = await stub(resumable_upload.UploadMediaRequest()) + assert isinstance(result2, resumable_transfer.AsyncResumableUploadSession) + assert transport._rest_transport is rest_transport + + with mock.patch.object( + rest_transport, "close", wraps=rest_transport.close + ) as mock_rest_close: + await transport.close() + mock_rest_close.assert_called_once() + + @pytest.mark.asyncio async def test_resumable_upload_service_upload_media_grpc_asyncio_without_async_rest_error(): with mock.patch.object(transports.grpc_asyncio, "HAS_ASYNC_REST", False): transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( - credentials=async_anonymous_credentials(), + credentials=ga_credentials.AnonymousCredentials(), ) with pytest.raises(NotImplementedError) as exc_info: await transport.upload_media(resumable_upload.UploadMediaRequest()) From c20ac4048dcafc374783bca102501e3843cc48e6 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 3 Oct 2026 16:06:15 +0000 Subject: [PATCH 33/45] test: add unit tests for bug where start_retry is not passed from client.upload_media --- .../unit/gapic/%name_%version/%sub/test_%service.py.j2 | 6 ++++++ .../gapic/showcase_v1beta1/test_resumable_upload_service.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 36b2f707ed6b..350b1824dc28 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1681,12 +1681,15 @@ def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_start_e upload_response.content = json_return_value.encode("UTF-8") req.side_effect = [start_response, upload_response] + retry = retries.Retry() response = client.{{ method.client_method_name|snake_case }}( {{ method.input.ident }}(), config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), + retry=retry, timeout=10.0, ) assert isinstance(response, {{ method.client_output.ident }}) + assert response._start_retry is retry result = response.upload(b"test data") assert isinstance(result, {{ method.output.ident }}) assert result == expected_result @@ -1727,12 +1730,15 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_a upload_response.close = mock.AsyncMock(return_value=None) req.side_effect = [start_response, upload_response] + retry = retries.AsyncRetry() response = await client.{{ method.client_method_name|snake_case }}( {{ method.input.ident }}(), config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), + retry=retry, timeout=10.0, ) assert isinstance(response, {{ method.client_output_async.ident }}) + assert response._start_retry is retry result = await response.upload(b"test data") assert isinstance(result, {{ method.output.ident }}) assert result == expected_result diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 78f220bd634c..12485b38e484 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -2417,12 +2417,15 @@ def test_resumable_upload_service_upload_media_rest_start_empty_body(): upload_response.content = json_return_value.encode("UTF-8") req.side_effect = [start_response, upload_response] + retry = retries.Retry() response = client.upload_media( resumable_upload.UploadMediaRequest(), config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), + retry=retry, timeout=10.0, ) assert isinstance(response, resumable_transfer.ResumableUploadSession) + assert response._start_retry is retry result = response.upload(b"test data") assert isinstance(result, resumable_upload.UploadMediaResponse) assert result == expected_result @@ -2455,12 +2458,15 @@ async def test_resumable_upload_service_upload_media_rest_asyncio_start_empty_bo upload_response.close = mock.AsyncMock(return_value=None) req.side_effect = [start_response, upload_response] + retry = retries.AsyncRetry() response = await client.upload_media( resumable_upload.UploadMediaRequest(), config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), + retry=retry, timeout=10.0, ) assert isinstance(response, resumable_transfer.AsyncResumableUploadSession) + assert response._start_retry is retry result = await response.upload(b"test data") assert isinstance(result, resumable_upload.UploadMediaResponse) assert result == expected_result From d52a6bf6058f8cd1142ff816a21eae2001226cf4 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 3 Oct 2026 16:31:40 +0000 Subject: [PATCH 34/45] address review feedback --- .../%sub/services/%service/_client_macros.j2 | 1 + .../%sub/services/%service/_shared_macros.j2 | 5 ++++- .../%sub/services/%service/async_client.py.j2 | 1 + .../%sub/services/%service/transports/rest.py.j2 | 9 +++++++-- .../services/%service/transports/rest_asyncio.py.j2 | 9 +++++++-- .../services/resumable_upload_service/async_client.py | 1 + .../services/resumable_upload_service/client.py | 1 + .../resumable_upload_service/transports/rest.py | 11 +++++++---- .../transports/rest_asyncio.py | 11 +++++++---- 9 files changed, 36 insertions(+), 13 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 index 57d452a29a95..08da6aaa052b 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_client_macros.j2 @@ -170,6 +170,7 @@ metadata=metadata, {% if method.is_resumable_upload %} config=config, + start_retry=retry, {% endif %} ) {% if method.lro %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 index 0098ac53fa72..0c331434006d 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 @@ -307,12 +307,15 @@ session_kwargs: Dict[str, Any] = ( if isinstance(timeout, (int, float)) else {} ) +# ``start_retry`` is used instead of ``retry`` because ``_GapicCallable`` +# consumes the ``retry`` argument before invoking the transport callable +# and only forwards extra keyword arguments such as ``start_retry``. return {{ client_output_ident }}( upload_url=upload_url, config=config, transport=self._session, response_type={{ method.output.ident }}, - start_retry=retry if isinstance(retry, {{ retry_class }}) else None, + start_retry=start_retry if isinstance(start_retry, {{ retry_class }}) else None, {% if body_spec %} request_body=body, {% endif %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 index 548c5019c971..8d96ab289388 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/async_client.py.j2 @@ -426,6 +426,7 @@ class {{ service.async_client_name }}: metadata=metadata, {% if method.is_resumable_upload %} config=config, + start_retry=retry, {% endif %} ) {% if method.lro %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 index ae015e11571c..3f4f3f4797b3 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest.py.j2 @@ -227,8 +227,10 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): request: {{method.input.ident}}, *, {% if method.is_resumable_upload %} config: Optional[resumable_transfer.ResumableUploadConfig]=None, - {% endif %} + start_retry: OptionalRetry=gapic_v1.method.DEFAULT, + {% else %} retry: OptionalRetry=gapic_v1.method.DEFAULT, + {% endif %} timeout: Optional[float]=None, {{ shared_macros.client_method_metadata_argument()|indent(8) }}={{ shared_macros.client_method_metadata_default_value() }}, ){% if not method.void %} -> {% if method.is_resumable_upload %}{{method.client_output.ident}}{% elif not method.server_streaming %}{{method.output.ident}}{% else %}rest_streaming.ResponseIterator{% endif %}{% endif %}: @@ -245,9 +247,12 @@ class {{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {% if method.is_resumable_upload %} config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): Optional configuration for the resumable upload session. - {% endif %} + start_retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried when initiating the resumable upload session. + {% else %} retry (google.api_core.retry.Retry): Designation of what errors, if any, should be retried. + {% endif %} timeout (float): The timeout for this request. {{ shared_macros.client_method_metadata_argument_doc()|indent(8) }} {% if not method.void %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index 2bbe5aa82129..afe7114846ed 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -184,8 +184,10 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): request: {{method.input.ident}}, *, {% if method.is_resumable_upload %} config: Optional[resumable_transfer.ResumableUploadConfig]=None, - {% endif %} + start_retry: OptionalRetry=gapic_v1.method.DEFAULT, + {% else %} retry: OptionalRetry=gapic_v1.method.DEFAULT, + {% endif %} timeout: Optional[float]=None, {{ shared_macros.client_method_metadata_argument()|indent(12) }}={{ shared_macros.client_method_metadata_default_value() }}, {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2169): Update return type for client streaming method. #} @@ -204,9 +206,12 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): {% if method.is_resumable_upload %} config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): Optional configuration for the resumable upload session. - {% endif %} + start_retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, + should be retried when initiating the resumable upload session. + {% else %} retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, should be retried. + {% endif %} timeout (float): The timeout for this request. {{ shared_macros.client_method_metadata_argument_doc()|indent(8) }} {% if not method.void %} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py index 60ecb78dfbe5..63b639314785 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/async_client.py @@ -334,6 +334,7 @@ async def sample_upload_media(): timeout=timeout, metadata=metadata, config=config, + start_retry=retry, ) # Done; return the response. diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py index 073962795207..059aac46ce1a 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py @@ -601,6 +601,7 @@ def sample_upload_media(): timeout=timeout, metadata=metadata, config=config, + start_retry=retry, ) # Done; return the response. diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py index 8014d3cc8894..8e6d6208e514 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py @@ -389,7 +389,7 @@ def __hash__(self): def __call__(self, request: resumable_upload.UploadMediaRequest, *, config: Optional[resumable_transfer.ResumableUploadConfig]=None, - retry: OptionalRetry=gapic_v1.method.DEFAULT, + start_retry: OptionalRetry=gapic_v1.method.DEFAULT, timeout: Optional[float]=None, metadata: Sequence[Tuple[str, Union[str, bytes]]]=(), ) -> resumable_transfer.ResumableUploadSession: @@ -400,8 +400,8 @@ def __call__(self, The request object. config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): Optional configuration for the resumable upload session. - retry (google.api_core.retry.Retry): Designation of what errors, if any, - should be retried. + start_retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried when initiating the resumable upload session. timeout (float): The timeout for this request. metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be sent along with the request as metadata. Normally, each value must be of type `str`, @@ -442,12 +442,15 @@ def __call__(self, if isinstance(timeout, (int, float)) else {} ) + # ``start_retry`` is used instead of ``retry`` because ``_GapicCallable`` + # consumes the ``retry`` argument before invoking the transport callable + # and only forwards extra keyword arguments such as ``start_retry``. return resumable_transfer.ResumableUploadSession( upload_url=upload_url, config=config, transport=self._session, response_type=resumable_upload.UploadMediaResponse, - start_retry=retry if isinstance(retry, retries.Retry) else None, + start_retry=start_retry if isinstance(start_retry, retries.Retry) else None, request_body=body, **session_kwargs, ) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py index 6930b8148752..c8271568728b 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -198,7 +198,7 @@ def __hash__(self): async def __call__(self, request: resumable_upload.UploadMediaRequest, *, config: Optional[resumable_transfer.ResumableUploadConfig]=None, - retry: OptionalRetry=gapic_v1.method.DEFAULT, + start_retry: OptionalRetry=gapic_v1.method.DEFAULT, timeout: Optional[float]=None, metadata: Sequence[Tuple[str, Union[str, bytes]]]=(), ) -> resumable_transfer.AsyncResumableUploadSession: @@ -209,8 +209,8 @@ async def __call__(self, The request object. config (Optional[google.api_core.resumable_transfer.ResumableUploadConfig]): Optional configuration for the resumable upload session. - retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, - should be retried. + start_retry (google.api_core.retry_async.AsyncRetry): Designation of what errors, if any, + should be retried when initiating the resumable upload session. timeout (float): The timeout for this request. metadata (Sequence[Tuple[str, Union[str, bytes]]]): Key/value pairs which should be sent along with the request as metadata. Normally, each value must be of type `str`, @@ -251,12 +251,15 @@ async def __call__(self, if isinstance(timeout, (int, float)) else {} ) + # ``start_retry`` is used instead of ``retry`` because ``_GapicCallable`` + # consumes the ``retry`` argument before invoking the transport callable + # and only forwards extra keyword arguments such as ``start_retry``. return resumable_transfer.AsyncResumableUploadSession( upload_url=upload_url, config=config, transport=self._session, response_type=resumable_upload.UploadMediaResponse, - start_retry=retry if isinstance(retry, retries.AsyncRetry) else None, + start_retry=start_retry if isinstance(start_retry, retries.AsyncRetry) else None, request_body=body, **session_kwargs, ) From 9af1477999ec3e2ba908fdf3f75cf89448b58016 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 3 Oct 2026 22:48:18 +0000 Subject: [PATCH 35/45] address review feedback --- .../gapic/generator/generator.py | 6 +- .../%sub/services/%service/client.py.j2 | 2 +- .../%service/transports/__init__.py.j2 | 2 +- .../%name_%version/%sub/test_%service.py.j2 | 126 ++++++- .../gapic/%name_%version/%sub/test_macros.j2 | 11 +- .../resumable_upload_service/client.py | 43 +-- .../transports/__init__.py | 15 +- .../test_resumable_upload_service.py | 340 ++++-------------- .../tests/unit/generator/test_generator.py | 10 +- 9 files changed, 217 insertions(+), 338 deletions(-) diff --git a/packages/gapic-generator/gapic/generator/generator.py b/packages/gapic-generator/gapic/generator/generator.py index c1a90b98b34c..76bd9ed4ccee 100644 --- a/packages/gapic-generator/gapic/generator/generator.py +++ b/packages/gapic-generator/gapic/generator/generator.py @@ -354,14 +354,16 @@ def _render_template( and not api_schema.all_library_settings[ api_schema.naming.proto_package ].python_settings.experimental_features.rest_async_io_enabled - and not service.has_resumable_upload_methods ) or ( "rest_asyncio" in template_name and not api_schema.all_library_settings[ api_schema.naming.proto_package ].python_settings.experimental_features.rest_async_io_enabled - and not service.has_resumable_upload_methods + and not ( + "grpc" in opts.transport + and service.has_resumable_upload_methods + ) ) or ( "rest_base" in template_name diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 index c2f2cc4b6234..423c8d61a2fa 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/client.py.j2 @@ -1,7 +1,7 @@ {% extends '_base.py.j2' %} {% block content %} {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2121): Remove the following variable (and the condition later in this file) for async rest transport once support for it is GA. #} -{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled or service.has_resumable_upload_methods %} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} {% set has_auto_populated_fields = api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|select|list %} {% import "%namespace/%name_%version/%sub/services/%service/_client_macros.j2" as macros %} {% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %} diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 index bc7187742991..5bba915d0d2b 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/__init__.py.j2 @@ -1,5 +1,5 @@ {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2121): Remove the following variable (and the condition later in this file) for async rest transport once support for it is GA. #} -{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled or service.has_resumable_upload_methods %} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} {% extends '_base.py.j2' %} {% block content %} diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 350b1824dc28..07081a1e28c8 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1,7 +1,7 @@ {% extends "_base.py.j2" %} {% block content %} {# TODO(https://github.com/googleapis/gapic-generator-python/issues/2121): Remove the following variable (and the condition later in this file) for async rest transport once support for it is GA. #} -{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled or service.has_resumable_upload_methods %} +{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} {% set has_auto_populated_fields = api.all_method_settings.values()|map(attribute="auto_populated_fields", default=[])|select|list %} {% import "tests/unit/gapic/%name_%version/%sub/test_macros.j2" as test_macros %} {% import "%namespace/%name_%version/%sub/services/%service/_shared_macros.j2" as shared_macros %} @@ -16,7 +16,7 @@ from unittest.mock import AsyncMock import grpc from grpc.experimental import aio -{% if "rest" in opts.transport %} +{% if "rest" in opts.transport or service.has_resumable_upload_methods %} from collections.abc import Iterable, AsyncIterable from google.protobuf import json_format {% endif %} @@ -27,8 +27,7 @@ from collections.abc import Sequence, Mapping from google.api_core import api_core_version from proto.marshal.rules.dates import DurationRule, TimestampRule from proto.marshal.rules import wrappers -{% if 'rest' in opts.transport %} -{% if rest_async_io_enabled %} +{% if ('rest' in opts.transport and rest_async_io_enabled) or ('grpc' in opts.transport and service.has_resumable_upload_methods) %} try: import aiohttp # type: ignore from google.auth.aio.transport.sessions import AsyncAuthorizedSession @@ -37,7 +36,8 @@ try: {# NOTE: `pragma: NO COVER` is needed since the coverage for presubmits isn't combined. #} except ImportError: # pragma: NO COVER HAS_ASYNC_REST_EXTRA = False -{% endif %}{# if rest_async_io_enabled #} +{% endif %} +{% if 'rest' in opts.transport %} from requests import Response from requests import Request, PreparedRequest from requests.sessions import Session @@ -1607,7 +1607,6 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a mock_rest_transport.close.assert_called_once() -{% if rest_async_io_enabled %} @pytest.mark.asyncio async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_rest_stub_sync_credentials(): if not HAS_ASYNC_REST_EXTRA: @@ -1633,7 +1632,120 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a ) as mock_rest_close: await transport.close() mock_rest_close.assert_called_once() -{% endif %} + + +@pytest.mark.asyncio +async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_start_empty_body(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip() + client = {{ service.async_client_name }}( + credentials=async_anonymous_credentials(), + ) + with mock.patch.object(AsyncAuthorizedSession, "request") as req: + start_response = mock.MagicMock() + start_response.status_code = 200 + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + start_response.read = mock.AsyncMock(return_value=b"") + start_response.close = mock.AsyncMock(return_value=None) + + expected_result = {{ 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 %} + ) + {% if method.output.ident.is_proto_plus_type %} + json_return_value = {{ method.output.ident }}.to_json(expected_result) + {% else %} + json_return_value = json_format.MessageToJson(expected_result) + {% endif %} + upload_response = mock.MagicMock() + upload_response.status_code = 200 + upload_response.headers = {"x-goog-upload-status": "final"} + upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) + upload_response.close = mock.AsyncMock(return_value=None) + + req.side_effect = [start_response, upload_response] + retry = retries.AsyncRetry() + response = await client.{{ method.client_method_name|snake_case }}( + {{ method.input.ident }}(), + config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), + retry=retry, + timeout=10.0, + ) + assert isinstance(response, {{ method.client_output_async.ident }}) + assert response._start_retry is retry + result = await response.upload(b"test data") + assert isinstance(result, {{ method.output.ident }}) + assert result == expected_result + + +@pytest.mark.asyncio +async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_body_and_query_params(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip() + client = {{ service.async_client_name }}( + credentials=async_anonymous_credentials(), + ) + with mock.patch.object( + AsyncAuthorizedSession, "request" + ) as req, mock.patch.object(path_template, "transcode") as transcode: + request = {{ method.input.ident }}( + {% for field in method.input.fields.values() | rejectattr('message') %} + {% if not field.oneof or field.proto3_optional %} + {{ field.name }}={{ field.mock_value }}, + {% endif %} + {% endfor %} + ) + {% if method.input.ident.is_proto_plus_type %} + pb_request = {{ method.input.ident }}.pb(request) + {% else %} + pb_request = request + {% endif %} + transcode.return_value = { + "uri": "v1/sample_method", + "method": "{{ method.http_options[0].method }}", + "body": pb_request, + "query_params": pb_request, + } + + start_response = mock.MagicMock() + start_response.status_code = 200 + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + start_response.read = mock.AsyncMock(return_value=b"") + start_response.close = mock.AsyncMock(return_value=None) + + expected_result = {{ method.output.ident }}() + {% if method.output.ident.is_proto_plus_type %} + json_return_value = {{ method.output.ident }}.to_json(expected_result) + {% else %} + json_return_value = json_format.MessageToJson(expected_result) + {% endif %} + upload_response = mock.MagicMock() + upload_response.status_code = 200 + upload_response.headers = {"x-goog-upload-status": "final"} + upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) + upload_response.close = mock.AsyncMock(return_value=None) + + req.side_effect = [start_response, upload_response] + response = await client.{{ method.client_method_name|snake_case }}(request) + result = await response.upload(b"test data") + assert result == expected_result + + start_call = req.call_args_list[0] + {% for field in method.input.fields.values() | rejectattr('message') %} + {% if not field.oneof or field.proto3_optional %} + assert "{{ field.name | camel_case }}=" in start_call.args[1] + {% endif %} + {% endfor %} + {% if method.http_options[0].body %} + expected_body = json_format.MessageToJson( + pb_request, + use_integers_for_enums={{ opts.rest_numeric_enums }}, + ) + assert start_call.kwargs["data"] == expected_body.encode("UTF-8") + {% endif %} @pytest.mark.asyncio diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 index 69c37a614fda..986f46f6c060 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_macros.j2 @@ -1740,8 +1740,7 @@ def test_unsupported_parameter_rest_asyncio(): # into `run_transport_tests_for_config` and make any of the rest specific specific macros which are called within more generic. #} {% macro run_transport_tests_for_config(service, api, transport, is_async) %} -{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} -{% for method in service.methods.values() if not (transport == 'rest' and is_async) or rest_async_io_enabled or method.is_resumable_upload %} +{% for method in service.methods.values() %} {% if is_rest_unsupported_method(method, is_async) == 'True' or not method.http_options %} {{ rest_method_not_implemented_error(service, method, transport, is_async) }} {% else %} @@ -1752,14 +1751,12 @@ def test_unsupported_parameter_rest_asyncio(): {% endif %}{# if 'rest' in transport #} {% endif %}{# is_rest_unsupported_method(method, is_async) == 'False' and method.http_options #} {% endfor %}{# for method in service.methods.values() #} -{% if not (transport == 'rest' and is_async) or rest_async_io_enabled %} {% for name, sig in api.mixin_api_signatures.items() %} {% if 'rest' in transport %} {{ bad_request_mixins_test(service, api, name, sig, transport, is_async) }} {{ call_success_mixins_test(service, api, name, sig, transport, is_async) }} {% endif %}{# if 'rest' in transport #} {% endfor %} -{% endif %} {{ initialize_client_with_transport_test(service, transport, is_async) }} {% endmacro %} @@ -2260,8 +2257,7 @@ def test_initialize_client_w_{{transport_name}}(): {% endmacro %}{# call_success_mixins_test #} {% macro empty_call_test(service, api, transport, is_async) %} -{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} -{% for method in service.methods.values() if not (transport == 'rest' and is_async) or rest_async_io_enabled or method.is_resumable_upload %}{# method #} +{% for method in service.methods.values() %}{# method #} {% if not method.client_streaming %} # This test is a coverage failsafe to make sure that totally empty calls, # i.e. request == None and no flattened fields passed, work. @@ -2271,8 +2267,7 @@ def test_initialize_client_w_{{transport_name}}(): {% endmacro %}{# empty_call_test #} {% macro routing_parameter_test(service, api, transport, is_async) %} -{% set rest_async_io_enabled = api.all_library_settings[api.naming.proto_package].python_settings.experimental_features.rest_async_io_enabled %} -{% for method in service.methods.values() if not (transport == 'rest' and is_async) or rest_async_io_enabled or method.is_resumable_upload %}{# method #} +{% for method in service.methods.values() %}{# method #} {# See existing proposal b/330610501 to add support for explicit routing in BIDI/client side streaming #} {% if method.explicit_routing and not method.client_streaming %} {# Any value that is part of the HTTP/1.1 URI should be sent as #} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py index 059aac46ce1a..4fedb635bc91 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/client.py @@ -62,11 +62,6 @@ from .transports.grpc import ResumableUploadServiceGrpcTransport from .transports.grpc_asyncio import ResumableUploadServiceGrpcAsyncIOTransport from .transports.rest import ResumableUploadServiceRestTransport -try: - from .transports.rest_asyncio import AsyncResumableUploadServiceRestTransport - HAS_ASYNC_REST_DEPENDENCIES = True # pragma: NO COVER -except ImportError: # pragma: NO COVER - HAS_ASYNC_REST_DEPENDENCIES = False class ResumableUploadServiceClientMeta(type): @@ -80,8 +75,6 @@ class ResumableUploadServiceClientMeta(type): _transport_registry["grpc"] = ResumableUploadServiceGrpcTransport _transport_registry["grpc_asyncio"] = ResumableUploadServiceGrpcAsyncIOTransport _transport_registry["rest"] = ResumableUploadServiceRestTransport - if HAS_ASYNC_REST_DEPENDENCIES: # pragma: NO COVER - _transport_registry["rest_asyncio"] = AsyncResumableUploadServiceRestTransport def get_transport_class(cls, label: Optional[str] = None, @@ -96,10 +89,6 @@ def get_transport_class(cls, The transport class to use. """ # If a specific transport is requested, return that one. - if label == "rest_asyncio" and not HAS_ASYNC_REST_DEPENDENCIES: # pragma: NO COVER - raise ImportError( - "`rest_asyncio` transport requires the library to be installed with the `async_rest` extra. Install the library with the `async_rest` extra using `pip install google-showcase[async_rest]`" - ) if label: return cls._transport_registry[label] @@ -467,38 +456,16 @@ def __init__(self, *, )) if not transport_provided: - transport_init: Union[Type[ResumableUploadServiceTransport], Callable[..., ResumableUploadServiceTransport]] = ( - ResumableUploadServiceClient.get_transport_class(transport) - if isinstance(transport, str) or transport is None - else cast(Callable[..., ResumableUploadServiceTransport], transport) - ) - - if "rest_asyncio" in str(transport_init): # pragma: NO COVER - unsupported_params = { - "google.api_core.client_options.ClientOptions.credentials_file": self._client_options.credentials_file, - "google.api_core.client_options.ClientOptions.scopes": self._client_options.scopes, - "google.api_core.client_options.ClientOptions.quota_project_id": self._client_options.quota_project_id, - "google.api_core.client_options.ClientOptions.client_cert_source": self._client_options.client_cert_source, - "google.api_core.client_options.ClientOptions.api_audience": self._client_options.api_audience, - - } - provided_unsupported_params = [name for name, value in unsupported_params.items() if value is not None] - if provided_unsupported_params: - raise core_exceptions.AsyncRestUnsupportedParameterError( # type: ignore - f"The following provided parameters are not supported for `transport=rest_asyncio`: {', '.join(provided_unsupported_params)}" - ) - self._transport = transport_init( - credentials=credentials, - host=self._api_endpoint, - client_info=client_info, - ) - return - import google.auth._default # type: ignore if api_key_value and hasattr(google.auth._default, "get_api_key_credentials"): credentials = google.auth._default.get_api_key_credentials(api_key_value) + transport_init: Union[Type[ResumableUploadServiceTransport], Callable[..., ResumableUploadServiceTransport]] = ( + ResumableUploadServiceClient.get_transport_class(transport) + if isinstance(transport, str) or transport is None + else cast(Callable[..., ResumableUploadServiceTransport], transport) + ) # initialize with the provided callable or the passed in class self._transport = transport_init( credentials=credentials, diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py index 6b0c61381549..432a8bdbc168 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/__init__.py @@ -14,22 +14,13 @@ # limitations under the License. # from collections import OrderedDict -from typing import Dict, Type, Tuple +from typing import Dict, Type from .base import ResumableUploadServiceTransport from .grpc import ResumableUploadServiceGrpcTransport from .grpc_asyncio import ResumableUploadServiceGrpcAsyncIOTransport from .rest import ResumableUploadServiceRestTransport from .rest import ResumableUploadServiceRestInterceptor -ASYNC_REST_CLASSES: Tuple[str, ...] -try: - from .rest_asyncio import AsyncResumableUploadServiceRestTransport - from .rest_asyncio import AsyncResumableUploadServiceRestInterceptor # pragma: NO COVER - ASYNC_REST_CLASSES = ('AsyncResumableUploadServiceRestTransport', 'AsyncResumableUploadServiceRestInterceptor') # pragma: NO COVER - HAS_REST_ASYNC = True # pragma: NO COVER -except ImportError: # pragma: NO COVER - ASYNC_REST_CLASSES = () - HAS_REST_ASYNC = False # Compile a registry of transports. @@ -37,8 +28,6 @@ _transport_registry['grpc'] = ResumableUploadServiceGrpcTransport _transport_registry['grpc_asyncio'] = ResumableUploadServiceGrpcAsyncIOTransport _transport_registry['rest'] = ResumableUploadServiceRestTransport -if HAS_REST_ASYNC: # pragma: NO COVER - _transport_registry['rest_asyncio'] = AsyncResumableUploadServiceRestTransport __all__ = ( 'ResumableUploadServiceTransport', @@ -46,4 +35,4 @@ 'ResumableUploadServiceGrpcAsyncIOTransport', 'ResumableUploadServiceRestTransport', 'ResumableUploadServiceRestInterceptor', -) + ASYNC_REST_CLASSES +) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 12485b38e484..f867eba111bd 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -1799,176 +1799,6 @@ def test_upload_media_empty_call_rest(): assert args[0] == request_msg -def test_transport_kind_rest_asyncio(): - if not HAS_ASYNC_REST_EXTRA: - pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") - transport = ResumableUploadServiceAsyncClient.get_transport_class("rest_asyncio")( - credentials=async_anonymous_credentials() - ) - assert transport.kind == "rest_asyncio" - - -@pytest.mark.asyncio -async def test_upload_media_rest_asyncio_bad_request(request_type=resumable_upload.UploadMediaRequest): - if not HAS_ASYNC_REST_EXTRA: - pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") - client = ResumableUploadServiceAsyncClient( - credentials=async_anonymous_credentials(), - transport="rest_asyncio" - ) - # send a request that will satisfy transcoding - request_init = {} - request = request_type(**request_init) - - # Mock the http request call within the method and fake a BadRequest error. - with mock.patch.object(AsyncAuthorizedSession, 'request') as req, pytest.raises(core_exceptions.BadRequest): - # Wrap the value into a proper Response obj - response_value = mock.MagicMock() - response_value.read = mock.AsyncMock(return_value=b'{}') - response_value.close = mock.AsyncMock(return_value=None) - response_value.status_code = 400 - response_value.request = mock.Mock() - req.return_value = response_value - req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} - response = await client.upload_media(request) - await response.upload(b"") - - -@pytest.mark.asyncio -@pytest.mark.parametrize("request_type", [ - resumable_upload.UploadMediaRequest, - dict, -]) -async def test_upload_media_rest_asyncio_call_success(request_type): - if not HAS_ASYNC_REST_EXTRA: - pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") - client = ResumableUploadServiceAsyncClient( - credentials=async_anonymous_credentials(), - transport="rest_asyncio" - ) - - # send a request that will satisfy transcoding - request_init = {} - request = request_type(**request_init) - - # Mock the http request call within the method and fake a response. - with mock.patch.object(type(client.transport._session), 'request') as req: - # Designate an appropriate value for the returned response. - return_value = resumable_upload.UploadMediaResponse( - name='name_value', - size=443, - ) - - # Wrap the value into a proper Response obj - response_value = mock.MagicMock() - response_value.status_code = 200 - - # Convert return value to protobuf type - return_value = resumable_upload.UploadMediaResponse.pb(return_value) - json_return_value = json_format.MessageToJson(return_value) - response_value.read = mock.AsyncMock(return_value=json_return_value.encode('UTF-8')) - start_response = mock.MagicMock() - start_response.status_code = 200 - start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} - response_value.headers = {"x-goog-upload-status": "final", "header-1": "value-1", "header-2": "value-2"} - start_response.read = mock.AsyncMock(return_value=b"") - start_response.close = mock.AsyncMock(return_value=None) - response_value.close = mock.AsyncMock(return_value=None) - req.side_effect = [start_response, response_value] - response = await client.upload_media(request) - assert isinstance(response, resumable_transfer.AsyncResumableUploadSession) - response = await response.upload(b"test data") - - # Establish that the response is the type that we expect. - assert isinstance(response, resumable_upload.UploadMediaResponse) - assert response.name == 'name_value' - assert response.size == 443 - - -@pytest.mark.asyncio -@pytest.mark.parametrize("null_interceptor", [True, False]) -async def test_upload_media_rest_asyncio_interceptors(null_interceptor): - if not HAS_ASYNC_REST_EXTRA: - pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") - transport = transports.AsyncResumableUploadServiceRestTransport( - credentials=async_anonymous_credentials(), - interceptor=None if null_interceptor else transports.AsyncResumableUploadServiceRestInterceptor(), - ) - client = ResumableUploadServiceAsyncClient(transport=transport) - - with mock.patch.object(type(client.transport._session), "request") as req, \ - mock.patch.object(path_template, "transcode") as transcode, \ - mock.patch.object(transports.AsyncResumableUploadServiceRestInterceptor, "pre_upload_media") as pre: - pre.assert_not_called() - pb_message = resumable_upload.UploadMediaRequest.pb(resumable_upload.UploadMediaRequest()) - transcode.return_value = { - "method": "post", - "uri": "my_uri", - "body": pb_message, - "query_params": pb_message, - } - - req.return_value = mock.Mock() - req.return_value.status_code = 200 - req.return_value.headers = {"header-1": "value-1", "header-2": "value-2"} - - request = resumable_upload.UploadMediaRequest() - metadata =[ - ("key", "val"), - ("cephalopod", "squid"), - ] - pre.return_value = request, metadata - - await client.upload_media(request, metadata=[("key", "val"), ("cephalopod", "squid"),]) - - pre.assert_called_once() - -def test_initialize_client_w_rest_asyncio(): - if not HAS_ASYNC_REST_EXTRA: - pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") - client = ResumableUploadServiceAsyncClient( - credentials=async_anonymous_credentials(), - transport="rest_asyncio" - ) - assert client is not None - - -# This test is a coverage failsafe to make sure that totally empty calls, -# i.e. request == None and no flattened fields passed, work. -@pytest.mark.asyncio -async def test_upload_media_empty_call_rest_asyncio(): - if not HAS_ASYNC_REST_EXTRA: - pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") - client = ResumableUploadServiceAsyncClient( - credentials=async_anonymous_credentials(), - transport="rest_asyncio", - ) - - # Mock the actual call, and fake the request. - with mock.patch.object( - type(client.transport.upload_media), - '__call__') as call: - await client.upload_media(request=None) - - # Establish that the underlying stub method was called. - call.assert_called() - _, args, _ = call.mock_calls[0] - request_msg = resumable_upload.UploadMediaRequest() - assert args[0] == request_msg - - -def test_unsupported_parameter_rest_asyncio(): - if not HAS_ASYNC_REST_EXTRA: - pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") - options = client_options.ClientOptions(quota_project_id="octopus") - with pytest.raises(core_exceptions.AsyncRestUnsupportedParameterError, match="google.api_core.client_options.ClientOptions.quota_project_id") as exc: # type: ignore - client = ResumableUploadServiceAsyncClient( - credentials=async_anonymous_credentials(), - transport="rest_asyncio", - client_options=options - ) - - def test_transport_grpc_default(): # A client should use the gRPC transport by default. client = ResumableUploadServiceClient( @@ -2384,62 +2214,13 @@ async def test_resumable_upload_service_upload_media_grpc_asyncio_rest_stub_sync @pytest.mark.asyncio -async def test_resumable_upload_service_upload_media_grpc_asyncio_without_async_rest_error(): - with mock.patch.object(transports.grpc_asyncio, "HAS_ASYNC_REST", False): - transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( - credentials=ga_credentials.AnonymousCredentials(), - ) - with pytest.raises(NotImplementedError) as exc_info: - await transport.upload_media(resumable_upload.UploadMediaRequest()) - assert "Async REST transport is required for async resumable upload methods." in str(exc_info.value) - -def test_resumable_upload_service_upload_media_rest_start_empty_body(): - client = ResumableUploadServiceClient( - credentials=ga_credentials.AnonymousCredentials(), - transport="rest", - ) - with mock.patch.object(type(client.transport._session), "request") as req: - start_response = mock.MagicMock() - start_response.ok = True - start_response.status_code = 200 - start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} - start_response.content = b"" - - expected_result = resumable_upload.UploadMediaResponse( - name='name_value', - size=443, - ) - json_return_value = resumable_upload.UploadMediaResponse.to_json(expected_result) - upload_response = mock.MagicMock() - upload_response.ok = True - upload_response.status_code = 200 - upload_response.headers = {"x-goog-upload-status": "final"} - upload_response.content = json_return_value.encode("UTF-8") - - req.side_effect = [start_response, upload_response] - retry = retries.Retry() - response = client.upload_media( - resumable_upload.UploadMediaRequest(), - config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), - retry=retry, - timeout=10.0, - ) - assert isinstance(response, resumable_transfer.ResumableUploadSession) - assert response._start_retry is retry - result = response.upload(b"test data") - assert isinstance(result, resumable_upload.UploadMediaResponse) - assert result == expected_result - - -@pytest.mark.asyncio -async def test_resumable_upload_service_upload_media_rest_asyncio_start_empty_body(): +async def test_resumable_upload_service_upload_media_grpc_asyncio_start_empty_body(): if not HAS_ASYNC_REST_EXTRA: pytest.skip() client = ResumableUploadServiceAsyncClient( credentials=async_anonymous_credentials(), - transport="rest_asyncio", ) - with mock.patch.object(type(client.transport._session), "request") as req: + with mock.patch.object(AsyncAuthorizedSession, "request") as req: start_response = mock.MagicMock() start_response.status_code = 200 start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} @@ -2472,14 +2253,16 @@ async def test_resumable_upload_service_upload_media_rest_asyncio_start_empty_bo assert result == expected_result -def test_resumable_upload_service_upload_media_rest_body_and_query_params(): - client = ResumableUploadServiceClient( - credentials=ga_credentials.AnonymousCredentials(), - transport="rest", +@pytest.mark.asyncio +async def test_resumable_upload_service_upload_media_grpc_asyncio_body_and_query_params(): + if not HAS_ASYNC_REST_EXTRA: + pytest.skip() + client = ResumableUploadServiceAsyncClient( + credentials=async_anonymous_credentials(), ) - with mock.patch.object(type(client.transport._session), "request") as req, mock.patch.object( - path_template, "transcode" - ) as transcode: + with mock.patch.object( + AsyncAuthorizedSession, "request" + ) as req, mock.patch.object(path_template, "transcode") as transcode: request = resumable_upload.UploadMediaRequest( name='name_value', ) @@ -2492,22 +2275,22 @@ def test_resumable_upload_service_upload_media_rest_body_and_query_params(): } start_response = mock.MagicMock() - start_response.ok = True start_response.status_code = 200 start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} - start_response.content = b"" + start_response.read = mock.AsyncMock(return_value=b"") + start_response.close = mock.AsyncMock(return_value=None) expected_result = resumable_upload.UploadMediaResponse() json_return_value = resumable_upload.UploadMediaResponse.to_json(expected_result) upload_response = mock.MagicMock() - upload_response.ok = True upload_response.status_code = 200 upload_response.headers = {"x-goog-upload-status": "final"} - upload_response.content = json_return_value.encode("UTF-8") + upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) + upload_response.close = mock.AsyncMock(return_value=None) req.side_effect = [start_response, upload_response] - response = client.upload_media(request) - result = response.upload(b"test data") + response = await client.upload_media(request) + result = await response.upload(b"test data") assert result == expected_result start_call = req.call_args_list[0] @@ -2520,16 +2303,61 @@ def test_resumable_upload_service_upload_media_rest_body_and_query_params(): @pytest.mark.asyncio -async def test_resumable_upload_service_upload_media_rest_asyncio_body_and_query_params(): - if not HAS_ASYNC_REST_EXTRA: - pytest.skip() - client = ResumableUploadServiceAsyncClient( - credentials=async_anonymous_credentials(), - transport="rest_asyncio", +async def test_resumable_upload_service_upload_media_grpc_asyncio_without_async_rest_error(): + with mock.patch.object(transports.grpc_asyncio, "HAS_ASYNC_REST", False): + transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + with pytest.raises(NotImplementedError) as exc_info: + await transport.upload_media(resumable_upload.UploadMediaRequest()) + assert "Async REST transport is required for async resumable upload methods." in str(exc_info.value) + +def test_resumable_upload_service_upload_media_rest_start_empty_body(): + client = ResumableUploadServiceClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", ) - with mock.patch.object( - type(client.transport._session), "request" - ) as req, mock.patch.object(path_template, "transcode") as transcode: + with mock.patch.object(type(client.transport._session), "request") as req: + start_response = mock.MagicMock() + start_response.ok = True + start_response.status_code = 200 + start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} + start_response.content = b"" + + expected_result = resumable_upload.UploadMediaResponse( + name='name_value', + size=443, + ) + json_return_value = resumable_upload.UploadMediaResponse.to_json(expected_result) + upload_response = mock.MagicMock() + upload_response.ok = True + upload_response.status_code = 200 + upload_response.headers = {"x-goog-upload-status": "final"} + upload_response.content = json_return_value.encode("UTF-8") + + req.side_effect = [start_response, upload_response] + retry = retries.Retry() + response = client.upload_media( + resumable_upload.UploadMediaRequest(), + config=resumable_transfer.ResumableUploadConfig(headers={"x-custom": "val"}), + retry=retry, + timeout=10.0, + ) + assert isinstance(response, resumable_transfer.ResumableUploadSession) + assert response._start_retry is retry + result = response.upload(b"test data") + assert isinstance(result, resumable_upload.UploadMediaResponse) + assert result == expected_result + + +def test_resumable_upload_service_upload_media_rest_body_and_query_params(): + client = ResumableUploadServiceClient( + credentials=ga_credentials.AnonymousCredentials(), + transport="rest", + ) + with mock.patch.object(type(client.transport._session), "request") as req, mock.patch.object( + path_template, "transcode" + ) as transcode: request = resumable_upload.UploadMediaRequest( name='name_value', ) @@ -2542,22 +2370,22 @@ async def test_resumable_upload_service_upload_media_rest_asyncio_body_and_query } start_response = mock.MagicMock() + start_response.ok = True start_response.status_code = 200 start_response.headers = {"x-goog-upload-status": "active", "x-goog-upload-url": "http://localhost/upload"} - start_response.read = mock.AsyncMock(return_value=b"") - start_response.close = mock.AsyncMock(return_value=None) + start_response.content = b"" expected_result = resumable_upload.UploadMediaResponse() json_return_value = resumable_upload.UploadMediaResponse.to_json(expected_result) upload_response = mock.MagicMock() + upload_response.ok = True upload_response.status_code = 200 upload_response.headers = {"x-goog-upload-status": "final"} - upload_response.read = mock.AsyncMock(return_value=json_return_value.encode("UTF-8")) - upload_response.close = mock.AsyncMock(return_value=None) + upload_response.content = json_return_value.encode("UTF-8") req.side_effect = [start_response, upload_response] - response = await client.upload_media(request) - result = await response.upload(b"test data") + response = client.upload_media(request) + result = response.upload(b"test data") assert result == expected_result start_call = req.call_args_list[0] @@ -4379,20 +4207,6 @@ def test_transport_close_rest(): close.assert_called_once() -@pytest.mark.asyncio -async def test_transport_close_rest_asyncio(): - if not HAS_ASYNC_REST_EXTRA: - pytest.skip("the library must be installed with the `async_rest` extra to test this feature.") - client = ResumableUploadServiceAsyncClient( - credentials=async_anonymous_credentials(), - transport="rest_asyncio" - ) - with mock.patch.object(type(getattr(client.transport, "_session")), "close") as close: - async with client: - close.assert_not_called() - close.assert_called_once() - - def test_client_ctx(): transports = [ 'rest', diff --git a/packages/gapic-generator/tests/unit/generator/test_generator.py b/packages/gapic-generator/tests/unit/generator/test_generator.py index 335738896c2c..3ca6af45f9b9 100644 --- a/packages/gapic-generator/tests/unit/generator/test_generator.py +++ b/packages/gapic-generator/tests/unit/generator/test_generator.py @@ -264,9 +264,11 @@ def test_get_response_ignores_unwanted_transports_and_clients(): def test_get_response_resumable_upload_generates_async_client_and_rest_asyncio(): - # Verify that async_client.py and REST transport files (rest.py, rest_base.py, - # rest_asyncio.py) are generated when a service has a resumable upload method, - # even if rest_async_io_enabled is False or only grpc transport is requested. + # Verify that REST transport files (rest.py, rest_base.py, rest_asyncio.py) + # are generated when a service has a resumable upload method and grpc + # transport is enabled, even if rest_async_io_enabled is False, while + # async_client.py and rest_asyncio.py are not generated for rest-only + # without rest_async_io_enabled. generator_obj = make_generator() with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as list_templates: list_templates.return_value = [ @@ -313,11 +315,9 @@ def test_get_response_resumable_upload_generates_async_client_and_rest_asyncio() ) assert {i.name for i in cgr.file} == { "foo/resumable_upload_service/transports/rest.py", - "foo/resumable_upload_service/transports/rest_asyncio.py", "foo/resumable_upload_service/transports/rest_base.py", "foo/resumable_upload_service/transports/__init__.py", "foo/resumable_upload_service/transports/base.py", - "foo/resumable_upload_service/async_client.py", "foo/resumable_upload_service/client.py", } From 723015e71dfc99f0e8a3de284f944c973b4535d5 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Sat, 3 Oct 2026 23:10:01 +0000 Subject: [PATCH 36/45] address review feedback --- .../services/%service/transports/grpc.py.j2 | 2 ++ .../%service/transports/grpc_asyncio.py.j2 | 10 +++++++ .../%name_%version/%sub/test_%service.py.j2 | 29 +++++++++++++++++++ .../transports/grpc.py | 2 ++ .../transports/grpc_asyncio.py | 10 +++++++ .../test_resumable_upload_service.py | 29 +++++++++++++++++++ 6 files changed, 82 insertions(+) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index 0e365bf4d60e..0f5d590723ee 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -200,6 +200,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): # gRPC transports delegate resumable upload calls to an internal REST # transport instance. self._rest_transport: Optional[{{ service.name }}RestTransport] = None + self._client_cert_source_for_mtls = client_cert_source_for_mtls {% endif %} if api_mtls_endpoint: @@ -389,6 +390,7 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): host=transport._host, credentials=transport._credentials, client_info=transport._client_info, + client_cert_source_for_mtls=transport._client_cert_source_for_mtls, ) return transport._rest_transport.{{ method.transport_safe_name|snake_case }}(*args, **kwargs) self._stubs['{{ method.transport_safe_name|snake_case }}'] = _RestStub() diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 73b36467e519..dd515aa4b047 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -252,6 +252,7 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): # gRPC transports delegate resumable upload calls to an internal REST # transport instance. self._rest_transport: Optional[Any] = None + self._client_cert_source_for_mtls = client_cert_source_for_mtls {% endif %} if api_mtls_endpoint: @@ -386,6 +387,15 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): ) return _error() self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub() + elif self._client_cert_source_for_mtls: + class _MtlsErrorStub: + def __call__(self, *args, **kwargs): + async def _mtls_error(): + raise core_exceptions.AsyncRestUnsupportedParameterError( + "Mutual TLS (client_cert_source_for_mtls) is not currently supported for async resumable upload methods." + ) + return _mtls_error() + self._stubs['{{ method.transport_safe_name|snake_case }}'] = _MtlsErrorStub() elif HAS_ASYNC_REST: transport = self class _AsyncRestStub: diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index 07081a1e28c8..f0647910e8c4 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1558,6 +1558,35 @@ def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_rest_st mock_rest_close.assert_called_once() +def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_rest_stub_mtls(): + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" + ) as mock_configure_mtls_channel: + transport = transports.{{ service.name }}GrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_cert_source_for_mtls=client_cert_source_callback, + ) + mock_configure_mtls_channel.assert_not_called() + stub = transport.{{ method.transport_safe_name|snake_case }} + stub({{ method.input.ident }}()) + mock_configure_mtls_channel.assert_called_once_with( + client_cert_source_callback + ) + transport.close() + + +@pytest.mark.asyncio +async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_mtls_error(): + transport = transports.{{ service.name }}GrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_cert_source_for_mtls=client_cert_source_callback, + ) + with pytest.raises(core_exceptions.AsyncRestUnsupportedParameterError) as exc_info: + await transport.{{ method.transport_safe_name|snake_case }}({{ method.input.ident }}()) + assert "Mutual TLS (client_cert_source_for_mtls) is not currently supported for async resumable upload methods." in str(exc_info.value) + await transport.close() + + @pytest.mark.asyncio async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_asyncio_channel_without_credentials_error(): channel = aio.secure_channel('http://localhost/', grpc.local_channel_credentials()) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py index e75efd2004da..7cb8fc2acc7c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py @@ -203,6 +203,7 @@ def __init__(self, *, # gRPC transports delegate resumable upload calls to an internal REST # transport instance. self._rest_transport: Optional[ResumableUploadServiceRestTransport] = None + self._client_cert_source_for_mtls = client_cert_source_for_mtls if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -362,6 +363,7 @@ def __call__(self, *args, **kwargs): host=transport._host, credentials=transport._credentials, client_info=transport._client_info, + client_cert_source_for_mtls=transport._client_cert_source_for_mtls, ) return transport._rest_transport.upload_media(*args, **kwargs) self._stubs['upload_media'] = _RestStub() diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index fb87091d2357..7c6cc1608bb1 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -254,6 +254,7 @@ def __init__(self, *, # gRPC transports delegate resumable upload calls to an internal REST # transport instance. self._rest_transport: Optional[Any] = None + self._client_cert_source_for_mtls = client_cert_source_for_mtls if api_mtls_endpoint: warnings.warn("api_mtls_endpoint is deprecated", DeprecationWarning) @@ -364,6 +365,15 @@ async def _error(): ) return _error() self._stubs['upload_media'] = _ErrorStub() + elif self._client_cert_source_for_mtls: + class _MtlsErrorStub: + def __call__(self, *args, **kwargs): + async def _mtls_error(): + raise core_exceptions.AsyncRestUnsupportedParameterError( + "Mutual TLS (client_cert_source_for_mtls) is not currently supported for async resumable upload methods." + ) + return _mtls_error() + self._stubs['upload_media'] = _MtlsErrorStub() elif HAS_ASYNC_REST: transport = self class _AsyncRestStub: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index f867eba111bd..3d7108a90e09 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -2137,6 +2137,35 @@ def test_resumable_upload_service_upload_media_grpc_rest_stub(): mock_rest_close.assert_called_once() +def test_resumable_upload_service_upload_media_grpc_rest_stub_mtls(): + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" + ) as mock_configure_mtls_channel: + transport = transports.ResumableUploadServiceGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_cert_source_for_mtls=client_cert_source_callback, + ) + mock_configure_mtls_channel.assert_not_called() + stub = transport.upload_media + stub(resumable_upload.UploadMediaRequest()) + mock_configure_mtls_channel.assert_called_once_with( + client_cert_source_callback + ) + transport.close() + + +@pytest.mark.asyncio +async def test_resumable_upload_service_upload_media_grpc_asyncio_mtls_error(): + transport = transports.ResumableUploadServiceGrpcAsyncIOTransport( + credentials=ga_credentials.AnonymousCredentials(), + client_cert_source_for_mtls=client_cert_source_callback, + ) + with pytest.raises(core_exceptions.AsyncRestUnsupportedParameterError) as exc_info: + await transport.upload_media(resumable_upload.UploadMediaRequest()) + assert "Mutual TLS (client_cert_source_for_mtls) is not currently supported for async resumable upload methods." in str(exc_info.value) + await transport.close() + + @pytest.mark.asyncio async def test_resumable_upload_service_upload_media_grpc_asyncio_channel_without_credentials_error(): channel = aio.secure_channel('http://localhost/', grpc.local_channel_credentials()) From adab36542bd483a276460996ec03b593e0b040d0 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 5 Oct 2026 16:55:38 +0000 Subject: [PATCH 37/45] fix: set Content-Type on resumable upload start requests --- .../%sub/services/%service/_shared_macros.j2 | 1 + .../gapic/%name_%version/%sub/test_%service.py.j2 | 12 ++++++++++++ .../resumable_upload_service/transports/rest.py | 1 + .../transports/rest_asyncio.py | 1 + .../test_resumable_upload_service.py | 8 ++++++++ 5 files changed, 23 insertions(+) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 index 0c331434006d..e6ce18bfb94c 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/_shared_macros.j2 @@ -297,6 +297,7 @@ params = rest_helpers.flatten_query_params(query_params, strict=True) query_string = f"?{urllib.parse.urlencode(params)}" if params else "" upload_url = f"{self._host}{uri}{query_string}" headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} +headers["Content-Type"] = "application/json" if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) else: diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index f0647910e8c4..b7d73be254ad 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1708,6 +1708,9 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a result = await response.upload(b"test data") assert isinstance(result, {{ method.output.ident }}) assert result == expected_result + start_call = req.call_args_list[0] + assert start_call.kwargs["headers"]["Content-Type"] == "application/json" + assert start_call.kwargs["headers"]["x-custom"] == "val" @pytest.mark.asyncio @@ -1763,6 +1766,7 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a assert result == expected_result start_call = req.call_args_list[0] + assert start_call.kwargs["headers"]["Content-Type"] == "application/json" {% for field in method.input.fields.values() | rejectattr('message') %} {% if not field.oneof or field.proto3_optional %} assert "{{ field.name | camel_case }}=" in start_call.args[1] @@ -1834,6 +1838,9 @@ def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_start_e result = response.upload(b"test data") assert isinstance(result, {{ method.output.ident }}) assert result == expected_result + start_call = req.call_args_list[0] + assert start_call.kwargs["headers"]["Content-Type"] == "application/json" + assert start_call.kwargs["headers"]["x-custom"] == "val" {% if rest_async_io_enabled %} @@ -1883,6 +1890,9 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_a result = await response.upload(b"test data") assert isinstance(result, {{ method.output.ident }}) assert result == expected_result + start_call = req.call_args_list[0] + assert start_call.kwargs["headers"]["Content-Type"] == "application/json" + assert start_call.kwargs["headers"]["x-custom"] == "val" {% endif %} @@ -1937,6 +1947,7 @@ def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_body_an assert result == expected_result start_call = req.call_args_list[0] + assert start_call.kwargs["headers"]["Content-Type"] == "application/json" {% for field in method.input.fields.values() | rejectattr('message') %} {% if not field.oneof or field.proto3_optional %} assert "{{ field.name | camel_case }}=" in start_call.args[1] @@ -2006,6 +2017,7 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_rest_a assert result == expected_result start_call = req.call_args_list[0] + assert start_call.kwargs["headers"]["Content-Type"] == "application/json" {% for field in method.input.fields.values() | rejectattr('message') %} {% if not field.oneof or field.proto3_optional %} assert "{{ field.name | camel_case }}=" in start_call.args[1] diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py index 8e6d6208e514..032336df4352 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest.py @@ -432,6 +432,7 @@ def __call__(self, query_string = f"?{urllib.parse.urlencode(params)}" if params else "" upload_url = f"{self._host}{uri}{query_string}" headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} + headers["Content-Type"] = "application/json" if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) else: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py index c8271568728b..ad5ba94439d7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -241,6 +241,7 @@ async def __call__(self, query_string = f"?{urllib.parse.urlencode(params)}" if params else "" upload_url = f"{self._host}{uri}{query_string}" headers: Dict[str, Any] = {**dict(metadata), **dict((config.headers or {}) if config else {})} + headers["Content-Type"] = "application/json" if config is None: config = resumable_transfer.ResumableUploadConfig(headers=headers) else: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 3d7108a90e09..459d6c075f1c 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -2280,6 +2280,9 @@ async def test_resumable_upload_service_upload_media_grpc_asyncio_start_empty_bo result = await response.upload(b"test data") assert isinstance(result, resumable_upload.UploadMediaResponse) assert result == expected_result + start_call = req.call_args_list[0] + assert start_call.kwargs["headers"]["Content-Type"] == "application/json" + assert start_call.kwargs["headers"]["x-custom"] == "val" @pytest.mark.asyncio @@ -2323,6 +2326,7 @@ async def test_resumable_upload_service_upload_media_grpc_asyncio_body_and_query assert result == expected_result start_call = req.call_args_list[0] + assert start_call.kwargs["headers"]["Content-Type"] == "application/json" assert "name=" in start_call.args[1] expected_body = json_format.MessageToJson( pb_request, @@ -2377,6 +2381,9 @@ def test_resumable_upload_service_upload_media_rest_start_empty_body(): result = response.upload(b"test data") assert isinstance(result, resumable_upload.UploadMediaResponse) assert result == expected_result + start_call = req.call_args_list[0] + assert start_call.kwargs["headers"]["Content-Type"] == "application/json" + assert start_call.kwargs["headers"]["x-custom"] == "val" def test_resumable_upload_service_upload_media_rest_body_and_query_params(): @@ -2418,6 +2425,7 @@ def test_resumable_upload_service_upload_media_rest_body_and_query_params(): assert result == expected_result start_call = req.call_args_list[0] + assert start_call.kwargs["headers"]["Content-Type"] == "application/json" assert "name=" in start_call.args[1] expected_body = json_format.MessageToJson( pb_request, From 7e8fb40367e0e4fa8c81f72788c861666e309600 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 5 Oct 2026 17:16:39 +0000 Subject: [PATCH 38/45] temporary change to test deps at head --- packages/gapic-generator/noxfile.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index a595355c24d8..02a6d156edf4 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -87,8 +87,8 @@ def unit(session): session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") session.run( @@ -207,8 +207,8 @@ def fragment(session, use_ads_templates=False): session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") @@ -297,8 +297,8 @@ def showcase_library( session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") @@ -775,8 +775,8 @@ def snippetgen(session): session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") @@ -794,8 +794,8 @@ def docs(session): session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") session.install( @@ -889,8 +889,8 @@ def mypy(session): session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") session.run("mypy", f"--config-file={MYPY_CONFIG_FILE}", "-p", "gapic") From aa3041d20a6ea964f65d30220017ee199515a327 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 5 Oct 2026 17:35:30 +0000 Subject: [PATCH 39/45] Revert "temporary change to test deps at head" This reverts commit 94252777fd865a8f2d3d229c1ecb223ff953e90e. --- packages/gapic-generator/noxfile.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index 02a6d156edf4..a595355c24d8 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -87,8 +87,8 @@ def unit(session): session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") session.run( @@ -207,8 +207,8 @@ def fragment(session, use_ads_templates=False): session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") @@ -297,8 +297,8 @@ def showcase_library( session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") @@ -775,8 +775,8 @@ def snippetgen(session): session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") @@ -794,8 +794,8 @@ def docs(session): session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") session.install( @@ -889,8 +889,8 @@ def mypy(session): session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") session.run( "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.41.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.60.0-py3-none-any.whl" + "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", + "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" ) session.run("uv", "pip", "install", ".") session.run("mypy", f"--config-file={MYPY_CONFIG_FILE}", "-p", "gapic") From dd724d60eced28b4142dbc68a60a07f2704efcdb Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 5 Oct 2026 17:35:38 +0000 Subject: [PATCH 40/45] Revert "temporary commit to be reverted" This reverts commit 9a68c48085b916048de00887c6e9fdfb85f4ddab. --- .github/workflows/docs.yml | 12 +-- .github/workflows/gapic-generator-tests.yml | 10 +-- .github/workflows/lint.yml | 14 +--- .github/workflows/unittest.yml | 10 +-- ci/run_single_test.sh | 7 -- .../gapic/templates/setup.py.j2 | 21 +----- .../templates/testing/_default_constraints.j2 | 6 +- .../constraints-3.10-async-rest.txt.j2 | 6 +- .../templates/testing/constraints-3.10.txt.j2 | 9 +-- packages/gapic-generator/noxfile.py | 73 +++---------------- packages/gapic-generator/requirements.in | 7 +- packages/gapic-generator/requirements.txt | 12 +-- .../asset/testing/constraints-3.10.txt | 6 +- .../asset/testing/constraints-3.11.txt | 6 +- .../asset/testing/constraints-3.12.txt | 6 +- .../credentials/testing/constraints-3.10.txt | 6 +- .../credentials/testing/constraints-3.11.txt | 6 +- .../credentials/testing/constraints-3.12.txt | 6 +- .../eventarc/testing/constraints-3.10.txt | 6 +- .../eventarc/testing/constraints-3.11.txt | 6 +- .../eventarc/testing/constraints-3.12.txt | 6 +- .../logging/testing/constraints-3.10.txt | 6 +- .../logging/testing/constraints-3.11.txt | 6 +- .../logging/testing/constraints-3.12.txt | 6 +- .../testing/constraints-3.10.txt | 6 +- .../testing/constraints-3.11.txt | 6 +- .../testing/constraints-3.12.txt | 6 +- .../tests/integration/goldens/redis/setup.py | 10 +-- .../testing/constraints-3.10-async-rest.txt | 3 +- .../redis/testing/constraints-3.10.txt | 6 +- .../redis/testing/constraints-3.11.txt | 6 +- .../redis/testing/constraints-3.12.txt | 6 +- .../goldens/redis_selective/setup.py | 10 +-- .../testing/constraints-3.10-async-rest.txt | 3 +- .../testing/constraints-3.10.txt | 6 +- .../testing/constraints-3.11.txt | 6 +- .../testing/constraints-3.12.txt | 6 +- .../integration/goldens/showcase/setup.py | 22 ++---- .../testing/constraints-3.10-async-rest.txt | 6 +- .../showcase/testing/constraints-3.10.txt | 6 +- .../showcase/testing/constraints-3.11.txt | 6 +- .../showcase/testing/constraints-3.12.txt | 6 +- .../testing/constraints-3.10.txt | 6 +- .../testing/constraints-3.11.txt | 6 +- .../testing/constraints-3.12.txt | 6 +- 45 files changed, 113 insertions(+), 290 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index dd8d3393ed20..e8fe1a06f6f2 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -105,10 +105,7 @@ jobs: python-version: "3.10" - name: Install nox run: | - # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition - # where setuptools deletes the temporary '/bdist' directory mid-build during - # highly parallelized 'uv pip install -e .' editable installations. - python -m pip install --upgrade "setuptools<70.0.0" pip wheel + python -m pip install --upgrade setuptools pip wheel python -m pip install nox uv - name: Run docs for ${{ matrix.package_shard.description }} env: @@ -157,11 +154,8 @@ jobs: python-version: "3.10" - name: Install nox run: | - python -m pip install --upgrade pip wheel - # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition - # where setuptools deletes the temporary '/bdist' directory mid-build during - # highly parallelized 'uv pip install -e .' editable installations. - python -m pip install nox uv "setuptools<70" + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox uv - name: Run docfx env: BUILD_TYPE: presubmit diff --git a/.github/workflows/gapic-generator-tests.yml b/.github/workflows/gapic-generator-tests.yml index 9a9fe2d1482f..c9fa162db280 100644 --- a/.github/workflows/gapic-generator-tests.yml +++ b/.github/workflows/gapic-generator-tests.yml @@ -88,7 +88,7 @@ jobs: GOOGLE_SDK_PYTHON_LOGGING_SCOPE: ${{ matrix.logging_scope }} MATRIX_PYTHON: ${{ matrix.python }} run: | - pip install nox uv + pip install nox cd packages/gapic-generator nox -s showcase_unit${{ matrix.variant }}-${MATRIX_PYTHON} @@ -125,7 +125,7 @@ jobs: ./gapic-showcase run & cd - - name: Install nox. - run: python -m pip install nox uv + run: python -m pip install nox - name: Install protoc run: | sudo mkdir -p /usr/src/protoc/ @@ -181,7 +181,7 @@ jobs: ./gapic-showcase run & cd - - name: Install nox. - run: python -m pip install nox uv + run: python -m pip install nox - name: Install protoc run: | sudo mkdir -p /usr/src/protoc/ @@ -220,7 +220,7 @@ jobs: run: sudo apt-get update && sudo apt-get install -y pandoc - name: Run Mypy run: | - pip install nox uv + pip install nox cd packages/gapic-generator nox -s showcase_mypy @@ -318,7 +318,7 @@ jobs: sudo ln -s /usr/src/protoc/bin/protoc /usr/local/bin/protoc - name: Run Tests run: | - pip install nox uv + pip install nox cd packages/gapic-generator # Run fragment for current matrix python nox -s fragment-${MATRIX_PYTHON} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f0cf848d835b..702487b9d0fe 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -88,11 +88,8 @@ jobs: python-version: "3.14" - name: Install nox run: | - python -m pip install --upgrade pip wheel build twine - # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition - # where setuptools deletes the temporary '/bdist' directory mid-build during - # highly parallelized 'uv pip install -e .' editable installations. - python -m pip install nox uv "setuptools<70" + python -m pip install --upgrade setuptools pip wheel build twine + python -m pip install nox uv - name: Run lint env: BUILD_TYPE: presubmit @@ -148,11 +145,8 @@ jobs: python-version: "3.14" - name: Install nox run: | - python -m pip install --upgrade pip wheel - # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition - # where setuptools deletes the temporary '/bdist' directory mid-build during - # highly parallelized 'uv pip install -e .' editable installations. - python -m pip install nox uv "setuptools<70" + python -m pip install --upgrade setuptools pip wheel + python -m pip install nox uv - name: Run mypy for ${{ matrix.package_shard.description }} env: BUILD_TYPE: presubmit diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 36a9addd29d5..a7006861b623 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -131,10 +131,7 @@ jobs: cache-dependency-glob: 'packages/**/testing/constraints*.txt' - name: Install nox and nox-uv run: | - # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition - # where setuptools deletes the temporary '/bdist' directory mid-build during - # highly parallelized 'uv pip install -e .' editable installations. - uv pip install --system nox nox-uv "setuptools<70.0.0" + uv pip install --system nox nox-uv # TODO(https://github.com/googleapis/google-cloud-python/issues/18532): # Remove UV_PRERELEASE once Python 3.15 is officially released and upstream binary wheels are available. - name: Enable prereleases for Python 3.15 @@ -242,10 +239,7 @@ jobs: - name: Install coverage if: ${{ needs.initialize.outputs.packages != '' }} run: | - # Workaround: Pin setuptools < 70 to avoid a directory cleanup race condition - # where setuptools deletes the temporary '/bdist' directory mid-build during - # highly parallelized 'uv pip install -e .' editable installations. - python -m pip install --upgrade "setuptools<70" pip wheel + python -m pip install --upgrade setuptools pip wheel python -m pip install coverage - name: Download coverage results if: ${{ needs.initialize.outputs.packages != '' }} diff --git a/ci/run_single_test.sh b/ci/run_single_test.sh index 942b1ce02203..ced52a121eae 100755 --- a/ci/run_single_test.sh +++ b/ci/run_single_test.sh @@ -26,13 +26,6 @@ set -e -# 1. Force UV to pin setuptools inside Nox environments, resolving the /bdist race condition. -# 2. Redirect TMPDIR to a unique path so parallel workers never cross-contaminate. -export UV_CONSTRAINT="/tmp/constraints.txt" -export TMPDIR=$(mktemp -d) -echo "setuptools<70.0.0" > "${UV_CONSTRAINT}" -export UV_CACHE_DIR="${TMPDIR}/uv_cache" - if [ -z "${TEST_TYPE}" ]; then echo "missing TEST_TYPE env var" exit 1 diff --git a/packages/gapic-generator/gapic/templates/setup.py.j2 b/packages/gapic-generator/gapic/templates/setup.py.j2 index 22f722f72675..8e0ccbcfdb8d 100644 --- a/packages/gapic-generator/gapic/templates/setup.py.j2 +++ b/packages/gapic-generator/gapic/templates/setup.py.j2 @@ -37,20 +37,13 @@ else: dependencies = [ {% if api.has_resumable_upload_methods %} - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged - "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", - # TODO: Uncomment the line below - # "google-api-core[grpc] >= 2.41.0, <3.0.0", - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged - "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", - # TODO: Uncomment the line below - # "google-auth[aiohttp] >= 2.60.0, <3.0.0", + "google-api-core[grpc] >= 2.41.0, <3.0.0", {% else %} "google-api-core[grpc] >= 2.28.0, <3.0.0", + {% endif %} # Exclude incompatible versions of `google-auth` # See https://github.com/googleapis/google-cloud-python/issues/12364 "google-auth >= 2.14.1, <3.0.0,!=2.24.0,!=2.25.0", - {% endif %} "grpcio >= 1.59.0, < 2.0.0", "grpcio >= 1.75.1, < 2.0.0; python_version >= '3.14'", "proto-plus >= 1.26.1, <2.0.0", @@ -68,14 +61,8 @@ dependencies = [ extras = { {% if rest_async_io_enabled %} "async_rest": [ - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged - "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", - # TODO: Uncomment the line below - # "google-auth[aiohttp] >= 2.60.0, <3.0.0", - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged - "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", - # TODO: Uncomment the line below - # "google-api-core >= 2.41.0, <3.0.0", + "google-auth[aiohttp] >= 2.60.0, <3.0.0", + "google-api-core >= 2.41.0, <3.0.0", ], {% endif %} } diff --git a/packages/gapic-generator/gapic/templates/testing/_default_constraints.j2 b/packages/gapic-generator/gapic/templates/testing/_default_constraints.j2 index ce93530e06c8..85d819aaff23 100644 --- a/packages/gapic-generator/gapic/templates/testing/_default_constraints.j2 +++ b/packages/gapic-generator/gapic/templates/testing/_default_constraints.j2 @@ -1,10 +1,8 @@ {% from '_pypi_packages.j2' import pypi_packages %} # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 index 9a416d2a8d04..332973808bb6 100644 --- a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 +++ b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 @@ -9,13 +9,11 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 {% if api.has_resumable_upload_methods %} -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.41.0 +google-api-core==2.41.0 {% else %} google-api-core==2.28.0 {% endif %} -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.35.0 +google-auth==2.35.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 b/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 index d8d846220727..85af16c110fd 100644 --- a/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 +++ b/packages/gapic-generator/gapic/templates/testing/constraints-3.10.txt.j2 @@ -6,14 +6,11 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 {% if api.has_resumable_upload_methods %} -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.41.0 +google-api-core==2.41.0 {% else %} -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.28.0 +google-api-core==2.28.0 {% endif %} -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.14.1 +google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/noxfile.py b/packages/gapic-generator/noxfile.py index a595355c24d8..b9d1870b9e21 100644 --- a/packages/gapic-generator/noxfile.py +++ b/packages/gapic-generator/noxfile.py @@ -82,15 +82,7 @@ def unit(session): "grpcio-status", "proto-plus", ) - session.install("setuptools<70.0.0", "wheel") - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") - session.run( - "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" - ) - session.run("uv", "pip", "install", ".") + session.install("-e", ".") session.run( "py.test", *( @@ -106,9 +98,6 @@ def unit(session): path.join("tests", "unit"), ] ), - # `test_utils` is not part of the installed package (only `gapic` is - # included in setup.py), but it is needed for running tests. - env={"PYTHONPATH": "."}, ) @@ -161,7 +150,7 @@ def __call__(self, frag): # Install the generated fragment library. if self.use_ads_templates: - self.session.install(tmp_dir, ".", "-qqq") + self.session.install(tmp_dir, "-e", ".", "-qqq") else: # Use the constraints file for the specific python runtime version. # We do this to make sure that we're testing against the lowest @@ -172,7 +161,7 @@ def __call__(self, frag): constraints_path = str( f"{tmp_dir}/testing/constraints-{self.session.python}.txt" ) - self.session.install(tmp_dir, ".", "-qqq", "-r", constraints_path) + self.session.install(tmp_dir, "-e", ".", "-qqq", "-r", constraints_path) # Run the fragment's generated unit tests. # Don't bother parallelizing them: we already parallelize @@ -202,15 +191,7 @@ def fragment(session, use_ads_templates=False): "pytest-asyncio", "grpcio-tools", ) - session.install("setuptools<70.0.0", "wheel") - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") - session.run( - "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" - ) - session.run("uv", "pip", "install", ".") + session.install("-e", ".") frag_files = ( [Path(f) for f in session.posargs] if session.posargs else FRAGMENT_FILES @@ -292,15 +273,7 @@ def showcase_library( session.log("-" * 70) # Install gapic-generator-python - session.install("setuptools<70.0.0", "wheel") - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") - session.run( - "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" - ) - session.run("uv", "pip", "install", ".") + session.install("-e", ".") # Install grpcio-tools for protoc session.install("grpcio-tools") @@ -472,7 +445,7 @@ def showcase( session.error( f"INSTALL_LOCAL_CORE is set to 'true' but {local_core} does not exist." ) - session.install(str(local_core)) + session.install("-e", str(local_core)) # Use pytest-asyncio<1.0.0 while we investigate the recent failure described in # https://github.com/googleapis/gapic-generator-python/issues/2399 @@ -518,7 +491,7 @@ def showcase_w_rest_async( session.error( f"INSTALL_LOCAL_CORE is set to 'true' but {local_core} does not exist." ) - session.install(str(local_core)) + session.install("-e", str(local_core)) # Use pytest-asyncio<1.0.0 while we investigate the recent failure described in # https://github.com/googleapis/gapic-generator-python/issues/2399 @@ -770,15 +743,7 @@ def snippetgen(session): ) # Install gapic-generator-python - session.install("setuptools<70.0.0", "wheel") - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") - session.run( - "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" - ) - session.run("uv", "pip", "install", ".") + session.install("-e", ".") session.install("grpcio-tools", "pytest", "pytest-asyncio") @@ -789,15 +754,8 @@ def snippetgen(session): def docs(session): """Build the docs for this generator.""" - session.install("setuptools<70.0.0", "wheel") - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") - session.run( - "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" - ) - session.run("uv", "pip", "install", ".") + session.install("-e", ".") + session.install( # We need to pin to specific versions of the `sphinxcontrib-*` packages # which still support sphinx 4.x. @@ -832,7 +790,7 @@ def docs(session): def docfx(session): """Build the docfx yaml files for this library.""" - session.install(".") + session.install("-e", ".") session.install( # We need to pin to specific versions of the `sphinxcontrib-*` packages # which still support sphinx 4.x. @@ -885,14 +843,7 @@ def mypy(session): "types-dataclasses", "click==8.1.3", ) - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-api-core") - session.run("uv", "build", "--wheel", "--out-dir", "/tmp/wheels", "../google-auth") - session.run( - "uv", "pip", "install", - "/tmp/wheels/google_api_core-2.40.0-py3-none-any.whl", - "/tmp/wheels/google_auth-2.59.1-py3-none-any.whl" - ) - session.run("uv", "pip", "install", ".") + session.install(".") session.run("mypy", f"--config-file={MYPY_CONFIG_FILE}", "-p", "gapic") diff --git a/packages/gapic-generator/requirements.in b/packages/gapic-generator/requirements.in index 05185f1c9201..2776bb919bb8 100644 --- a/packages/gapic-generator/requirements.in +++ b/packages/gapic-generator/requirements.in @@ -1,10 +1,5 @@ click -# TODO Revert this change once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -google-api-core @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core -# TODO uncomment the line below -# google-api-core -# TODO Revert this change once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -google-auth @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth +google-api-core googleapis-common-protos jinja2 MarkupSafe diff --git a/packages/gapic-generator/requirements.txt b/packages/gapic-generator/requirements.txt index 6e5efea886eb..ff61f2e8a44e 100644 --- a/packages/gapic-generator/requirements.txt +++ b/packages/gapic-generator/requirements.txt @@ -611,12 +611,14 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -google-api-core @ git+https://github.com/googleapis/google-cloud-python.git@9b96af76254c33b9c3e0ceb390bbaeab2e33bbea#subdirectory=packages/google-api-core +google-api-core==2.40.0 \ + --hash=sha256:4b9e0a80024c269ae173136d5439f4ed284651d6e5e2773ba5c694f481f9c0f4 \ + --hash=sha256:ebee7d1b138b5362beecec260e6e8988ac97346562c7382ccb6f0ad8435c599f # via -r requirements.in -google-auth @ git+https://github.com/googleapis/google-cloud-python.git@9b96af76254c33b9c3e0ceb390bbaeab2e33bbea#subdirectory=packages/google-auth - # via - # -r requirements.in - # google-api-core +google-auth==2.59.0 \ + --hash=sha256:43e47b1d3067fb90d77bd9ba21f139bb0577b3f2b9f3e2516fc00f37435efcad \ + --hash=sha256:eb32f44f89f6b577947ebee5887c1db46e6b1a278889ba369a88179643f32240 + # via google-api-core googleapis-common-protos==1.75.5 \ --hash=sha256:c7a866fc34ed29a3b10af627a4b9b1dc2433313ca6e959f0ae4feb132047ed72 \ --hash=sha256:d7285525c23039db98f2463e6d5a4f9b958b94d497f03a844ece3259c4e72d5d diff --git a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.10.txt index 6fd63a4545b0..71d3c4c8d058 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.10.txt @@ -4,10 +4,8 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.28.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.14.1 +google-api-core==2.28.0 +google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.11.txt index 083b31dbe2b4..c5602c5029bf 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.11.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.12.txt index 083b31dbe2b4..c5602c5029bf 100755 --- a/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/asset/testing/constraints-3.12.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.10.txt index 1e69a37f1459..0dadf6857ff5 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.10.txt @@ -4,10 +4,8 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.28.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.14.1 +google-api-core==2.28.0 +google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.11.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.11.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.12.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/credentials/testing/constraints-3.12.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.10.txt index 034c13d55698..9c33fedfb264 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.10.txt @@ -4,10 +4,8 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.28.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.14.1 +google-api-core==2.28.0 +google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.11.txt index 6f8057b0c797..1cd0c5a2c3d4 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.11.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.12.txt index 6f8057b0c797..1cd0c5a2c3d4 100755 --- a/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/eventarc/testing/constraints-3.12.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.10.txt index 1e69a37f1459..0dadf6857ff5 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.10.txt @@ -4,10 +4,8 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.28.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.14.1 +google-api-core==2.28.0 +google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.11.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.11.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.12.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging/testing/constraints-3.12.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.10.txt index 1e69a37f1459..0dadf6857ff5 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.10.txt @@ -4,10 +4,8 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.28.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.14.1 +google-api-core==2.28.0 +google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.11.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.11.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.12.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/logging_internal/testing/constraints-3.12.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/redis/setup.py b/packages/gapic-generator/tests/integration/goldens/redis/setup.py index 58f6a4edb6fc..fdadb0f8c9a8 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/setup.py @@ -53,14 +53,8 @@ ] extras = { "async_rest": [ - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged - "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", - # TODO: Uncomment the line below - # "google-auth[aiohttp] >= 2.60.0, <3.0.0", - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged - "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", - # TODO: Uncomment the line below - # "google-api-core >= 2.41.0, <3.0.0", + "google-auth[aiohttp] >= 2.60.0, <3.0.0", + "google-api-core >= 2.41.0, <3.0.0", ], } url = "https://github.com/googleapis/google-cloud-python/tree/main/packages/google-cloud-redis" diff --git a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt index 78f9f3578bfa..c0c1ba32f46f 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt @@ -6,8 +6,7 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 google-api-core==2.28.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.35.0 +google-auth==2.35.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10.txt index 1e69a37f1459..0dadf6857ff5 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10.txt @@ -4,10 +4,8 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.28.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.14.1 +google-api-core==2.28.0 +google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.11.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.11.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.12.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.12.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py index 58f6a4edb6fc..fdadb0f8c9a8 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/setup.py @@ -53,14 +53,8 @@ ] extras = { "async_rest": [ - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged - "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", - # TODO: Uncomment the line below - # "google-auth[aiohttp] >= 2.60.0, <3.0.0", - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged - "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", - # TODO: Uncomment the line below - # "google-api-core >= 2.41.0, <3.0.0", + "google-auth[aiohttp] >= 2.60.0, <3.0.0", + "google-api-core >= 2.41.0, <3.0.0", ], } url = "https://github.com/googleapis/google-cloud-python/tree/main/packages/google-cloud-redis" diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt index 78f9f3578bfa..c0c1ba32f46f 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt @@ -6,8 +6,7 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 google-api-core==2.28.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.35.0 +google-auth==2.35.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10.txt index 1e69a37f1459..0dadf6857ff5 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10.txt @@ -4,10 +4,8 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.28.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.14.1 +google-api-core==2.28.0 +google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.11.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.11.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.12.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.12.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py index adb5903ec5f5..263074e55d01 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/setup.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/setup.py @@ -42,14 +42,10 @@ release_status = "Development Status :: 5 - Production/Stable" dependencies = [ - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged - "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", - # TODO: Uncomment the line below - # "google-api-core[grpc] >= 2.41.0, <3.0.0", - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged - "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", - # TODO: Uncomment the line below - # "google-auth[aiohttp] >= 2.60.0, <3.0.0", + "google-api-core[grpc] >= 2.41.0, <3.0.0", + # Exclude incompatible versions of `google-auth` + # See https://github.com/googleapis/google-cloud-python/issues/12364 + "google-auth >= 2.14.1, <3.0.0,!=2.24.0,!=2.25.0", "grpcio >= 1.59.0, < 2.0.0", "grpcio >= 1.75.1, < 2.0.0; python_version >= '3.14'", "proto-plus >= 1.26.1, <2.0.0", @@ -58,14 +54,8 @@ ] extras = { "async_rest": [ - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18552 is merged - "google-auth[aiohttp] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-auth", - # TODO: Uncomment the line below - # "google-auth[aiohttp] >= 2.60.0, <3.0.0", - # TODO: For prototyping purposes only, revert once https://github.com/googleapis/google-cloud-python/pull/18551 is merged - "google-api-core[grpc] @ git+https://github.com/googleapis/google-cloud-python.git#subdirectory=packages/google-api-core", - # TODO: Uncomment the line below - # "google-api-core >= 2.41.0, <3.0.0", + "google-auth[aiohttp] >= 2.60.0, <3.0.0", + "google-api-core >= 2.41.0, <3.0.0", ], } url = "https://github.com/googleapis/google-cloud-python/tree/main/packages/google-showcase" diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt index e519a640decd..85cef31887ec 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt @@ -5,10 +5,8 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.41.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.35.0 +google-api-core==2.41.0 +google-auth==2.35.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt index cd2f9dae7f91..a75b92057fdb 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10.txt @@ -4,10 +4,8 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.41.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.14.1 +google-api-core==2.41.0 +google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.11.txt index 6f8057b0c797..1cd0c5a2c3d4 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.11.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.12.txt index 6f8057b0c797..1cd0c5a2c3d4 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.12.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.10.txt b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.10.txt index 1e69a37f1459..0dadf6857ff5 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.10.txt +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.10.txt @@ -4,10 +4,8 @@ # pinning their versions to their lower bounds. # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core==2.28.0 -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth==2.14.1 +google-api-core==2.28.0 +google-auth==2.14.1 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.11.txt b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.11.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.11.txt +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.11.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf diff --git a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.12.txt b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.12.txt index 9f35b36d441e..7599dea499ed 100755 --- a/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.12.txt +++ b/packages/gapic-generator/tests/integration/goldens/storagebatchoperations/testing/constraints-3.12.txt @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- # This constraints file is required for unit tests. # List all library dependencies and extras in this file. -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18551 is merged -# google-api-core -# TODO: Uncomment the line below once https://github.com/googleapis/google-cloud-python/pull/18552 is merged -# google-auth +google-api-core +google-auth grpcio proto-plus protobuf From 944f07b0a48082f92e7530cf6494b16ddfd99520 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 5 Oct 2026 17:54:45 +0000 Subject: [PATCH 41/45] address review feedback --- .../%sub/services/%service/transports/grpc_asyncio.py.j2 | 6 +----- .../%sub/services/%service/transports/rest_asyncio.py.j2 | 5 +++-- .../services/cloud_redis/transports/rest_asyncio.py | 5 +++-- .../services/cloud_redis/transports/rest_asyncio.py | 5 +++-- .../resumable_upload_service/transports/grpc_asyncio.py | 2 +- .../resumable_upload_service/transports/rest_asyncio.py | 5 +++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index dd515aa4b047..7cba28640d65 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -401,13 +401,9 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): class _AsyncRestStub: def __call__(self, *args, **kwargs): if transport._rest_transport is None: - {# Note: Type for creds is ignored due to the type hint mismatch for credentials in the base transport. - # TODO(https://github.com/googleapis/google-cloud-python/issues/16268): Remove `# type: ignore` once - # the type hints for credentials are updated to include asynchronous credentials. - #} transport._rest_transport = Async{{ service.name }}RestTransport( host=transport._host, - credentials=transport._credentials, # type: ignore + credentials=transport._credentials, client_info=transport._client_info, ) return transport._rest_transport.{{ method.transport_safe_name|snake_case }}(*args, **kwargs) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 index afe7114846ed..8ff5ece64292 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/rest_asyncio.py.j2 @@ -17,6 +17,7 @@ try: except ImportError as e: # pragma: NO COVER raise ImportError("`rest_asyncio` transport requires the library to be installed with the `async_rest` extra. Install the library with the `async_rest` extra using `pip install {{ api.naming.warehouse_package_name }}[async_rest]`") from e +from google.auth import credentials as ga_credentials # type: ignore from google.auth.aio import credentials as ga_credentials_async # type: ignore from google.api_core import exceptions as core_exceptions @@ -111,7 +112,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): def __init__(self, *, host: str{% if service.host %} = '{{ service.host }}'{% endif %}, - credentials: Optional[ga_credentials_async.Credentials] = None, + credentials: Optional[Union[ga_credentials.Credentials, ga_credentials_async.Credentials]] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, url_scheme: str = 'https', interceptor: Optional[Async{{ service.name }}RestInterceptor] = None, @@ -127,7 +128,7 @@ class Async{{service.name}}RestTransport(_Base{{ service.name }}RestTransport): Args: host ({% if service.host %}Optional[str]{% else %}str{% endif %}): {{ ' ' }}The hostname to connect to {% if service.host %}(default: '{{ service.host }}'){% endif %}. - credentials (Optional[google.auth.aio.credentials.Credentials]): The + credentials (Optional[Union[google.auth.credentials.Credentials, google.auth.aio.credentials.Credentials]]): The authorization credentials to attach to requests. These credentials identify the application to the service; if none are specified, the client will attempt to ascertain the diff --git a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py index 4d629a5a8443..fa3f65ce9e36 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py @@ -23,6 +23,7 @@ except ImportError as e: # pragma: NO COVER raise ImportError("`rest_asyncio` transport requires the library to be installed with the `async_rest` extra. Install the library with the `async_rest` extra using `pip install google-cloud-redis[async_rest]`") from e +from google.auth import credentials as ga_credentials # type: ignore from google.auth.aio import credentials as ga_credentials_async # type: ignore from google.api_core import exceptions as core_exceptions @@ -770,7 +771,7 @@ class AsyncCloudRedisRestTransport(_BaseCloudRedisRestTransport): def __init__(self, *, host: str = 'redis.googleapis.com', - credentials: Optional[ga_credentials_async.Credentials] = None, + credentials: Optional[Union[ga_credentials.Credentials, ga_credentials_async.Credentials]] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, url_scheme: str = 'https', interceptor: Optional[AsyncCloudRedisRestInterceptor] = None, @@ -784,7 +785,7 @@ def __init__(self, Args: host (Optional[str]): The hostname to connect to (default: 'redis.googleapis.com'). - credentials (Optional[google.auth.aio.credentials.Credentials]): The + credentials (Optional[Union[google.auth.credentials.Credentials, google.auth.aio.credentials.Credentials]]): The authorization credentials to attach to requests. These credentials identify the application to the service; if none are specified, the client will attempt to ascertain the diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py index 960d9639a214..d79be431bd8a 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/google/cloud/redis_v1/services/cloud_redis/transports/rest_asyncio.py @@ -23,6 +23,7 @@ except ImportError as e: # pragma: NO COVER raise ImportError("`rest_asyncio` transport requires the library to be installed with the `async_rest` extra. Install the library with the `async_rest` extra using `pip install google-cloud-redis[async_rest]`") from e +from google.auth import credentials as ga_credentials # type: ignore from google.auth.aio import credentials as ga_credentials_async # type: ignore from google.api_core import exceptions as core_exceptions @@ -506,7 +507,7 @@ class AsyncCloudRedisRestTransport(_BaseCloudRedisRestTransport): def __init__(self, *, host: str = 'redis.googleapis.com', - credentials: Optional[ga_credentials_async.Credentials] = None, + credentials: Optional[Union[ga_credentials.Credentials, ga_credentials_async.Credentials]] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, url_scheme: str = 'https', interceptor: Optional[AsyncCloudRedisRestInterceptor] = None, @@ -520,7 +521,7 @@ def __init__(self, Args: host (Optional[str]): The hostname to connect to (default: 'redis.googleapis.com'). - credentials (Optional[google.auth.aio.credentials.Credentials]): The + credentials (Optional[Union[google.auth.credentials.Credentials, google.auth.aio.credentials.Credentials]]): The authorization credentials to attach to requests. These credentials identify the application to the service; if none are specified, the client will attempt to ascertain the diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 7c6cc1608bb1..7f5fc7f71df3 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -381,7 +381,7 @@ def __call__(self, *args, **kwargs): if transport._rest_transport is None: transport._rest_transport = AsyncResumableUploadServiceRestTransport( host=transport._host, - credentials=transport._credentials, # type: ignore + credentials=transport._credentials, client_info=transport._client_info, ) return transport._rest_transport.upload_media(*args, **kwargs) diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py index ad5ba94439d7..fa206afac970 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/rest_asyncio.py @@ -23,6 +23,7 @@ except ImportError as e: # pragma: NO COVER raise ImportError("`rest_asyncio` transport requires the library to be installed with the `async_rest` extra. Install the library with the `async_rest` extra using `pip install google-showcase[async_rest]`") from e +from google.auth import credentials as ga_credentials # type: ignore from google.auth.aio import credentials as ga_credentials_async # type: ignore from google.api_core import exceptions as core_exceptions @@ -136,7 +137,7 @@ class AsyncResumableUploadServiceRestTransport(_BaseResumableUploadServiceRestTr def __init__(self, *, host: str = 'localhost:7469', - credentials: Optional[ga_credentials_async.Credentials] = None, + credentials: Optional[Union[ga_credentials.Credentials, ga_credentials_async.Credentials]] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, url_scheme: str = 'https', interceptor: Optional[AsyncResumableUploadServiceRestInterceptor] = None, @@ -146,7 +147,7 @@ def __init__(self, Args: host (Optional[str]): The hostname to connect to (default: 'localhost:7469'). - credentials (Optional[google.auth.aio.credentials.Credentials]): The + credentials (Optional[Union[google.auth.credentials.Credentials, google.auth.aio.credentials.Credentials]]): The authorization credentials to attach to requests. These credentials identify the application to the service; if none are specified, the client will attempt to ascertain the From 98ed451e7133f654c0a6d8099876da14357163f6 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 5 Oct 2026 18:43:51 +0000 Subject: [PATCH 42/45] address review feedback --- .../gapic/templates/testing/constraints-3.10-async-rest.txt.j2 | 3 ++- .../goldens/redis/testing/constraints-3.10-async-rest.txt | 3 ++- .../redis_selective/testing/constraints-3.10-async-rest.txt | 3 ++- .../goldens/showcase/testing/constraints-3.10-async-rest.txt | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 index 332973808bb6..54179f0b0cba 100644 --- a/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 +++ b/packages/gapic-generator/gapic/templates/testing/constraints-3.10-async-rest.txt.j2 @@ -13,7 +13,8 @@ google-api-core==2.41.0 {% else %} google-api-core==2.28.0 {% endif %} -google-auth==2.35.0 +google-auth==2.60.0 +aiohttp==3.8.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt index c0c1ba32f46f..4a2700a3bf8a 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis/testing/constraints-3.10-async-rest.txt @@ -6,7 +6,8 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 google-api-core==2.28.0 -google-auth==2.35.0 +google-auth==2.60.0 +aiohttp==3.8.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt index c0c1ba32f46f..4a2700a3bf8a 100755 --- a/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt +++ b/packages/gapic-generator/tests/integration/goldens/redis_selective/testing/constraints-3.10-async-rest.txt @@ -6,7 +6,8 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 google-api-core==2.28.0 -google-auth==2.35.0 +google-auth==2.60.0 +aiohttp==3.8.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt index 85cef31887ec..17cd83a2f715 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt +++ b/packages/gapic-generator/tests/integration/goldens/showcase/testing/constraints-3.10-async-rest.txt @@ -6,7 +6,8 @@ # For example, if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0", # then this file should have google-cloud-foo==1.14.0 google-api-core==2.41.0 -google-auth==2.35.0 +google-auth==2.60.0 +aiohttp==3.8.0 grpcio==1.59.0 proto-plus==1.26.1 protobuf==6.33.5 From bb8e10c0d82eca4fc55ad11f150a7a16f8a4d462 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 5 Oct 2026 19:17:12 +0000 Subject: [PATCH 43/45] address review feedback --- .../services/%service/transports/grpc.py.j2 | 14 ++++-- .../%service/transports/grpc_asyncio.py.j2 | 44 +++++++++---------- .../transports/grpc.py | 14 ++++-- .../transports/grpc_asyncio.py | 44 +++++++++---------- 4 files changed, 64 insertions(+), 52 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 index 0f5d590723ee..7edaf533e71e 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc.py.j2 @@ -376,11 +376,17 @@ class {{ service.name }}GrpcTransport({{ service.name }}Transport): {% if method.is_resumable_upload %} if not self._credentials: class _ErrorStub: + def __init__(self, error: Exception): + self._error = error + def __call__(self, *args, **kwargs): - raise core_exceptions.GoogleAPICallError( - "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." - ) - self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub() + raise self._error + + self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub( + core_exceptions.GoogleAPICallError( + "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." + ) + ) else: transport = self class _RestStub: diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 7cba28640d65..53ba928f3568 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -378,24 +378,27 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): # to pass in the functions for each. if '{{ method.transport_safe_name|snake_case }}' not in self._stubs: {% if method.is_resumable_upload %} + class _ErrorStub: + def __init__(self, error: Exception): + self._error = error + + def __call__(self, *args, **kwargs): + async def _raise(): + raise self._error + return _raise() + if not self._credentials: - class _ErrorStub: - def __call__(self, *args, **kwargs): - async def _error(): - raise core_exceptions.GoogleAPICallError( - "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." - ) - return _error() - self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub() + self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub( + core_exceptions.GoogleAPICallError( + "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." + ) + ) elif self._client_cert_source_for_mtls: - class _MtlsErrorStub: - def __call__(self, *args, **kwargs): - async def _mtls_error(): - raise core_exceptions.AsyncRestUnsupportedParameterError( - "Mutual TLS (client_cert_source_for_mtls) is not currently supported for async resumable upload methods." - ) - return _mtls_error() - self._stubs['{{ method.transport_safe_name|snake_case }}'] = _MtlsErrorStub() + self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub( + core_exceptions.AsyncRestUnsupportedParameterError( + "Mutual TLS (client_cert_source_for_mtls) is not currently supported for async resumable upload methods." + ) + ) elif HAS_ASYNC_REST: transport = self class _AsyncRestStub: @@ -409,12 +412,9 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): return transport._rest_transport.{{ method.transport_safe_name|snake_case }}(*args, **kwargs) self._stubs['{{ method.transport_safe_name|snake_case }}'] = _AsyncRestStub() else: - class _UnsupportedStub: - def __call__(self, *args, **kwargs): - async def _unsupported(): - raise NotImplementedError("Async REST transport is required for async resumable upload methods.") - return _unsupported() - self._stubs['{{ method.transport_safe_name|snake_case }}'] = _UnsupportedStub() + self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub( + NotImplementedError("Async REST transport is required for async resumable upload methods.") + ) {% else %} self._stubs['{{ method.transport_safe_name|snake_case }}'] = self._logged_channel.{{ method.grpc_stub_type }}( '/{{ '.'.join(method.meta.address.package) }}.{{ service.name }}/{{ method.name }}', diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py index 7cb8fc2acc7c..dd52dbc790da 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc.py @@ -349,11 +349,17 @@ def upload_media(self) -> Callable[ if 'upload_media' not in self._stubs: if not self._credentials: class _ErrorStub: + def __init__(self, error: Exception): + self._error = error + def __call__(self, *args, **kwargs): - raise core_exceptions.GoogleAPICallError( - "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." - ) - self._stubs['upload_media'] = _ErrorStub() + raise self._error + + self._stubs['upload_media'] = _ErrorStub( + core_exceptions.GoogleAPICallError( + "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." + ) + ) else: transport = self class _RestStub: diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index 7f5fc7f71df3..b047f12c2a4a 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -356,24 +356,27 @@ def upload_media(self) -> Callable[ # gRPC handles serialization and deserialization, so we just need # to pass in the functions for each. if 'upload_media' not in self._stubs: + class _ErrorStub: + def __init__(self, error: Exception): + self._error = error + + def __call__(self, *args, **kwargs): + async def _raise(): + raise self._error + return _raise() + if not self._credentials: - class _ErrorStub: - def __call__(self, *args, **kwargs): - async def _error(): - raise core_exceptions.GoogleAPICallError( - "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." - ) - return _error() - self._stubs['upload_media'] = _ErrorStub() + self._stubs['upload_media'] = _ErrorStub( + core_exceptions.GoogleAPICallError( + "Resumable upload methods operate over REST and cannot be invoked when the transport is initialized with a pre-constructed gRPC channel. Please supply credentials directly instead of a gRPC channel to use resumable upload functionality." + ) + ) elif self._client_cert_source_for_mtls: - class _MtlsErrorStub: - def __call__(self, *args, **kwargs): - async def _mtls_error(): - raise core_exceptions.AsyncRestUnsupportedParameterError( - "Mutual TLS (client_cert_source_for_mtls) is not currently supported for async resumable upload methods." - ) - return _mtls_error() - self._stubs['upload_media'] = _MtlsErrorStub() + self._stubs['upload_media'] = _ErrorStub( + core_exceptions.AsyncRestUnsupportedParameterError( + "Mutual TLS (client_cert_source_for_mtls) is not currently supported for async resumable upload methods." + ) + ) elif HAS_ASYNC_REST: transport = self class _AsyncRestStub: @@ -387,12 +390,9 @@ def __call__(self, *args, **kwargs): return transport._rest_transport.upload_media(*args, **kwargs) self._stubs['upload_media'] = _AsyncRestStub() else: - class _UnsupportedStub: - def __call__(self, *args, **kwargs): - async def _unsupported(): - raise NotImplementedError("Async REST transport is required for async resumable upload methods.") - return _unsupported() - self._stubs['upload_media'] = _UnsupportedStub() + self._stubs['upload_media'] = _ErrorStub( + NotImplementedError("Async REST transport is required for async resumable upload methods.") + ) return self._stubs['upload_media'] def _prep_wrapped_messages(self, client_info): From 87b4c0e3a65e8ba656f847b488a9e58982b3784e Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Mon, 5 Oct 2026 19:22:41 +0000 Subject: [PATCH 44/45] address review feedback --- .../%sub/services/%service/transports/grpc_asyncio.py.j2 | 6 +++++- .../unit/gapic/%name_%version/%sub/test_%service.py.j2 | 6 +++++- .../resumable_upload_service/transports/grpc_asyncio.py | 6 +++++- .../gapic/showcase_v1beta1/test_resumable_upload_service.py | 6 +++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 index 53ba928f3568..e597d1b55de4 100644 --- a/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 +++ b/packages/gapic-generator/gapic/templates/%namespace/%name_%version/%sub/services/%service/transports/grpc_asyncio.py.j2 @@ -413,7 +413,11 @@ class {{ service.grpc_asyncio_transport_name }}({{ service.name }}Transport): self._stubs['{{ method.transport_safe_name|snake_case }}'] = _AsyncRestStub() else: self._stubs['{{ method.transport_safe_name|snake_case }}'] = _ErrorStub( - NotImplementedError("Async REST transport is required for async resumable upload methods.") + NotImplementedError( + "Async REST transport is required for async resumable upload methods. " + "Install the library with the `async_rest` extra using " + "`pip install {{ api.naming.warehouse_package_name }}[async_rest]`." + ) ) {% else %} self._stubs['{{ method.transport_safe_name|snake_case }}'] = self._logged_channel.{{ method.grpc_stub_type }}( diff --git a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 index b7d73be254ad..8240485ce167 100644 --- a/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 +++ b/packages/gapic-generator/gapic/templates/tests/unit/gapic/%name_%version/%sub/test_%service.py.j2 @@ -1789,7 +1789,11 @@ async def test_{{ service.name|snake_case }}_{{ method.name|snake_case }}_grpc_a ) with pytest.raises(NotImplementedError) as exc_info: await transport.{{ method.transport_safe_name|snake_case }}({{ method.input.ident }}()) - assert "Async REST transport is required for async resumable upload methods." in str(exc_info.value) + assert ( + "Async REST transport is required for async resumable upload methods. " + "Install the library with the `async_rest` extra using " + "`pip install {{ api.naming.warehouse_package_name }}[async_rest]`." + ) in str(exc_info.value) {% endfor %} {% endif %} diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py index b047f12c2a4a..40782c9a3092 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/google/showcase_v1beta1/services/resumable_upload_service/transports/grpc_asyncio.py @@ -391,7 +391,11 @@ def __call__(self, *args, **kwargs): self._stubs['upload_media'] = _AsyncRestStub() else: self._stubs['upload_media'] = _ErrorStub( - NotImplementedError("Async REST transport is required for async resumable upload methods.") + NotImplementedError( + "Async REST transport is required for async resumable upload methods. " + "Install the library with the `async_rest` extra using " + "`pip install google-showcase[async_rest]`." + ) ) return self._stubs['upload_media'] diff --git a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py index 459d6c075f1c..1ff0d43412d7 100755 --- a/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py +++ b/packages/gapic-generator/tests/integration/goldens/showcase/tests/unit/gapic/showcase_v1beta1/test_resumable_upload_service.py @@ -2343,7 +2343,11 @@ async def test_resumable_upload_service_upload_media_grpc_asyncio_without_async_ ) with pytest.raises(NotImplementedError) as exc_info: await transport.upload_media(resumable_upload.UploadMediaRequest()) - assert "Async REST transport is required for async resumable upload methods." in str(exc_info.value) + assert ( + "Async REST transport is required for async resumable upload methods. " + "Install the library with the `async_rest` extra using " + "`pip install google-showcase[async_rest]`." + ) in str(exc_info.value) def test_resumable_upload_service_upload_media_rest_start_empty_body(): client = ResumableUploadServiceClient( From 78ebf32dbf44d22fdcbcb733dfca854f9d570b89 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Tue, 6 Oct 2026 00:06:05 +0000 Subject: [PATCH 45/45] bump deps --- packages/gapic-generator/requirements.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/gapic-generator/requirements.txt b/packages/gapic-generator/requirements.txt index ff61f2e8a44e..38eb25a0b012 100644 --- a/packages/gapic-generator/requirements.txt +++ b/packages/gapic-generator/requirements.txt @@ -611,13 +611,13 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -google-api-core==2.40.0 \ - --hash=sha256:4b9e0a80024c269ae173136d5439f4ed284651d6e5e2773ba5c694f481f9c0f4 \ - --hash=sha256:ebee7d1b138b5362beecec260e6e8988ac97346562c7382ccb6f0ad8435c599f +google-api-core==2.41.0 \ + --hash=sha256:1f3d626b7a8022dda8deef200b845057231ddc7cbf0d999026bdf61ba38d7fdf \ + --hash=sha256:73e89a86baef6680934adeee6fbd0ceaf20c1393ab229b2f9b34efb23b0fdef3 # via -r requirements.in -google-auth==2.59.0 \ - --hash=sha256:43e47b1d3067fb90d77bd9ba21f139bb0577b3f2b9f3e2516fc00f37435efcad \ - --hash=sha256:eb32f44f89f6b577947ebee5887c1db46e6b1a278889ba369a88179643f32240 +google-auth==2.60.0 \ + --hash=sha256:34aa0283ef72c99cd410e35db4417211fa3e6aaa5399183227bbe3b82bd15192 \ + --hash=sha256:e78414cd2e22cc02884768707def024ca32da17695f2c0e6194cdbe1316c4cd4 # via google-api-core googleapis-common-protos==1.75.5 \ --hash=sha256:c7a866fc34ed29a3b10af627a4b9b1dc2433313ca6e959f0ae4feb132047ed72 \