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

Commit e942156

Browse files
committed
feat: add support for experimental host
1 parent 8818c30 commit e942156

File tree

13 files changed

+96
-12
lines changed

13 files changed

+96
-12
lines changed

google/cloud/spanner_dbapi/connection.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""DB-API Connection for the Google Cloud Spanner."""
1616
import warnings
1717

18+
from google.api_core.client_options import ClientOptions
1819
from google.api_core.exceptions import Aborted
1920
from google.api_core.gapic_v1.client_info import ClientInfo
2021
from google.auth.credentials import AnonymousCredentials
@@ -734,6 +735,7 @@ def connect(
734735
client=None,
735736
route_to_leader_enabled=True,
736737
database_role=None,
738+
experimental_host=None,
737739
**kwargs,
738740
):
739741
"""Creates a connection to a Google Cloud Spanner database.
@@ -805,6 +807,10 @@ def connect(
805807
client_options = None
806808
if isinstance(credentials, AnonymousCredentials):
807809
client_options = kwargs.get("client_options")
810+
if experimental_host is not None:
811+
project="default"
812+
credentials = AnonymousCredentials()
813+
client_options = ClientOptions(api_endpoint=experimental_host)
808814
client = spanner.Client(
809815
project=project,
810816
credentials=credentials,

google/cloud/spanner_v1/client.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,10 @@ def __init__(
200200
directed_read_options=None,
201201
observability_options=None,
202202
default_transaction_options: Optional[DefaultTransactionOptions] = None,
203+
experimental_host=None,
203204
):
204205
self._emulator_host = _get_spanner_emulator_host()
206+
self._experimental_host = experimental_host
205207

206208
if client_options and type(client_options) is dict:
207209
self._client_options = google.api_core.client_options.from_dict(
@@ -212,6 +214,8 @@ def __init__(
212214

213215
if self._emulator_host:
214216
credentials = AnonymousCredentials()
217+
elif self._experimental_host:
218+
credentials = AnonymousCredentials()
215219
elif isinstance(credentials, AnonymousCredentials):
216220
self._emulator_host = self._client_options.api_endpoint
217221

@@ -324,6 +328,15 @@ def instance_admin_api(self):
324328
client_options=self._client_options,
325329
transport=transport,
326330
)
331+
elif self._experimental_host:
332+
transport = InstanceAdminGrpcTransport(
333+
channel=grpc.insecure_channel(target=self._experimental_host)
334+
)
335+
self._instance_admin_api = InstanceAdminClient(
336+
client_info=self._client_info,
337+
client_options=self._client_options,
338+
transport=transport,
339+
)
327340
else:
328341
self._instance_admin_api = InstanceAdminClient(
329342
credentials=self.credentials,
@@ -345,6 +358,15 @@ def database_admin_api(self):
345358
client_options=self._client_options,
346359
transport=transport,
347360
)
361+
elif self._experimental_host:
362+
transport = DatabaseAdminGrpcTransport(
363+
channel=grpc.insecure_channel(target=self._experimental_host)
364+
)
365+
self._database_admin_api = DatabaseAdminClient(
366+
client_info=self._client_info,
367+
client_options=self._client_options,
368+
transport=transport,
369+
)
348370
else:
349371
self._database_admin_api = DatabaseAdminClient(
350372
credentials=self.credentials,
@@ -485,6 +507,7 @@ def instance(
485507
self._emulator_host,
486508
labels,
487509
processing_units,
510+
self._experimental_host,
488511
)
489512

490513
def list_instances(self, filter_="", page_size=None):

google/cloud/spanner_v1/database.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,9 @@ def __init__(
203203

204204
self._pool = pool
205205
pool.bind(self)
206+
is_experimental_host = self._instance.experimental_host is not None
206207

207-
self._sessions_manager = DatabaseSessionsManager(self, pool)
208+
self._sessions_manager = DatabaseSessionsManager(self, pool, is_experimental_host)
208209

209210
@classmethod
210211
def from_pb(cls, database_pb, instance, pool=None):
@@ -449,6 +450,14 @@ def spanner_api(self):
449450
client_info=client_info, transport=transport
450451
)
451452
return self._spanner_api
453+
if self._instance.experimental_host is not None:
454+
transport = SpannerGrpcTransport(
455+
channel=grpc.insecure_channel(self._instance.experimental_host)
456+
)
457+
self._spanner_api = SpannerClient(
458+
client_info=client_info, transport=transport, client_options=client_options
459+
)
460+
return self._spanner_api
452461
credentials = self._instance._client.credentials
453462
if isinstance(credentials, google.auth.credentials.Scoped):
454463
credentials = credentials.with_scopes((SPANNER_DATA_SCOPE,))

google/cloud/spanner_v1/database_sessions_manager.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,10 @@ class DatabaseSessionsManager(object):
6262
_MAINTENANCE_THREAD_POLLING_INTERVAL = timedelta(minutes=10)
6363
_MAINTENANCE_THREAD_REFRESH_INTERVAL = timedelta(days=7)
6464

65-
def __init__(self, database, pool):
65+
def __init__(self, database, pool, is_experimental_host: bool = False):
6666
self._database = database
6767
self._pool = pool
68+
self._is_experimental_host = is_experimental_host
6869

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

8990
session = (
9091
self._get_multiplexed_session()
91-
if self._use_multiplexed(transaction_type)
92+
if self._use_multiplexed(transaction_type) or self._is_experimental_host
9293
else self._pool.get()
9394
)
9495

google/cloud/spanner_v1/instance.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def __init__(
122122
emulator_host=None,
123123
labels=None,
124124
processing_units=None,
125+
experimental_host=None,
125126
):
126127
self.instance_id = instance_id
127128
self._client = client
@@ -142,6 +143,7 @@ def __init__(
142143
self._node_count = processing_units // PROCESSING_UNITS_PER_NODE
143144
self.display_name = display_name or instance_id
144145
self.emulator_host = emulator_host
146+
self.experimental_host = experimental_host
145147
if labels is None:
146148
labels = {}
147149
self.labels = labels

google/cloud/spanner_v1/testing/database_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ def spanner_api(self):
8686
transport=transport,
8787
)
8888
return self._spanner_api
89+
if self._instance.experimental_host is not None:
90+
channel = grpc.insecure_channel(self._instance.experimental_host)
91+
self._x_goog_request_id_interceptor = XGoogRequestIDHeaderInterceptor()
92+
self._interceptors.append(self._x_goog_request_id_interceptor)
93+
channel = grpc.intercept_channel(channel, *self._interceptors)
94+
transport = SpannerGrpcTransport(channel=channel)
95+
self._spanner_api = SpannerClient(
96+
client_info=client_info, transport=transport, client_options=client_options
97+
)
98+
return self._spanner_api
8999
credentials = client.credentials
90100
if isinstance(credentials, google.auth.credentials.Scoped):
91101
credentials = credentials.with_scopes((SPANNER_DATA_SCOPE,))

tests/system/_helpers.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@
5656
EMULATOR_PROJECT_DEFAULT = "emulator-test-project"
5757
EMULATOR_PROJECT = os.getenv(EMULATOR_PROJECT_ENVVAR, EMULATOR_PROJECT_DEFAULT)
5858

59+
USE_EXPERIMENTAL_HOST_ENVVAR = "SPANNER_EXPERIMENTAL_HOST"
60+
EXPERIMENTAL_HOST = os.getenv(USE_EXPERIMENTAL_HOST_ENVVAR)
61+
USE_EXPERIMENTAL_HOST = EXPERIMENTAL_HOST is not None
62+
63+
EXPERIMENTAL_HOST_PROJECT = "default"
64+
EXPERIMENTAL_HOST_INSTANCE = "default"
5965

6066
DDL_STATEMENTS = (
6167
_fixtures.PG_DDL_STATEMENTS

tests/system/conftest.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ def not_emulator():
4848
if _helpers.USE_EMULATOR:
4949
pytest.skip(f"{_helpers.USE_EMULATOR_ENVVAR} set in environment.")
5050

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

5256
@pytest.fixture(scope="session")
5357
def not_postgres(database_dialect):
@@ -104,6 +108,15 @@ def spanner_client():
104108
project=_helpers.EMULATOR_PROJECT,
105109
credentials=credentials,
106110
)
111+
elif _helpers.USE_EXPERIMENTAL_HOST:
112+
from google.auth.credentials import AnonymousCredentials
113+
114+
credentials = AnonymousCredentials()
115+
return spanner_v1.Client(
116+
project=_helpers.EXPERIMENTAL_HOST_PROJECT,
117+
credentials=credentials,
118+
experimental_host=_helpers.EXPERIMENTAL_HOST
119+
)
107120
else:
108121
client_options = {"api_endpoint": _helpers.API_ENDPOINT}
109122
return spanner_v1.Client(
@@ -130,15 +143,16 @@ def backup_operation_timeout():
130143
def shared_instance_id():
131144
if _helpers.CREATE_INSTANCE:
132145
return f"{_helpers.unique_id('google-cloud')}"
133-
146+
if _helpers.USE_EXPERIMENTAL_HOST:
147+
return _helpers.EXPERIMENTAL_HOST_INSTANCE
134148
return _helpers.INSTANCE_ID
135149

136150

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

141-
if not _helpers.USE_EMULATOR:
155+
if not _helpers.USE_EMULATOR and not _helpers.USE_EXPERIMENTAL_HOST:
142156
# Defend against back-end returning configs for regions we aren't
143157
# actually allowed to use.
144158
configs = [config for config in configs if "-us-" in config.name]

tests/system/test_backup_api.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@
2626
Remove {_helpers.SKIP_BACKUP_TESTS_ENVVAR} from environment to run these tests.\
2727
"""
2828
skip_emulator_reason = "Backup operations not supported by emulator."
29+
skip_experimental_host_reason = "Backup operations not supported on experimental host yet."
2930

3031
pytestmark = [
3132
pytest.mark.skipif(_helpers.SKIP_BACKUP_TESTS, reason=skip_env_reason),
3233
pytest.mark.skipif(_helpers.USE_EMULATOR, reason=skip_emulator_reason),
34+
pytest.mark.skipif(_helpers.USE_EXPERIMENTAL_HOST, reason=skip_experimental_host_reason),
35+
3336
]
3437

3538

tests/system/test_database_api.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747

4848

4949
@pytest.fixture(scope="module")
50-
def multiregion_instance(spanner_client, instance_operation_timeout, not_postgres):
50+
def multiregion_instance(spanner_client, instance_operation_timeout, not_postgres, not_experimental_host):
5151
multi_region_instance_id = _helpers.unique_id("multi-region")
5252
multi_region_config = "nam3"
5353
config_name = "{}/instanceConfigs/{}".format(
@@ -97,6 +97,7 @@ def test_database_binding_of_fixed_size_pool(
9797
databases_to_delete,
9898
not_postgres,
9999
proto_descriptor_file,
100+
not_experimental_host,
100101
):
101102
temp_db_id = _helpers.unique_id("fixed_size_db", separator="_")
102103
temp_db = shared_instance.database(temp_db_id)
@@ -130,6 +131,7 @@ def test_database_binding_of_pinging_pool(
130131
databases_to_delete,
131132
not_postgres,
132133
proto_descriptor_file,
134+
not_experimental_host,
133135
):
134136
temp_db_id = _helpers.unique_id("binding_db", separator="_")
135137
temp_db = shared_instance.database(temp_db_id)
@@ -217,6 +219,7 @@ def test_create_database_pitr_success(
217219
def test_create_database_with_default_leader_success(
218220
not_emulator, # Default leader setting not supported by the emulator
219221
not_postgres,
222+
not_experimental_host,
220223
multiregion_instance,
221224
databases_to_delete,
222225
):
@@ -253,6 +256,7 @@ def test_create_database_with_default_leader_success(
253256

254257
def test_iam_policy(
255258
not_emulator,
259+
not_experimental_host,
256260
shared_instance,
257261
databases_to_delete,
258262
):
@@ -414,6 +418,7 @@ def test_update_ddl_w_pitr_success(
414418
def test_update_ddl_w_default_leader_success(
415419
not_emulator,
416420
not_postgres,
421+
not_experimental_host,
417422
multiregion_instance,
418423
databases_to_delete,
419424
proto_descriptor_file,
@@ -448,6 +453,7 @@ def test_update_ddl_w_default_leader_success(
448453

449454
def test_create_role_grant_access_success(
450455
not_emulator,
456+
not_experimental_host,
451457
shared_instance,
452458
databases_to_delete,
453459
database_dialect,
@@ -514,6 +520,7 @@ def test_create_role_grant_access_success(
514520

515521
def test_list_database_role_success(
516522
not_emulator,
523+
not_experimental_host,
517524
shared_instance,
518525
databases_to_delete,
519526
database_dialect,
@@ -757,7 +764,7 @@ def test_information_schema_referential_constraints_fkadc(
757764

758765

759766
def test_update_database_success(
760-
not_emulator, shared_database, shared_instance, database_operation_timeout
767+
not_emulator, not_experimental_host, shared_database, shared_instance, database_operation_timeout
761768
):
762769
old_protection = shared_database.enable_drop_protection
763770
new_protection = True

0 commit comments

Comments
 (0)