google-api-core 2.34.0__tar.gz → 2.36.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 (137) hide show
  1. {google_api_core-2.34.0/google_api_core.egg-info → google_api_core-2.36.0}/PKG-INFO +6 -1
  2. google_api_core-2.36.0/google/api_core/_observability.py +135 -0
  3. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/_python_package_support.py +27 -3
  4. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/client_options.py +8 -0
  5. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/exceptions.py +2 -0
  6. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/grpc_helpers.py +68 -1
  7. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/path_template.py +57 -4
  8. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/rest_helpers.py +2 -1
  9. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/version.py +1 -1
  10. {google_api_core-2.34.0 → google_api_core-2.36.0/google_api_core.egg-info}/PKG-INFO +6 -1
  11. {google_api_core-2.34.0 → google_api_core-2.36.0}/google_api_core.egg-info/SOURCES.txt +2 -0
  12. {google_api_core-2.34.0 → google_api_core-2.36.0}/google_api_core.egg-info/requires.txt +7 -0
  13. {google_api_core-2.34.0 → google_api_core-2.36.0}/pyproject.toml +9 -0
  14. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/test_grpc_helpers_async.py +9 -3
  15. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_client_options.py +4 -0
  16. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_exceptions.py +25 -0
  17. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_feature_gating_helpers.py +1 -0
  18. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_grpc_helpers.py +109 -2
  19. google_api_core-2.36.0/tests/unit/test_observability.py +255 -0
  20. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_path_template.py +325 -0
  21. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_python_package_support.py +51 -3
  22. {google_api_core-2.34.0 → google_api_core-2.36.0}/LICENSE +0 -0
  23. {google_api_core-2.34.0 → google_api_core-2.36.0}/MANIFEST.in +0 -0
  24. {google_api_core-2.34.0 → google_api_core-2.36.0}/README.rst +0 -0
  25. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/__init__.py +0 -0
  26. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/_feature_gating_helpers.py +0 -0
  27. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/_python_version_support.py +0 -0
  28. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/_rest_streaming_base.py +0 -0
  29. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/bidi.py +0 -0
  30. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/bidi_async.py +0 -0
  31. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/bidi_base.py +0 -0
  32. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/client_info.py +0 -0
  33. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/client_logging.py +0 -0
  34. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/datetime_helpers.py +0 -0
  35. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/extended_operation.py +0 -0
  36. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/future/__init__.py +0 -0
  37. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/future/_helpers.py +0 -0
  38. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/future/async_future.py +0 -0
  39. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/future/base.py +0 -0
  40. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/future/polling.py +0 -0
  41. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/gapic_v1/__init__.py +0 -0
  42. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/gapic_v1/client_info.py +0 -0
  43. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/gapic_v1/config.py +0 -0
  44. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/gapic_v1/config_async.py +0 -0
  45. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/gapic_v1/method.py +0 -0
  46. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/gapic_v1/method_async.py +0 -0
  47. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/gapic_v1/requests.py +0 -0
  48. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/gapic_v1/routing_header.py +0 -0
  49. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/general_helpers.py +0 -0
  50. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/grpc_helpers_async.py +0 -0
  51. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/iam.py +0 -0
  52. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operation.py +0 -0
  53. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operation_async.py +0 -0
  54. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/__init__.py +0 -0
  55. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/abstract_operations_base_client.py +0 -0
  56. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/abstract_operations_client.py +0 -0
  57. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/operations_async_client.py +0 -0
  58. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/operations_client.py +0 -0
  59. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/operations_client_config.py +0 -0
  60. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/operations_rest_client_async.py +0 -0
  61. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/pagers.py +0 -0
  62. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/pagers_async.py +0 -0
  63. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/pagers_base.py +0 -0
  64. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/transports/__init__.py +0 -0
  65. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/transports/base.py +0 -0
  66. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/transports/rest.py +0 -0
  67. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/operations_v1/transports/rest_asyncio.py +0 -0
  68. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/page_iterator.py +0 -0
  69. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/page_iterator_async.py +0 -0
  70. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/protobuf_helpers.py +0 -0
  71. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/py.typed +0 -0
  72. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/rest_streaming.py +0 -0
  73. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/rest_streaming_async.py +0 -0
  74. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/retry/__init__.py +0 -0
  75. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/retry/retry_base.py +0 -0
  76. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/retry/retry_streaming.py +0 -0
  77. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/retry/retry_streaming_async.py +0 -0
  78. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/retry/retry_unary.py +0 -0
  79. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/retry/retry_unary_async.py +0 -0
  80. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/retry_async.py +0 -0
  81. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/timeout.py +0 -0
  82. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/universe.py +0 -0
  83. {google_api_core-2.34.0 → google_api_core-2.36.0}/google/api_core/version_header.py +0 -0
  84. {google_api_core-2.34.0 → google_api_core-2.36.0}/google_api_core.egg-info/dependency_links.txt +0 -0
  85. {google_api_core-2.34.0 → google_api_core-2.36.0}/google_api_core.egg-info/top_level.txt +0 -0
  86. {google_api_core-2.34.0 → google_api_core-2.36.0}/setup.cfg +0 -0
  87. {google_api_core-2.34.0 → google_api_core-2.36.0}/setup.py +0 -0
  88. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/__init__.py +0 -0
  89. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/__init__.py +0 -0
  90. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/future/__init__.py +0 -0
  91. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/future/test_async_future.py +0 -0
  92. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/gapic/test_config_async.py +0 -0
  93. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/gapic/test_method_async.py +0 -0
  94. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/operations_v1/__init__.py +0 -0
  95. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/operations_v1/test_operations_async_client.py +0 -0
  96. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/retry/__init__.py +0 -0
  97. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/retry/test_retry_streaming_async.py +0 -0
  98. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/retry/test_retry_unary_async.py +0 -0
  99. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/test_bidi_async.py +0 -0
  100. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/test_operation_async.py +0 -0
  101. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/test_page_iterator_async.py +0 -0
  102. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/asyncio/test_rest_streaming_async.py +0 -0
  103. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/conftest.py +0 -0
  104. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/helpers.py +0 -0
  105. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/__init__.py +0 -0
  106. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/future/__init__.py +0 -0
  107. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/future/test__helpers.py +0 -0
  108. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/future/test_polling.py +0 -0
  109. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/gapic/test_client_info.py +0 -0
  110. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/gapic/test_config.py +0 -0
  111. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/gapic/test_method.py +0 -0
  112. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/gapic/test_requests.py +0 -0
  113. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/gapic/test_routing_header.py +0 -0
  114. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/operations_v1/__init__.py +0 -0
  115. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/operations_v1/test_operations_client.py +0 -0
  116. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/operations_v1/test_operations_rest_client.py +0 -0
  117. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/retry/__init__.py +0 -0
  118. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/retry/test_retry_base.py +0 -0
  119. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/retry/test_retry_imports.py +0 -0
  120. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/retry/test_retry_streaming.py +0 -0
  121. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/retry/test_retry_unary.py +0 -0
  122. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_bidi.py +0 -0
  123. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_client_info.py +0 -0
  124. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_client_logging.py +0 -0
  125. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_datetime_helpers.py +0 -0
  126. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_extended_operation.py +0 -0
  127. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_iam.py +0 -0
  128. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_operation.py +0 -0
  129. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_packaging.py +0 -0
  130. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_page_iterator.py +0 -0
  131. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_protobuf_helpers.py +0 -0
  132. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_python_version_support.py +0 -0
  133. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_rest_helpers.py +1 -1
  134. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_rest_streaming.py +0 -0
  135. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_timeout.py +0 -0
  136. {google_api_core-2.34.0 → google_api_core-2.36.0}/tests/unit/test_universe.py +0 -0
  137. {google_api_core-2.34.0 → google_api_core-2.36.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.34.0
3
+ Version: 2.36.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
@@ -27,6 +27,7 @@ Requires-Dist: protobuf<8.0.0,>=6.33.5
27
27
  Requires-Dist: proto-plus<2.0.0,>=1.26.1
28
28
  Requires-Dist: google-auth<3.0.0,>=2.14.1
29
29
  Requires-Dist: requests<3.0.0,>=2.33.0
30
+ Requires-Dist: opentelemetry-api<2.0.0,>=1.44.0
30
31
  Provides-Extra: async-rest
31
32
  Requires-Dist: google-auth[aiohttp]<3.0.0,>=2.14.1; extra == "async-rest"
32
33
  Requires-Dist: aiohttp>=3.13.4; extra == "async-rest"
@@ -35,6 +36,10 @@ Requires-Dist: grpcio<2.0.0,>=1.59.0; extra == "grpc"
35
36
  Requires-Dist: grpcio<2.0.0,>=1.75.1; python_version >= "3.14" and extra == "grpc"
36
37
  Requires-Dist: grpcio-status<2.0.0,>=1.59.0; extra == "grpc"
37
38
  Requires-Dist: grpcio-status<2.0.0,>=1.75.1; python_version >= "3.14" and extra == "grpc"
39
+ Provides-Extra: tracing
40
+ Requires-Dist: opentelemetry-instrumentation-grpc<1.0.0,>=0.65b0; extra == "tracing"
41
+ Provides-Extra: testing
42
+ Requires-Dist: opentelemetry-sdk<2.0.0,>=1.44.0; extra == "testing"
38
43
  Dynamic: license-file
39
44
 
40
45
  Core Library for Google Client Libraries
@@ -0,0 +1,135 @@
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
+ """OpenTelemetry helpers for resolving and instantiating interceptors."""
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import TYPE_CHECKING, Any, Callable, Sequence
22
+
23
+ from google.api_core import _feature_gating_helpers
24
+ from google.api_core.client_options import ClientOptions
25
+
26
+ if TYPE_CHECKING:
27
+ # flake8: grpc, trace, and ClientInterceptor are imported only for static analysis and type annotations
28
+ # The `# noqa: F401` comment avoids flake8 "imported but not used" errors.
29
+ import grpc # noqa: F401
30
+ import opentelemetry.trace # noqa: F401
31
+
32
+ from google.api_core.grpc_helpers import ClientInterceptor # noqa: F401
33
+
34
+ _TRACER_PROVIDER = "tracer_provider"
35
+
36
+
37
+ def is_otel_capabilities_enabled(
38
+ client_options: ClientOptions | dict[str, Any] | None = None,
39
+ env_var: str = "GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED",
40
+ ) -> bool:
41
+ """Checks if OTel capabilities are enabled and installed.
42
+
43
+ Args:
44
+ client_options: The client options object or dictionary.
45
+ env_var: The environment variable to check for enablement.
46
+
47
+ Returns:
48
+ bool: True if enabled and installed, False otherwise.
49
+ """
50
+ is_tracing_enabled = _feature_gating_helpers.resolve_feature_flags(
51
+ env_var=env_var,
52
+ feature_key=_TRACER_PROVIDER,
53
+ configuration=client_options,
54
+ )
55
+
56
+ if is_tracing_enabled:
57
+ try:
58
+ import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] # noqa: F401
59
+
60
+ return True
61
+ except ImportError:
62
+ pass
63
+
64
+ return False
65
+
66
+
67
+ def _get_tracer_provider(
68
+ client_options: ClientOptions | dict[str, Any] | None = None,
69
+ ) -> opentelemetry.trace.TracerProvider | None:
70
+ """Extracts the OpenTelemetry tracer provider from client options if present.
71
+
72
+ Args:
73
+ client_options: The client options object or dictionary.
74
+
75
+ Returns:
76
+ opentelemetry.trace.TracerProvider | None: The tracer provider if present,
77
+ None otherwise.
78
+ """
79
+ if isinstance(client_options, dict):
80
+ return client_options.get(_TRACER_PROVIDER)
81
+ elif client_options is not None:
82
+ return getattr(client_options, _TRACER_PROVIDER, None)
83
+ return None
84
+
85
+
86
+ def get_otel_interceptor(
87
+ client_options: ClientOptions | dict[str, Any] | None = None,
88
+ ) -> Callable[[grpc.Channel], grpc.Channel] | None:
89
+ """Returns an interceptor callable that wraps a sync gRPC channel with OpenTelemetry tracing.
90
+
91
+ Args:
92
+ client_options: The client options object or dictionary used for feature gating
93
+ and extracting the tracer provider.
94
+
95
+ Returns:
96
+ Callable[[grpc.Channel], grpc.Channel] | None: An interceptor callable if OpenTelemetry
97
+ tracing is enabled and installed, None otherwise.
98
+ """
99
+ if not is_otel_capabilities_enabled(client_options):
100
+ return None
101
+
102
+ import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found]
103
+
104
+ interceptor: ClientInterceptor = otel_grpc.client_interceptor(
105
+ tracer_provider=_get_tracer_provider(client_options)
106
+ )
107
+
108
+ def otel_interceptor(channel: grpc.Channel) -> grpc.Channel:
109
+ return otel_grpc.intercept_channel(channel, interceptor)
110
+
111
+ return otel_interceptor
112
+
113
+
114
+ def get_otel_async_interceptor(
115
+ client_options: ClientOptions | dict[str, Any] | None = None,
116
+ ) -> Sequence[grpc.aio.ClientInterceptor] | None:
117
+ """Returns async gRPC client interceptors for OpenTelemetry tracing.
118
+
119
+ Args:
120
+ client_options: The client options object or dictionary used for feature gating
121
+ and extracting the tracer provider.
122
+
123
+ Returns:
124
+ Sequence[grpc.aio.ClientInterceptor] | None: Instantiated OpenTelemetry async
125
+ client interceptors if tracing is enabled and installed, None otherwise.
126
+ """
127
+ if not is_otel_capabilities_enabled(client_options):
128
+ return None
129
+
130
+ # Ignored by mypy: Optional dependency only loaded if early-return is skipped
131
+ import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found]
132
+
133
+ return otel_grpc.aio_client_interceptors(
134
+ tracer_provider=_get_tracer_provider(client_options)
135
+ )
@@ -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
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
 
