google-api-core 2.33.0__tar.gz → 2.35.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. {google_api_core-2.33.0/google_api_core.egg-info → google_api_core-2.35.0}/PKG-INFO +6 -9
  2. google_api_core-2.35.0/google/api_core/_feature_gating_helpers.py +139 -0
  3. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/_python_package_support.py +29 -5
  4. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/method.py +72 -11
  5. google_api_core-2.35.0/google/api_core/gapic_v1/requests.py +100 -0
  6. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/grpc_helpers.py +14 -5
  7. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/path_template.py +45 -2
  8. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/rest_helpers.py +68 -0
  9. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/version.py +1 -1
  10. {google_api_core-2.33.0 → google_api_core-2.35.0/google_api_core.egg-info}/PKG-INFO +6 -9
  11. {google_api_core-2.33.0 → google_api_core-2.35.0}/google_api_core.egg-info/SOURCES.txt +3 -0
  12. google_api_core-2.35.0/google_api_core.egg-info/requires.txt +17 -0
  13. {google_api_core-2.33.0 → google_api_core-2.35.0}/pyproject.toml +7 -8
  14. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/test_grpc_helpers_async.py +9 -3
  15. google_api_core-2.35.0/tests/conftest.py +31 -0
  16. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/gapic/test_method.py +98 -0
  17. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/gapic/test_requests.py +25 -0
  18. google_api_core-2.35.0/tests/unit/test_feature_gating_helpers.py +224 -0
  19. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_grpc_helpers.py +8 -2
  20. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_path_template.py +294 -0
  21. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_python_package_support.py +51 -3
  22. google_api_core-2.35.0/tests/unit/test_rest_helpers.py +278 -0
  23. google_api_core-2.33.0/google/api_core/gapic_v1/requests.py +0 -72
  24. google_api_core-2.33.0/google_api_core.egg-info/requires.txt +0 -24
  25. google_api_core-2.33.0/tests/unit/test_rest_helpers.py +0 -94
  26. {google_api_core-2.33.0 → google_api_core-2.35.0}/LICENSE +0 -0
  27. {google_api_core-2.33.0 → google_api_core-2.35.0}/MANIFEST.in +0 -0
  28. {google_api_core-2.33.0 → google_api_core-2.35.0}/README.rst +0 -0
  29. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/__init__.py +0 -0
  30. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/_python_version_support.py +0 -0
  31. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/_rest_streaming_base.py +0 -0
  32. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/bidi.py +0 -0
  33. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/bidi_async.py +0 -0
  34. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/bidi_base.py +0 -0
  35. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/client_info.py +0 -0
  36. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/client_logging.py +0 -0
  37. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/client_options.py +0 -0
  38. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/datetime_helpers.py +0 -0
  39. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/exceptions.py +0 -0
  40. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/extended_operation.py +0 -0
  41. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/future/__init__.py +0 -0
  42. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/future/_helpers.py +0 -0
  43. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/future/async_future.py +0 -0
  44. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/future/base.py +0 -0
  45. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/future/polling.py +0 -0
  46. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/__init__.py +0 -0
  47. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/client_info.py +0 -0
  48. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/config.py +0 -0
  49. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/config_async.py +0 -0
  50. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/method_async.py +0 -0
  51. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/gapic_v1/routing_header.py +0 -0
  52. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/general_helpers.py +0 -0
  53. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/grpc_helpers_async.py +0 -0
  54. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/iam.py +0 -0
  55. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operation.py +0 -0
  56. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operation_async.py +0 -0
  57. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/__init__.py +0 -0
  58. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/abstract_operations_base_client.py +0 -0
  59. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/abstract_operations_client.py +0 -0
  60. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/operations_async_client.py +0 -0
  61. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/operations_client.py +0 -0
  62. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/operations_client_config.py +0 -0
  63. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/operations_rest_client_async.py +0 -0
  64. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/pagers.py +0 -0
  65. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/pagers_async.py +0 -0
  66. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/pagers_base.py +0 -0
  67. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/transports/__init__.py +0 -0
  68. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/transports/base.py +0 -0
  69. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/transports/rest.py +0 -0
  70. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/operations_v1/transports/rest_asyncio.py +0 -0
  71. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/page_iterator.py +0 -0
  72. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/page_iterator_async.py +0 -0
  73. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/protobuf_helpers.py +0 -0
  74. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/py.typed +0 -0
  75. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/rest_streaming.py +0 -0
  76. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/rest_streaming_async.py +0 -0
  77. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/retry/__init__.py +0 -0
  78. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/retry/retry_base.py +0 -0
  79. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/retry/retry_streaming.py +0 -0
  80. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/retry/retry_streaming_async.py +0 -0
  81. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/retry/retry_unary.py +0 -0
  82. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/retry/retry_unary_async.py +0 -0
  83. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/retry_async.py +0 -0
  84. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/timeout.py +0 -0
  85. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/universe.py +0 -0
  86. {google_api_core-2.33.0 → google_api_core-2.35.0}/google/api_core/version_header.py +0 -0
  87. {google_api_core-2.33.0 → google_api_core-2.35.0}/google_api_core.egg-info/dependency_links.txt +0 -0
  88. {google_api_core-2.33.0 → google_api_core-2.35.0}/google_api_core.egg-info/top_level.txt +0 -0
  89. {google_api_core-2.33.0 → google_api_core-2.35.0}/setup.cfg +0 -0
  90. {google_api_core-2.33.0 → google_api_core-2.35.0}/setup.py +0 -0
  91. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/__init__.py +0 -0
  92. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/__init__.py +0 -0
  93. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/future/__init__.py +0 -0
  94. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/future/test_async_future.py +0 -0
  95. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/gapic/test_config_async.py +0 -0
  96. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/gapic/test_method_async.py +0 -0
  97. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/operations_v1/__init__.py +0 -0
  98. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/operations_v1/test_operations_async_client.py +0 -0
  99. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/retry/__init__.py +0 -0
  100. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/retry/test_retry_streaming_async.py +0 -0
  101. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/retry/test_retry_unary_async.py +0 -0
  102. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/test_bidi_async.py +0 -0
  103. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/test_operation_async.py +0 -0
  104. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/test_page_iterator_async.py +0 -0
  105. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/asyncio/test_rest_streaming_async.py +0 -0
  106. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/helpers.py +0 -0
  107. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/__init__.py +0 -0
  108. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/future/__init__.py +0 -0
  109. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/future/test__helpers.py +0 -0
  110. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/future/test_polling.py +0 -0
  111. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/gapic/test_client_info.py +0 -0
  112. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/gapic/test_config.py +0 -0
  113. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/gapic/test_routing_header.py +0 -0
  114. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/operations_v1/__init__.py +0 -0
  115. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/operations_v1/test_operations_client.py +0 -0
  116. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/operations_v1/test_operations_rest_client.py +0 -0
  117. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/retry/__init__.py +0 -0
  118. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/retry/test_retry_base.py +0 -0
  119. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/retry/test_retry_imports.py +0 -0
  120. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/retry/test_retry_streaming.py +0 -0
  121. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/retry/test_retry_unary.py +0 -0
  122. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_bidi.py +0 -0
  123. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_client_info.py +0 -0
  124. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_client_logging.py +0 -0
  125. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_client_options.py +0 -0
  126. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_datetime_helpers.py +0 -0
  127. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_exceptions.py +0 -0
  128. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_extended_operation.py +0 -0
  129. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_iam.py +0 -0
  130. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_operation.py +0 -0
  131. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_packaging.py +0 -0
  132. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_page_iterator.py +0 -0
  133. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_protobuf_helpers.py +0 -0
  134. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_python_version_support.py +0 -0
  135. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_rest_streaming.py +0 -0
  136. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_timeout.py +0 -0
  137. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_universe.py +0 -0
  138. {google_api_core-2.33.0 → google_api_core-2.35.0}/tests/unit/test_version_header.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-api-core
