Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions google/cloud/spanner_dbapi/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""DB-API Connection for the Google Cloud Spanner."""
import warnings

from google.api_core.client_options import ClientOptions
from google.api_core.exceptions import Aborted
from google.api_core.gapic_v1.client_info import ClientInfo
from google.auth.credentials import AnonymousCredentials
Expand Down Expand Up @@ -734,6 +735,7 @@ def connect(
client=None,
route_to_leader_enabled=True,
database_role=None,
experimental_host=None,
**kwargs,
):
"""Creates a connection to a Google Cloud Spanner database.
Expand Down Expand Up @@ -805,6 +807,10 @@ def connect(
client_options = None
if isinstance(credentials, AnonymousCredentials):
client_options = kwargs.get("client_options")
if experimental_host is not None:
project = "default"
credentials = AnonymousCredentials()
client_options = ClientOptions(api_endpoint=experimental_host)
client = spanner.Client(
project=project,
credentials=credentials,
Expand Down
28 changes: 28 additions & 0 deletions google/cloud/spanner_v1/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ class Client(ClientWithProject):
or :class:`dict`
:param default_transaction_options: (Optional) Default options to use for all transactions.

:type experimental_host: str
:param experimental_host: (Optional) The endpoint for a spanner experimental host deployment.
This is intended only for experimental host spanner endpoints.
If set, this will override the `api_endpoint` in `client_options`.

:raises: :class:`ValueError <exceptions.ValueError>` if both ``read_only``
and ``admin`` are :data:`True`
"""
Expand All @@ -200,8 +205,10 @@ def __init__(
directed_read_options=None,
observability_options=None,
default_transaction_options: Optional[DefaultTransactionOptions] = None,
experimental_host=None,
):
self._emulator_host = _get_spanner_emulator_host()
self._experimental_host = experimental_host

if client_options and type(client_options) is dict:
self._client_options = google.api_core.client_options.from_dict(
Expand All @@ -212,6 +219,8 @@ def __init__(

if self._emulator_host:
credentials = AnonymousCredentials()
elif self._experimental_host:
credentials = AnonymousCredentials()
elif isinstance(credentials, AnonymousCredentials):
self._emulator_host = self._client_options.api_endpoint

Expand Down Expand Up @@ -324,6 +333,15 @@ def instance_admin_api(self):
client_options=self._client_options,
transport=transport,
)
elif self._experimental_host:
transport = InstanceAdminGrpcTransport(
channel=grpc.insecure_channel(target=self._experimental_host)
)
self._instance_admin_api = InstanceAdminClient(
client_info=self._client_info,
client_options=self._client_options,
transport=transport,
)
else:
self._instance_admin_api = InstanceAdminClient(
credentials=self.credentials,
Expand All @@ -345,6 +363,15 @@ def database_admin_api(self):
client_options=self._client_options,
transport=transport,
)
elif self._experimental_host:
transport = DatabaseAdminGrpcTransport(
channel=grpc.insecure_channel(target=self._experimental_host)
)
self._database_admin_api = DatabaseAdminClient(
client_info=self._client_info,
client_options=self._client_options,
transport=transport,
)
else:
self._database_admin_api = DatabaseAdminClient(
credentials=self.credentials,
Expand Down Expand Up @@ -485,6 +512,7 @@ def instance(
self._emulator_host,
labels,
processing_units,
self._experimental_host,
)

def list_instances(self, filter_="", page_size=None):
Expand Down
15 changes: 14 additions & 1 deletion google/cloud/spanner_v1/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,11 @@ def __init__(

self._pool = pool
pool.bind(self)
is_experimental_host = self._instance.experimental_host is not None

self._sessions_manager = DatabaseSessionsManager(self, pool)
self._sessions_manager = DatabaseSessionsManager(
self, pool, is_experimental_host
)

@classmethod
def from_pb(cls, database_pb, instance, pool=None):
Expand Down Expand Up @@ -449,6 +452,16 @@ def spanner_api(self):
client_info=client_info, transport=transport
)
return self._spanner_api
if self._instance.experimental_host is not None:
transport = SpannerGrpcTransport(
channel=grpc.insecure_channel(self._instance.experimental_host)
)
self._spanner_api = SpannerClient(
client_info=client_info,
transport=transport,
client_options=client_options,
)
return self._spanner_api
credentials = self._instance._client.credentials
if isinstance(credentials, google.auth.credentials.Scoped):
credentials = credentials.with_scopes((SPANNER_DATA_SCOPE,))
Expand Down
5 changes: 3 additions & 2 deletions google/cloud/spanner_v1/database_sessions_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@ class DatabaseSessionsManager(object):
_MAINTENANCE_THREAD_POLLING_INTERVAL = timedelta(minutes=10)
_MAINTENANCE_THREAD_REFRESH_INTERVAL = timedelta(days=7)

def __init__(self, database, pool):
def __init__(self, database, pool, is_experimental_host: bool = False):
self._database = database
self._pool = pool
self._is_experimental_host = is_experimental_host
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep the name consistent everywhere experimental_host or _experimental_host


# Declare multiplexed session attributes. When a multiplexed session for the
# database session manager is created, a maintenance thread is initialized to
Expand All @@ -88,7 +89,7 @@ def get_session(self, transaction_type: TransactionType) -> Session:

session = (
self._get_multiplexed_session()
if self._use_multiplexed(transaction_type)
if self._use_multiplexed(transaction_type) or self._is_experimental_host
else self._pool.get()
)

Expand Down
2 changes: 2 additions & 0 deletions google/cloud/spanner_v1/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def __init__(
emulator_host=None,
labels=None,
processing_units=None,
experimental_host=None,
):
self.instance_id = instance_id
self._client = client
Expand All @@ -142,6 +143,7 @@ def __init__(
self._node_count = processing_units // PROCESSING_UNITS_PER_NODE
self.display_name = display_name or instance_id
self.emulator_host = emulator_host
self.experimental_host = experimental_host
if labels is None:
labels = {}
self.labels = labels
Expand Down
12 changes: 12 additions & 0 deletions google/cloud/spanner_v1/testing/database_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,18 @@ def spanner_api(self):
transport=transport,
)
return self._spanner_api
if self._instance.experimental_host is not None:
channel = grpc.insecure_channel(self._instance.experimental_host)
self._x_goog_request_id_interceptor = XGoogRequestIDHeaderInterceptor()
self._interceptors.append(self._x_goog_request_id_interceptor)
channel = grpc.intercept_channel(channel, *self._interceptors)
transport = SpannerGrpcTransport(channel=channel)
self._spanner_api = SpannerClient(
client_info=client_info,
transport=transport,
client_options=client_options,
)
return self._spanner_api
credentials = client.credentials
if isinstance(credentials, google.auth.credentials.Scoped):
credentials = credentials.with_scopes((SPANNER_DATA_SCOPE,))
Expand Down
6 changes: 6 additions & 0 deletions tests/system/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@
EMULATOR_PROJECT_DEFAULT = "emulator-test-project"
EMULATOR_PROJECT = os.getenv(EMULATOR_PROJECT_ENVVAR, EMULATOR_PROJECT_DEFAULT)

USE_EXPERIMENTAL_HOST_ENVVAR = "SPANNER_EXPERIMENTAL_HOST"
EXPERIMENTAL_HOST = os.getenv(USE_EXPERIMENTAL_HOST_ENVVAR)
USE_EXPERIMENTAL_HOST = EXPERIMENTAL_HOST is not None

EXPERIMENTAL_HOST_PROJECT = "default"
EXPERIMENTAL_HOST_INSTANCE = "default"

DDL_STATEMENTS = (
_fixtures.PG_DDL_STATEMENTS
Expand Down
20 changes: 18 additions & 2 deletions tests/system/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ def not_emulator():
pytest.skip(f"{_helpers.USE_EMULATOR_ENVVAR} set in environment.")


@pytest.fixture(scope="module")
def not_experimental_host():
if _helpers.USE_EXPERIMENTAL_HOST:
pytest.skip(f"{_helpers.USE_EXPERIMENTAL_HOST_ENVVAR} set in environment.")


@pytest.fixture(scope="session")
def not_postgres(database_dialect):
if database_dialect == DatabaseDialect.POSTGRESQL:
Expand Down Expand Up @@ -104,6 +110,15 @@ def spanner_client():
project=_helpers.EMULATOR_PROJECT,
credentials=credentials,
)
elif _helpers.USE_EXPERIMENTAL_HOST:
from google.auth.credentials import AnonymousCredentials

credentials = AnonymousCredentials()
return spanner_v1.Client(
project=_helpers.EXPERIMENTAL_HOST_PROJECT,
credentials=credentials,
experimental_host=_helpers.EXPERIMENTAL_HOST,
)
else:
client_options = {"api_endpoint": _helpers.API_ENDPOINT}
return spanner_v1.Client(
Expand All @@ -130,15 +145,16 @@ def backup_operation_timeout():
def shared_instance_id():
if _helpers.CREATE_INSTANCE:
return f"{_helpers.unique_id('google-cloud')}"

if _helpers.USE_EXPERIMENTAL_HOST:
return _helpers.EXPERIMENTAL_HOST_INSTANCE
return _helpers.INSTANCE_ID


@pytest.fixture(scope="session")
def instance_configs(spanner_client):
configs = list(_helpers.retry_503(spanner_client.list_instance_configs)())

if not _helpers.USE_EMULATOR:
if not _helpers.USE_EMULATOR and not _helpers.USE_EXPERIMENTAL_HOST:
# Defend against back-end returning configs for regions we aren't
# actually allowed to use.
configs = [config for config in configs if "-us-" in config.name]
Expand Down
6 changes: 6 additions & 0 deletions tests/system/test_backup_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@
Remove {_helpers.SKIP_BACKUP_TESTS_ENVVAR} from environment to run these tests.\
"""
skip_emulator_reason = "Backup operations not supported by emulator."
skip_experimental_host_reason = (
"Backup operations not supported on experimental host yet."
)

pytestmark = [
pytest.mark.skipif(_helpers.SKIP_BACKUP_TESTS, reason=skip_env_reason),
pytest.mark.skipif(_helpers.USE_EMULATOR, reason=skip_emulator_reason),
pytest.mark.skipif(
_helpers.USE_EXPERIMENTAL_HOST, reason=skip_experimental_host_reason
),
]


Expand Down
17 changes: 15 additions & 2 deletions tests/system/test_database_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@


@pytest.fixture(scope="module")
def multiregion_instance(spanner_client, instance_operation_timeout, not_postgres):
def multiregion_instance(
spanner_client, instance_operation_timeout, not_postgres, not_experimental_host
):
multi_region_instance_id = _helpers.unique_id("multi-region")
multi_region_config = "nam3"
config_name = "{}/instanceConfigs/{}".format(
Expand Down Expand Up @@ -97,6 +99,7 @@ def test_database_binding_of_fixed_size_pool(
databases_to_delete,
not_postgres,
proto_descriptor_file,
not_experimental_host,
):
temp_db_id = _helpers.unique_id("fixed_size_db", separator="_")
temp_db = shared_instance.database(temp_db_id)
Expand Down Expand Up @@ -130,6 +133,7 @@ def test_database_binding_of_pinging_pool(
databases_to_delete,
not_postgres,
proto_descriptor_file,
not_experimental_host,
):
temp_db_id = _helpers.unique_id("binding_db", separator="_")
temp_db = shared_instance.database(temp_db_id)
Expand Down Expand Up @@ -217,6 +221,7 @@ def test_create_database_pitr_success(
def test_create_database_with_default_leader_success(
not_emulator, # Default leader setting not supported by the emulator
not_postgres,
not_experimental_host,
multiregion_instance,
databases_to_delete,
):
Expand Down Expand Up @@ -253,6 +258,7 @@ def test_create_database_with_default_leader_success(

def test_iam_policy(
not_emulator,
not_experimental_host,
shared_instance,
databases_to_delete,
):
Expand Down Expand Up @@ -414,6 +420,7 @@ def test_update_ddl_w_pitr_success(
def test_update_ddl_w_default_leader_success(
not_emulator,
not_postgres,
not_experimental_host,
multiregion_instance,
databases_to_delete,
proto_descriptor_file,
Expand Down Expand Up @@ -448,6 +455,7 @@ def test_update_ddl_w_default_leader_success(

def test_create_role_grant_access_success(
not_emulator,
not_experimental_host,
shared_instance,
databases_to_delete,
database_dialect,
Expand Down Expand Up @@ -514,6 +522,7 @@ def test_create_role_grant_access_success(

def test_list_database_role_success(
not_emulator,
not_experimental_host,
shared_instance,
databases_to_delete,
database_dialect,
Expand Down Expand Up @@ -757,7 +766,11 @@ def test_information_schema_referential_constraints_fkadc(


def test_update_database_success(
not_emulator, shared_database, shared_instance, database_operation_timeout
not_emulator,
not_experimental_host,
shared_database,
shared_instance,
database_operation_timeout,
):
old_protection = shared_database.enable_drop_protection
new_protection = True
Expand Down
8 changes: 7 additions & 1 deletion tests/system/test_dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1436,7 +1436,13 @@ def test_ping(self):
@pytest.mark.noautofixt
def test_user_agent(self, shared_instance, dbapi_database):
"""Check that DB API uses an appropriate user agent."""
conn = connect(shared_instance.name, dbapi_database.name)
conn = connect(
shared_instance.name,
dbapi_database.name,
experimental_host=_helpers.EXPERIMENTAL_HOST
if _helpers.USE_EXPERIMENTAL_HOST
else None,
)
assert (
conn.instance._client._client_info.user_agent
== "gl-dbapi/" + package_version.__version__
Expand Down
1 change: 1 addition & 0 deletions tests/system/test_instance_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def test_update_instance(
shared_instance,
shared_instance_id,
instance_operation_timeout,
not_experimental_host,
):
old_display_name = shared_instance.display_name
new_display_name = "Foo Bar Baz"
Expand Down
Loading
Loading