@@ -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
  )
@@ -48,9 +48,13 @@ You can also pass a mapping object.
48
48
 
49
49
  """
50
50
 
51
+ import typing
51
52
  import warnings
52
53
  from typing import Callable, Mapping, Optional, Sequence, Tuple
53
54
 
55
+ if typing.TYPE_CHECKING:
56
+ import opentelemetry.trace
57
+
54
58
  from google.api_core import general_helpers
55
59
 
56
60
 
@@ -98,6 +102,8 @@ class ClientOptions(object):
98
102
  `googleapis.com`. If both `api_endpoint` and `universe_domain` are set,
99
103
  then `api_endpoint` is used as the service endpoint. If `api_endpoint` is
100
104
  not specified, the format will be `{service}.{universe_domain}`.
105
+ tracer_provider (Optional["opentelemetry.trace.TracerProvider"]): The OpenTelemetry tracer provider to use
106
+ for tracing in supported libraries.
101
107
 
102
108
  Raises:
103
109
  ValueError: If both ``client_cert_source`` and ``client_encrypted_cert_source``
@@ -117,6 +123,7 @@ class ClientOptions(object):
117
123
  api_key: Optional[str] = None,
118
124
  api_audience: Optional[str] = None,
119
125
  universe_domain: Optional[str] = None,
126
+ tracer_provider: Optional["opentelemetry.trace.TracerProvider"] = None,
120
127
  ):
121
128
  if credentials_file is not None:
122
129
  warnings.warn(general_helpers._CREDENTIALS_FILE_WARNING, DeprecationWarning)
@@ -136,6 +143,7 @@ class ClientOptions(object):
136
143
  self.api_key = api_key
137
144
  self.api_audience = api_audience
138
145
  self.universe_domain = universe_domain
146
+ self.tracer_provider = tracer_provider
139
147
 
140
148
  def __repr__(self) -> str:
141
149
  return "ClientOptions: " + repr(self.__dict__)
@@ -510,6 +510,8 @@ def format_http_response_error(
510
510
  :class:`GoogleAPICallError`, with the message and errors populated
511
511
  from the response.
512
512
  """
