Skip to content

Commit 85c0232

Browse files
authored
feat(gapic): support mTLS certificates when available (#658)
chore: librarian update image pull request: 20251216T200852Z
1 parent 84f880b commit 85c0232

File tree

14 files changed

+733
-459
lines changed

14 files changed

+733
-459
lines changed

.librarian/generator-input/setup.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,6 @@
9797
install_requires=dependencies,
9898
extras_require=extras,
9999
python_requires=">=3.7",
100-
scripts=[
101-
"scripts/fixup_datastore_v1_keywords.py",
102-
"scripts/fixup_datastore_admin_v1_keywords.py",
103-
],
104100
include_package_data=True,
105101
zip_safe=False,
106102
)

.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:ce48ed695c727f7e13efd1fd68f466a55a0d772c87b69158720cec39965bc8b2
1+
image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/python-librarian-generator@sha256:b8058df4c45e9a6e07f6b4d65b458d0d059241dd34c814f151c8bf6b89211209
22
libraries:
33
- id: google-cloud-datastore
44
version: 2.22.0

google/cloud/datastore_admin_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.datastore_admin_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.datastore_admin import DatastoreAdminClient
2232
from .services.datastore_admin import DatastoreAdminAsyncClient
@@ -43,6 +53,100 @@
4353
from .types.migration import MigrationState
4454
from .types.migration import MigrationStep
4555