3
- Version: 2.33.0
3
+ Version: 2.35.0
4
4
  Summary: Google API client core library
5
5
  Author-email: Google LLC <googleapis-packages@google.com>
6
6
  License: Apache 2.0
@@ -22,21 +22,18 @@ Classifier: Topic :: Internet
22
22
  Requires-Python: >=3.10
23
23
  Description-Content-Type: text/x-rst
24
24
  License-File: LICENSE
25
- Requires-Dist: googleapis-common-protos<2.0.0,>=1.63.2
26
- Requires-Dist: protobuf<8.0.0,>=5.29.6
27
- Requires-Dist: proto-plus<2.0.0,>=1.24.0
28
- Requires-Dist: proto-plus<2.0.0,>=1.25.0; python_version >= "3.13"
25
+ Requires-Dist: googleapis-common-protos<2.0.0,>=1.69.2
26
+ Requires-Dist: protobuf<8.0.0,>=6.33.5
27
+ Requires-Dist: proto-plus<2.0.0,>=1.26.1
29
28
  Requires-Dist: google-auth<3.0.0,>=2.14.1
30
29
  Requires-Dist: requests<3.0.0,>=2.33.0
31
30
  Provides-Extra: async-rest
32
31
  Requires-Dist: google-auth[aiohttp]<3.0.0,>=2.14.1; extra == "async-rest"