513
+ if isinstance(payload, list):
514
+ payload = next((item for item in payload if isinstance(item, dict)), {})
513
515
  payload = {} if not payload else payload
514
516
  error_message = payload.get("error", {}).get("message", "unknown error")
515
517
  errors = payload.get("error", {}).get("errors", ())
@@ -17,7 +17,17 @@
17
17
  import collections
18
18
  import functools
19
19
  import warnings
20
- from typing import Generic, Iterator, Optional, TypeVar
20
+ from typing import (
21
+ Callable,
22
+ Generic,
23
+ Iterator,
24
+ Optional,
25
+ Sequence,
26
+ TypeAlias,
27
+ TypeVar,
28
+ cast,
29
+ get_args,
30
+ )
21
31
 
22
32
  import google.auth
23
33
  import google.auth.credentials
@@ -34,6 +44,17 @@ _STREAM_WRAP_CLASSES = (grpc.UnaryStreamMultiCallable, grpc.StreamStreamMultiCal
34
44
  # denotes the proto response type for grpc calls
35
45
  P = TypeVar("P")
36
46
 
47
+ # Type alias representing any client-side gRPC interceptor
48
+ ClientInterceptor: TypeAlias = (
49
+ grpc.UnaryUnaryClientInterceptor
50
+ | grpc.UnaryStreamClientInterceptor
51
+ | grpc.StreamUnaryClientInterceptor
52
+ | grpc.StreamStreamClientInterceptor
53
+ )
54
+
55
+ # Runtime tuple of gRPC client interceptor base classes for isinstance checks
56
+ _CLIENT_INTERCEPTOR_CLASSES = get_args(ClientInterceptor)
57
+
37
58
 
38
59
  def _patch_callable_name(callable_):
39
60
  """Fix-up gRPC callable attributes.
@@ -419,6 +440,52 @@ def _modify_target_for_direct_path(target: str) -> str:
419
440
  return target
420
441
 
421
442
 
443
+ def apply_channel_interceptors(
444
+ channel: grpc.Channel,
445
+ interceptors: (
446
+ Sequence[ClientInterceptor | Callable[[grpc.Channel], grpc.Channel]] | None
447
+ ) = None,
448
+ ) -> grpc.Channel:
449
+ """Applies client interceptors or channel-intercepting callables to a gRPC channel.
450
+
451
+ Executes in reverse order so the first interceptor in the sequence becomes the
452
+ outermost layer on outbound requests and the innermost layer on inbound responses,
453
+ aligning with the behavior of ``grpc.intercept_channel``.
454
+
455
+ Args:
456
+ channel (grpc.Channel): The channel to intercept.
457
+ interceptors (Optional[Sequence[Union[ClientInterceptor, Callable[[grpc.Channel], grpc.Channel]]]]):
458
+ Additional interceptors (or callables that apply interceptors) to apply to the gRPC channel.
459
+
460
+ Returns:
461
+ grpc.Channel: The intercepted channel, or the original channel if no
462
+ interceptors were provided.
463
+
464
+ Raises:
465
+ TypeError: If an item in ``interceptors`` is neither a gRPC ClientInterceptor
466
+ nor a Callable[[Channel], Channel].
467
+ """
468
+ if not interceptors:
469
+ return channel
470
+
471
+ modified_channel = channel
472
+ # Reverse the inputs to align with the behavior of grpc.create_channel(*interceptors)
473
+ for interceptor in reversed(list(interceptors)):
474
+ if isinstance(interceptor, _CLIENT_INTERCEPTOR_CLASSES):
475
+ modified_channel = grpc.intercept_channel(modified_channel, interceptor)
476
+ elif callable(interceptor):
477
+ interceptor_callable = cast(
478
+ Callable[[grpc.Channel], grpc.Channel], interceptor
479
+ )
480
+ modified_channel = interceptor_callable(modified_channel)
481
+ else:
482
+ raise TypeError(
483
+ f"Expected ClientInterceptor or Callable[[Channel], Channel], got {type(interceptor).__name__}"
484
+ )
485
+
486
+ return modified_channel
487
+
488
+
422
489
  _MethodCall = collections.namedtuple(
423
490
  "_MethodCall", ("request", "timeout", "metadata", "credentials", "compression")
424
491
  )
@@ -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 val
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 val
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
 
@@ -172,12 +215,17 @@ def _generate_pattern_for_template(tmpl):
172
215
  return _VARIABLE_RE.sub(_replace_variable_with_pattern, tmpl)
173
216
 
174
217
 
175
- def get_field(request, field):
218
+ def get_field(request, field, encode=False):
176
219
  """Get the value of a field from a given dictionary.
177
220
 
178
221
  Args:
179
222
  request (dict | Message): A dictionary or a Message object.
180
223
  field (str): The key to the request in dot notation.
224
+ encode (bool): Whether to percent-encode the field value. If enabled,
225
+ will encode all characters except `[-_.~/0-9a-zA-Z]` for URI path
226
+ variable parts per
227
+ https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L44-L312.
228
+ Defaults to False.
181
229
 
182
230
  Returns:
183
231
  The value of the field.
@@ -192,6 +240,8 @@ def get_field(request, field):
192
240
  value = value.get(part)
193
241
  if isinstance(value, dict):
194
242
  return
243
+ if encode and value is not None:
244
+ return urllib.parse.quote(str(value), safe="/")
195
245
  return value
196
246
 
197
247
 
@@ -284,7 +334,10 @@ def transcode(http_options, message=None, **request_kwargs):
284
334
  ]