56+
if hasattr(api_core, "check_python_version") and hasattr(
57+
api_core, "check_dependency_versions"
58+
): # pragma: NO COVER
59+
api_core.check_python_version("google.cloud.datastore_admin_v1") # type: ignore
60+
api_core.check_dependency_versions("google.cloud.datastore_admin_v1") # type: ignore
61+
else: # pragma: NO COVER
62+
# An older version of api_core is installed which does not define the
63+
# functions above. We do equivalent checks manually.
64+
try:
65+
import warnings
66+
import sys
67+
68+
_py_version_str = sys.version.split()[0]
69+
_package_label = "google.cloud.datastore_admin_v1"
70+
if sys.version_info < (3, 9):
71+
warnings.warn(
72+
"You are using a non-supported Python version "
73+
+ f"({_py_version_str}). Google will not post any further "
74+
+ f"updates to {_package_label} supporting this Python version. "
75+
+ "Please upgrade to the latest Python version, or at "
76+
+ f"least to Python 3.9, and then update {_package_label}.",
77+
FutureWarning,
78+
)
79+
if sys.version_info[:2] == (3, 9):
80+
warnings.warn(
81+
f"You are using a Python version ({_py_version_str}) "
82+
+ f"which Google will stop supporting in {_package_label} in "
83+
+ "January 2026. Please "
84+
+ "upgrade to the latest Python version, or at "
85+
+ "least to Python 3.10, before then, and "
86+
+ f"then update {_package_label}.",
87+
FutureWarning,
88+
)
89+
90+
def parse_version_to_tuple(version_string: str):
91+
"""Safely converts a semantic version string to a comparable tuple of integers.
92+
Example: "4.25.8" -> (4, 25, 8)
93+
Ignores non-numeric parts and handles common version formats.
94+
Args:
95+
version_string: Version string in the format "x.y.z" or "x.y.z<suffix>"
96+
Returns:
97+
Tuple of integers for the parsed version string.
98+
"""
99+
parts = []
100+
for part in version_string.split("."):
101+
try:
102+
parts.append(int(part))
103+
except ValueError:
104+
# If it's a non-numeric part (e.g., '1.0.0b1' -> 'b1'), stop here.
105+
# This is a simplification compared to 'packaging.parse_version', but sufficient
106+
# for comparing strictly numeric semantic versions.
107+
break
108+
return tuple(parts)
109+
110+
def _get_version(dependency_name):
111+
try:
112+
version_string: str = metadata.version(dependency_name)
113+
parsed_version = parse_version_to_tuple(version_string)
114+
return (parsed_version, version_string)
115+
except Exception:
116+
# Catch exceptions from metadata.version() (e.g., PackageNotFoundError)
117+
# or errors during parse_version_to_tuple
118+
return (None, "--")
119+
120+
_dependency_package = "google.protobuf"
121+
_next_supported_version = "4.25.8"
122+
_next_supported_version_tuple = (4, 25, 8)
123+
_recommendation = " (we recommend 6.x)"
124+
(_version_used, _version_used_string) = _get_version(_dependency_package)
125+
if _version_used and _version_used < _next_supported_version_tuple:
126+
warnings.warn(
127+
f"Package {_package_label} depends on "
128+
+ f"{_dependency_package}, currently installed at version "
129+
+ f"{_version_used_string}. Future updates to "
130+
+ f"{_package_label} will require {_dependency_package} at "
131+
+ f"version {_next_supported_version} or higher{_recommendation}."
132+
+ " Please ensure "
133+
+ "that either (a) your Python environment doesn't pin the "
134+
+ f"version of {_dependency_package}, so that updates to "
135+
+ f"{_package_label} can require the higher version, or "
136+
+ "(b) you manually update your Python environment to use at "
137+
+ f"least version {_next_supported_version} of "
138+
+ f"{_dependency_package}.",
139+
FutureWarning,
140+
)
141+
except Exception:
142+
warnings.warn(
143+
"Could not determine the version of Python "
144+
+ "currently being used. To continue receiving "
145+
+ "updates for {_package_label}, ensure you are "
146+
+ "using a supported version of Python; see "
147+
+ "https://devguide.python.org/versions/"
148+
)
149+
46150
__all__ = (
47151
"DatastoreAdminAsyncClient",
48152
"CommonMetadata",

google/cloud/datastore_admin_v1/services/datastore_admin/client.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,34 @@ def _get_default_mtls_endpoint(api_endpoint):
207207
_DEFAULT_ENDPOINT_TEMPLATE = "datastore.{UNIVERSE_DOMAIN}"
208208
_DEFAULT_UNIVERSE = "googleapis.com"
209209

210+
@staticmethod
211+
def _use_client_cert_effective():
212+
"""Returns whether client certificate should be used for mTLS if the
213+
google-auth version supports should_use_client_cert automatic mTLS enablement.
214+
215+
Alternatively, read from the GOOGLE_API_USE_CLIENT_CERTIFICATE env var.
216+
217+
Returns:
218+
bool: whether client certificate should be used for mTLS
219+
Raises:
220+
ValueError: (If using a version of google-auth without should_use_client_cert and
221+
GOOGLE_API_USE_CLIENT_CERTIFICATE is set to an unexpected value.)
222+
"""
223+
# check if google-auth version supports should_use_client_cert for automatic mTLS enablement
224+
if hasattr(mtls, "should_use_client_cert"): # pragma: NO COVER
225+
return mtls.should_use_client_cert()
226+
else: # pragma: NO COVER
227+
# if unsupported, fallback to reading from env var
228+
use_client_cert_str = os.getenv(
229+
"GOOGLE_API_USE_CLIENT_CERTIFICATE", "false"
230+
).lower()
231+
if use_client_cert_str not in ("true", "false"):
232+
raise ValueError(
233+
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be"
234+
" either `true` or `false`"
235+
)
236+
return use_client_cert_str == "true"
237+
210238
@classmethod
211239
def from_service_account_info(cls, info: dict, *args, **kwargs):
212240
"""Creates an instance of this client using the provided credentials
@@ -372,20 +400,16 @@ def get_mtls_endpoint_and_cert_source(
372400
)
373401
if client_options is None:
374402
client_options = client_options_lib.ClientOptions()
375-
use_client_cert = os.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE", "false")
403+
use_client_cert = DatastoreAdminClient._use_client_cert_effective()
376404
use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto")
377-
if use_client_cert not in ("true", "false"):
378-
raise ValueError(
379-
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`"
380-
)
381405
if use_mtls_endpoint not in ("auto", "never", "always"):
382406
raise MutualTLSChannelError(
383407
"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`"
384408
)
385409

386410
# Figure out the client cert source to use.
387411
client_cert_source = None
388-
if use_client_cert == "true":
412+
if use_client_cert:
389413
if client_options.client_cert_source:
390414
client_cert_source = client_options.client_cert_source
391415
elif mtls.has_default_client_cert_source():
@@ -417,20 +441,14 @@ def _read_environment_variables():
417441
google.auth.exceptions.MutualTLSChannelError: If GOOGLE_API_USE_MTLS_ENDPOINT
418442
is not any of ["auto", "never", "always"].
419443
"""
420-
use_client_cert = os.getenv(
421-
"GOOGLE_API_USE_CLIENT_CERTIFICATE", "false"
422-
).lower()
444+
use_client_cert = DatastoreAdminClient._use_client_cert_effective()
423445
use_mtls_endpoint = os.getenv("GOOGLE_API_USE_MTLS_ENDPOINT", "auto").lower()
424446
universe_domain_env = os.getenv("GOOGLE_CLOUD_UNIVERSE_DOMAIN")
425-
if use_client_cert not in ("true", "false"):
426-
raise ValueError(
427-
"Environment variable `GOOGLE_API_USE_CLIENT_CERTIFICATE` must be either `true` or `false`"
428-
)
429447
if use_mtls_endpoint not in ("auto", "never", "always"):
430448
raise MutualTLSChannelError(
431449
"Environment variable `GOOGLE_API_USE_MTLS_ENDPOINT` must be `never`, `auto` or `always`"
432450
)
433-
return use_client_cert == "true", use_mtls_endpoint, universe_domain_env
451+
return use_client_cert, use_mtls_endpoint, universe_domain_env
434452

435453
@staticmethod
436454
def _get_client_cert_source(provided_cert_source, use_cert_flag):

google/cloud/datastore_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.datastore_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.datastore import DatastoreClient
2232
from .services.datastore import DatastoreAsyncClient
@@ -69,6 +79,100 @@
6979
from .types.query_profile import ExplainOptions
7080
from .types.query_profile import PlanSummary
7181

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

0 commit comments

Comments
 (0)