From 09ce3b07f8255e94cbc97255d1af0403670b90d5 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Fri, 20 Sep 2024 17:26:13 +0000 Subject: [PATCH 01/16] feat: Add OperationsRestAsyncTransport to support long running operations --- google/api_core/operations_v1/__init__.py | 13 + .../operations_v1/transports/__init__.py | 13 + .../api_core/operations_v1/transports/base.py | 64 ++- .../api_core/operations_v1/transports/rest.py | 42 -- .../operations_v1/transports/rest_asyncio.py | 443 ++++++++++++++++++ .../test_operations_rest_client.py | 51 +- 6 files changed, 569 insertions(+), 57 deletions(-) create mode 100644 google/api_core/operations_v1/transports/rest_asyncio.py diff --git a/google/api_core/operations_v1/__init__.py b/google/api_core/operations_v1/__init__.py index 8b75426b..ef766996 100644 --- a/google/api_core/operations_v1/__init__.py +++ b/google/api_core/operations_v1/__init__.py @@ -21,9 +21,22 @@ from google.api_core.operations_v1.operations_client import OperationsClient from google.api_core.operations_v1.transports.rest import OperationsRestTransport +try: + from google.api_core.operations_v1.transports.rest_asyncio import ( + OperationsRestAsyncTransport, + ) + + HAS_ASYNC_TRANSPORT = True +except ImportError: + # This import requires the `async_rest` extra + # Don't raise an exception if `OperationsRestAsyncTransport` cannot be imported + # as other transports are still available + HAS_ASYNC_TRANSPORT = False + __all__ = [ "AbstractOperationsClient", "OperationsAsyncClient", "OperationsClient", "OperationsRestTransport", + "OperationsRestAsyncTransport", ] diff --git a/google/api_core/operations_v1/transports/__init__.py b/google/api_core/operations_v1/transports/__init__.py index df53e15e..0de5d16d 100644 --- a/google/api_core/operations_v1/transports/__init__.py +++ b/google/api_core/operations_v1/transports/__init__.py @@ -18,12 +18,25 @@ from .base import OperationsTransport from .rest import OperationsRestTransport +try: + from .rest_asyncio import OperationsRestAsyncTransport + + HAS_ASYNC_TRANSPORT = True +except ImportError: + # This import requires the `async_rest` extra + # Don't raise an exception if `OperationsRestAsyncTransport` cannot be imported + # as other transports are still available + HAS_ASYNC_TRANSPORT = False # Compile a registry of transports. _transport_registry = OrderedDict() _transport_registry["rest"] = OperationsRestTransport +if HAS_ASYNC_TRANSPORT: + _transport_registry["rest_asyncio"] = OperationsRestAsyncTransport + __all__ = ( "OperationsTransport", "OperationsRestTransport", + "OperationsRestAsyncTransport", ) diff --git a/google/api_core/operations_v1/transports/base.py b/google/api_core/operations_v1/transports/base.py index fb1d4fc9..ad207d89 100644 --- a/google/api_core/operations_v1/transports/base.py +++ b/google/api_core/operations_v1/transports/base.py @@ -14,21 +14,26 @@ # limitations under the License. # import abc +import re from typing import Awaitable, Callable, Optional, Sequence, Union import google.api_core # type: ignore from google.api_core import exceptions as core_exceptions # type: ignore from google.api_core import gapic_v1 # type: ignore from google.api_core import retry as retries # type: ignore +from google.api_core import retry_async as retries_async # type: ignore from google.api_core import version import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore from google.longrunning import operations_pb2 from google.oauth2 import service_account # type: ignore -from google.protobuf import empty_pb2 # type: ignore +import google.protobuf +from google.protobuf import empty_pb2, json_format # type: ignore from grpc import Compression +PROTOBUF_VERSION = google.protobuf.__version__ + DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=version.__version__, ) @@ -51,6 +56,7 @@ def __init__( quota_project_id: Optional[str] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, + url_scheme="https", **kwargs, ) -> None: """Instantiate the transport. @@ -76,7 +82,20 @@ def __init__( your own client library. always_use_jwt_access (Optional[bool]): Whether self signed JWT should be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. """ + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) + if maybe_url_match is None: + raise ValueError( + f"Unexpected hostname structure: {host}" + ) # pragma: NO COVER + + url_match_items = maybe_url_match.groupdict() + + host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host + # Save the hostname. Default to port 443 (HTTPS) if none is specified. if ":" not in host: host += ":443" @@ -115,12 +134,13 @@ def __init__( # Save the credentials. self._credentials = credentials - def _prep_wrapped_messages(self, client_info): + def _prep_wrapped_messages(self, client_info, is_async=False): # Precompute the wrapped methods. + retry_class = retries_async.AsyncRetry if is_async else retries.Retry self._wrapped_methods = { self.list_operations: gapic_v1.method.wrap_method( self.list_operations, - default_retry=retries.Retry( + default_retry=retry_class( initial=0.5, maximum=10.0, multiplier=2.0, @@ -135,7 +155,7 @@ def _prep_wrapped_messages(self, client_info): ), self.get_operation: gapic_v1.method.wrap_method( self.get_operation, - default_retry=retries.Retry( + default_retry=retry_class( initial=0.5, maximum=10.0, multiplier=2.0, @@ -150,7 +170,7 @@ def _prep_wrapped_messages(self, client_info): ), self.delete_operation: gapic_v1.method.wrap_method( self.delete_operation, - default_retry=retries.Retry( + default_retry=retry_class( initial=0.5, maximum=10.0, multiplier=2.0, @@ -165,7 +185,7 @@ def _prep_wrapped_messages(self, client_info): ), self.cancel_operation: gapic_v1.method.wrap_method( self.cancel_operation, - default_retry=retries.Retry( + default_retry=retry_class( initial=0.5, maximum=10.0, multiplier=2.0, @@ -189,6 +209,38 @@ def close(self): """ raise NotImplementedError() + def _convert_protobuf_message_to_dict( + self, message: google.protobuf.message.Message + ): + r"""Converts protobuf message to a dictionary. + + When the dictionary is encoded to JSON, it conforms to proto3 JSON spec. + + Args: + message(google.protobuf.message.Message): The protocol buffers message + instance to serialize. + + Returns: + A dict representation of the protocol buffer message. + """ + # For backwards compatibility with protobuf 3.x 4.x + # Remove once support for protobuf 3.x and 4.x is dropped + # https://github.com/googleapis/python-api-core/issues/643 + if PROTOBUF_VERSION[0:2] in ["3.", "4."]: + result = json_format.MessageToDict( + message, + preserving_proto_field_name=True, + including_default_value_fields=True, # type: ignore # backward compatibility + ) + else: + result = json_format.MessageToDict( + message, + preserving_proto_field_name=True, + always_print_fields_with_no_presence=True, + ) + + return result + @property def list_operations( self, diff --git a/google/api_core/operations_v1/transports/rest.py b/google/api_core/operations_v1/transports/rest.py index f37bb344..e30f5c88 100644 --- a/google/api_core/operations_v1/transports/rest.py +++ b/google/api_core/operations_v1/transports/rest.py @@ -123,16 +123,6 @@ def __init__( # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the # credentials object - maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) - if maybe_url_match is None: - raise ValueError( - f"Unexpected hostname structure: {host}" - ) # pragma: NO COVER - - url_match_items = maybe_url_match.groupdict() - - host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host - super().__init__( host=host, credentials=credentials, @@ -441,38 +431,6 @@ def _cancel_operation( return empty_pb2.Empty() - def _convert_protobuf_message_to_dict( - self, message: google.protobuf.message.Message - ): - r"""Converts protobuf message to a dictionary. - - When the dictionary is encoded to JSON, it conforms to proto3 JSON spec. - - Args: - message(google.protobuf.message.Message): The protocol buffers message - instance to serialize. - - Returns: - A dict representation of the protocol buffer message. - """ - # For backwards compatibility with protobuf 3.x 4.x - # Remove once support for protobuf 3.x and 4.x is dropped - # https://github.com/googleapis/python-api-core/issues/643 - if PROTOBUF_VERSION[0:2] in ["3.", "4."]: - result = json_format.MessageToDict( - message, - preserving_proto_field_name=True, - including_default_value_fields=True, # type: ignore # backward compatibility - ) - else: - result = json_format.MessageToDict( - message, - preserving_proto_field_name=True, - always_print_fields_with_no_presence=True, - ) - - return result - @property def list_operations( self, diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py new file mode 100644 index 00000000..a8aa6656 --- /dev/null +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -0,0 +1,443 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 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 re +from typing import Callable, Dict, Optional, Sequence, Tuple, Union + +from requests import __version__ as requests_version + +try: + import google.auth.aio.transport +except ImportError as e: # pragma: NO COVER + raise ImportError( + "`google-api-core[async_rest]` is required to use asynchronous rest streaming. " + "Install the `async_rest` extra of `google-api-core` using " + "`pip install google-api-core[async_rest]`." + ) from e + +from google.api_core import exceptions as core_exceptions # type: ignore +from google.api_core import gapic_v1 # type: ignore +from google.api_core import path_template # type: ignore +from google.api_core import rest_helpers # type: ignore +from google.api_core import retry_async as retries # type: ignore +from google.auth.aio import credentials as ga_credentials # type: ignore +from google.auth.aio.transport.sessions import AsyncAuthorizedSession # type: ignore +from google.longrunning import operations_pb2 # type: ignore +from google.protobuf import empty_pb2 # type: ignore +from google.protobuf import json_format # type: ignore + +from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO, OperationsTransport + + +DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( + gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, + grpc_version=None, + rest_version=requests_version, +) + + +class OperationsRestAsyncTransport(OperationsTransport): + """REST Async backend transport for Operations inherited from + `OperationsTransport`. + + Manages async long-running operations with an API service. + + When an API method normally takes long time to complete, it can be + designed to return [Operation][google.api_core.operations_v1.Operation] to the + client, and the client can use this interface to receive the real + response asynchronously by polling the operation resource, or pass + the operation resource to another API (such as Google Cloud Pub/Sub + API) to receive the response. Any API service that returns + long-running operations should implement the ``Operations`` + interface so developers can have a consistent client experience. + + 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 = "longrunning.googleapis.com", + credentials: Optional[ga_credentials.Credentials] = None, + client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, + always_use_jwt_access: Optional[bool] = False, + url_scheme: str = "https", + http_options: Optional[Dict] = None, + path_prefix: str = "v1", + ) -> None: + """Instantiate the transport. + + Args: + host (Optional[str]): + The hostname to connect to. + 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're developing + your own client library. + always_use_jwt_access (Optional[bool]): Whether self signed JWT should + be used for service account credentials. + url_scheme: the protocol scheme for the API endpoint. Normally + "https", but for testing or local servers, + "http" can be specified. + http_options: a dictionary of http_options for transcoding, to override + the defaults from operations.proto. Each method has an entry + with the corresponding http rules as value. + path_prefix: path prefix (usually represents API version). Set to + "v1" by default. + + """ + # Run the base constructor + # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. + # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the + # credentials object + maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) + if maybe_url_match is None: + raise ValueError( + f"Unexpected hostname structure: {host}" + ) # pragma: NO COVER + + url_match_items = maybe_url_match.groupdict() + + host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host + + super().__init__( + host=host, + credentials=credentials, + client_info=client_info, + always_use_jwt_access=always_use_jwt_access, + ) + # TODO file bug in auth library to add support for default_host argument in AsyncAuthorizedSession + self._session = AsyncAuthorizedSession(self._credentials) + self._prep_wrapped_messages(client_info, is_async=True) + self._http_options = http_options or {} + self._path_prefix = path_prefix + + async def _list_operations( + self, + request: operations_pb2.ListOperationsRequest, + *, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.ListOperationsResponse: + r"""Asynchronously call the list operations method over HTTP. + + Args: + request (~.operations_pb2.ListOperationsRequest): + The request object. The request message for + [Operations.ListOperations][google.api_core.operations_v1.Operations.ListOperations]. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.ListOperationsResponse: + The response message for + [Operations.ListOperations][google.api_core.operations_v1.Operations.ListOperations]. + + """ + + http_options = [ + { + "method": "get", + "uri": "/{}/{{name=**}}/operations".format(self._path_prefix), + }, + ] + if "google.longrunning.Operations.ListOperations" in self._http_options: + http_options = self._http_options[ + "google.longrunning.Operations.ListOperations" + ] + + request_kwargs = self._convert_protobuf_message_to_dict(request) + transcoded_request = path_template.transcode(http_options, **request_kwargs) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params_request = operations_pb2.ListOperationsRequest() + json_format.ParseDict(transcoded_request["query_params"], query_params_request) + query_params = json_format.MessageToDict( + query_params_request, + preserving_proto_field_name=False, + use_integers_for_enums=False, + ) + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = await getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params), + ) + + # 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 + api_response = operations_pb2.ListOperationsResponse() + json_format.Parse(response.content, api_response, ignore_unknown_fields=False) + return api_response + + async def _get_operation( + self, + request: operations_pb2.GetOperationRequest, + *, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> operations_pb2.Operation: + r"""Asynchronously call the get operation method over HTTP. + + Args: + request (~.operations_pb2.GetOperationRequest): + The request object. The request message for + [Operations.GetOperation][google.api_core.operations_v1.Operations.GetOperation]. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + + Returns: + ~.operations_pb2.Operation: + This resource represents a long- + running operation that is the result of a + network API call. + + """ + + http_options = [ + { + "method": "get", + "uri": "/{}/{{name=**/operations/*}}".format(self._path_prefix), + }, + ] + if "google.longrunning.Operations.GetOperation" in self._http_options: + http_options = self._http_options[ + "google.longrunning.Operations.GetOperation" + ] + + request_kwargs = self._convert_protobuf_message_to_dict(request) + transcoded_request = path_template.transcode(http_options, **request_kwargs) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params_request = operations_pb2.GetOperationRequest() + json_format.ParseDict(transcoded_request["query_params"], query_params_request) + query_params = json_format.MessageToDict( + query_params_request, + preserving_proto_field_name=False, + use_integers_for_enums=False, + ) + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = await getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params), + ) + + # 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 + api_response = operations_pb2.Operation() + json_format.Parse( + await response.read(), api_response, ignore_unknown_fields=False + ) + return api_response + + async def _delete_operation( + self, + request: operations_pb2.DeleteOperationRequest, + *, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> empty_pb2.Empty: + r"""Asynchronously call the delete operation method over HTTP. + + Args: + request (~.operations_pb2.DeleteOperationRequest): + The request object. The request message for + [Operations.DeleteOperation][google.api_core.operations_v1.Operations.DeleteOperation]. + + retry (google.api_core.retry.Retry): Designation of what errors, if any, + should be retried. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + + http_options = [ + { + "method": "delete", + "uri": "/{}/{{name=**/operations/*}}".format(self._path_prefix), + }, + ] + if "google.longrunning.Operations.DeleteOperation" in self._http_options: + http_options = self._http_options[ + "google.longrunning.Operations.DeleteOperation" + ] + + request_kwargs = self._convert_protobuf_message_to_dict(request) + transcoded_request = path_template.transcode(http_options, **request_kwargs) + + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params_request = operations_pb2.DeleteOperationRequest() + json_format.ParseDict(transcoded_request["query_params"], query_params_request) + query_params = json_format.MessageToDict( + query_params_request, + preserving_proto_field_name=False, + use_integers_for_enums=False, + ) + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = await getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params), + ) + + # 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 empty_pb2.Empty() + + async def _cancel_operation( + self, + request: operations_pb2.CancelOperationRequest, + *, + timeout: Optional[float] = None, + metadata: Sequence[Tuple[str, str]] = (), + ) -> empty_pb2.Empty: + r"""Asynchronously call the cancel operation method over HTTP. + + Args: + request (~.operations_pb2.CancelOperationRequest): + The request object. The request message for + [Operations.CancelOperation][google.api_core.operations_v1.Operations.CancelOperation]. + timeout (float): The timeout for this request. + metadata (Sequence[Tuple[str, str]]): Strings which should be + sent along with the request as metadata. + """ + + http_options = [ + { + "method": "post", + "uri": "/{}/{{name=**/operations/*}}:cancel".format(self._path_prefix), + "body": "*", + }, + ] + if "google.longrunning.Operations.CancelOperation" in self._http_options: + http_options = self._http_options[ + "google.longrunning.Operations.CancelOperation" + ] + + request_kwargs = self._convert_protobuf_message_to_dict(request) + transcoded_request = path_template.transcode(http_options, **request_kwargs) + + # Jsonify the request body + body_request = operations_pb2.CancelOperationRequest() + json_format.ParseDict(transcoded_request["body"], body_request) + body = json_format.MessageToDict( + body_request, + preserving_proto_field_name=False, + use_integers_for_enums=False, + ) + uri = transcoded_request["uri"] + method = transcoded_request["method"] + + # Jsonify the query params + query_params_request = operations_pb2.CancelOperationRequest() + json_format.ParseDict(transcoded_request["query_params"], query_params_request) + query_params = json_format.MessageToDict( + query_params_request, + preserving_proto_field_name=False, + use_integers_for_enums=False, + ) + + # Send the request + headers = dict(metadata) + headers["Content-Type"] = "application/json" + response = await getattr(self._session, method)( + "{host}{uri}".format(host=self._host, uri=uri), + timeout=timeout, + headers=headers, + params=rest_helpers.flatten_query_params(query_params), + data=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 empty_pb2.Empty() + + @property + def list_operations( + self, + ) -> Callable[ + [operations_pb2.ListOperationsRequest], operations_pb2.ListOperationsResponse + ]: + return self._list_operations + + @property + def get_operation( + self, + ) -> Callable[[operations_pb2.GetOperationRequest], operations_pb2.Operation]: + return self._get_operation + + @property + def delete_operation( + self, + ) -> Callable[[operations_pb2.DeleteOperationRequest], empty_pb2.Empty]: + return self._delete_operation + + @property + def cancel_operation( + self, + ) -> Callable[[operations_pb2.CancelOperationRequest], empty_pb2.Empty]: + return self._cancel_operation + + +__all__ = ("OperationsRestAsyncTransport",) diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index 4ab4f1f7..1c8fa83e 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -33,6 +33,16 @@ from google.api_core.operations_v1 import transports import google.auth from google.auth import credentials as ga_credentials +from google.auth.aio import credentials as ga_credentials_async + +try: + import aiohttp + import google.auth.aio.transport + + GOOGLE_AUTH_AIO_INSTALLED = True +except ImportError: + GOOGLE_AUTH_AIO_INSTALLED = False + from google.auth.exceptions import MutualTLSChannelError from google.longrunning import operations_pb2 from google.oauth2 import service_account @@ -125,11 +135,14 @@ def test_operations_client_from_service_account_info(client_class): @pytest.mark.parametrize( - "transport_class,transport_name", [(transports.OperationsRestTransport, "rest")] + "transport_class", + [ + transports.OperationsRestTransport, + # TODO: Add support for service account credentials + # transports.OperationsRestAsyncTransport, + ], ) -def test_operations_client_service_account_always_use_jwt( - transport_class, transport_name -): +def test_operations_client_service_account_always_use_jwt(transport_class): with mock.patch.object( service_account.Credentials, "with_always_use_jwt_access", create=True ) as use_jwt: @@ -712,11 +725,22 @@ def test_transport_instance(): assert client.transport is transport -@pytest.mark.parametrize("transport_class", [transports.OperationsRestTransport]) -def test_transport_adc(transport_class): +@pytest.mark.parametrize( + "transport_class,credentials", + [ + (transports.OperationsRestTransport, ga_credentials.AnonymousCredentials()), + ( + transports.OperationsRestAsyncTransport, + ga_credentials_async.AnonymousCredentials(), + ), + ], +) +def test_transport_adc(transport_class, credentials): + if not GOOGLE_AUTH_AIO_INSTALLED: + pytest.skip("Skipped because google-api-core[async_rest] is not installed") # Test default credentials are used if not provided. with mock.patch.object(google.auth, "default") as adc: - adc.return_value = (ga_credentials.AnonymousCredentials(), None) + adc.return_value = (credentials, None) transport_class() adc.assert_called_once() @@ -800,12 +824,21 @@ def test_operations_auth_adc(): ) -def test_operations_http_transport_client_cert_source_for_mtls(): +# TODO(https://github.com/googleapis/python-api-core/issues/705): Add +# testing for `transports.OperationsRestAsyncTransport` once it mtls is supported +# in `google.auth.aio.transport` +@pytest.mark.parametrize( + "transport_class", + [ + transports.OperationsRestTransport, + ], +) +def test_operations_http_transport_client_cert_source_for_mtls(transport_class): cred = ga_credentials.AnonymousCredentials() with mock.patch( "google.auth.transport.requests.AuthorizedSession.configure_mtls_channel" ) as mock_configure_mtls_channel: - transports.OperationsRestTransport( + transport_class( credentials=cred, client_cert_source_for_mtls=client_cert_source_callback ) mock_configure_mtls_channel.assert_called_once_with(client_cert_source_callback) From daca264d0cbbd37581d41961b1ae19b4709948f1 Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 25 Sep 2024 15:28:35 +0000 Subject: [PATCH 02/16] update TODO comment --- tests/unit/operations_v1/test_operations_rest_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index 1c8fa83e..5ba052f6 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -138,8 +138,8 @@ def test_operations_client_from_service_account_info(client_class): "transport_class", [ transports.OperationsRestTransport, - # TODO: Add support for service account credentials - # transports.OperationsRestAsyncTransport, + # TODO(https://github.com/googleapis/python-api-core/issues/706): Add support for + # service account credentials in transports.OperationsRestAsyncTransport ], ) def test_operations_client_service_account_always_use_jwt(transport_class): From 9f980fdef7b13a42b5e3541998a153e1042164da Mon Sep 17 00:00:00 2001 From: Anthonios Partheniou Date: Wed, 25 Sep 2024 15:45:36 +0000 Subject: [PATCH 03/16] update TODO comment --- google/api_core/operations_v1/transports/rest_asyncio.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index a8aa6656..8c736447 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -129,7 +129,9 @@ def __init__( client_info=client_info, always_use_jwt_access=always_use_jwt_access, ) - # TODO file bug in auth library to add support for default_host argument in AsyncAuthorizedSession + # TODO(https://github.com/googleapis/python-api-core/issues/708): add support for + # `default_host` in AsyncAuthorizedSession for feature parity with the synchronous + # code. self._session = AsyncAuthorizedSession(self._credentials) self._prep_wrapped_messages(client_info, is_async=True) self._http_options = http_options or {} From f52378b6c3030906fe0c34b2dedbb556aeef0e1e Mon Sep 17 00:00:00 2001 From: ohmayr Date: Mon, 30 Sep 2024 15:12:10 +0000 Subject: [PATCH 04/16] address feedback --- google/api_core/operations_v1/__init__.py | 5 +- .../operations_v1/transports/__init__.py | 5 +- .../api_core/operations_v1/transports/base.py | 11 +-- .../operations_v1/transports/rest_asyncio.py | 94 +++++++++++++++++-- .../test_operations_rest_client.py | 9 +- 5 files changed, 99 insertions(+), 25 deletions(-) diff --git a/google/api_core/operations_v1/__init__.py b/google/api_core/operations_v1/__init__.py index ef766996..d5805dde 100644 --- a/google/api_core/operations_v1/__init__.py +++ b/google/api_core/operations_v1/__init__.py @@ -30,7 +30,7 @@ except ImportError: # This import requires the `async_rest` extra # Don't raise an exception if `OperationsRestAsyncTransport` cannot be imported - # as other transports are still available + # as other transports are still available. HAS_ASYNC_TRANSPORT = False __all__ = [ @@ -38,5 +38,4 @@ "OperationsAsyncClient", "OperationsClient", "OperationsRestTransport", - "OperationsRestAsyncTransport", -] +].extend("OperationsRestAsyncTransport") diff --git a/google/api_core/operations_v1/transports/__init__.py b/google/api_core/operations_v1/transports/__init__.py index 0de5d16d..b3922110 100644 --- a/google/api_core/operations_v1/transports/__init__.py +++ b/google/api_core/operations_v1/transports/__init__.py @@ -22,11 +22,13 @@ from .rest_asyncio import OperationsRestAsyncTransport HAS_ASYNC_TRANSPORT = True + ASYNC_REST_CLASSES = ("OperationsRestAsyncTransport",) except ImportError: # This import requires the `async_rest` extra # Don't raise an exception if `OperationsRestAsyncTransport` cannot be imported # as other transports are still available HAS_ASYNC_TRANSPORT = False + ASYNC_REST_CLASSES = () # Compile a registry of transports. _transport_registry = OrderedDict() @@ -38,5 +40,4 @@ __all__ = ( "OperationsTransport", "OperationsRestTransport", - "OperationsRestAsyncTransport", -) +) + ASYNC_REST_CLASSES diff --git a/google/api_core/operations_v1/transports/base.py b/google/api_core/operations_v1/transports/base.py index ad207d89..30a109a5 100644 --- a/google/api_core/operations_v1/transports/base.py +++ b/google/api_core/operations_v1/transports/base.py @@ -134,13 +134,12 @@ def __init__( # Save the credentials. self._credentials = credentials - def _prep_wrapped_messages(self, client_info, is_async=False): + def _prep_wrapped_messages(self, client_info): # Precompute the wrapped methods. - retry_class = retries_async.AsyncRetry if is_async else retries.Retry self._wrapped_methods = { self.list_operations: gapic_v1.method.wrap_method( self.list_operations, - default_retry=retry_class( + default_retry=retries.Retry( initial=0.5, maximum=10.0, multiplier=2.0, @@ -155,7 +154,7 @@ def _prep_wrapped_messages(self, client_info, is_async=False): ), self.get_operation: gapic_v1.method.wrap_method( self.get_operation, - default_retry=retry_class( + default_retry=retries.Retry( initial=0.5, maximum=10.0, multiplier=2.0, @@ -170,7 +169,7 @@ def _prep_wrapped_messages(self, client_info, is_async=False): ), self.delete_operation: gapic_v1.method.wrap_method( self.delete_operation, - default_retry=retry_class( + default_retry=retries.Retry( initial=0.5, maximum=10.0, multiplier=2.0, @@ -185,7 +184,7 @@ def _prep_wrapped_messages(self, client_info, is_async=False): ), self.cancel_operation: gapic_v1.method.wrap_method( self.cancel_operation, - default_retry=retry_class( + default_retry=retries.Retry( initial=0.5, maximum=10.0, multiplier=2.0, diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index 8c736447..30e01cb7 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -14,6 +14,7 @@ # limitations under the License. # +import json import re from typing import Callable, Dict, Optional, Sequence, Tuple, Union @@ -32,8 +33,8 @@ from google.api_core import gapic_v1 # type: ignore from google.api_core import path_template # type: ignore from google.api_core import rest_helpers # type: ignore -from google.api_core import retry_async as retries # type: ignore -from google.auth.aio import credentials as ga_credentials # type: ignore +from google.api_core import retry_async as retries_async # type: ignore +from google.auth.aio import credentials as ga_credentials_async # type: ignore from google.auth.aio.transport.sessions import AsyncAuthorizedSession # type: ignore from google.longrunning import operations_pb2 # type: ignore from google.protobuf import empty_pb2 # type: ignore @@ -75,7 +76,7 @@ def __init__( self, *, host: str = "longrunning.googleapis.com", - credentials: Optional[ga_credentials.Credentials] = None, + credentials: Optional[ga_credentials_async.Credentials] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, url_scheme: str = "https", @@ -133,10 +134,71 @@ def __init__( # `default_host` in AsyncAuthorizedSession for feature parity with the synchronous # code. self._session = AsyncAuthorizedSession(self._credentials) - self._prep_wrapped_messages(client_info, is_async=True) + self._prep_wrapped_messages(client_info) self._http_options = http_options or {} self._path_prefix = path_prefix + def _prep_wrapped_messages(self, client_info): + # Precompute the wrapped methods. + self._wrapped_methods = { + self.list_operations: gapic_v1.method_async.wrap_method( + self.list_operations, + default_retry=retries_async.AsyncRetry( + initial=0.5, + maximum=10.0, + multiplier=2.0, + predicate=retries_async.if_exception_type( + core_exceptions.ServiceUnavailable, + ), + deadline=10.0, + ), + default_timeout=10.0, + client_info=client_info, + ), + self.get_operation: gapic_v1.method_async.wrap_method( + self.get_operation, + default_retry=retries_async.AsyncRetry( + initial=0.5, + maximum=10.0, + multiplier=2.0, + predicate=retries_async.if_exception_type( + core_exceptions.ServiceUnavailable, + ), + deadline=10.0, + ), + default_timeout=10.0, + client_info=client_info, + ), + self.delete_operation: gapic_v1.method_async.wrap_method( + self.delete_operation, + default_retry=retries_async.AsyncRetry( + initial=0.5, + maximum=10.0, + multiplier=2.0, + predicate=retries_async.if_exception_type( + core_exceptions.ServiceUnavailable, + ), + deadline=10.0, + ), + default_timeout=10.0, + client_info=client_info, + ), + self.cancel_operation: gapic_v1.method_async.wrap_method( + self.cancel_operation, + default_retry=retries_async.AsyncRetry( + initial=0.5, + maximum=10.0, + multiplier=2.0, + predicate=retries_async.if_exception_type( + core_exceptions.ServiceUnavailable, + ), + deadline=10.0, + ), + default_timeout=10.0, + client_info=client_info, + ), + } + async def _list_operations( self, request: operations_pb2.ListOperationsRequest, @@ -196,15 +258,18 @@ async def _list_operations( headers=headers, params=rest_helpers.flatten_query_params(query_params), ) + content = await response.read() # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. if response.status_code >= 400: - raise core_exceptions.from_http_response(response) + payload = json.loads(content.decode('utf-8')) + request_url = "{host}{uri}".format(host=self._host, uri=uri) + raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore # Return the response api_response = operations_pb2.ListOperationsResponse() - json_format.Parse(response.content, api_response, ignore_unknown_fields=False) + json_format.Parse(content, api_response, ignore_unknown_fields=False) return api_response async def _get_operation( @@ -267,16 +332,19 @@ async def _get_operation( headers=headers, params=rest_helpers.flatten_query_params(query_params), ) + content = await response.read() # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. if response.status_code >= 400: - raise core_exceptions.from_http_response(response) + payload = json.loads(content.decode('utf-8')) + request_url = "{host}{uri}".format(host=self._host, uri=uri) + raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore # Return the response api_response = operations_pb2.Operation() json_format.Parse( - await response.read(), api_response, ignore_unknown_fields=False + content, api_response, ignore_unknown_fields=False ) return api_response @@ -340,7 +408,10 @@ async def _delete_operation( # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. if response.status_code >= 400: - raise core_exceptions.from_http_response(response) + content = await response.read() + payload = json.loads(content.decode('utf-8')) + request_url = "{host}{uri}".format(host=self._host, uri=uri) + raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore return empty_pb2.Empty() @@ -411,7 +482,10 @@ async def _cancel_operation( # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. if response.status_code >= 400: - raise core_exceptions.from_http_response(response) + content = await response.read() + payload = json.loads(content.decode('utf-8')) + request_url = "{host}{uri}".format(host=self._host, uri=uri) + raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore return empty_pb2.Empty() diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index 5ba052f6..b0aac6a2 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -736,8 +736,9 @@ def test_transport_instance(): ], ) def test_transport_adc(transport_class, credentials): - if not GOOGLE_AUTH_AIO_INSTALLED: - pytest.skip("Skipped because google-api-core[async_rest] is not installed") + if "async" in str(transport_class).lower() and not GOOGLE_AUTH_AIO_INSTALLED: + pytest.skip("Skipped because google-api-core[async_rest] is not installed.") + # Test default credentials are used if not provided. with mock.patch.object(google.auth, "default") as adc: adc.return_value = (credentials, None) @@ -825,8 +826,8 @@ def test_operations_auth_adc(): # TODO(https://github.com/googleapis/python-api-core/issues/705): Add -# testing for `transports.OperationsRestAsyncTransport` once it mtls is supported -# in `google.auth.aio.transport` +# testing for `transports.OperationsRestAsyncTransport` once MTLS is supported +# in `google.auth.aio.transport`. @pytest.mark.parametrize( "transport_class", [ From f880c72589c9d5b9944110b58fb0b4b1af5b2659 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Mon, 30 Sep 2024 15:14:40 +0000 Subject: [PATCH 05/16] address feedback --- google/api_core/operations_v1/transports/rest_asyncio.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index 30e01cb7..40370a94 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -18,8 +18,7 @@ import re from typing import Callable, Dict, Optional, Sequence, Tuple, Union -from requests import __version__ as requests_version - +from google.auth import __version__ as auth_version try: import google.auth.aio.transport except ImportError as e: # pragma: NO COVER @@ -46,7 +45,7 @@ DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, grpc_version=None, - rest_version=requests_version, + rest_version=auth_version, ) From 34b795fd59880be39c14e57a4c66dbfbd5b03f61 Mon Sep 17 00:00:00 2001 From: Owl Bot Date: Mon, 30 Sep 2024 21:01:06 +0000 Subject: [PATCH 06/16] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20?= =?UTF-8?q?post-processor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --- .../operations_v1/transports/rest_asyncio.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index 40370a94..20034526 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -19,6 +19,7 @@ from typing import Callable, Dict, Optional, Sequence, Tuple, Union from google.auth import __version__ as auth_version + try: import google.auth.aio.transport except ImportError as e: # pragma: NO COVER @@ -262,7 +263,7 @@ async def _list_operations( # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. if response.status_code >= 400: - payload = json.loads(content.decode('utf-8')) + payload = json.loads(content.decode("utf-8")) request_url = "{host}{uri}".format(host=self._host, uri=uri) raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore @@ -336,15 +337,13 @@ async def _get_operation( # In case of error, raise the appropriate core_exceptions.GoogleAPICallError exception # subclass. if response.status_code >= 400: - payload = json.loads(content.decode('utf-8')) + payload = json.loads(content.decode("utf-8")) request_url = "{host}{uri}".format(host=self._host, uri=uri) raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore # Return the response api_response = operations_pb2.Operation() - json_format.Parse( - content, api_response, ignore_unknown_fields=False - ) + json_format.Parse(content, api_response, ignore_unknown_fields=False) return api_response async def _delete_operation( @@ -408,7 +407,7 @@ async def _delete_operation( # subclass. if response.status_code >= 400: content = await response.read() - payload = json.loads(content.decode('utf-8')) + payload = json.loads(content.decode("utf-8")) request_url = "{host}{uri}".format(host=self._host, uri=uri) raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore @@ -482,7 +481,7 @@ async def _cancel_operation( # subclass. if response.status_code >= 400: content = await response.read() - payload = json.loads(content.decode('utf-8')) + payload = json.loads(content.decode("utf-8")) request_url = "{host}{uri}".format(host=self._host, uri=uri) raise core_exceptions.format_http_response_error(response, method, request_url, payload) # type: ignore From 62f0c39baf4473dd432cf91d5962f09a897cd317 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 1 Oct 2024 16:23:14 +0000 Subject: [PATCH 07/16] fix mypy and lint issues --- google/api_core/operations_v1/__init__.py | 23 ++++++-------- .../operations_v1/transports/__init__.py | 27 +++++++--------- .../api_core/operations_v1/transports/base.py | 2 +- .../api_core/operations_v1/transports/rest.py | 1 - .../operations_v1/transports/rest_asyncio.py | 31 ++++++++++++------- .../test_operations_rest_client.py | 31 +++++++++++++------ 6 files changed, 64 insertions(+), 51 deletions(-) diff --git a/google/api_core/operations_v1/__init__.py b/google/api_core/operations_v1/__init__.py index d5805dde..fef4b5c9 100644 --- a/google/api_core/operations_v1/__init__.py +++ b/google/api_core/operations_v1/__init__.py @@ -21,21 +21,18 @@ from google.api_core.operations_v1.operations_client import OperationsClient from google.api_core.operations_v1.transports.rest import OperationsRestTransport -try: - from google.api_core.operations_v1.transports.rest_asyncio import ( - OperationsRestAsyncTransport, - ) +__all__ = [ + "AbstractOperationsClient", + "OperationsAsyncClient", + "OperationsClient", + "OperationsRestTransport" +] - HAS_ASYNC_TRANSPORT = True +try: + from google.api_core.operations_v1.transports.rest_asyncio import OperationsRestAsyncTransport # noqa: F401 + __all__.append("OperationsRestAsyncTransport") except ImportError: # This import requires the `async_rest` extra # Don't raise an exception if `OperationsRestAsyncTransport` cannot be imported # as other transports are still available. - HAS_ASYNC_TRANSPORT = False - -__all__ = [ - "AbstractOperationsClient", - "OperationsAsyncClient", - "OperationsClient", - "OperationsRestTransport", -].extend("OperationsRestAsyncTransport") + pass diff --git a/google/api_core/operations_v1/transports/__init__.py b/google/api_core/operations_v1/transports/__init__.py index b3922110..eead8504 100644 --- a/google/api_core/operations_v1/transports/__init__.py +++ b/google/api_core/operations_v1/transports/__init__.py @@ -14,30 +14,25 @@ # limitations under the License. # from collections import OrderedDict +from typing import cast, Dict, Tuple from .base import OperationsTransport from .rest import OperationsRestTransport +# Compile a registry of transports. +_transport_registry: Dict[str, OperationsTransport] = OrderedDict() +_transport_registry["rest"] = cast(OperationsTransport, OperationsRestTransport) + +__all__: Tuple[str, ...] = ("OperationsTransport", "OperationsRestTransport") try: from .rest_asyncio import OperationsRestAsyncTransport - HAS_ASYNC_TRANSPORT = True - ASYNC_REST_CLASSES = ("OperationsRestAsyncTransport",) + __all__ += ("OperationsRestAsyncTransport",) + _transport_registry["rest_asyncio"] = cast( + OperationsTransport, OperationsRestAsyncTransport + ) except ImportError: # This import requires the `async_rest` extra # Don't raise an exception if `OperationsRestAsyncTransport` cannot be imported # as other transports are still available - HAS_ASYNC_TRANSPORT = False - ASYNC_REST_CLASSES = () - -# Compile a registry of transports. -_transport_registry = OrderedDict() -_transport_registry["rest"] = OperationsRestTransport - -if HAS_ASYNC_TRANSPORT: - _transport_registry["rest_asyncio"] = OperationsRestAsyncTransport - -__all__ = ( - "OperationsTransport", - "OperationsRestTransport", -) + ASYNC_REST_CLASSES + pass diff --git a/google/api_core/operations_v1/transports/base.py b/google/api_core/operations_v1/transports/base.py index 30a109a5..2c8a655b 100644 --- a/google/api_core/operations_v1/transports/base.py +++ b/google/api_core/operations_v1/transports/base.py @@ -21,7 +21,6 @@ from google.api_core import exceptions as core_exceptions # type: ignore from google.api_core import gapic_v1 # type: ignore from google.api_core import retry as retries # type: ignore -from google.api_core import retry_async as retries_async # type: ignore from google.api_core import version import google.auth # type: ignore from google.auth import credentials as ga_credentials # type: ignore @@ -50,6 +49,7 @@ def __init__( self, *, host: str = DEFAULT_HOST, + # TODO(https://github.com/googleapis/python-api-core/issues/709): update type hint for credentials to include `google.auth.aio.Credentials`. credentials: Optional[ga_credentials.Credentials] = None, credentials_file: Optional[str] = None, scopes: Optional[Sequence[str]] = None, diff --git a/google/api_core/operations_v1/transports/rest.py b/google/api_core/operations_v1/transports/rest.py index e30f5c88..390ffcb5 100644 --- a/google/api_core/operations_v1/transports/rest.py +++ b/google/api_core/operations_v1/transports/rest.py @@ -14,7 +14,6 @@ # limitations under the License. # -import re from typing import Callable, Dict, Optional, Sequence, Tuple, Union from requests import __version__ as requests_version diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index 20034526..d9cd91ee 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -16,15 +16,15 @@ import json import re -from typing import Callable, Dict, Optional, Sequence, Tuple, Union +from typing import Any, Callable, Coroutine, Dict, Optional, Sequence, Tuple from google.auth import __version__ as auth_version try: - import google.auth.aio.transport + from google.auth.aio.transport.sessions import AsyncAuthorizedSession # type: ignore except ImportError as e: # pragma: NO COVER raise ImportError( - "`google-api-core[async_rest]` is required to use asynchronous rest streaming. " + "`google-api-core[async_rest]` is required to use long running operations. " "Install the `async_rest` extra of `google-api-core` using " "`pip install google-api-core[async_rest]`." ) from e @@ -35,7 +35,6 @@ from google.api_core import rest_helpers # type: ignore from google.api_core import retry_async as retries_async # type: ignore from google.auth.aio import credentials as ga_credentials_async # type: ignore -from google.auth.aio.transport.sessions import AsyncAuthorizedSession # type: ignore from google.longrunning import operations_pb2 # type: ignore from google.protobuf import empty_pb2 # type: ignore from google.protobuf import json_format # type: ignore @@ -76,7 +75,7 @@ def __init__( self, *, host: str = "longrunning.googleapis.com", - credentials: Optional[ga_credentials_async.Credentials] = None, + credentials: ga_credentials_async.Credentials, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, url_scheme: str = "https", @@ -126,14 +125,16 @@ def __init__( super().__init__( host=host, - credentials=credentials, + # TODO(https://github.com/googleapis/python-api-core/issues/709): Remove `type: ignore` when the linked issue is resolved. + credentials=credentials, # type: ignore client_info=client_info, always_use_jwt_access=always_use_jwt_access, ) # TODO(https://github.com/googleapis/python-api-core/issues/708): add support for # `default_host` in AsyncAuthorizedSession for feature parity with the synchronous # code. - self._session = AsyncAuthorizedSession(self._credentials) + # TODO(https://github.com/googleapis/python-api-core/issues/709): Remove `type: ignore` when the linked issue is resolved. + self._session = AsyncAuthorizedSession(self._credentials) # type: ignore self._prep_wrapped_messages(client_info) self._http_options = http_options or {} self._path_prefix = path_prefix @@ -491,26 +492,34 @@ async def _cancel_operation( def list_operations( self, ) -> Callable[ - [operations_pb2.ListOperationsRequest], operations_pb2.ListOperationsResponse + [operations_pb2.ListOperationsRequest], + Coroutine[Any, Any, operations_pb2.ListOperationsResponse], ]: return self._list_operations @property def get_operation( self, - ) -> Callable[[operations_pb2.GetOperationRequest], operations_pb2.Operation]: + ) -> Callable[ + [operations_pb2.GetOperationRequest], + Coroutine[Any, Any, operations_pb2.Operation], + ]: return self._get_operation @property def delete_operation( self, - ) -> Callable[[operations_pb2.DeleteOperationRequest], empty_pb2.Empty]: + ) -> Callable[ + [operations_pb2.DeleteOperationRequest], Coroutine[Any, Any, empty_pb2.Empty] + ]: return self._delete_operation @property def cancel_operation( self, - ) -> Callable[[operations_pb2.CancelOperationRequest], empty_pb2.Empty]: + ) -> Callable[ + [operations_pb2.CancelOperationRequest], Coroutine[Any, Any, empty_pb2.Empty] + ]: return self._cancel_operation diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index b0aac6a2..bda506e1 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -33,11 +33,11 @@ from google.api_core.operations_v1 import transports import google.auth from google.auth import credentials as ga_credentials -from google.auth.aio import credentials as ga_credentials_async try: - import aiohttp + import aiohttp # noqa: F401 import google.auth.aio.transport + from google.auth.aio import credentials as ga_credentials_async GOOGLE_AUTH_AIO_INSTALLED = True except ImportError: @@ -50,6 +50,25 @@ from google.rpc import status_pb2 # type: ignore +if GOOGLE_AUTH_AIO_INSTALLED: + TEST_TRANSPORT_CREDS_PARAMS = [ + ( + transports.OperationsRestTransport, + ga_credentials.AnonymousCredentials(), + ), + ( + transports.OperationsRestAsyncTransport, + ga_credentials_async.AnonymousCredentials(), + ), + ] +else: + TEST_TRANSPORT_CREDS_PARAMS = [ + ( + transports.OperationsRestTransport, + ga_credentials.AnonymousCredentials(), + ) + ] + HTTP_OPTIONS = { "google.longrunning.Operations.CancelOperation": [ {"method": "post", "uri": "/v3/{name=operations/*}:cancel", "body": "*"}, @@ -727,13 +746,7 @@ def test_transport_instance(): @pytest.mark.parametrize( "transport_class,credentials", - [ - (transports.OperationsRestTransport, ga_credentials.AnonymousCredentials()), - ( - transports.OperationsRestAsyncTransport, - ga_credentials_async.AnonymousCredentials(), - ), - ], + TEST_TRANSPORT_CREDS_PARAMS, ) def test_transport_adc(transport_class, credentials): if "async" in str(transport_class).lower() and not GOOGLE_AUTH_AIO_INSTALLED: From 3bfad42f582acdf6ccc879cb34a8fa474ee38fe1 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 1 Oct 2024 16:29:49 +0000 Subject: [PATCH 08/16] minor fix --- google/api_core/operations_v1/transports/rest_asyncio.py | 2 +- tests/unit/operations_v1/test_operations_rest_client.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index d9cd91ee..6d973101 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -75,7 +75,7 @@ def __init__( self, *, host: str = "longrunning.googleapis.com", - credentials: ga_credentials_async.Credentials, + credentials: Optional[ga_credentials_async.Credentials] = None, client_info: gapic_v1.client_info.ClientInfo = DEFAULT_CLIENT_INFO, always_use_jwt_access: Optional[bool] = False, url_scheme: str = "https", diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index bda506e1..e8a75bb9 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -749,9 +749,6 @@ def test_transport_instance(): TEST_TRANSPORT_CREDS_PARAMS, ) def test_transport_adc(transport_class, credentials): - if "async" in str(transport_class).lower() and not GOOGLE_AUTH_AIO_INSTALLED: - pytest.skip("Skipped because google-api-core[async_rest] is not installed.") - # Test default credentials are used if not provided. with mock.patch.object(google.auth, "default") as adc: adc.return_value = (credentials, None) From 3e9a1a62a2cb83e292694c8f06c6ddc02e631b13 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 1 Oct 2024 19:22:24 +0000 Subject: [PATCH 09/16] add no cover --- google/api_core/operations_v1/transports/rest_asyncio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index 6d973101..c0842fb2 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -279,7 +279,7 @@ async def _get_operation( *, timeout: Optional[float] = None, metadata: Sequence[Tuple[str, str]] = (), - ) -> operations_pb2.Operation: + ) -> operations_pb2.Operation: # pragme: NO COVER r"""Asynchronously call the get operation method over HTTP. Args: @@ -353,7 +353,7 @@ async def _delete_operation( *, timeout: Optional[float] = None, metadata: Sequence[Tuple[str, str]] = (), - ) -> empty_pb2.Empty: + ) -> empty_pb2.Empty: # pragme: NO COVER r"""Asynchronously call the delete operation method over HTTP. Args: @@ -420,7 +420,7 @@ async def _cancel_operation( *, timeout: Optional[float] = None, metadata: Sequence[Tuple[str, str]] = (), - ) -> empty_pb2.Empty: + ) -> empty_pb2.Empty: # pragme: NO COVER r"""Asynchronously call the cancel operation method over HTTP. Args: From 7c08eeb45dc156c2489093f5649d4112e09bca06 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 1 Oct 2024 19:27:44 +0000 Subject: [PATCH 10/16] fix no cover tag --- google/api_core/operations_v1/transports/rest_asyncio.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index c0842fb2..9b7d3de7 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -206,7 +206,7 @@ async def _list_operations( *, timeout: Optional[float] = None, metadata: Sequence[Tuple[str, str]] = (), - ) -> operations_pb2.ListOperationsResponse: + ) -> operations_pb2.ListOperationsResponse: # pragma: NO COVER r"""Asynchronously call the list operations method over HTTP. Args: @@ -279,7 +279,7 @@ async def _get_operation( *, timeout: Optional[float] = None, metadata: Sequence[Tuple[str, str]] = (), - ) -> operations_pb2.Operation: # pragme: NO COVER + ) -> operations_pb2.Operation: # pragma: NO COVER r"""Asynchronously call the get operation method over HTTP. Args: @@ -420,7 +420,7 @@ async def _cancel_operation( *, timeout: Optional[float] = None, metadata: Sequence[Tuple[str, str]] = (), - ) -> empty_pb2.Empty: # pragme: NO COVER + ) -> empty_pb2.Empty: # pragma: NO COVER r"""Asynchronously call the cancel operation method over HTTP. Args: From cdfa64fdc110f2c1c87adafd63fe5c0bacfadaa6 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 1 Oct 2024 19:33:44 +0000 Subject: [PATCH 11/16] link coverage issue --- google/api_core/operations_v1/transports/rest_asyncio.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index 9b7d3de7..bd0456aa 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -206,6 +206,7 @@ async def _list_operations( *, timeout: Optional[float] = None, metadata: Sequence[Tuple[str, str]] = (), + # TODO(https://github.com/googleapis/python-api-core/issues/710): Add coverage and remove `# pragma: NO COVER`. ) -> operations_pb2.ListOperationsResponse: # pragma: NO COVER r"""Asynchronously call the list operations method over HTTP. @@ -279,6 +280,7 @@ async def _get_operation( *, timeout: Optional[float] = None, metadata: Sequence[Tuple[str, str]] = (), + # TODO(https://github.com/googleapis/python-api-core/issues/710): Add coverage and remove `# pragma: NO COVER`. ) -> operations_pb2.Operation: # pragma: NO COVER r"""Asynchronously call the get operation method over HTTP. @@ -353,7 +355,8 @@ async def _delete_operation( *, timeout: Optional[float] = None, metadata: Sequence[Tuple[str, str]] = (), - ) -> empty_pb2.Empty: # pragme: NO COVER + # TODO(https://github.com/googleapis/python-api-core/issues/710): Add coverage and remove `# pragma: NO COVER`. + ) -> empty_pb2.Empty: # pragma: NO COVER r"""Asynchronously call the delete operation method over HTTP. Args: @@ -420,6 +423,7 @@ async def _cancel_operation( *, timeout: Optional[float] = None, metadata: Sequence[Tuple[str, str]] = (), + # TODO(https://github.com/googleapis/python-api-core/issues/710): Add coverage and remove `# pragma: NO COVER`. ) -> empty_pb2.Empty: # pragma: NO COVER r"""Asynchronously call the cancel operation method over HTTP. From 2891173361a4993dc3809a79ddbf9b1cadee978d Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 1 Oct 2024 19:37:16 +0000 Subject: [PATCH 12/16] silence coverage issue --- google/api_core/operations_v1/transports/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/api_core/operations_v1/transports/base.py b/google/api_core/operations_v1/transports/base.py index 2c8a655b..a4f690c4 100644 --- a/google/api_core/operations_v1/transports/base.py +++ b/google/api_core/operations_v1/transports/base.py @@ -98,7 +98,7 @@ def __init__( # Save the hostname. Default to port 443 (HTTPS) if none is specified. if ":" not in host: - host += ":443" + host += ":443" # pragme: NO COVER self._host = host scopes_kwargs = {"scopes": scopes, "default_scopes": self.AUTH_SCOPES} From acdadeec642c1a213e2a28813969edd2fb07d918 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Tue, 1 Oct 2024 19:42:23 +0000 Subject: [PATCH 13/16] fix statement name error --- google/api_core/operations_v1/transports/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/api_core/operations_v1/transports/base.py b/google/api_core/operations_v1/transports/base.py index a4f690c4..51b4e872 100644 --- a/google/api_core/operations_v1/transports/base.py +++ b/google/api_core/operations_v1/transports/base.py @@ -98,7 +98,7 @@ def __init__( # Save the hostname. Default to port 443 (HTTPS) if none is specified. if ":" not in host: - host += ":443" # pragme: NO COVER + host += ":443" # pragma: NO COVER self._host = host scopes_kwargs = {"scopes": scopes, "default_scopes": self.AUTH_SCOPES} From 62dfff9a6d8c3ef052f38a700e864cc89856c7ee Mon Sep 17 00:00:00 2001 From: ohmayr Date: Wed, 2 Oct 2024 19:34:36 +0000 Subject: [PATCH 14/16] address PR feedback --- .../operations_v1/transports/__init__.py | 5 +++-- .../api_core/operations_v1/transports/base.py | 5 ++--- .../operations_v1/transports/rest_asyncio.py | 21 ++----------------- .../test_operations_rest_client.py | 13 +++++------- 4 files changed, 12 insertions(+), 32 deletions(-) diff --git a/google/api_core/operations_v1/transports/__init__.py b/google/api_core/operations_v1/transports/__init__.py index eead8504..2ae79e4e 100644 --- a/google/api_core/operations_v1/transports/__init__.py +++ b/google/api_core/operations_v1/transports/__init__.py @@ -24,6 +24,7 @@ _transport_registry["rest"] = cast(OperationsTransport, OperationsRestTransport) __all__: Tuple[str, ...] = ("OperationsTransport", "OperationsRestTransport") + try: from .rest_asyncio import OperationsRestAsyncTransport @@ -32,7 +33,7 @@ OperationsTransport, OperationsRestAsyncTransport ) except ImportError: - # This import requires the `async_rest` extra + # This import requires the `async_rest` extra. # Don't raise an exception if `OperationsRestAsyncTransport` cannot be imported - # as other transports are still available + # as other transports are still available. pass diff --git a/google/api_core/operations_v1/transports/base.py b/google/api_core/operations_v1/transports/base.py index 51b4e872..50e13761 100644 --- a/google/api_core/operations_v1/transports/base.py +++ b/google/api_core/operations_v1/transports/base.py @@ -222,9 +222,8 @@ def _convert_protobuf_message_to_dict( Returns: A dict representation of the protocol buffer message. """ - # For backwards compatibility with protobuf 3.x 4.x - # Remove once support for protobuf 3.x and 4.x is dropped - # https://github.com/googleapis/python-api-core/issues/643 + # TODO(https://github.com/googleapis/python-api-core/issues/643): For backwards compatibility + # with protobuf 3.x 4.x, Remove once support for protobuf 3.x and 4.x is dropped. if PROTOBUF_VERSION[0:2] in ["3.", "4."]: result = json_format.MessageToDict( message, diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index bd0456aa..e3c50103 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -15,7 +15,6 @@ # import json -import re from typing import Any, Callable, Coroutine, Dict, Optional, Sequence, Tuple from google.auth import __version__ as auth_version @@ -24,8 +23,7 @@ from google.auth.aio.transport.sessions import AsyncAuthorizedSession # type: ignore except ImportError as e: # pragma: NO COVER raise ImportError( - "`google-api-core[async_rest]` is required to use long running operations. " - "Install the `async_rest` extra of `google-api-core` using " + "The `async_rest` extra of `google-api-core` is required to use long-running operations. Install it by running " "`pip install google-api-core[async_rest]`." ) from e @@ -50,8 +48,7 @@ class OperationsRestAsyncTransport(OperationsTransport): - """REST Async backend transport for Operations inherited from - `OperationsTransport`. + """Asynchronous REST backend transport for Operations. Manages async long-running operations with an API service. @@ -109,20 +106,6 @@ def __init__( "v1" by default. """ - # Run the base constructor - # TODO(yon-mg): resolve other ctor params i.e. scopes, quota, etc. - # TODO: When custom host (api_endpoint) is set, `scopes` must *also* be set on the - # credentials object - maybe_url_match = re.match("^(?Phttp(?:s)?://)?(?P.*)$", host) - if maybe_url_match is None: - raise ValueError( - f"Unexpected hostname structure: {host}" - ) # pragma: NO COVER - - url_match_items = maybe_url_match.groupdict() - - host = f"{url_scheme}://{host}" if not url_match_items["scheme"] else host - super().__init__( host=host, # TODO(https://github.com/googleapis/python-api-core/issues/709): Remove `type: ignore` when the linked issue is resolved. diff --git a/tests/unit/operations_v1/test_operations_rest_client.py b/tests/unit/operations_v1/test_operations_rest_client.py index e8a75bb9..556c35c3 100644 --- a/tests/unit/operations_v1/test_operations_rest_client.py +++ b/tests/unit/operations_v1/test_operations_rest_client.py @@ -31,8 +31,12 @@ from google.api_core.operations_v1 import AbstractOperationsClient from google.api_core.operations_v1 import pagers from google.api_core.operations_v1 import transports -import google.auth from google.auth import credentials as ga_credentials +from google.auth.exceptions import MutualTLSChannelError +from google.longrunning import operations_pb2 +from google.oauth2 import service_account +from google.protobuf import json_format # type: ignore +from google.rpc import status_pb2 # type: ignore try: import aiohttp # noqa: F401 @@ -43,13 +47,6 @@ except ImportError: GOOGLE_AUTH_AIO_INSTALLED = False -from google.auth.exceptions import MutualTLSChannelError -from google.longrunning import operations_pb2 -from google.oauth2 import service_account -from google.protobuf import json_format # type: ignore -from google.rpc import status_pb2 # type: ignore - - if GOOGLE_AUTH_AIO_INSTALLED: TEST_TRANSPORT_CREDS_PARAMS = [ ( From f93e6bd64b723baff7ea1e79850fb16c3d42845d Mon Sep 17 00:00:00 2001 From: ohmayr Date: Wed, 2 Oct 2024 19:56:00 +0000 Subject: [PATCH 15/16] address PR feedback --- google/api_core/client_info.py | 2 +- google/api_core/gapic_v1/client_info.py | 1 + google/api_core/operations_v1/__init__.py | 2 +- google/api_core/operations_v1/transports/rest.py | 2 +- google/api_core/operations_v1/transports/rest_asyncio.py | 3 +-- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/google/api_core/client_info.py b/google/api_core/client_info.py index 48326799..f94a9321 100644 --- a/google/api_core/client_info.py +++ b/google/api_core/client_info.py @@ -57,7 +57,7 @@ class ClientInfo(object): user_agent (Optional[str]): Prefix to the user agent header. This is used to supply information such as application name or partner tool. Recommended format: ``application-or-tool-ID/major.minor.version``. - rest_version (Optional[str]): The requests library version. + rest_version (Optional[str]): The google-auth or requests library version. """ def __init__( diff --git a/google/api_core/gapic_v1/client_info.py b/google/api_core/gapic_v1/client_info.py index 2de1be7f..2a698acf 100644 --- a/google/api_core/gapic_v1/client_info.py +++ b/google/api_core/gapic_v1/client_info.py @@ -45,6 +45,7 @@ class ClientInfo(client_info.ClientInfo): user_agent (Optional[str]): Prefix to the user agent header. This is used to supply information such as application name or partner tool. Recommended format: ``application-or-tool-ID/major.minor.version``. + rest_version (Optional[str]): The google-auth or requests library version. """ def to_grpc_metadata(self): diff --git a/google/api_core/operations_v1/__init__.py b/google/api_core/operations_v1/__init__.py index fef4b5c9..52f83fcd 100644 --- a/google/api_core/operations_v1/__init__.py +++ b/google/api_core/operations_v1/__init__.py @@ -32,7 +32,7 @@ from google.api_core.operations_v1.transports.rest_asyncio import OperationsRestAsyncTransport # noqa: F401 __all__.append("OperationsRestAsyncTransport") except ImportError: - # This import requires the `async_rest` extra + # This import requires the `async_rest` extra. # Don't raise an exception if `OperationsRestAsyncTransport` cannot be imported # as other transports are still available. pass diff --git a/google/api_core/operations_v1/transports/rest.py b/google/api_core/operations_v1/transports/rest.py index 390ffcb5..a11a5ce7 100644 --- a/google/api_core/operations_v1/transports/rest.py +++ b/google/api_core/operations_v1/transports/rest.py @@ -40,7 +40,7 @@ DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, grpc_version=None, - rest_version=requests_version, + rest_version=f"rest@{requests_version}", ) diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index e3c50103..30dfa19b 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -39,11 +39,10 @@ from .base import DEFAULT_CLIENT_INFO as BASE_DEFAULT_CLIENT_INFO, OperationsTransport - DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, grpc_version=None, - rest_version=auth_version, + rest_version=f"rest@{auth_version}", ) From 637a36134501ddbe18e12fec86bfd002d6a3961c Mon Sep 17 00:00:00 2001 From: ohmayr Date: Wed, 2 Oct 2024 22:35:15 +0000 Subject: [PATCH 16/16] address PR comments --- google/api_core/client_info.py | 3 ++- google/api_core/gapic_v1/client_info.py | 3 ++- google/api_core/operations_v1/transports/rest.py | 7 ++++++- google/api_core/operations_v1/transports/rest_asyncio.py | 7 ++++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/google/api_core/client_info.py b/google/api_core/client_info.py index f94a9321..90926beb 100644 --- a/google/api_core/client_info.py +++ b/google/api_core/client_info.py @@ -57,7 +57,8 @@ class ClientInfo(object): user_agent (Optional[str]): Prefix to the user agent header. This is used to supply information such as application name or partner tool. Recommended format: ``application-or-tool-ID/major.minor.version``. - rest_version (Optional[str]): The google-auth or requests library version. + rest_version (Optional[str]): A string with labeled versions of the + dependencies used for REST transport. """ def __init__( diff --git a/google/api_core/gapic_v1/client_info.py b/google/api_core/gapic_v1/client_info.py index 2a698acf..4516f339 100644 --- a/google/api_core/gapic_v1/client_info.py +++ b/google/api_core/gapic_v1/client_info.py @@ -45,7 +45,8 @@ class ClientInfo(client_info.ClientInfo): user_agent (Optional[str]): Prefix to the user agent header. This is used to supply information such as application name or partner tool. Recommended format: ``application-or-tool-ID/major.minor.version``. - rest_version (Optional[str]): The google-auth or requests library version. + rest_version (Optional[str]): A string with labeled versions of the + dependencies used for REST transport. """ def to_grpc_metadata(self): diff --git a/google/api_core/operations_v1/transports/rest.py b/google/api_core/operations_v1/transports/rest.py index a11a5ce7..b0599baa 100644 --- a/google/api_core/operations_v1/transports/rest.py +++ b/google/api_core/operations_v1/transports/rest.py @@ -40,7 +40,7 @@ DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, grpc_version=None, - rest_version=f"rest@{requests_version}", + rest_version=f"requests@{requests_version}", ) @@ -133,6 +133,7 @@ def __init__( ) if client_cert_source_for_mtls: self._session.configure_mtls_channel(client_cert_source_for_mtls) + # TODO(https://github.com/googleapis/python-api-core/issues/720): Add wrap logic directly to the property methods for callables. self._prep_wrapped_messages(client_info) self._http_options = http_options or {} self._path_prefix = path_prefix @@ -195,6 +196,7 @@ def _list_operations( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout, @@ -271,6 +273,7 @@ def _get_operation( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout, @@ -340,6 +343,7 @@ def _delete_operation( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout, @@ -415,6 +419,7 @@ def _cancel_operation( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout, diff --git a/google/api_core/operations_v1/transports/rest_asyncio.py b/google/api_core/operations_v1/transports/rest_asyncio.py index 30dfa19b..5c903b90 100644 --- a/google/api_core/operations_v1/transports/rest_asyncio.py +++ b/google/api_core/operations_v1/transports/rest_asyncio.py @@ -42,7 +42,7 @@ DEFAULT_CLIENT_INFO = gapic_v1.client_info.ClientInfo( gapic_version=BASE_DEFAULT_CLIENT_INFO.gapic_version, grpc_version=None, - rest_version=f"rest@{auth_version}", + rest_version=f"google-auth@{auth_version}", ) @@ -117,6 +117,7 @@ def __init__( # code. # TODO(https://github.com/googleapis/python-api-core/issues/709): Remove `type: ignore` when the linked issue is resolved. self._session = AsyncAuthorizedSession(self._credentials) # type: ignore + # TODO(https://github.com/googleapis/python-api-core/issues/720): Add wrap logic directly to the property methods for callables. self._prep_wrapped_messages(client_info) self._http_options = http_options or {} self._path_prefix = path_prefix @@ -236,6 +237,7 @@ async def _list_operations( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = await getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout, @@ -311,6 +313,7 @@ async def _get_operation( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = await getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout, @@ -382,6 +385,7 @@ async def _delete_operation( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = await getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout, @@ -456,6 +460,7 @@ async def _cancel_operation( # Send the request headers = dict(metadata) headers["Content-Type"] = "application/json" + # TODO(https://github.com/googleapis/python-api-core/issues/721): Update incorrect use of `uri`` variable name. response = await getattr(self._session, method)( "{host}{uri}".format(host=self._host, uri=uri), timeout=timeout,