285
335
  bindings.append((uri_template, fields))
286
336
 
287
- path_args = {field: get_field(transcoded_value, field) for field, _ in fields}
337
+ path_args = {
338
+ field: get_field(transcoded_value, field, encode=True)
339
+ for field, _ in fields
340
+ }
288
341
  request["uri"] = expand(uri_template, **path_args)
289
342
 
290
343
  if not validate(uri_template, request["uri"]) or not all(path_args.values()):
@@ -19,9 +19,10 @@ import json
19
19
  import operator
20
20
  from typing import Any, Dict, List, Optional, Tuple
21
21
 
22
- from google.api_core import path_template
23
22
  from google.protobuf import json_format
24
23
 
24
+ from google.api_core import path_template
25
+
25
26
  __all__ = ["flatten_query_params", "transcode", "transcode_request"]
26
27
 
27
28
 
@@ -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.34.0"
15
+ __version__ = "2.36.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-api-core
3
- Version: 2.34.0
3
+ Version: 2.36.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
@@ -27,6 +27,7 @@ Requires-Dist: protobuf<8.0.0,>=6.33.5
27
27
  Requires-Dist: proto-plus<2.0.0,>=1.26.1
28
28
  Requires-Dist: google-auth<3.0.0,>=2.14.1
29
29
  Requires-Dist: requests<3.0.0,>=2.33.0
