GeezerVison is showing github.com — feat(gapic): support mTLS certificates when available (#1467) · googleapis/python-spanner@df87c3e
This repository was archived by the owner on Jun 8, 2026. It is now read-only.
File treeExpand file treeCollapse file tree Open diff view settings spanner_admin_database_v1 spanner_admin_instance_v1 spanner_v1/services/spanner
spanner_admin_database_v1 spanner_admin_instance_v1
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: .librarian/generator-input/noxfile.py +23-5Lines changed: 23 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 35 | 35 | DEFAULT_PYTHON_VERSION = "3.14" | 36 | 36 |
| 37 | 37 | 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"] | 39 | 39 |
| 40 | 40 | UNIT_TEST_PYTHON_VERSIONS: List[str] = [ | 41 | 41 | "3.9", | 42 | 42 | "3.10", | 43 | 43 | "3.11", | 44 | 44 | "3.12", | 45 | 45 | "3.13", | | 46 | + "3.14", | 46 | 47 | ] | 47 | 48 | UNIT_TEST_STANDARD_DEPENDENCIES = [ | 48 | 49 | "mock", | | 81 | 82 | "unit-3.11", | 82 | 83 | "unit-3.12", | 83 | 84 | "unit-3.13", | | 85 | + "unit-3.14", | 84 | 86 | "system", | 85 | 87 | "cover", | 86 | 88 | "lint", | @@ -195,7 +197,12 @@ def install_unittest_dependencies(session, *constraints): | 195 | 197 | def unit(session, protobuf_implementation): | 196 | 198 | # Install all test dependencies, then install this package in-place. | 197 | 199 |
| 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 | + ): | 199 | 206 | session.skip("cpp implementation is not supported in python 3.11+") | 200 | 207 |
| 201 | 208 | constraints_path = str( | @@ -213,6 +220,7 @@ def unit(session, protobuf_implementation): | 213 | 220 | session.run( | 214 | 221 | "py.test", | 215 | 222 | "--quiet", | | 223 | + "-s", | 216 | 224 | f"--junitxml=unit_{session.python}_sponge_log.xml", | 217 | 225 | "--cov=google", | 218 | 226 | "--cov=tests/unit", | @@ -326,7 +334,12 @@ def system(session, protobuf_implementation, database_dialect): | 326 | 334 | "Only run system tests on real Spanner with one protobuf implementation to speed up the build" | 327 | 335 | ) | 328 | 336 |
| 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 | + ): | 330 | 343 | session.skip("cpp implementation is not supported in python 3.11+") | 331 | 344 |
| 332 | 345 | # Install pyopenssl for mTLS testing. | @@ -470,7 +483,7 @@ def docfx(session): | 470 | 483 | ) | 471 | 484 |
| 472 | 485 |
| 473 | | -@nox.session(python="3.13") | | 486 | +@nox.session(python="3.14") | 474 | 487 | @nox.parametrize( | 475 | 488 | "protobuf_implementation,database_dialect", | 476 | 489 | [ | @@ -485,7 +498,12 @@ def docfx(session): | 485 | 498 | def prerelease_deps(session, protobuf_implementation, database_dialect): | 486 | 499 | """Run all tests with prerelease versions of dependencies installed.""" | 487 | 500 |
| 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 | + ): | 489 | 507 | session.skip("cpp implementation is not supported in python 3.11+") | 490 | 508 |
| 491 | 509 | # Install all dependencies | |
Collapse file Copy file name to clipboardExpand all lines: .librarian/generator-input/setup.py +9-11Lines changed: 9 additions & 11 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 44 | 44 | "proto-plus >= 1.22.2, <2.0.0; python_version>='3.11'", | 45 | 45 | "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", | 46 | 46 | "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 ", | 47 | 54 | ] | 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"} | 59 | 56 |
| 60 | 57 | url = "https://github.com/googleapis/python-spanner" | 61 | 58 |
| | 90 | 87 | "Programming Language :: Python :: 3.10", | 91 | 88 | "Programming Language :: Python :: 3.11", | 92 | 89 | "Programming Language :: Python :: 3.12", | | 90 | + "Programming Language :: Python :: 3.14", | 93 | 91 | "Operating System :: OS Independent", | 94 | 92 | "Topic :: Internet", | 95 | 93 | ], | |
Collapse file Copy file name to clipboardExpand all lines: .librarian/state.yaml +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 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 | 2 | 2 | libraries: | 3 | 3 | - id: google-cloud-spanner | 4 | 4 | version: 3.60.0 | |
Collapse file Copy file name to clipboardExpand all lines: google/cloud/spanner_admin_database_v1/__init__.py +104Lines changed: 104 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 15 | 15 | # | 16 | 16 | from google.cloud.spanner_admin_database_v1 import gapic_version as package_version | 17 | 17 |
| | 18 | +import google.api_core as api_core | | 19 | +import sys | | 20 | + | 18 | 21 | __version__ = package_version.__version__ | 19 | 22 |
| | 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 | + | 20 | 30 |
| 21 | 31 | from .services.database_admin import DatabaseAdminClient | 22 | 32 | from .services.database_admin import DatabaseAdminAsyncClient | | 83 | 93 | from .types.spanner_database_admin import UpdateDatabaseRequest | 84 | 94 | from .types.spanner_database_admin import RestoreSourceType | 85 | 95 |
| | 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 | + | 86 | 190 | __all__ = ( | 87 | 191 | "DatabaseAdminAsyncClient", | 88 | 192 | "AddSplitPointsRequest", | |
Collapse file Copy file name to clipboardExpand all lines: google/cloud/spanner_admin_database_v1/services/database_admin/client.py +32-14Lines changed: 32 additions & 14 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -172,6 +172,34 @@ def _get_default_mtls_endpoint(api_endpoint): | 172 | 172 | _DEFAULT_ENDPOINT_TEMPLATE = "spanner.{UNIVERSE_DOMAIN}" | 173 | 173 | _DEFAULT_UNIVERSE = "googleapis.com" | 174 | 174 |
| | 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 | + | 175 | 203 | @classmethod | 176 | 204 | def from_service_account_info(cls, info: dict, *args, **kwargs): | 177 | 205 | """Creates an instance of this client using the provided credentials | @@ -518,20 +546,16 @@ def get_mtls_endpoint_and_cert_source( | 518 | 546 | ) | 519 | 547 | if client_options is None: | 520 | 548 | 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() | 522 | 550 | 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 | | - ) | 527 | 551 | if use_mtls_endpoint not in ("auto", "never", "always"): | 528 | 552 | raise MutualTLSChannelError( | 529 | 553 | "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" | 530 | 554 | ) | 531 | 555 |
| 532 | 556 | # Figure out the client cert source to use. | 533 | 557 | client_cert_source = None | 534 | | - if use_client_cert == "true": | | 558 | + if use_client_cert: | 535 | 559 | if client_options.client_cert_source: | 536 | 560 | client_cert_source = client_options.client_cert_source | 537 | 561 | elif mtls.has_default_client_cert_source(): | @@ -563,20 +587,14 @@ def _read_environment_variables(): | 563 | 587 | google.auth.exceptions.MutualTLSChannelError: If GOOGLE_API_USE_MTLS_ENDPOINT | 564 | 588 | is not any of ["auto", "never", "always"]. | 565 | 589 | """ | 566 | | - use_client_cert = os.getenv( | 567 | | - "GOOGLE_API_USE_CLIENT_CERTIFICATE", "false" | 568 | | - ).lower() | | 590 | + use_client_cert = DatabaseAdminClient._use_client_cert_effective() | 569 | 591 | use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower() | 570 | 592 | 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 | | - ) | 575 | 593 | if use_mtls_endpoint not in ("auto", "never", "always"): | 576 | 594 | raise MutualTLSChannelError( | 577 | 595 | "Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`" | 578 | 596 | ) | 579 | | - return use_client_cert == "true", use_mtls_endpoint, universe_domain_env | | 597 | + return use_client_cert, use_mtls_endpoint, universe_domain_env | 580 | 598 |
| 581 | 599 | @staticmethod | 582 | 600 | def _get_client_cert_source(provided_cert_source, use_cert_flag): | |
|