diff --git a/google/cloud/spanner_admin_database_v1/__init__.py b/google/cloud/spanner_admin_database_v1/__init__.py index a6272a0ea2..f7d3a4f557 100644 --- a/google/cloud/spanner_admin_database_v1/__init__.py +++ b/google/cloud/spanner_admin_database_v1/__init__.py @@ -32,6 +32,7 @@ from .types.common import EncryptionConfig from .types.common import EncryptionInfo from .types.common import OperationProgress +from .types.common import DatabaseDialect from .types.spanner_database_admin import CreateDatabaseMetadata from .types.spanner_database_admin import CreateDatabaseRequest from .types.spanner_database_admin import Database @@ -63,6 +64,7 @@ "CreateDatabaseRequest", "Database", "DatabaseAdminClient", + "DatabaseDialect", "DeleteBackupRequest", "DropDatabaseRequest", "EncryptionConfig", diff --git a/google/cloud/spanner_admin_database_v1/services/database_admin/async_client.py b/google/cloud/spanner_admin_database_v1/services/database_admin/async_client.py index d8487ba26d..a2e09ae083 100644 --- a/google/cloud/spanner_admin_database_v1/services/database_admin/async_client.py +++ b/google/cloud/spanner_admin_database_v1/services/database_admin/async_client.py @@ -16,7 +16,7 @@ from collections import OrderedDict import functools import re -from typing import Dict, Sequence, Tuple, Type, Union +from typing import Dict, Optional, Sequence, Tuple, Type, Union import pkg_resources from google.api_core.client_options import ClientOptions @@ -51,11 +51,13 @@ class DatabaseAdminAsyncClient: """Cloud Spanner Database Admin API - The Cloud Spanner Database Admin API can be used to create, - drop, and list databases. It also enables updating the schema of - pre-existing databases. It can be also used to create, delete - and list backups for a database and to restore from an existing - backup. + + The Cloud Spanner Database Admin API can be used to: + + - create, drop, and list databases + - update the schema of pre-existing databases + - create, delete and list backups for a database + - restore a database from an existing backup """ _client: DatabaseAdminClient @@ -133,6 +135,42 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): from_service_account_json = from_service_account_file + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + return DatabaseAdminClient.get_mtls_endpoint_and_cert_source(client_options) # type: ignore + @property def transport(self) -> DatabaseAdminTransport: """Returns the transport used by the client instance. @@ -617,7 +655,8 @@ async def drop_database( ) -> None: r"""Drops (aka deletes) a Cloud Spanner database. Completed backups for the database will be retained according to their - ``expire_time``. + ``expire_time``. Note: Cloud Spanner might continue to accept + requests for a few seconds after the database has been deleted. Args: request (Union[google.cloud.spanner_admin_database_v1.types.DropDatabaseRequest, dict]): diff --git a/google/cloud/spanner_admin_database_v1/services/database_admin/client.py b/google/cloud/spanner_admin_database_v1/services/database_admin/client.py index e04c6c1d7f..b4e0d0a853 100644 --- a/google/cloud/spanner_admin_database_v1/services/database_admin/client.py +++ b/google/cloud/spanner_admin_database_v1/services/database_admin/client.py @@ -85,11 +85,13 @@ def get_transport_class(cls, label: str = None,) -> Type[DatabaseAdminTransport] class DatabaseAdminClient(metaclass=DatabaseAdminClientMeta): """Cloud Spanner Database Admin API - The Cloud Spanner Database Admin API can be used to create, - drop, and list databases. It also enables updating the schema of - pre-existing databases. It can be also used to create, delete - and list backups for a database and to restore from an existing - backup. + + The Cloud Spanner Database Admin API can be used to: + + - create, drop, and list databases + - update the schema of pre-existing databases + - create, delete and list backups for a database + - restore a database from an existing backup """ @staticmethod @@ -325,6 +327,73 @@ def parse_common_location_path(path: str) -> Dict[str, str]: m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) return m.groupdict() if m else {} + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[client_options_lib.ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + if client_options is None: + client_options = client_options_lib.ClientOptions() + use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") + if use_client_cert not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" + ) + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" + ) + + # Figure out the client cert source to use. + client_cert_source = None + if use_client_cert == "true": + if client_options.client_cert_source: + client_cert_source = client_options.client_cert_source + elif mtls.has_default_client_cert_source(): + client_cert_source = mtls.default_client_cert_source() + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + api_endpoint = cls.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = cls.DEFAULT_ENDPOINT + + return api_endpoint, client_cert_source + def __init__( self, *, @@ -375,57 +444,22 @@ def __init__( if client_options is None: client_options = client_options_lib.ClientOptions() - # Create SSL credentials for mutual TLS if needed. - if os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") not in ( - "true", - "false", - ): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - use_client_cert = ( - os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") == "true" + api_endpoint, client_cert_source_func = self.get_mtls_endpoint_and_cert_source( + client_options ) - client_cert_source_func = None - is_mtls = False - if use_client_cert: - if client_options.client_cert_source: - is_mtls = True - client_cert_source_func = client_options.client_cert_source - else: - is_mtls = mtls.has_default_client_cert_source() - if is_mtls: - client_cert_source_func = mtls.default_client_cert_source() - else: - client_cert_source_func = None - - # Figure out which api endpoint to use. - if client_options.api_endpoint is not None: - api_endpoint = client_options.api_endpoint - else: - use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") - if use_mtls_env == "never": - api_endpoint = self.DEFAULT_ENDPOINT - elif use_mtls_env == "always": - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - elif use_mtls_env == "auto": - if is_mtls: - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = self.DEFAULT_ENDPOINT - else: - raise MutualTLSChannelError( - "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted " - "values: never, auto, always" - ) + api_key_value = getattr(client_options, "api_key", None) + if api_key_value and credentials: + raise ValueError( + "client_options.api_key and credentials are mutually exclusive" + ) # Save or instantiate the transport. # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, DatabaseAdminTransport): # transport is a DatabaseAdminTransport instance. - if credentials or client_options.credentials_file: + if credentials or client_options.credentials_file or api_key_value: raise ValueError( "When providing a transport instance, " "provide its credentials directly." @@ -437,6 +471,15 @@ def __init__( ) self._transport = transport else: + 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 = type(self).get_transport_class(transport) self._transport = Transport( credentials=credentials, @@ -843,7 +886,8 @@ def drop_database( ) -> None: r"""Drops (aka deletes) a Cloud Spanner database. Completed backups for the database will be retained according to their - ``expire_time``. + ``expire_time``. Note: Cloud Spanner might continue to accept + requests for a few seconds after the database has been deleted. Args: request (Union[google.cloud.spanner_admin_database_v1.types.DropDatabaseRequest, dict]): diff --git a/google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc.py b/google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc.py index b137130c69..06c2143924 100644 --- a/google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc.py +++ b/google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc.py @@ -39,11 +39,13 @@ class DatabaseAdminGrpcTransport(DatabaseAdminTransport): """gRPC backend transport for DatabaseAdmin. Cloud Spanner Database Admin API - The Cloud Spanner Database Admin API can be used to create, - drop, and list databases. It also enables updating the schema of - pre-existing databases. It can be also used to create, delete - and list backups for a database and to restore from an existing - backup. + + The Cloud Spanner Database Admin API can be used to: + + - create, drop, and list databases + - update the schema of pre-existing databases + - create, delete and list backups for a database + - restore a database from an existing backup This class defines the same methods as the primary client, so the primary client can load the underlying transport implementation @@ -390,7 +392,8 @@ def drop_database( Drops (aka deletes) a Cloud Spanner database. Completed backups for the database will be retained according to their - ``expire_time``. + ``expire_time``. Note: Cloud Spanner might continue to accept + requests for a few seconds after the database has been deleted. Returns: Callable[[~.DropDatabaseRequest], diff --git a/google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc_asyncio.py b/google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc_asyncio.py index 6a392183de..45ff3e166f 100644 --- a/google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc_asyncio.py +++ b/google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc_asyncio.py @@ -40,11 +40,13 @@ class DatabaseAdminGrpcAsyncIOTransport(DatabaseAdminTransport): """gRPC AsyncIO backend transport for DatabaseAdmin. Cloud Spanner Database Admin API - The Cloud Spanner Database Admin API can be used to create, - drop, and list databases. It also enables updating the schema of - pre-existing databases. It can be also used to create, delete - and list backups for a database and to restore from an existing - backup. + + The Cloud Spanner Database Admin API can be used to: + + - create, drop, and list databases + - update the schema of pre-existing databases + - create, delete and list backups for a database + - restore a database from an existing backup This class defines the same methods as the primary client, so the primary client can load the underlying transport implementation @@ -399,7 +401,8 @@ def drop_database( Drops (aka deletes) a Cloud Spanner database. Completed backups for the database will be retained according to their - ``expire_time``. + ``expire_time``. Note: Cloud Spanner might continue to accept + requests for a few seconds after the database has been deleted. Returns: Callable[[~.DropDatabaseRequest], diff --git a/google/cloud/spanner_admin_database_v1/types/__init__.py b/google/cloud/spanner_admin_database_v1/types/__init__.py index 1c31fe536e..f671adc0cf 100644 --- a/google/cloud/spanner_admin_database_v1/types/__init__.py +++ b/google/cloud/spanner_admin_database_v1/types/__init__.py @@ -31,6 +31,7 @@ EncryptionConfig, EncryptionInfo, OperationProgress, + DatabaseDialect, ) from .spanner_database_admin import ( CreateDatabaseMetadata, @@ -70,6 +71,7 @@ "EncryptionConfig", "EncryptionInfo", "OperationProgress", + "DatabaseDialect", "CreateDatabaseMetadata", "CreateDatabaseRequest", "Database", diff --git a/google/cloud/spanner_admin_database_v1/types/backup.py b/google/cloud/spanner_admin_database_v1/types/backup.py index 486503f344..c27a5a5f31 100644 --- a/google/cloud/spanner_admin_database_v1/types/backup.py +++ b/google/cloud/spanner_admin_database_v1/types/backup.py @@ -104,6 +104,9 @@ class Backup(proto.Message): encryption_info (google.cloud.spanner_admin_database_v1.types.EncryptionInfo): Output only. The encryption information for the backup. + database_dialect (google.cloud.spanner_admin_database_v1.types.DatabaseDialect): + Output only. The database dialect information + for the backup. """ class State(proto.Enum): @@ -125,6 +128,7 @@ class State(proto.Enum): encryption_info = proto.Field( proto.MESSAGE, number=8, message=common.EncryptionInfo, ) + database_dialect = proto.Field(proto.ENUM, number=10, enum=common.DatabaseDialect,) class CreateBackupRequest(proto.Message): diff --git a/google/cloud/spanner_admin_database_v1/types/common.py b/google/cloud/spanner_admin_database_v1/types/common.py index b0c47fdb66..81e3433617 100644 --- a/google/cloud/spanner_admin_database_v1/types/common.py +++ b/google/cloud/spanner_admin_database_v1/types/common.py @@ -21,10 +21,22 @@ __protobuf__ = proto.module( package="google.spanner.admin.database.v1", - manifest={"OperationProgress", "EncryptionConfig", "EncryptionInfo",}, + manifest={ + "DatabaseDialect", + "OperationProgress", + "EncryptionConfig", + "EncryptionInfo", + }, ) +class DatabaseDialect(proto.Enum): + r"""Indicates the dialect type of a database.""" + DATABASE_DIALECT_UNSPECIFIED = 0 + GOOGLE_STANDARD_SQL = 1 + POSTGRESQL = 2 + + class OperationProgress(proto.Message): r"""Encapsulates progress related information for a Cloud Spanner long running operation. diff --git a/google/cloud/spanner_admin_database_v1/types/spanner_database_admin.py b/google/cloud/spanner_admin_database_v1/types/spanner_database_admin.py index 210e46bb32..7b598b09d9 100644 --- a/google/cloud/spanner_admin_database_v1/types/spanner_database_admin.py +++ b/google/cloud/spanner_admin_database_v1/types/spanner_database_admin.py @@ -135,6 +135,9 @@ class Database(proto.Message): option set using DatabaseAdmin.CreateDatabase or DatabaseAdmin.UpdateDatabaseDdl. If not explicitly set, this is empty. + database_dialect (google.cloud.spanner_admin_database_v1.types.DatabaseDialect): + Output only. The dialect of the Cloud Spanner + Database. """ class State(proto.Enum): @@ -159,6 +162,7 @@ class State(proto.Enum): proto.MESSAGE, number=7, message=timestamp_pb2.Timestamp, ) default_leader = proto.Field(proto.STRING, number=9,) + database_dialect = proto.Field(proto.ENUM, number=10, enum=common.DatabaseDialect,) class ListDatabasesRequest(proto.Message): @@ -235,6 +239,9 @@ class CreateDatabaseRequest(proto.Message): the database. If this field is not specified, Cloud Spanner will encrypt/decrypt all data at rest using Google default encryption. + database_dialect (google.cloud.spanner_admin_database_v1.types.DatabaseDialect): + Optional. The dialect of the Cloud Spanner + Database. """ parent = proto.Field(proto.STRING, number=1,) @@ -243,6 +250,7 @@ class CreateDatabaseRequest(proto.Message): encryption_config = proto.Field( proto.MESSAGE, number=4, message=common.EncryptionConfig, ) + database_dialect = proto.Field(proto.ENUM, number=5, enum=common.DatabaseDialect,) class CreateDatabaseMetadata(proto.Message): diff --git a/google/cloud/spanner_admin_instance_v1/services/instance_admin/async_client.py b/google/cloud/spanner_admin_instance_v1/services/instance_admin/async_client.py index f82a01b016..2dd189b841 100644 --- a/google/cloud/spanner_admin_instance_v1/services/instance_admin/async_client.py +++ b/google/cloud/spanner_admin_instance_v1/services/instance_admin/async_client.py @@ -16,7 +16,7 @@ from collections import OrderedDict import functools import re -from typing import Dict, Sequence, Tuple, Type, Union +from typing import Dict, Optional, Sequence, Tuple, Type, Union import pkg_resources from google.api_core.client_options import ClientOptions @@ -136,6 +136,42 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): from_service_account_json = from_service_account_file + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + return InstanceAdminClient.get_mtls_endpoint_and_cert_source(client_options) # type: ignore + @property def transport(self) -> InstanceAdminTransport: """Returns the transport used by the client instance. diff --git a/google/cloud/spanner_admin_instance_v1/services/instance_admin/client.py b/google/cloud/spanner_admin_instance_v1/services/instance_admin/client.py index c89877dce5..b67ac50ffd 100644 --- a/google/cloud/spanner_admin_instance_v1/services/instance_admin/client.py +++ b/google/cloud/spanner_admin_instance_v1/services/instance_admin/client.py @@ -271,6 +271,73 @@ def parse_common_location_path(path: str) -> Dict[str, str]: m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) return m.groupdict() if m else {} + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[client_options_lib.ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + if client_options is None: + client_options = client_options_lib.ClientOptions() + use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") + if use_client_cert not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" + ) + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" + ) + + # Figure out the client cert source to use. + client_cert_source = None + if use_client_cert == "true": + if client_options.client_cert_source: + client_cert_source = client_options.client_cert_source + elif mtls.has_default_client_cert_source(): + client_cert_source = mtls.default_client_cert_source() + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + api_endpoint = cls.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = cls.DEFAULT_ENDPOINT + + return api_endpoint, client_cert_source + def __init__( self, *, @@ -321,57 +388,22 @@ def __init__( if client_options is None: client_options = client_options_lib.ClientOptions() - # Create SSL credentials for mutual TLS if needed. - if os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") not in ( - "true", - "false", - ): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - use_client_cert = ( - os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") == "true" + api_endpoint, client_cert_source_func = self.get_mtls_endpoint_and_cert_source( + client_options ) - client_cert_source_func = None - is_mtls = False - if use_client_cert: - if client_options.client_cert_source: - is_mtls = True - client_cert_source_func = client_options.client_cert_source - else: - is_mtls = mtls.has_default_client_cert_source() - if is_mtls: - client_cert_source_func = mtls.default_client_cert_source() - else: - client_cert_source_func = None - - # Figure out which api endpoint to use. - if client_options.api_endpoint is not None: - api_endpoint = client_options.api_endpoint - else: - use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") - if use_mtls_env == "never": - api_endpoint = self.DEFAULT_ENDPOINT - elif use_mtls_env == "always": - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - elif use_mtls_env == "auto": - if is_mtls: - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = self.DEFAULT_ENDPOINT - else: - raise MutualTLSChannelError( - "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted " - "values: never, auto, always" - ) + api_key_value = getattr(client_options, "api_key", None) + if api_key_value and credentials: + raise ValueError( + "client_options.api_key and credentials are mutually exclusive" + ) # Save or instantiate the transport. # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, InstanceAdminTransport): # transport is a InstanceAdminTransport instance. - if credentials or client_options.credentials_file: + if credentials or client_options.credentials_file or api_key_value: raise ValueError( "When providing a transport instance, " "provide its credentials directly." @@ -383,6 +415,15 @@ def __init__( ) self._transport = transport else: + 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 = type(self).get_transport_class(transport) self._transport = Transport( credentials=credentials, diff --git a/google/cloud/spanner_v1/services/spanner/async_client.py b/google/cloud/spanner_v1/services/spanner/async_client.py index eb59f009c2..4b7139c718 100644 --- a/google/cloud/spanner_v1/services/spanner/async_client.py +++ b/google/cloud/spanner_v1/services/spanner/async_client.py @@ -16,7 +16,16 @@ from collections import OrderedDict import functools import re -from typing import Dict, AsyncIterable, Awaitable, Sequence, Tuple, Type, Union +from typing import ( + Dict, + Optional, + AsyncIterable, + Awaitable, + Sequence, + Tuple, + Type, + Union, +) import pkg_resources from google.api_core.client_options import ClientOptions @@ -110,6 +119,42 @@ def from_service_account_file(cls, filename: str, *args, **kwargs): from_service_account_json = from_service_account_file + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + return SpannerClient.get_mtls_endpoint_and_cert_source(client_options) # type: ignore + @property def transport(self) -> SpannerTransport: """Returns the transport used by the client instance. diff --git a/google/cloud/spanner_v1/services/spanner/client.py b/google/cloud/spanner_v1/services/spanner/client.py index 8fb7064e40..845e8b8d9b 100644 --- a/google/cloud/spanner_v1/services/spanner/client.py +++ b/google/cloud/spanner_v1/services/spanner/client.py @@ -258,6 +258,73 @@ def parse_common_location_path(path: str) -> Dict[str, str]: m = re.match(r"^projects/(?P.+?)/locations/(?P.+?)$", path) return m.groupdict() if m else {} + @classmethod + def get_mtls_endpoint_and_cert_source( + cls, client_options: Optional[client_options_lib.ClientOptions] = None + ): + """Return the API endpoint and client cert source for mutual TLS. + + The client cert source is determined in the following order: + (1) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is not "true", the + client cert source is None. + (2) if `client_options.client_cert_source` is provided, use the provided one; if the + default client cert source exists, use the default one; otherwise the client cert + source is None. + + The API endpoint is determined in the following order: + (1) if `client_options.api_endpoint` if provided, use the provided one. + (2) if `GOOGLE_API_USE_CLIENT_CERTIFICATE` environment variable is "always", use the + default mTLS endpoint; if the environment variabel is "never", use the default API + endpoint; otherwise if client cert source exists, use the default mTLS endpoint, otherwise + use the default API endpoint. + + More details can be found at https://google.aip.dev/auth/4114. + + Args: + client_options (google.api_core.client_options.ClientOptions): Custom options for the + client. Only the `api_endpoint` and `client_cert_source` properties may be used + in this method. + + Returns: + Tuple[str, Callable[[], Tuple[bytes, bytes]]]: returns the API endpoint and the + client cert source to use. + + Raises: + google.auth.exceptions.MutualTLSChannelError: If any errors happen. + """ + if client_options is None: + client_options = client_options_lib.ClientOptions() + use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") + use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") + if use_client_cert not in ("true", "false"): + raise ValueError( + "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" + ) + if use_mtls_endpoint not in ("auto", "never", "always"): + raise MutualTLSChannelError( + "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" + ) + + # Figure out the client cert source to use. + client_cert_source = None + if use_client_cert == "true": + if client_options.client_cert_source: + client_cert_source = client_options.client_cert_source + elif mtls.has_default_client_cert_source(): + client_cert_source = mtls.default_client_cert_source() + + # Figure out which api endpoint to use. + if client_options.api_endpoint is not None: + api_endpoint = client_options.api_endpoint + elif use_mtls_endpoint == "always" or ( + use_mtls_endpoint == "auto" and client_cert_source + ): + api_endpoint = cls.DEFAULT_MTLS_ENDPOINT + else: + api_endpoint = cls.DEFAULT_ENDPOINT + + return api_endpoint, client_cert_source + def __init__( self, *, @@ -308,57 +375,22 @@ def __init__( if client_options is None: client_options = client_options_lib.ClientOptions() - # Create SSL credentials for mutual TLS if needed. - if os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") not in ( - "true", - "false", - ): - raise ValueError( - "Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`" - ) - use_client_cert = ( - os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false") == "true" + api_endpoint, client_cert_source_func = self.get_mtls_endpoint_and_cert_source( + client_options ) - client_cert_source_func = None - is_mtls = False - if use_client_cert: - if client_options.client_cert_source: - is_mtls = True - client_cert_source_func = client_options.client_cert_source - else: - is_mtls = mtls.has_default_client_cert_source() - if is_mtls: - client_cert_source_func = mtls.default_client_cert_source() - else: - client_cert_source_func = None - - # Figure out which api endpoint to use. - if client_options.api_endpoint is not None: - api_endpoint = client_options.api_endpoint - else: - use_mtls_env = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto") - if use_mtls_env == "never": - api_endpoint = self.DEFAULT_ENDPOINT - elif use_mtls_env == "always": - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - elif use_mtls_env == "auto": - if is_mtls: - api_endpoint = self.DEFAULT_MTLS_ENDPOINT - else: - api_endpoint = self.DEFAULT_ENDPOINT - else: - raise MutualTLSChannelError( - "Unsupported GOOGLE_API_USE_MTLS_ENDPOINT value. Accepted " - "values: never, auto, always" - ) + api_key_value = getattr(client_options, "api_key", None) + if api_key_value and credentials: + raise ValueError( + "client_options.api_key and credentials are mutually exclusive" + ) # Save or instantiate the transport. # Ordinarily, we provide the transport, but allowing a custom transport # instance provides an extensibility point for unusual situations. if isinstance(transport, SpannerTransport): # transport is a SpannerTransport instance. - if credentials or client_options.credentials_file: + if credentials or client_options.credentials_file or api_key_value: raise ValueError( "When providing a transport instance, " "provide its credentials directly." @@ -370,6 +402,15 @@ def __init__( ) self._transport = transport else: + 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 = type(self).get_transport_class(transport) self._transport = Transport( credentials=credentials, diff --git a/google/cloud/spanner_v1/types/__init__.py b/google/cloud/spanner_v1/types/__init__.py index 5f7bbfb8b1..01dde4208a 100644 --- a/google/cloud/spanner_v1/types/__init__.py +++ b/google/cloud/spanner_v1/types/__init__.py @@ -60,6 +60,7 @@ from .type import ( StructType, Type, + TypeAnnotationCode, TypeCode, ) @@ -100,5 +101,6 @@ "TransactionSelector", "StructType", "Type", + "TypeAnnotationCode", "TypeCode", ) diff --git a/google/cloud/spanner_v1/types/commit_response.py b/google/cloud/spanner_v1/types/commit_response.py index 1c9ccab0e8..e9a289f0ce 100644 --- a/google/cloud/spanner_v1/types/commit_response.py +++ b/google/cloud/spanner_v1/types/commit_response.py @@ -44,10 +44,10 @@ class CommitStats(proto.Message): number of mutations in a transaction and minimize the number of API round trips. You can also monitor this value to prevent transactions from exceeding the system - `limit `__. + `limit `__. If the number of mutations exceeds the limit, the server returns - `INVALID_ARGUMENT `__. + `INVALID_ARGUMENT `__. """ mutation_count = proto.Field(proto.INT64, number=1,) diff --git a/google/cloud/spanner_v1/types/spanner.py b/google/cloud/spanner_v1/types/spanner.py index 73a9af290b..494f88d7e7 100644 --- a/google/cloud/spanner_v1/types/spanner.py +++ b/google/cloud/spanner_v1/types/spanner.py @@ -242,18 +242,20 @@ class RequestOptions(proto.Message): characters for ``request_tag`` values are all printable characters (ASCII 32 - 126) and the length of a request_tag is limited to 50 characters. Values that exceed this limit - are truncated. + are truncated. Any leading underscore (_) characters will be + removed from the string. transaction_tag (str): A tag used for statistics collection about this transaction. Both request_tag and transaction_tag can be specified for a read or query that belongs to a transaction. The value of transaction_tag should be the same for all requests - belonging to the same transaction. If this request doesn’t + belonging to the same transaction. If this request doesn't belong to any transaction, transaction_tag will be ignored. Legal characters for ``transaction_tag`` values are all printable characters (ASCII 32 - 126) and the length of a transaction_tag is limited to 50 characters. Values that - exceed this limit are truncated. + exceed this limit are truncated. Any leading underscore (_) + characters will be removed from the string. """ class Priority(proto.Enum): diff --git a/google/cloud/spanner_v1/types/transaction.py b/google/cloud/spanner_v1/types/transaction.py index c295f16020..04b8552a48 100644 --- a/google/cloud/spanner_v1/types/transaction.py +++ b/google/cloud/spanner_v1/types/transaction.py @@ -27,6 +27,7 @@ class TransactionOptions(proto.Message): r"""Transactions: + Each session can have at most one active transaction at a time (note that standalone reads and queries use a transaction internally and do count towards the one transaction limit). After the active @@ -34,8 +35,7 @@ class TransactionOptions(proto.Message): the next transaction. It is not necessary to create a new session for each transaction. - Transaction Modes: - Cloud Spanner supports three transaction modes: + Transaction Modes: Cloud Spanner supports three transaction modes: 1. Locking read-write. This type of transaction is the only way to write data into Cloud Spanner. These transactions rely on @@ -66,10 +66,9 @@ class TransactionOptions(proto.Message): may, however, read/write data in different tables within that database. - Locking Read-Write Transactions: - Locking transactions may be used to atomically read-modify-write - data anywhere in a database. This type of transaction is externally - consistent. + Locking Read-Write Transactions: Locking transactions may be used to + atomically read-modify-write data anywhere in a database. This type + of transaction is externally consistent. Clients should attempt to minimize the amount of time a transaction is active. Faster transactions commit with higher probability and @@ -88,49 +87,48 @@ class TransactionOptions(proto.Message): [Rollback][google.spanner.v1.Spanner.Rollback] request to abort the transaction. - Semantics: - Cloud Spanner can commit the transaction if all read locks it - acquired are still valid at commit time, and it is able to acquire - write locks for all writes. Cloud Spanner can abort the transaction - for any reason. If a commit attempt returns ``ABORTED``, Cloud - Spanner guarantees that the transaction has not modified any user - data in Cloud Spanner. + Semantics: Cloud Spanner can commit the transaction if all read + locks it acquired are still valid at commit time, and it is able to + acquire write locks for all writes. Cloud Spanner can abort the + transaction for any reason. If a commit attempt returns ``ABORTED``, + Cloud Spanner guarantees that the transaction has not modified any + user data in Cloud Spanner. Unless the transaction commits, Cloud Spanner makes no guarantees about how long the transaction's locks were held for. It is an error to use Cloud Spanner locks for any sort of mutual exclusion other than between Cloud Spanner transactions themselves. - Retrying Aborted Transactions: - When a transaction aborts, the application can choose to retry the - whole transaction again. To maximize the chances of successfully - committing the retry, the client should execute the retry in the - same session as the original attempt. The original session's lock - priority increases with each consecutive abort, meaning that each - attempt has a slightly better chance of success than the previous. + Retrying Aborted Transactions: When a transaction aborts, the + application can choose to retry the whole transaction again. To + maximize the chances of successfully committing the retry, the + client should execute the retry in the same session as the original + attempt. The original session's lock priority increases with each + consecutive abort, meaning that each attempt has a slightly better + chance of success than the previous. - Under some circumstances (e.g., many transactions attempting to - modify the same row(s)), a transaction can abort many times in a + Under some circumstances (for example, many transactions attempting + to modify the same row(s)), a transaction can abort many times in a short period before successfully committing. Thus, it is not a good idea to cap the number of retries a transaction can attempt; - instead, it is better to limit the total amount of wall time spent + instead, it is better to limit the total amount of time spent retrying. - Idle Transactions: - A transaction is considered idle if it has no outstanding reads or - SQL queries and has not started a read or SQL query within the last - 10 seconds. Idle transactions can be aborted by Cloud Spanner so - that they don't hold on to locks indefinitely. In that case, the - commit will fail with error ``ABORTED``. + Idle Transactions: A transaction is considered idle if it has no + outstanding reads or SQL queries and has not started a read or SQL + query within the last 10 seconds. Idle transactions can be aborted + by Cloud Spanner so that they don't hold on to locks indefinitely. + If an idle transaction is aborted, the commit will fail with error + ``ABORTED``. If this behavior is undesirable, periodically executing a simple SQL - query in the transaction (e.g., ``SELECT 1``) prevents the + query in the transaction (for example, ``SELECT 1``) prevents the transaction from becoming idle. - Snapshot Read-Only Transactions: - Snapshot read-only transactions provides a simpler method than - locking read-write transactions for doing several consistent reads. - However, this type of transaction does not support writes. + Snapshot Read-Only Transactions: Snapshot read-only transactions + provides a simpler method than locking read-write transactions for + doing several consistent reads. However, this type of transaction + does not support writes. Snapshot transactions do not take locks. Instead, they work by choosing a Cloud Spanner timestamp, then executing all reads at that @@ -164,12 +162,11 @@ class TransactionOptions(proto.Message): Each type of timestamp bound is discussed in detail below. - Strong: - Strong reads are guaranteed to see the effects of all transactions - that have committed before the start of the read. Furthermore, all - rows yielded by a single read are consistent with each other -- if - any part of the read observes a transaction, all parts of the read - see the transaction. + Strong: Strong reads are guaranteed to see the effects of all + transactions that have committed before the start of the read. + Furthermore, all rows yielded by a single read are consistent with + each other -- if any part of the read observes a transaction, all + parts of the read see the transaction. Strong reads are not repeatable: two consecutive strong read-only transactions might return inconsistent results if there are @@ -180,15 +177,14 @@ class TransactionOptions(proto.Message): See [TransactionOptions.ReadOnly.strong][google.spanner.v1.TransactionOptions.ReadOnly.strong]. - Exact Staleness: - These timestamp bounds execute reads at a user-specified timestamp. - Reads at a timestamp are guaranteed to see a consistent prefix of - the global transaction history: they observe modifications done by - all transactions with a commit timestamp <= the read timestamp, and - observe none of the modifications done by transactions with a larger - commit timestamp. They will block until all conflicting transactions - that may be assigned commit timestamps <= the read timestamp have - finished. + Exact Staleness: These timestamp bounds execute reads at a + user-specified timestamp. Reads at a timestamp are guaranteed to see + a consistent prefix of the global transaction history: they observe + modifications done by all transactions with a commit timestamp less + than or equal to the read timestamp, and observe none of the + modifications done by transactions with a larger commit timestamp. + They will block until all conflicting transactions that may be + assigned commit timestamps <= the read timestamp have finished. The timestamp can either be expressed as an absolute Cloud Spanner commit timestamp or a staleness relative to the current time. @@ -203,12 +199,11 @@ class TransactionOptions(proto.Message): and [TransactionOptions.ReadOnly.exact_staleness][google.spanner.v1.TransactionOptions.ReadOnly.exact_staleness]. - Bounded Staleness: - Bounded staleness modes allow Cloud Spanner to pick the read - timestamp, subject to a user-provided staleness bound. Cloud Spanner - chooses the newest timestamp within the staleness bound that allows - execution of the reads at the closest available replica without - blocking. + Bounded Staleness: Bounded staleness modes allow Cloud Spanner to + pick the read timestamp, subject to a user-provided staleness bound. + Cloud Spanner chooses the newest timestamp within the staleness + bound that allows execution of the reads at the closest available + replica without blocking. All rows yielded are consistent with each other -- if any part of the read observes a transaction, all parts of the read see the @@ -234,23 +229,23 @@ class TransactionOptions(proto.Message): and [TransactionOptions.ReadOnly.min_read_timestamp][google.spanner.v1.TransactionOptions.ReadOnly.min_read_timestamp]. - Old Read Timestamps and Garbage Collection: - Cloud Spanner continuously garbage collects deleted and overwritten - data in the background to reclaim storage space. This process is - known as "version GC". By default, version GC reclaims versions - after they are one hour old. Because of this, Cloud Spanner cannot - perform reads at read timestamps more than one hour in the past. - This restriction also applies to in-progress reads and/or SQL - queries whose timestamp become too old while executing. Reads and - SQL queries with too-old read timestamps fail with the error + Old Read Timestamps and Garbage Collection: Cloud Spanner + continuously garbage collects deleted and overwritten data in the + background to reclaim storage space. This process is known as + "version GC". By default, version GC reclaims versions after they + are one hour old. Because of this, Cloud Spanner cannot perform + reads at read timestamps more than one hour in the past. This + restriction also applies to in-progress reads and/or SQL queries + whose timestamp become too old while executing. Reads and SQL + queries with too-old read timestamps fail with the error ``FAILED_PRECONDITION``. - Partitioned DML Transactions: - Partitioned DML transactions are used to execute DML statements with - a different execution strategy that provides different, and often - better, scalability properties for large, table-wide operations than - DML in a ReadWrite transaction. Smaller scoped statements, such as - an OLTP workload, should prefer using ReadWrite transactions. + Partitioned DML Transactions: Partitioned DML transactions are used + to execute DML statements with a different execution strategy that + provides different, and often better, scalability properties for + large, table-wide operations than DML in a ReadWrite transaction. + Smaller scoped statements, such as an OLTP workload, should prefer + using ReadWrite transactions. Partitioned DML partitions the keyspace and runs the DML statement on each partition in separate, internal transactions. These diff --git a/google/cloud/spanner_v1/types/type.py b/google/cloud/spanner_v1/types/type.py index 2c00626c7a..5673fcb77d 100644 --- a/google/cloud/spanner_v1/types/type.py +++ b/google/cloud/spanner_v1/types/type.py @@ -17,7 +17,8 @@ __protobuf__ = proto.module( - package="google.spanner.v1", manifest={"TypeCode", "Type", "StructType",}, + package="google.spanner.v1", + manifest={"TypeCode", "TypeAnnotationCode", "Type", "StructType",}, ) @@ -44,6 +45,18 @@ class TypeCode(proto.Enum): JSON = 11 +class TypeAnnotationCode(proto.Enum): + r"""``TypeAnnotationCode`` is used as a part of + [Type][google.spanner.v1.Type] to disambiguate SQL types that should + be used for a given Cloud Spanner value. Disambiguation is needed + because the same Cloud Spanner type can be mapped to different SQL + types depending on SQL dialect. TypeAnnotationCode doesn't affect + the way value is serialized. + """ + TYPE_ANNOTATION_CODE_UNSPECIFIED = 0 + PG_NUMERIC = 2 + + class Type(proto.Message): r"""``Type`` indicates the type of a Cloud Spanner value, as might be stored in a table cell or returned from an SQL query. @@ -61,11 +74,24 @@ class Type(proto.Message): [STRUCT][google.spanner.v1.TypeCode.STRUCT], then ``struct_type`` provides type information for the struct's fields. + type_annotation (google.cloud.spanner_v1.types.TypeAnnotationCode): + The + [TypeAnnotationCode][google.spanner.v1.TypeAnnotationCode] + that disambiguates SQL type that Spanner will use to + represent values of this type during query processing. This + is necessary for some type codes because a single + [TypeCode][google.spanner.v1.TypeCode] can be mapped to + different SQL types depending on the SQL dialect. + [type_annotation][google.spanner.v1.Type.type_annotation] + typically is not needed to process the content of a value + (it doesn't affect serialization) and clients can ignore it + on the read path. """ code = proto.Field(proto.ENUM, number=1, enum="TypeCode",) array_element_type = proto.Field(proto.MESSAGE, number=2, message="Type",) struct_type = proto.Field(proto.MESSAGE, number=3, message="StructType",) + type_annotation = proto.Field(proto.ENUM, number=4, enum="TypeAnnotationCode",) class StructType(proto.Message): diff --git a/scripts/fixup_spanner_admin_database_v1_keywords.py b/scripts/fixup_spanner_admin_database_v1_keywords.py index cc4c78d884..9ac9f80702 100644 --- a/scripts/fixup_spanner_admin_database_v1_keywords.py +++ b/scripts/fixup_spanner_admin_database_v1_keywords.py @@ -40,7 +40,7 @@ class spanner_admin_databaseCallTransformer(cst.CSTTransformer): CTRL_PARAMS: Tuple[str] = ('retry', 'timeout', 'metadata') METHOD_TO_PARAMS: Dict[str, Tuple[str]] = { 'create_backup': ('parent', 'backup_id', 'backup', 'encryption_config', ), - 'create_database': ('parent', 'create_statement', 'extra_statements', 'encryption_config', ), + 'create_database': ('parent', 'create_statement', 'extra_statements', 'encryption_config', 'database_dialect', ), 'delete_backup': ('name', ), 'drop_database': ('database', ), 'get_backup': ('name', ), diff --git a/tests/unit/gapic/spanner_admin_database_v1/test_database_admin.py b/tests/unit/gapic/spanner_admin_database_v1/test_database_admin.py index 53f91de384..83ab11e870 100644 --- a/tests/unit/gapic/spanner_admin_database_v1/test_database_admin.py +++ b/tests/unit/gapic/spanner_admin_database_v1/test_database_admin.py @@ -415,6 +415,87 @@ def test_database_admin_client_mtls_env_auto( ) +@pytest.mark.parametrize( + "client_class", [DatabaseAdminClient, DatabaseAdminAsyncClient] +) +@mock.patch.object( + DatabaseAdminClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(DatabaseAdminClient), +) +@mock.patch.object( + DatabaseAdminAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(DatabaseAdminAsyncClient), +) +def test_database_admin_client_get_mtls_endpoint_and_cert_source(client_class): + mock_client_cert_source = mock.Mock() + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "true". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source == mock_client_cert_source + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "false". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"}): + mock_client_cert_source = mock.Mock() + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "never". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "always". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert doesn't exist. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=False, + ): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert exists. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=mock_client_cert_source, + ): + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source == mock_client_cert_source + + @pytest.mark.parametrize( "client_class,transport_class,transport_name", [ @@ -1103,6 +1184,7 @@ def test_get_database(request_type, transport: str = "grpc"): state=spanner_database_admin.Database.State.CREATING, version_retention_period="version_retention_period_value", default_leader="default_leader_value", + database_dialect=common.DatabaseDialect.GOOGLE_STANDARD_SQL, ) response = client.get_database(request) @@ -1117,6 +1199,7 @@ def test_get_database(request_type, transport: str = "grpc"): assert response.state == spanner_database_admin.Database.State.CREATING assert response.version_retention_period == "version_retention_period_value" assert response.default_leader == "default_leader_value" + assert response.database_dialect == common.DatabaseDialect.GOOGLE_STANDARD_SQL def test_get_database_empty_call(): @@ -1156,6 +1239,7 @@ async def test_get_database_async( state=spanner_database_admin.Database.State.CREATING, version_retention_period="version_retention_period_value", default_leader="default_leader_value", + database_dialect=common.DatabaseDialect.GOOGLE_STANDARD_SQL, ) ) response = await client.get_database(request) @@ -1171,6 +1255,7 @@ async def test_get_database_async( assert response.state == spanner_database_admin.Database.State.CREATING assert response.version_retention_period == "version_retention_period_value" assert response.default_leader == "default_leader_value" + assert response.database_dialect == common.DatabaseDialect.GOOGLE_STANDARD_SQL @pytest.mark.asyncio @@ -2862,6 +2947,7 @@ def test_get_backup(request_type, transport: str = "grpc"): size_bytes=1089, state=backup.Backup.State.CREATING, referencing_databases=["referencing_databases_value"], + database_dialect=common.DatabaseDialect.GOOGLE_STANDARD_SQL, ) response = client.get_backup(request) @@ -2877,6 +2963,7 @@ def test_get_backup(request_type, transport: str = "grpc"): assert response.size_bytes == 1089 assert response.state == backup.Backup.State.CREATING assert response.referencing_databases == ["referencing_databases_value"] + assert response.database_dialect == common.DatabaseDialect.GOOGLE_STANDARD_SQL def test_get_backup_empty_call(): @@ -2916,6 +3003,7 @@ async def test_get_backup_async( size_bytes=1089, state=backup.Backup.State.CREATING, referencing_databases=["referencing_databases_value"], + database_dialect=common.DatabaseDialect.GOOGLE_STANDARD_SQL, ) ) response = await client.get_backup(request) @@ -2932,6 +3020,7 @@ async def test_get_backup_async( assert response.size_bytes == 1089 assert response.state == backup.Backup.State.CREATING assert response.referencing_databases == ["referencing_databases_value"] + assert response.database_dialect == common.DatabaseDialect.GOOGLE_STANDARD_SQL @pytest.mark.asyncio @@ -3079,6 +3168,7 @@ def test_update_backup(request_type, transport: str = "grpc"): size_bytes=1089, state=gsad_backup.Backup.State.CREATING, referencing_databases=["referencing_databases_value"], + database_dialect=common.DatabaseDialect.GOOGLE_STANDARD_SQL, ) response = client.update_backup(request) @@ -3094,6 +3184,7 @@ def test_update_backup(request_type, transport: str = "grpc"): assert response.size_bytes == 1089 assert response.state == gsad_backup.Backup.State.CREATING assert response.referencing_databases == ["referencing_databases_value"] + assert response.database_dialect == common.DatabaseDialect.GOOGLE_STANDARD_SQL def test_update_backup_empty_call(): @@ -3133,6 +3224,7 @@ async def test_update_backup_async( size_bytes=1089, state=gsad_backup.Backup.State.CREATING, referencing_databases=["referencing_databases_value"], + database_dialect=common.DatabaseDialect.GOOGLE_STANDARD_SQL, ) ) response = await client.update_backup(request) @@ -3149,6 +3241,7 @@ async def test_update_backup_async( assert response.size_bytes == 1089 assert response.state == gsad_backup.Backup.State.CREATING assert response.referencing_databases == ["referencing_databases_value"] + assert response.database_dialect == common.DatabaseDialect.GOOGLE_STANDARD_SQL @pytest.mark.asyncio @@ -4802,6 +4895,23 @@ def test_credentials_transport_error(): transport=transport, ) + # It is an error to provide an api_key and a transport instance. + transport = transports.DatabaseAdminGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + options = client_options.ClientOptions() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = DatabaseAdminClient(client_options=options, transport=transport,) + + # It is an error to provide an api_key and a credential. + options = mock.Mock() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = DatabaseAdminClient( + client_options=options, credentials=ga_credentials.AnonymousCredentials() + ) + # It is an error to provide scopes and a transport instance. transport = transports.DatabaseAdminGrpcTransport( credentials=ga_credentials.AnonymousCredentials(), @@ -5526,3 +5636,33 @@ def test_client_ctx(): with client: pass close.assert_called() + + +@pytest.mark.parametrize( + "client_class,transport_class", + [ + (DatabaseAdminClient, transports.DatabaseAdminGrpcTransport), + (DatabaseAdminAsyncClient, transports.DatabaseAdminGrpcAsyncIOTransport), + ], +) +def test_api_key_credentials(client_class, transport_class): + with mock.patch.object( + google.auth._default, "get_api_key_credentials", create=True + ) as get_api_key_credentials: + mock_cred = mock.Mock() + get_api_key_credentials.return_value = mock_cred + options = client_options.ClientOptions() + options.api_key = "api_key" + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=mock_cred, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) diff --git a/tests/unit/gapic/spanner_admin_instance_v1/test_instance_admin.py b/tests/unit/gapic/spanner_admin_instance_v1/test_instance_admin.py index e6835d7a3b..7a6d7f5d1f 100644 --- a/tests/unit/gapic/spanner_admin_instance_v1/test_instance_admin.py +++ b/tests/unit/gapic/spanner_admin_instance_v1/test_instance_admin.py @@ -408,6 +408,87 @@ def test_instance_admin_client_mtls_env_auto( ) +@pytest.mark.parametrize( + "client_class", [InstanceAdminClient, InstanceAdminAsyncClient] +) +@mock.patch.object( + InstanceAdminClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(InstanceAdminClient), +) +@mock.patch.object( + InstanceAdminAsyncClient, + "DEFAULT_ENDPOINT", + modify_default_endpoint(InstanceAdminAsyncClient), +) +def test_instance_admin_client_get_mtls_endpoint_and_cert_source(client_class): + mock_client_cert_source = mock.Mock() + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "true". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source == mock_client_cert_source + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "false". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"}): + mock_client_cert_source = mock.Mock() + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "never". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "always". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert doesn't exist. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=False, + ): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert exists. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=mock_client_cert_source, + ): + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source == mock_client_cert_source + + @pytest.mark.parametrize( "client_class,transport_class,transport_name", [ @@ -3069,6 +3150,23 @@ def test_credentials_transport_error(): transport=transport, ) + # It is an error to provide an api_key and a transport instance. + transport = transports.InstanceAdminGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + options = client_options.ClientOptions() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = InstanceAdminClient(client_options=options, transport=transport,) + + # It is an error to provide an api_key and a credential. + options = mock.Mock() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = InstanceAdminClient( + client_options=options, credentials=ga_credentials.AnonymousCredentials() + ) + # It is an error to provide scopes and a transport instance. transport = transports.InstanceAdminGrpcTransport( credentials=ga_credentials.AnonymousCredentials(), @@ -3698,3 +3796,33 @@ def test_client_ctx(): with client: pass close.assert_called() + + +@pytest.mark.parametrize( + "client_class,transport_class", + [ + (InstanceAdminClient, transports.InstanceAdminGrpcTransport), + (InstanceAdminAsyncClient, transports.InstanceAdminGrpcAsyncIOTransport), + ], +) +def test_api_key_credentials(client_class, transport_class): + with mock.patch.object( + google.auth._default, "get_api_key_credentials", create=True + ) as get_api_key_credentials: + mock_cred = mock.Mock() + get_api_key_credentials.return_value = mock_cred + options = client_options.ClientOptions() + options.api_key = "api_key" + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=mock_cred, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + ) diff --git a/tests/unit/gapic/spanner_v1/test_spanner.py b/tests/unit/gapic/spanner_v1/test_spanner.py index 401b56d752..c767af43e8 100644 --- a/tests/unit/gapic/spanner_v1/test_spanner.py +++ b/tests/unit/gapic/spanner_v1/test_spanner.py @@ -379,6 +379,81 @@ def test_spanner_client_mtls_env_auto( ) +@pytest.mark.parametrize("client_class", [SpannerClient, SpannerAsyncClient]) +@mock.patch.object( + SpannerClient, "DEFAULT_ENDPOINT", modify_default_endpoint(SpannerClient) +) +@mock.patch.object( + SpannerAsyncClient, "DEFAULT_ENDPOINT", modify_default_endpoint(SpannerAsyncClient) +) +def test_spanner_client_get_mtls_endpoint_and_cert_source(client_class): + mock_client_cert_source = mock.Mock() + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "true". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source == mock_client_cert_source + + # Test the case GOOGLE_API_USE_CLIENT_CERTIFICATE is "false". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "false"}): + mock_client_cert_source = mock.Mock() + mock_api_endpoint = "foo" + options = client_options.ClientOptions( + client_cert_source=mock_client_cert_source, api_endpoint=mock_api_endpoint + ) + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source( + options + ) + assert api_endpoint == mock_api_endpoint + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "never". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "never"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "always". + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_MTLS_ENDPOINT": "always"}): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert doesn't exist. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=False, + ): + api_endpoint, cert_source = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_ENDPOINT + assert cert_source is None + + # Test the case GOOGLE_API_USE_MTLS_ENDPOINT is "auto" and default cert exists. + with mock.patch.dict(os.environ, {"GOOGLE_API_USE_CLIENT_CERTIFICATE": "true"}): + with mock.patch( + "google.auth.transport.mtls.has_default_client_cert_source", + return_value=True, + ): + with mock.patch( + "google.auth.transport.mtls.default_client_cert_source", + return_value=mock_client_cert_source, + ): + ( + api_endpoint, + cert_source, + ) = client_class.get_mtls_endpoint_and_cert_source() + assert api_endpoint == client_class.DEFAULT_MTLS_ENDPOINT + assert cert_source == mock_client_cert_source + + @pytest.mark.parametrize( "client_class,transport_class,transport_name", [ @@ -3148,6 +3223,23 @@ def test_credentials_transport_error(): transport=transport, ) + # It is an error to provide an api_key and a transport instance. + transport = transports.SpannerGrpcTransport( + credentials=ga_credentials.AnonymousCredentials(), + ) + options = client_options.ClientOptions() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = SpannerClient(client_options=options, transport=transport,) + + # It is an error to provide an api_key and a credential. + options = mock.Mock() + options.api_key = "api_key" + with pytest.raises(ValueError): + client = SpannerClient( + client_options=options, credentials=ga_credentials.AnonymousCredentials() + ) + # It is an error to provide scopes and a transport instance. transport = transports.SpannerGrpcTransport( credentials=ga_credentials.AnonymousCredentials(), @@ -3742,3 +3834,33 @@ def test_client_ctx(): with client: pass close.assert_called() + + +@pytest.mark.parametrize( + "client_class,transport_class", + [ + (SpannerClient, transports.SpannerGrpcTransport), + (SpannerAsyncClient, transports.SpannerGrpcAsyncIOTransport), + ], +) +def test_api_key_credentials(client_class, transport_class): + with mock.patch.object( + google.auth._default, "get_api_key_credentials", create=True + ) as get_api_key_credentials: + mock_cred = mock.Mock() + get_api_key_credentials.return_value = mock_cred + options = client_options.ClientOptions() + options.api_key = "api_key" + with mock.patch.object(transport_class, "__init__") as patched: + patched.return_value = None + client = client_class(client_options=options) + patched.assert_called_once_with( + credentials=mock_cred, + credentials_file=None, + host=client.DEFAULT_ENDPOINT, + scopes=None, + client_cert_source_for_mtls=None, + quota_project_id=None, + client_info=transports.base.DEFAULT_CLIENT_INFO, + always_use_jwt_access=True, + )