30
+ Requires-Dist: opentelemetry-api<2.0.0,>=1.44.0
30
31
  Provides-Extra: async-rest
31
32
  Requires-Dist: google-auth[aiohttp]<3.0.0,>=2.14.1; extra == "async-rest"
32
33
  Requires-Dist: aiohttp>=3.13.4; extra == "async-rest"
@@ -35,6 +36,10 @@ Requires-Dist: grpcio<2.0.0,>=1.59.0; extra == "grpc"
35
36
  Requires-Dist: grpcio<2.0.0,>=1.75.1; python_version >= "3.14" and extra == "grpc"
36
37
  Requires-Dist: grpcio-status<2.0.0,>=1.59.0; extra == "grpc"
37
38
  Requires-Dist: grpcio-status<2.0.0,>=1.75.1; python_version >= "3.14" and extra == "grpc"
39
+ Provides-Extra: tracing
40
+ Requires-Dist: opentelemetry-instrumentation-grpc<1.0.0,>=0.65b0; extra == "tracing"
41
+ Provides-Extra: testing
42
+ Requires-Dist: opentelemetry-sdk<2.0.0,>=1.44.0; extra == "testing"
38
43
  Dynamic: license-file
39
44
 
40
45
  Core Library for Google Client Libraries
@@ -6,6 +6,7 @@ setup.cfg
6
6
  setup.py
