Skip to content

Commit df87c3e

Browse files
feat(gapic): support mTLS certificates when available (#1467)
feat: update image to us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:b8058df4c45e9a6e07f6b4d65b458d0d059241dd34c814f151c8bf6b89211209
1 parent 24394d6 commit df87c3e

File tree

23 files changed

+1023
-742
lines changed

23 files changed

+1023
-742
lines changed

.librarian/generator-input/noxfile.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,15 @@
3535
DEFAULT_PYTHON_VERSION = "3.14"
3636

3737
DEFAULT_MOCK_SERVER_TESTS_PYTHON_VERSION = "3.12"
38-
SYSTEM_TEST_PYTHON_VERSIONS: List[str] = ["3.12"]
38+
SYSTEM_TEST_PYTHON_VERSIONS: List[str] = ["3.14"]
3939

4040
UNIT_TEST_PYTHON_VERSIONS: List[str] = [
4141
"3.9",
4242
"3.10",
4343
"3.11",
4444
"3.12",
4545
"3.13",
46+
"3.14",
4647
]
4748
UNIT_TEST_STANDARD_DEPENDENCIES = [
4849
"mock",
@@ -81,6 +82,7 @@
8182
"unit-3.11",
8283
"unit-3.12",
8384
"unit-3.13",
85+
"unit-3.14",
8486
"system",
8587
"cover",
8688
"lint",
@@ -195,7 +197,12 @@ def install_unittest_dependencies(session, *constraints):
195197
def unit(session, protobuf_implementation):
196198
# Install all test dependencies, then install this package in-place.
197199

198-
if protobuf_implementation == "cpp" and session.python in ("3.11", "3.12", "3.13"):
200+
if protobuf_implementation == "cpp" and session.python in (
201+
"3.11",
202+
"3.12",
203+
"3.13",
204+
"3.14",
205+
):
199206
session.skip("cpp implementation is not supported in python 3.11+")
200207

201208
constraints_path = str(
@@ -213,6 +220,7 @@ def unit(session, protobuf_implementation):
213220
session.run(
214221
"py.test",
215222
"--quiet",
223+
"-s",
216224
f"--junitxml=unit_{session.python}_sponge_log.xml",
217225
"--cov=google",
218226
"--cov=tests/unit",
@@ -326,7 +334,12 @@ def system(session, protobuf_implementation, database_dialect):
326334
"Only run system tests on real Spanner with one protobuf implementation to speed up the build"
327335
)
328336

329-
if protobuf_implementation == "cpp" and session.python in ("3.11", "3.12", "3.13"):
337+
if protobuf_implementation == "cpp" and session.python in (
338+
"3.11",
339+
"3.12",
340+
"3.13",
341+
"3.14",
342+
):
330343
session.skip("cpp implementation is not supported in python 3.11+")
331344

332345
# Install pyopenssl for mTLS testing.
@@ -470,7 +483,7 @@ def docfx(session):
470483
)
471484

472485

473-
@nox.session(python="3.13")
486+
@nox.session(python="3.14")
474487
@nox.parametrize(
475488
"protobuf_implementation,database_dialect",
476489
[
@@ -485,7 +498,12 @@ def docfx(session):
485498
def prerelease_deps(session, protobuf_implementation, database_dialect):
486499
"""Run all tests with prerelease versions of dependencies installed."""
487500

488-
if protobuf_implementation == "cpp" and session.python in ("3.11", "3.12", "3.13"):
501+
if protobuf_implementation == "cpp" and session.python in (
502+
"3.11",
503+
"3.12",
504+
"3.13",
505+
"3.14",
506+
):
489507
session.skip("cpp implementation is not supported in python 3.11+")
490508

491509
# Install all dependencies

.librarian/generator-input/setup.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,15 @@
4444
"proto-plus >= 1.22.2, <2.0.0; python_version>='3.11'",
4545
"protobuf>=3.20.2,<7.0.0,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5",
4646
"grpc-interceptor >= 0.15.4",
47+
# Make OpenTelemetry a core dependency
48+
"opentelemetry-api >= 1.22.0",
49+
"opentelemetry-sdk >= 1.22.0",
50+
"opentelemetry-semantic-conventions >= 0.43b0",
51+
"opentelemetry-resourcedetector-gcp >= 1.8.0a0",
52+
"google-cloud-monitoring >= 2.16.0",
53+
"mmh3 >= 4.1.0 ",
4754
]
48-
extras = {
49-
"tracing": [
50-
"opentelemetry-api >= 1.22.0",
51-
"opentelemetry-sdk >= 1.22.0",
52-
"opentelemetry-semantic-conventions >= 0.43b0",
53-
"opentelemetry-resourcedetector-gcp >= 1.8.0a0",
54-
"google-cloud-monitoring >= 2.16.0",
55-
"mmh3 >= 4.1.0 ",
56-
],
57-
"libcst": "libcst >= 0.2.5",
58-
}
55+
extras = {"libcst": "libcst >= 0.2.5"}
5956

6057
url = "https://github.com/googleapis/python-spanner"
6158

@@ -90,6 +87,7 @@
9087
"Programming Language :: Python :: 3.10",
9188
"Programming Language :: Python :: 3.11",
9289
"Programming Language :: Python :: 3.12",
90+
"Programming Language :: Python :: 3.14",
9391
"Operating System :: OS Independent",
9492
"Topic :: Internet",
9593
],

.librarian/state.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:8e2c32496077054105bd06c54a59d6a6694287bc053588e24debe6da6920ad91
1+
image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:b8058df4c45e9a6e07f6b4d65b458d0d059241dd34c814f151c8bf6b89211209
22
libraries:
33
- id: google-cloud-spanner
44
version: 3.60.0

google/cloud/spanner_admin_database_v1/__init__.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,18 @@
1515
#
1616
from google.cloud.spanner_admin_database_v1 import gapic_version as package_version
1717

18+
import google.api_core as api_core
19+
import sys
20+
1821
__version__ = package_version.__version__
1922

23+
if sys.version_info >= (3, 8): # pragma: NO COVER
24+
from importlib import metadata
25+
else: # pragma: NO COVER
26+
# TODO(https://github.com/googleapis/python-api-core/issues/835): Remove
27+
# this code path once we drop support for Python 3.7
28+
import importlib_metadata as metadata
29+
2030

2131
from .services.database_admin import DatabaseAdminClient
2232
from .services.database_admin import DatabaseAdminAsyncClient
@@ -83,6 +93,100 @@
8393
from .types.spanner_database_admin import UpdateDatabaseRequest
8494
from .types.spanner_database_admin import RestoreSourceType
8595

96+
if hasattr(api_core, "check_python_version") and hasattr(
97+
api_core, "check_dependency_versions"
98+
): # pragma: NO COVER
99+
api_core.check_python_version("google.cloud.spanner_admin_database_v1") # type: ignore
100+
api_core.check_dependency_versions("google.cloud.spanner_admin_database_v1") # type: ignore
101+
else: # pragma: NO COVER
102+
# An older version of api_core is installed which does not define the
103+
# functions above. We do equivalent checks manually.
104+
try:
105+
import warnings
106+
import sys
107+
108+
_py_version_str = sys.version.split()[0]
109+
_package_label = "google.cloud.spanner_admin_database_v1"
110+
if sys.version_info < (3, 9):
111+
warnings.warn(
112+
"You are using a non-supported Python version "
113+
+ f"({_py_version_str}). Google will not post any further "
114+
+ f"updates to {_package_label} supporting this Python version. "
115+
+ "Please upgrade to the latest Python version, or at "
116+
+ f"least to Python 3.9, and then update {_package_label}.",
117+
FutureWarning,
118+
)
119+
if sys.version_info[:2] == (3, 9):
120+
warnings.warn(
121+
f"You are using a Python version ({_py_version_str}) "
122+
+ f"which Google will stop supporting in {_package_label} in "
123+
+ "January 2026. Please "
124+
+ "upgrade to the latest Python version, or at "
125+
+ "least to Python 3.10, before then, and "
126+
+ f"then update {_package_label}.",
127+
FutureWarning,
128+
)
129+
130+
def parse_version_to_tuple(version_string: str):
131+
"""Safely converts a semantic version string to a comparable tuple of integers.
132+
Example: "4.25.8" -> (4, 25, 8)
133+
Ignores non-numeric parts and handles common version formats.
134+
Args:
135+
version_string: Version string in the format "x.y.z" or "x.y.z<suffix>"
136+
Returns:
137+
Tuple of integers for the parsed version string.
138+
"""
139+
parts = []
140+
for part in version_string.split("."):
141+
try:
142+
parts.append(int(part))
143+
except ValueError:
144+
# If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here.
145+
# This is a simplification compared to 'packaging.parse_version', but sufficient
146+
# for comparing strictly numeric semantic versions.
147+
break
148+
return tuple(parts)
149+
150+
def _get_version(dependency_name):
151+
try:
152+
version_string: str = metadata.version(dependency_name)
153+
parsed_version = parse_version_to_tuple(version_string)
154+
return (parsed_version, version_string)
155+
except Exception:
156+
# Catch exceptions from metadata.version() (e.g., PackageNotFoundError)
157+
# or errors during parse_version_to_tuple
158+
return (None, "--")
159+
160+
_dependency_package = "google.protobuf"
161+
_next_supported_version = "4.25.8"
162+
_next_supported_version_tuple = (4, 25, 8)
163+
_recommendation = " (we recommend 6.x)"
164+
(_version_used, _version_used_string) = _get_version(_dependency_package)
165+
if _version_used and _version_used < _next_supported_version_tuple:
166+
warnings.warn(
167+
f"Package {_package_label} depends on "
168+
+ f"{_dependency_package}, currently installed at version "
169+
+ f"{_version_used_string}. Future updates to "
170+
+ f"{_package_label} will require {_dependency_package} at "
171+
+ f"version {_next_supported_version} or higher{_recommendation}."
172+
+ " Please ensure "
173+
+ "that either (a) your Python environment doesn't pin the "
174+
+ f"version of {_dependency_package}, so that updates to "
175+
+ f"{_package_label} can require the higher version, or "
176+
+ "(b) you manually update your Python environment to use at "
177+
+ f"least version {_next_supported_version} of "
178+
+ f"{_dependency_package}.",
179+
FutureWarning,
180+
)
181+
except Exception:
182+
warnings.warn(
183+
"Could not determine the version of Python "
184+
+ "currently being used. To continue receiving "
185+
+ "updates for {_package_label}, ensure you are "
186+
+ "using a supported version of Python; see "
187+
+ "https://devguide.python.org/versions/"
188+
)
189+
86190
__all__ = (
87191
"DatabaseAdminAsyncClient",
88192
"AddSplitPointsRequest",

google/cloud/spanner_admin_database_v1/services/database_admin/client.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,34 @@ def _get_default_mtls_endpoint(api_endpoint):
172172
_DEFAULT_ENDPOINT_TEMPLATE = "spanner.{UNIVERSE_DOMAIN}"
173173
_DEFAULT_UNIVERSE = "googleapis.com"
174174

175+
@staticmethod
176+
def _use_client_cert_effective():
177+
"""Returns whether client certificate should be used for mTLS if the
178+
google-auth version supports should_use_client_cert automatic mTLS enablement.
179+
180+
Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var.
181+
182+
Returns:
183+
bool: whether client certificate should be used for mTLS
184+
Raises:
185+
ValueError: (If using a version of google-auth without should_use_client_cert and
186+
GOOGLE_API_USE_CLIENT_CERTIFICATE is set to an unexpected value.)
187+
"""
188+
# check if google-auth version supports should_use_client_cert for automatic mTLS enablement
189+
if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER
190+
return mtls.should_use_client_cert()
191+
else: # pragma: NO COVER
192+
# if unsupported, fallback to reading from env var
193+
use_client_cert_str = os.getenv(
194+
"GOOGLE_API_USE_CLIENT_CERTIFICATE", "false"
195+
).lower()
196+
if use_client_cert_str not in ("true", "false"):
197+
raise ValueError(
198+
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be"
199+
" either `true` or `false`"
200+
)
201+
return use_client_cert_str == "true"
202+
175203
@classmethod
176204
def from_service_account_info(cls, info: dict, *args, **kwargs):
177205
"""Creates an instance of this client using the provided credentials
@@ -518,20 +546,16 @@ def get_mtls_endpoint_and_cert_source(
518546
)
519547
if client_options is None:
520548
client_options = client_options_lib.ClientOptions()
521-
use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")
549+
use_client_cert = DatabaseAdminClient._use_client_cert_effective()
522550
use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto")
523-
if use_client_cert not in ("true", "false"):
524-
raise ValueError(
525-
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`"
526-
)
527551
if use_mtls_endpoint not in ("auto", "never", "always"):
528552
raise MutualTLSChannelError(
529553
"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`"
530554
)
531555

532556
# Figure out the client cert source to use.
533557
client_cert_source = None
534-
if use_client_cert == "true":
558+
if use_client_cert:
535559
if client_options.client_cert_source:
536560
client_cert_source = client_options.client_cert_source
537561
elif mtls.has_default_client_cert_source():
@@ -563,20 +587,14 @@ def _read_environment_variables():
563587
google.auth.exceptions.MutualTLSChannelError: If GOOGLE_API_USE_MTLS_ENDPOINT
564588
is not any of ["auto", "never", "always"].
565589
"""
566-
use_client_cert = os.getenv(
567-
"GOOGLE_API_USE_CLIENT_CERTIFICATE", "false"
568-
).lower()
590+
use_client_cert = DatabaseAdminClient._use_client_cert_effective()
569591
use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower()
570592
universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN")
571-
if use_client_cert not in ("true", "false"):
572-
raise ValueError(
573-
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`"
574-
)
575593
if use_mtls_endpoint not in ("auto", "never", "always"):
576594
raise MutualTLSChannelError(
577595
"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`"
578596
)
579-
return use_client_cert == "true", use_mtls_endpoint, universe_domain_env
597+
return use_client_cert, use_mtls_endpoint, universe_domain_env
580598

581599
@staticmethod
582600
def _get_client_cert_source(provided_cert_source, use_cert_flag):

0 commit comments

Comments
 (0)