33
32
  Requires-Dist: aiohttp>=3.13.4; extra == "async-rest"
34
33
  Provides-Extra: grpc
35
- Requires-Dist: grpcio<2.0.0,>=1.41.0; extra == "grpc"
36
- Requires-Dist: grpcio<2.0.0,>=1.49.1; python_version >= "3.11" and extra == "grpc"
34
+ Requires-Dist: grpcio<2.0.0,>=1.59.0; extra == "grpc"
37
35
  Requires-Dist: grpcio<2.0.0,>=1.75.1; python_version >= "3.14" and extra == "grpc"
38
- Requires-Dist: grpcio-status<2.0.0,>=1.41.0; extra == "grpc"
39
- Requires-Dist: grpcio-status<2.0.0,>=1.49.1; python_version >= "3.11" and extra == "grpc"
36
+ Requires-Dist: grpcio-status<2.0.0,>=1.59.0; extra == "grpc"
40
37
  Requires-Dist: grpcio-status<2.0.0,>=1.75.1; python_version >= "3.14" and extra == "grpc"
41
38
  Dynamic: license-file
42
39
 
@@ -0,0 +1,139 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2026 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Observability environment variable and client options resolution helpers."""
18
+
19
+ import os
20
+ import warnings
21
+ from typing import Any
22
+
23
+ # We import ClientOptions for type hinting
24
+ from google.api_core.client_options import ClientOptions
25
+
26
+ # Allowed truthy and falsy patterns for environment variables
27
+ _TRUTHY_VALUES = ("y", "yes", "t", "true", "on", "1")
28
+ _FALSY_VALUES = ("n", "no", "f", "false", "off", "0")
29
+
30
+
31
+ class FeatureGatingError(ValueError):
32
+ """Raised when feature gating resolution fails or is misconfigured."""
33
+
34
+ pass
35
+
36
+
37
+ def _strtobool(val: str) -> bool | None:
38
+ """Convert a string representation of truth to a boolean."""
39
+ clean_val = val.lower().strip()
40
+ if not clean_val:
41
+ return None
42
+ if clean_val in _TRUTHY_VALUES:
43
+ return True
44
+ if clean_val in _FALSY_VALUES:
45
+ return False
46
+ raise ValueError(f"Invalid truth value: {val!r}")
47
+
48
+
49
+ def _get_env_bool(name: str) -> bool | None:
50
+ """Retrieve the boolean value of an environment variable."""
51
+ val = os.getenv(name)
52
+ if val is None:
53
+ return None
54
+ try:
55
+ return _strtobool(val)
56
+ except ValueError as e:
57
+ warnings.warn(f"Ignored invalid value for {name}: {e}", RuntimeWarning)
58
+ return None
59
+
60
+
61
+ def _has_feature_key(
62
+ *, configuration: ClientOptions | dict[str, Any] | None, feature_key: str
63
+ ) -> bool:
64
+ """Checks if a specific feature key is present and not None in configuration."""
65
+ if configuration is None:
66
+ return False
67
+
68
+ if feature_key.startswith("__"):
69
+ return False
70
+
71
+ if isinstance(configuration, dict):
72
+ return configuration.get(feature_key) is not None
73
+
74
+ return getattr(configuration, feature_key, None) is not None
75
+
76
+
77
+ def resolve_feature_flags(
78
+ *,
79
+ env_var: str,
80
+ feature_key: str,
81
+ configuration: ClientOptions | dict[str, Any] | None = None,
82
+ ) -> bool:
83
+ """Determines if a feature is enabled based on environment variables and configuration.
84
+
85
+ Behavior depends on whether the `env_var` name contains "EXPERIMENTAL":
86
+
87
+ - **Experimental Path** (env_var contains "EXPERIMENTAL"):
88
+ Strict control. Requires the environment variable to be explicitly 'true'.
89
+ If a programmatic feature key is passed but the environment variable is not 'true',
90
+ raises FeatureGatingError (Fail Fast).
91
+
92
+ - **GA Path** (env_var does not contain "EXPERIMENTAL"):
93
+ Standard precedence. Enabled if a programmatic feature key is passed,
94
+ otherwise falls back to the environment variable value.
95
+
96
+ Args:
97
+ env_var: The name of the environment variable controlling this feature.
98
+ feature_key: The key in configuration/attributes for the programmatic configuration.
99
+ configuration: Optional. A dictionary or object containing client configuration.
100
+
101
+ Returns:
102
+ bool: True if the feature is resolved to enabled, False otherwise.
103
+
104
+ Raises:
105
+ FeatureGatingError: If a feature key is provided for an experimental feature without enabling the experimental environment variable.
106
+ """
107
+
108
+ # Check for programmatic feature configuration
109
+ has_feature_key = _has_feature_key(
110
+ configuration=configuration, feature_key=feature_key
111
+ )
112
+
113
+ # Read environment variable
114
+ env_var_setting = _get_env_bool(env_var)
115
+
116
+ # EXPERIMENTAL PATH:
117
+ # Resolution Hierarchy:
118
+ # 1. EXPERIMENTAL Environment Variable
119
+ # 2. Fail Fast if Feature Key present but EXPERIMENTAL Environment Variable is not enabled
120
+ if "EXPERIMENTAL" in env_var:
121
+ # Fail Fast if feature key present but experimental environment variable is not enabled
122
+ if env_var_setting is not True and has_feature_key:
123
+ raise FeatureGatingError(
124
+ f"Experimental feature requires {env_var} to be set to 'true' to use programmatic configuration."
125
+ )
126
+
127
+ return bool(env_var_setting)
128
+
129
+ # GENERAL AVAILABILITY PATH:
130
+ # Resolution Hierarchy:
131
+ # 1. Programmatic Configuration (Feature Key)
132
+ # 2. Environment Variable
133
+
134
+ # Check Programmatic Configuration
135
+ if has_feature_key:
136
+ return True
137
+
138
+ # Check Environment Variable
139
+ return bool(env_var_setting)
@@ -30,14 +30,37 @@ ParsedVersion = Tuple[int, ...]
30
30
  # about deprecated and unsupported versions.
31
31
  DependencyConstraint = namedtuple(
32
32
  "DependencyConstraint",
33
- ["package_name", "minimum_fully_supported_version", "recommended_version"],
33
+ [
34
+ "package_name",
35
+ "minimum_fully_supported_version",
36
+ "recommended_version",
37
+ "message_template",
38
+ ],
39
+ defaults=(None, None),
34
40
  )
41
+
42
+ PQC_GRPC_WARNING_TEMPLATE = (
43
+ "Package {consumer_package} depends on {dependency_package}, currently installed at version {version_used_string}. "
44
+ "grpcio < 1.83.0 does not support Post-Quantum Cryptography (PQC). "
45
+ "Support for non-PQC environments is deprecated. In October 2026, "
46
+ "Google Cloud Python packages will raise their minimum requirements "
47
+ "(including google-api-core, grpcio, and grpcio-status) to enforce grpcio >= 1.83.0. "
48
+ "For more details on Google Cloud's post-quantum security migration, visit: "
49
+ "https://cloud.google.com/security/resources/post-quantum-cryptography"
50
+ )
51
+
35
52
  _PACKAGE_DEPENDENCY_WARNINGS = [
36
53
  DependencyConstraint(
37
54
  "google.protobuf",
38
- minimum_fully_supported_version="4.25.8",
55
+ minimum_fully_supported_version="6.33.5",
39
56
  recommended_version="6.x",
40
- )
57
+ ),
58
+ DependencyConstraint(
59
+ "grpcio",
60
+ minimum_fully_supported_version="1.83.0",
61
+ recommended_version="1.83.x",
62
+ message_template=PQC_GRPC_WARNING_TEMPLATE,
63
+ ),
41
64
  ]
42
65
 
43
66
 
@@ -50,7 +73,7 @@ UNKNOWN_VERSION_STRING = "--"
50
73
  def parse_version_to_tuple(version_string: str) -> ParsedVersion:
51
74
  """Safely converts a semantic version string to a comparable tuple of integers.