7
7
  google/api_core/__init__.py
8
8
  google/api_core/_feature_gating_helpers.py
9
+ google/api_core/_observability.py
9
10
  google/api_core/_python_package_support.py
10
11
  google/api_core/_python_version_support.py
11
12
  google/api_core/_rest_streaming_base.py
@@ -104,6 +105,7 @@ tests/unit/test_extended_operation.py
104
105
  tests/unit/test_feature_gating_helpers.py
105
106
  tests/unit/test_grpc_helpers.py
106
107
  tests/unit/test_iam.py
108
+ tests/unit/test_observability.py
107
109
  tests/unit/test_operation.py
108
110
  tests/unit/test_packaging.py
109
111
  tests/unit/test_page_iterator.py
@@ -3,6 +3,7 @@ protobuf<8.0.0,>=6.33.5
3
3
  proto-plus<2.0.0,>=1.26.1
4
4
  google-auth<3.0.0,>=2.14.1
5
5
  requests<3.0.0,>=2.33.0
6
+ opentelemetry-api<2.0.0,>=1.44.0
6
7
 
7
8
  [async_rest]
8
9
  google-auth[aiohttp]<3.0.0,>=2.14.1
@@ -15,3 +16,9 @@ grpcio-status<2.0.0,>=1.59.0
15
16
  [grpc:python_version >= "3.14"]
16
17
  grpcio<2.0.0,>=1.75.1
17
18
  grpcio-status<2.0.0,>=1.75.1
19
+
20
+ [testing]
21
+ opentelemetry-sdk<2.0.0,>=1.44.0
22
+
23
+ [tracing]
24
+ opentelemetry-instrumentation-grpc<1.0.0,>=0.65b0
@@ -48,6 +48,7 @@ dependencies = [
48
48
  "proto-plus >= 1.26.1, < 2.0.0",
49
49
  "google-auth >= 2.14.1, < 3.0.0",
50
50
  "requests >= 2.33.0, < 3.0.0",
51
+ "opentelemetry-api >= 1.44.0, < 2.0.0",
51
52
  ]
52
53
  dynamic = ["version"]
53
54
 
@@ -64,6 +65,12 @@ grpc = [
64
65
  "grpcio-status >= 1.59.0, < 2.0.0",
65
66
  "grpcio-status >= 1.75.1, < 2.0.0; python_version >= '3.14'",
66
67
  ]
68
+ tracing = [
69
+ "opentelemetry-instrumentation-grpc >= 0.65b0, < 1.0.0",
70
+ ]
71
+ testing = [
72
+ "opentelemetry-sdk >= 1.44.0, < 2.0.0",
73
+ ]
67
74
 
68
75
 
69
76
  [tool.setuptools.dynamic]