52
75
 
53
- Example: "4.25.8" -> (4, 25, 8)
76
+ Example: "6.33.5" -> (6, 33, 5)
54
77
  Ignores non-numeric parts and handles common version formats.
55
78
 
56
79
  Args:
@@ -162,7 +185,7 @@ def warn_deprecation_for_versions_less_than(
162
185
  ) = _get_distribution_and_import_packages(consumer_import_package)
163
186
 
164
187
  recommendation = (
165
- " (we recommend {recommended_version})" if recommended_version else ""
188
+ f" (we recommend {recommended_version})" if recommended_version else ""
166
189
  )
167
190
  message_template = message_template or _flatten_message(
168
191
  """
@@ -222,4 +245,5 @@ def check_dependency_versions(
222
245
  package_info.package_name,
223
246
  package_info.minimum_fully_supported_version,
224
247
  recommended_version=package_info.recommended_version,
248
+ message_template=package_info.message_template,
225
249
  )
@@ -20,6 +20,7 @@ compression, pagination, and long-running operations to gRPC methods.
20
20
 
21
21
  import enum
22
22
  import functools
23
+ from typing import List, Tuple
23
24
 
24
25
  from google.api_core import grpc_helpers
25
26
  from google.api_core.gapic_v1 import client_info
@@ -57,6 +58,52 @@ def _apply_decorators(func, decorators):
57
58
  return func
58
59
 
59
60
 
61
+ def _deduplicate_metadata_tokens(*headers: str | None) -> str:
62
+ """
63
+ Given one or more metadata payload strings, create a combined
64
+ string with deduplicated tokens, while preserving token order.
65
+
66
+ Inputs are expected to contain a set of metadata tokens separated by spaces
67
+ Example: `gl-python/3.14.0 grpc/1.76.0 gax/2.29.0 gapic/3.8.0 pb/6.33.4`
68
+
69
+ Args:
70
+ *headers: one or more metadata payload strings
71
+
72
+ Returns:
73
+ a single combined payload string
74
+ """
75
+ # Split all non-empty headers into individual tokens
76
+ token_list = " ".join(filter(None, headers)).split()
77
+ # Deduplicate while preserving order
78
+ return " ".join(dict.fromkeys(token_list))
79
+
80
+
81
+ def _extract_metrics_header(metadata) -> Tuple[str, List[Tuple[str, str]]]:
82
+ """Extract x-google-api-client header from metadata list.
83
+
84
+ Args:
85
+ metadata (Sequence[Tuple[str, str]]): The metadata to extract from.
86
+
87
+ Returns:
88
+ A tuple containing:
89
+ - a string representing the header value.
90
+ - A sequence of remaining metadata tuples.
91
+ """
92
+ if not metadata:
93
+ return "", []
94
+
95
+ key_to_find = client_info.METRICS_METADATA_KEY
96
+
97
+ metric_str = _deduplicate_metadata_tokens(
98
+ " ".join([v for k, v in metadata if k == key_to_find])
99
+ )
100
+ if not metric_str:
101
+ return "", list(metadata)
102
+
103
+ arbitrary_metadata = [item for item in metadata if item[0] != key_to_find]
104
+ return metric_str, arbitrary_metadata
105
+
106
+
60
107
  class _GapicCallable(object):
61
108
  """Callable that applies retry, timeout, and metadata logic.
62
109
 
@@ -90,7 +137,16 @@ class _GapicCallable(object):
90
137
  self._retry = retry
91
138
  self._timeout = timeout
92
139
  self._compression = compression
93
- self._metadata = metadata
140
+ # Pre-extract the x-goog-api-client header from the initialized metadata.
141
+ self._x_goog_api_client, remaining = _extract_metrics_header(metadata)
142
+ self._static_metadata = tuple(remaining)
143
+ if self._x_goog_api_client:
144
+ self._default_metadata = (
145
+ (client_info.METRICS_METADATA_KEY, self._x_goog_api_client),
146
+ *self._static_metadata,
147
+ )
148
+ else:
149
+ self._default_metadata = self._static_metadata
94
150
 
95
151
  def __call__(
96
152
  self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs
@@ -112,16 +168,21 @@ class _GapicCallable(object):
112
168
  # Apply all applicable decorators.
113
169
  wrapped_func = _apply_decorators(self._target, [retry, timeout])
114
170
 
115
- # Add the user agent metadata to the call.
116
- if self._metadata is not None:
117
- metadata = kwargs.get("metadata", [])
118
- # Due to the nature of invocation, None should be treated the same
119
- # as not specified.
120
- if metadata is None:
121
- metadata = []
122
- metadata = list(metadata)
123
- metadata.extend(self._metadata)
124
- kwargs["metadata"] = metadata
171
+ if user_metadata := kwargs.get("metadata"):
172
+ # Add the user agent metadata to the call.
173
+ final_metadata = list(self._static_metadata)
174
+ user_x_goog, remaining = _extract_metrics_header(user_metadata)
175
+
176
+ merged_header = _deduplicate_metadata_tokens(
177
+ self._x_goog_api_client, user_x_goog
178
+ )
179
+ if merged_header:
180
+ final_metadata.append((client_info.METRICS_METADATA_KEY, merged_header))
181
+ final_metadata.extend(remaining)
182
+ kwargs["metadata"] = final_metadata
183
+ elif self._default_metadata:
184
+ kwargs["metadata"] = self._default_metadata
185
+
125
186
  if self._compression is not None:
126
187
  kwargs["compression"] = compression
127
188
 
@@ -0,0 +1,100 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2026 Google LLC
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ """Helpers for preparing and structuring API requests.
18
+
19
+ This module provides utilities to preprocess request parameters and objects
20
+ before invoking API methods, such as automatically generating request IDs
21
+ if they are not already set.
22
+ """
23
+
24
+ import uuid
25
+ from typing import TYPE_CHECKING, Union
26
+
27
+ import google.protobuf.message
28
+
29
+ if TYPE_CHECKING: # pragma: NO COVER
30
+ import proto # type: ignore[import-untyped]
31
+
32
+
33
+ def setup_request_id(
34
+ request: Union[google.protobuf.message.Message, "proto.Message", dict, None],
35
+ field_name: str,
36
+ is_proto3_optional: bool,
37
+ ) -> None:
38
+ """Populate a UUID4 field in the request if it is not already set.
39
+
40
+ This helper is used to ensure request idempotency by automatically
41
+ generating a unique identifier (such as `request_id`) for requests
42
+ that support it. If a request is retried, the same identifier can be
43
+ sent on subsequent retries, allowing the server to recognize the retried
44
+ request and prevent duplicate processing (e.g., creating duplicate
45
+ resources).
46
+
47
+ Args:
48
+ request (Union[google.protobuf.message.Message, proto.Message, dict, None]): The
49
+ request object or dictionary.
50
+ field_name (str): The name of the field to populate (e.g., "request_id").
51
+ is_proto3_optional (bool): Whether the field supports explicit presence
52
+ (defined with `optional` in proto3 syntax). When True, empty strings ("")
53
+ are preserved as explicit user input per AIP-4235, and UUID auto-population
54
+ occurs only if the field is unset. When False, any empty or falsy value is
55
+ populated with a UUID.
56
+ """
57
+ if request is None:
58
+ return
59
+
60
+ # Evaluate whether the field is considered "unset" and needs auto-population.
61
+ #
62
+ # According to AIP-4235, optional request ID fields must be populated
63
+ # if and only if they have explicit presence (`is_proto3_optional=True`)
64
+ # and were not set by the user (i.e. unset). Explicitly provided empty
65
+ # strings ('') must be preserved when `is_proto3_optional=True`.
66
+ should_populate = False
67
+ if isinstance(request, dict):
68
+ if is_proto3_optional:
69
+ # Case 1a: Dictionary request with explicit presence (`is_proto3_optional=True`).
70
+ # Per AIP-4235, auto-populate only if the key is completely missing from
71
+ # the dictionary or its value is explicitly set to None.
72
+ # An explicit empty string ('') must NOT be overwritten.
73
+ should_populate = field_name not in request or request[field_name] is None
74
+ else:
75
+ # Case 1b: Dictionary request without explicit presence (`is_proto3_optional=False`).
76
+ # Auto-populate if the key is missing, None, or falsy (e.g., empty string '').
77
+ should_populate = not request.get(field_name)
78
+ else:
79
+ if is_proto3_optional:
80
+ # Case 2a: Proto request with explicit presence (`is_proto3_optional=True`)
81
+ # (proto-plus wrapper or pure protobuf message).
82
+ # Extract the protobuf from proto-plus if wrapped.
83
+ pure_pb: google.protobuf.message.Message = getattr(request, "_pb", request)
84
+ try:
85
+ should_populate = not pure_pb.HasField(field_name)
86
+ except (AttributeError, ValueError):
87
+ # Fall back if `HasField` fails or is unsupported.
88
+ should_populate = getattr(pure_pb, field_name, None) is None
89
+ else:
90
+ # Case 2b: Proto request without explicit presence (`is_proto3_optional=False`).
91
+ # Auto-populate if the field value is falsy (None or empty string '').
92
+ should_populate = not bool(getattr(request, field_name, False))
93
+
94
+ # If the field was found to be empty, set random id
95
+ if should_populate:
96
+ generated_id = str(uuid.uuid4())
97
+ if isinstance(request, dict):
98
+ request[field_name] = generated_id
99
+ else:
100
+ setattr(request, field_name, generated_id)
@@ -254,11 +254,20 @@ def _create_composite_credentials(
254
254
  request = google.auth.transport.requests.Request()
255
255
 
256
256
  # Create the metadata plugin for inserting the authorization header.
257
- metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin(
258
- credentials,
259
- request,
260
- default_host=default_host,
261
- )
257
+ try:
258
+ metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin(
259
+ credentials,
260
+ request,
261
+ default_host=default_host,
262
+ suppress_metrics_header=True,
263
+ )
264
+ except TypeError:
265
+ # Support older versions of google-auth that do not accept suppress_metrics_header
266
+ metadata_plugin = google.auth.transport.grpc.AuthMetadataPlugin(
267
+ credentials,
268
+ request,
269
+ default_host=default_host,
270
+ )
262
271
 
263
272
  # Create a set of grpc.CallCredentials using the metadata plugin.
264
273
  google_auth_credentials = grpc.metadata_call_credentials(metadata_plugin)
@@ -28,6 +28,7 @@ from __future__ import unicode_literals
28
28
  import copy
29
29
  import functools
30
30
  import re
31
+ import urllib.parse
31
32
  from collections import deque
32
33
 
33
34
  # Regular expression for extracting variable parts from a path template.
@@ -62,6 +63,41 @@ _SINGLE_SEGMENT_PATTERN = r"([^/]+)"
62
63
  _MULTI_SEGMENT_PATTERN = r"(.+)"
63
64
 
64
65
 
66
+ def _extract_and_validate_wildcards(
67
+ val: str, template_str: str | None, property_name: str = "property"
68
+ ) -> None:
69
+ """Extract and validate wildcard variables against path traversal.
70
+
71
+ Ensures that values matched against single-segment wildcards ('*') are
72
+ not '.' or '..', and values matched against multi-segment wildcards ('**')
73
+ do not contain '.' or '..' segments.
74
+
75
+ If the value does not match the sub-template structure, validation is
76
+ deferred to allow subsequent bindings to be evaluated.
77
+
78
+ Args:
79
+ val (str): The raw string value to validate.
80
+ template_str (str | None): The template string of the variable (e.g.
81
+ 'projects/*/locations/*').
82
+ property_name (str): The name of the property being validated, for use
83
+ in error messages.
84
+
85
+ Raises:
86
+ ValueError: If a wildcard value contains invalid dot segments.
87
+ """
88
+ tmpl = template_str or "*"
89
+ m = re.fullmatch(_generate_pattern_for_template(tmpl), val)
90
+ if m is not None:
91
+ groups = m.groups()
92
+ for g in groups:
93
+ if "." in g and any(seg in (".", "..") for seg in g.split("/")):
94
+ if "**" in tmpl or "/" in tmpl:
95
+ raise ValueError(
96
+ f"Value for {property_name} must not contain segments that are exactly . or .. ."
97
+ )
98
+ raise ValueError(f"Invalid value {g} for {property_name}.")
99
+
100
+
65
101
  def _expand_variable_match(positional_vars, named_vars, match):
66
102
  """Expand a matched variable with its value.
67
103
 
@@ -81,9 +117,13 @@ def _expand_variable_match(positional_vars, named_vars, match):
81
117
  """
82
118
  positional = match.group("positional")
83
119
  name = match.group("name")
120
+ template = match.group("template")
121
+
84
122
  if name is not None:
85
123
  try:
86
- return str(named_vars[name])
124
+ val = str(named_vars[name])
125
+ _extract_and_validate_wildcards(val, template, name)
126
+ return urllib.parse.quote(val, safe="/")
87
127
  except KeyError:
88
128
  raise ValueError(
89
129
  "Named variable '{}' not specified and needed by template "
@@ -91,7 +131,9 @@ def _expand_variable_match(positional_vars, named_vars, match):
91
131
  )
92
132
  elif positional is not None:
93
133
  try:
94
- return str(positional_vars.pop(0))
134
+ val = str(positional_vars.pop(0))
135
+ _extract_and_validate_wildcards(val, positional, "positional variable")
136
+ return urllib.parse.quote(val, safe="/")
95
137
  except IndexError:
96
138
  raise ValueError(
97
139
  "Positional variable not specified and needed by template "
@@ -159,6 +201,7 @@ def _replace_variable_with_pattern(match):
159
201
  raise ValueError("Unknown template expression {}".format(match.group(0)))
160
202
 
161
203
 
204
+ @functools.lru_cache(maxsize=256)
162
205
  def _generate_pattern_for_template(tmpl):
163
206
  """Generate a pattern that can validate a path template.
164
207
 
@@ -15,7 +15,14 @@
15
15
  """Helpers for rest transports."""
16
16
 
17
17
  import functools
18
+ import json
18
19
  import operator
20
+ from typing import Any, Dict, List, Optional, Tuple
21
+
22
+ from google.api_core import path_template
23
+ from google.protobuf import json_format
24
+
25
+ __all__ = ["flatten_query_params", "transcode", "transcode_request"]
19
26
 
20
27
 
21
28
  def flatten_query_params(obj, strict=False):
@@ -107,3 +114,64 @@ def _canonicalize(obj, strict=False):
107
114
  value = value.lower()
108
115
  return value
109
116
  return obj
117
+
118
+
119
+ def transcode_request(
120
+ http_options: List[Dict[str, str]],
121
+ request: Any,
122
+ required_fields_default_values: Optional[Dict[str, Any]] = None,
123
+ rest_numeric_enums: bool = False,
124
+ ) -> Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]:
125
+ """Transcodes a request into HTTP method, URI, body, and query parameters.
126
+
127
+ Args:
128
+ http_options (List[Dict[str, str]]): List of HTTP transcoding rules.
129
+ request (Any): The protobuf or proto-plus request message.
130
+ required_fields_default_values (Optional[Dict[str, Any]]): Dictionary
131
+ of required fields default values to merge into query parameters if missing.
132
+ rest_numeric_enums (bool): Whether to encode enums as integers.
133
+
134
+ Returns:
135
+ Tuple[Dict[str, Any], Optional[str], Dict[str, Any]]: A tuple containing:
136
+ - The raw transcoded request dictionary (containing keys like 'uri', 'method').
137
+ - The serialized request body JSON string, or None if no body.
138
+ - The query parameters dictionary.
139
+ """
140
+ if request is None:
141
+ raise TypeError("request cannot be None")
142
+
143
+ # Convert proto-plus message to its underlying protobuf message if needed
144
+ pb_request = getattr(request, "_pb", request)
145
+
146
+ transcoded_request = path_template.transcode(http_options, pb_request)
147
+
148
+ body_json = None
149
+ if transcoded_request.get("body") is not None:
150
+ body_json = json_format.MessageToJson(
151
+ transcoded_request["body"],
152
+ use_integers_for_enums=rest_numeric_enums,
153
+ )
154
+
155
+ query_params_json = {}
156
+ if transcoded_request.get("query_params") is not None:
157
+ query_params_json = json.loads(
158
+ json_format.MessageToJson(
159
+ transcoded_request["query_params"],
160
+ use_integers_for_enums=rest_numeric_enums,
161
+ )
162
+ )
163
+
164
+ # If required_fields_default_values is provided, we merge default values for missing
165
+ # required fields into the query parameters.
166
+ if required_fields_default_values:
167
+ for k, v in required_fields_default_values.items():
168
+ if k not in query_params_json:
169
+ query_params_json[k] = v
170
+
171
+ if rest_numeric_enums:
172
+ query_params_json["$alt"] = "json;enum-encoding=int"
173
+
174
+ return transcoded_request, body_json, query_params_json
175
+
176
+
177
+ transcode = transcode_request
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "2.33.0"
15
+ __version__ = "2.35.0"