@@ -85,6 +92,8 @@ filterwarnings = [
85
92
  "error",
86
93
  # Prevent Python version warnings from interfering with tests
87
94
  "ignore:.* Python version .*:FutureWarning",
95
+ # TODO: Remove once b/544782314 is resolved (when grpcio >= 1.83.0 is enforced)
96
+ "ignore:.*does not support Post-Quantum Cryptography.*:FutureWarning",
88
97
  # Remove once https://github.com/pytest-dev/pytest-cov/issues/621 is fixed
89
98
  "ignore:.*The --rsyncdir command line argument and rsyncdirs config variable are deprecated:DeprecationWarning",
90
99
  # Remove once https://github.com/protocolbuffers/protobuf/issues/12186 is fixed
@@ -421,9 +421,15 @@ def test_create_channel_implicit_with_default_host(
421
421
  assert channel is grpc_secure_channel.return_value
422
422
 
423
423
  google_auth_default.assert_called_once_with(scopes=None, default_scopes=None)
424
- auth_metadata_plugin.assert_called_once_with(
425
- mock.sentinel.credentials, mock.sentinel.Request, default_host=default_host
426
- )
424
+ assert auth_metadata_plugin.call_count == 1
425
+ assert auth_metadata_plugin.call_args.args == (
426
+ mock.sentinel.credentials,
427
+ mock.sentinel.Request,
428
+ )
429
+ assert auth_metadata_plugin.call_args.kwargs["default_host"] == default_host
430
+ # google-auth >= 2.56.3 supports suppress_metrics_header; older versions omit it during fallback
431
+ if "suppress_metrics_header" in auth_metadata_plugin.call_args.kwargs:
432
+ assert auth_metadata_plugin.call_args.kwargs["suppress_metrics_header"] is True
427
433
  grpc_secure_channel.assert_called_once_with(
428
434
  expected_target, composite_creds, compression=None
429
435
  )
@@ -30,6 +30,7 @@ def get_client_encrypted_cert():
30
30
 
31
31
 
32
32
  def test_constructor():
33
+ mock_tracer_provider = object()
33
34
  with warn_deprecated_credentials_file():
34
35
  options = client_options.ClientOptions(
35
36
  api_endpoint="foo.googleapis.com",
@@ -42,6 +43,7 @@ def test_constructor():
42
43
  ],
43
44
  api_audience="foo2.googleapis.com",
44
45
  universe_domain="googleapis.com",
46
+ tracer_provider=mock_tracer_provider,
45
47
  )
46
48
 
47
49
  assert options.api_endpoint == "foo.googleapis.com"
@@ -54,6 +56,7 @@ def test_constructor():
54
56
  ]
55
57
  assert options.api_audience == "foo2.googleapis.com"
56
58
  assert options.universe_domain == "googleapis.com"
59
+ assert options.tracer_provider is mock_tracer_provider
57
60
 
58
61
 
59
62
  def test_constructor_with_encrypted_cert_source():
@@ -162,6 +165,7 @@ def test_repr():
162
165
  "scopes",
163
166
  "api_key",
164
167
  "api_audience",
168
+ "tracer_provider",
165
169
  ]
166
170
  )
167
171
  options = client_options.ClientOptions(api_endpoint="foo.googleapis.com")
@@ -132,6 +132,31 @@ def test_from_http_response_json_content():
132
132
  assert exception.errors == ["1", "2"]
133
133
 
134
134
 
135
+ def test_from_http_response_json_list_content():
136
+ response = make_response(
137
+ json.dumps(
138
+ [{"error": {"message": "json message", "errors": ["1", "2"]}}]
139
+ ).encode("utf-8")
140
+ )
141
+
142
+ exception = exceptions.from_http_response(response)
143
+
144
+ assert isinstance(exception, exceptions.NotFound)
145
+ assert exception.code == http.client.NOT_FOUND
146
+ assert exception.message == "POST https://example.com/: json message"
147
+ assert exception.errors == ["1", "2"]
148
+
149
+
150
+ def test_from_http_response_json_list_content_without_error_dict():
151
+ response = make_response(json.dumps(["error message"]).encode("utf-8"))
152
+
153
+ exception = exceptions.from_http_response(response)
154
+
155
+ assert isinstance(exception, exceptions.NotFound)
156
+ assert exception.code == http.client.NOT_FOUND
157
+ assert exception.message == "POST https://example.com/: unknown error"
158
+
159
+
135
160
  def test_from_http_response_bad_json_content():
136
161
  response = make_response(json.dumps({"meep": "moop"}).encode("utf-8"))
137
162