google-api-core 2.36.0__tar.gz → 2.37.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 (139) hide show
  1. {google_api_core-2.36.0/google_api_core.egg-info → google_api_core-2.37.0}/PKG-INFO +1 -1
  2. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/method.py +222 -6
  3. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/version.py +1 -1
  4. {google_api_core-2.36.0 → google_api_core-2.37.0/google_api_core.egg-info}/PKG-INFO +1 -1
  5. google_api_core-2.37.0/tests/conftest.py +66 -0
  6. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/helpers.py +25 -0
  7. google_api_core-2.37.0/tests/unit/gapic/test_method.py +908 -0
  8. google_api_core-2.36.0/tests/conftest.py +0 -31
  9. google_api_core-2.36.0/tests/unit/gapic/test_method.py +0 -348
  10. {google_api_core-2.36.0 → google_api_core-2.37.0}/LICENSE +0 -0
  11. {google_api_core-2.36.0 → google_api_core-2.37.0}/MANIFEST.in +0 -0
  12. {google_api_core-2.36.0 → google_api_core-2.37.0}/README.rst +0 -0
  13. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/__init__.py +0 -0
  14. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/_feature_gating_helpers.py +0 -0
  15. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/_observability.py +0 -0
  16. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/_python_package_support.py +0 -0
  17. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/_python_version_support.py +0 -0
  18. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/_rest_streaming_base.py +0 -0
  19. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/bidi.py +0 -0
  20. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/bidi_async.py +0 -0
  21. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/bidi_base.py +0 -0
  22. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/client_info.py +0 -0
  23. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/client_logging.py +0 -0
  24. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/client_options.py +0 -0
  25. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/datetime_helpers.py +0 -0
  26. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/exceptions.py +0 -0
  27. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/extended_operation.py +0 -0
  28. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/future/__init__.py +0 -0
  29. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/future/_helpers.py +0 -0
  30. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/future/async_future.py +0 -0
  31. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/future/base.py +0 -0
  32. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/future/polling.py +0 -0
  33. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/__init__.py +0 -0
  34. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/client_info.py +0 -0
  35. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/config.py +0 -0
  36. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/config_async.py +0 -0
  37. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/method_async.py +0 -0
  38. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/requests.py +0 -0
  39. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/routing_header.py +0 -0
  40. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/general_helpers.py +0 -0
  41. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/grpc_helpers.py +0 -0
  42. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/grpc_helpers_async.py +0 -0
  43. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/iam.py +0 -0
  44. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operation.py +0 -0
  45. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operation_async.py +0 -0
  46. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/__init__.py +0 -0
  47. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/abstract_operations_base_client.py +0 -0
  48. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/abstract_operations_client.py +0 -0
  49. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/operations_async_client.py +0 -0
  50. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/operations_client.py +0 -0
  51. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/operations_client_config.py +0 -0
  52. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/operations_rest_client_async.py +0 -0
  53. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/pagers.py +0 -0
  54. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/pagers_async.py +0 -0
  55. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/pagers_base.py +0 -0
  56. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/transports/__init__.py +0 -0
  57. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/transports/base.py +0 -0
  58. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/transports/rest.py +0 -0
  59. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/operations_v1/transports/rest_asyncio.py +0 -0
  60. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/page_iterator.py +0 -0
  61. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/page_iterator_async.py +0 -0
  62. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/path_template.py +0 -0
  63. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/protobuf_helpers.py +0 -0
  64. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/py.typed +0 -0
  65. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/rest_helpers.py +0 -0
  66. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/rest_streaming.py +0 -0
  67. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/rest_streaming_async.py +0 -0
  68. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/retry/__init__.py +0 -0
  69. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/retry/retry_base.py +0 -0
  70. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/retry/retry_streaming.py +0 -0
  71. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/retry/retry_streaming_async.py +0 -0
  72. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/retry/retry_unary.py +0 -0
  73. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/retry/retry_unary_async.py +0 -0
  74. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/retry_async.py +0 -0
  75. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/timeout.py +0 -0
  76. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/universe.py +0 -0
  77. {google_api_core-2.36.0 → google_api_core-2.37.0}/google/api_core/version_header.py +0 -0
  78. {google_api_core-2.36.0 → google_api_core-2.37.0}/google_api_core.egg-info/SOURCES.txt +0 -0
  79. {google_api_core-2.36.0 → google_api_core-2.37.0}/google_api_core.egg-info/dependency_links.txt +0 -0
  80. {google_api_core-2.36.0 → google_api_core-2.37.0}/google_api_core.egg-info/requires.txt +0 -0
  81. {google_api_core-2.36.0 → google_api_core-2.37.0}/google_api_core.egg-info/top_level.txt +0 -0
  82. {google_api_core-2.36.0 → google_api_core-2.37.0}/pyproject.toml +0 -0
  83. {google_api_core-2.36.0 → google_api_core-2.37.0}/setup.cfg +0 -0
  84. {google_api_core-2.36.0 → google_api_core-2.37.0}/setup.py +0 -0
  85. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/__init__.py +0 -0
  86. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/__init__.py +0 -0
  87. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/future/__init__.py +0 -0
  88. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/future/test_async_future.py +0 -0
  89. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/gapic/test_config_async.py +0 -0
  90. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/gapic/test_method_async.py +0 -0
  91. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/operations_v1/__init__.py +0 -0
  92. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/operations_v1/test_operations_async_client.py +0 -0
  93. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/retry/__init__.py +0 -0
  94. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/retry/test_retry_streaming_async.py +0 -0
  95. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/retry/test_retry_unary_async.py +0 -0
  96. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/test_bidi_async.py +0 -0
  97. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/test_grpc_helpers_async.py +0 -0
  98. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/test_operation_async.py +0 -0
  99. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/test_page_iterator_async.py +0 -0
  100. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/asyncio/test_rest_streaming_async.py +0 -0
  101. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/__init__.py +0 -0
  102. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/future/__init__.py +0 -0
  103. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/future/test__helpers.py +0 -0
  104. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/future/test_polling.py +0 -0
  105. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/gapic/test_client_info.py +0 -0
  106. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/gapic/test_config.py +0 -0
  107. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/gapic/test_requests.py +0 -0
  108. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/gapic/test_routing_header.py +0 -0
  109. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/operations_v1/__init__.py +0 -0
  110. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/operations_v1/test_operations_client.py +0 -0
  111. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/operations_v1/test_operations_rest_client.py +0 -0
  112. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/retry/__init__.py +0 -0
  113. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/retry/test_retry_base.py +0 -0
  114. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/retry/test_retry_imports.py +0 -0
  115. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/retry/test_retry_streaming.py +0 -0
  116. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/retry/test_retry_unary.py +0 -0
  117. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_bidi.py +0 -0
  118. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_client_info.py +0 -0
  119. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_client_logging.py +0 -0
  120. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_client_options.py +0 -0
  121. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_datetime_helpers.py +0 -0
  122. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_exceptions.py +0 -0
  123. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_extended_operation.py +0 -0
  124. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_feature_gating_helpers.py +0 -0
  125. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_grpc_helpers.py +0 -0
  126. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_iam.py +0 -0
  127. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_observability.py +0 -0
  128. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_operation.py +0 -0
  129. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_packaging.py +0 -0
  130. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_page_iterator.py +0 -0
  131. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_path_template.py +0 -0
  132. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_protobuf_helpers.py +0 -0
  133. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_python_package_support.py +0 -0
  134. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_python_version_support.py +0 -0
  135. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_rest_helpers.py +0 -0
  136. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_rest_streaming.py +0 -0
  137. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_timeout.py +0 -0
  138. {google_api_core-2.36.0 → google_api_core-2.37.0}/tests/unit/test_universe.py +0 -0
  139. {google_api_core-2.36.0 → google_api_core-2.37.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.36.0
3
+ Version: 2.37.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
@@ -18,12 +18,14 @@ This is used by gapic clients to provide common error mapping, retry, timeout,
18
18
  compression, pagination, and long-running operations to gRPC methods.
19
19
  """
20
20
 
21
+ import contextlib
21
22
  import enum
22
23
  import functools
23
- from typing import List, Tuple
24
+ from typing import Any, List, Optional, Tuple
24
25
 
25
- from google.api_core import grpc_helpers
26
+ from google.api_core import _observability, grpc_helpers
26
27
  from google.api_core.gapic_v1 import client_info
28
+ from google.api_core.gapic_v1.client_info import METRICS_METADATA_KEY
27
29
  from google.api_core.timeout import TimeToDeadlineTimeout
28
30
 
29
31
  USE_DEFAULT_METADATA = object()
@@ -92,7 +94,7 @@ def _extract_metrics_header(metadata) -> Tuple[str, List[Tuple[str, str]]]:
92
94
  if not metadata:
93
95
  return "", []
94
96
 
95
- key_to_find = client_info.METRICS_METADATA_KEY
97
+ key_to_find = METRICS_METADATA_KEY
96
98
 
97
99
  metric_str = _deduplicate_metadata_tokens(
98
100
  " ".join([v for k, v in metadata if k == key_to_find])
@@ -104,6 +106,124 @@ def _extract_metrics_header(metadata) -> Tuple[str, List[Tuple[str, str]]]:
104
106
  return metric_str, arbitrary_metadata
105
107
 
106
108
 
109
+ def _extract_rpc_identity(
110
+ method_name: str,
111
+ ) -> Tuple[str, str, str]:
112
+ """Extract (full_rpc_name, service_name, rpc_method_name) from an explicit method name.
113
+
114
+ Args:
115
+ method_name: Explicit RPC name (e.g. "/google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion").
116
+
117
+ Returns:
118
+ Tuple[str, str, str]: A 3-tuple of (full_rpc_name, service_name, rpc_method_name).
119
+ """
120
+ method_str = method_name.lstrip("/")
121
+ service, _, method = method_str.rpartition("/")
122
+ return method_str, service, method
123
+
124
+
125
+ def _extract_status_code(exc: Optional[Exception]) -> str:
126
+ """Extract canonical status code name string from an exception.
127
+
128
+ Status code name strings are resolved by inspecting the following locations:
129
+ * Chained exceptions: Unwraps RetryError or __cause__ to the root exception.
130
+ * Enum & code attributes: Inspects .grpc_status_code on GoogleAPICallError or .code on gRPC errors.
131
+ * Integer status codes: Maps raw gRPC integer status codes to canonical enum names.
132
+ * Fallback: Defaults to the exception class name for standard Python errors.
133
+
134
+ Args:
135
+ exc (Optional[Exception]): The exception to extract the status code name from.
136
+
137
+ Returns:
138
+ str: The canonical status code name (e.g. "NOT_FOUND", "UNAVAILABLE") or class name.
139
+ """
140
+ if exc is None:
141
+ return ""
142
+
143
+ # 1. Unwrap chained exceptions: unwrap RetryError or __cause__ to the root failure
144
+ target = getattr(exc, "cause", None) or getattr(exc, "__cause__", None) or exc
145
+
146
+ # 2. Check enum & code attributes: .grpc_status_code enum or callable/non-callable .code
147
+ status = getattr(target, "grpc_status_code", None)
148
+ if status is None and hasattr(target, "code"):
149
+ try:
150
+ status = target.code() if callable(target.code) else target.code
151
+ except Exception:
152
+ status = None
153
+
154
+ name = getattr(status, "name", None)
155
+ if name:
156
+ return str(name)
157
+
158
+ # 3. Check integer status codes: map raw gRPC integer status codes to canonical enum names
159
+ if isinstance(status, int):
160
+ from google.api_core import exceptions
161
+
162
+ status = exceptions._INT_TO_GRPC_CODE.get(status, status)
163
+ return getattr(status, "name", str(status))
164
+
165
+ # 4. Fallback: default to the exception class name for standard Python errors
166
+ return target.__class__.__name__
167
+
168
+
169
+ def _extract_error_attributes(exc: Optional[Exception]) -> dict[str, Any]:
170
+ """Extract gcp.errors.* and error.type attributes from an exception.
171
+
172
+ Error details and ErrorInfo structures are resolved by inspecting the following locations:
173
+ * Chained exceptions: Unwraps RetryError or __cause__ to the root exception.
174
+ * GoogleAPICallError attributes: Reads ErrorInfo from ._error_info or .error_info.
175
+ * Native gRPC trailing metadata: Parses google.rpc.Status binary details from trailing_metadata.
176
+ * Unified attribute extraction: Extracts domain, reason, and metadata from ErrorInfo or exception attributes.
177
+
178
+ Args:
179
+ exc (Optional[Exception]): An exception (such as GoogleAPICallError or grpc.RpcError) or ErrorInfo object.
180
+
181
+ Returns:
182
+ dict[str, Any]: Extracted error attributes (e.g. gcp.errors.domain, error.type, gcp.errors.metadata.*).
183
+ """
184
+ attrs: dict[str, Any] = {}
185
+ if exc is None:
186
+ return attrs
187
+
188
+ # 1. Unwrap chained exceptions: unwrap RetryError or __cause__ to the root failure
189
+ target_exc = getattr(exc, "cause", None) or getattr(exc, "__cause__", None) or exc
190
+
191
+ # 2. Check GoogleAPICallError ErrorInfo attributes
192
+ error_info = getattr(target_exc, "_error_info", None) or getattr(
193
+ target_exc, "error_info", None
194
+ )
195
+
196
+ # 3. Check native gRPC trailing metadata for binary google.rpc.Status details
197
+ if error_info is None:
198
+ rpc_call = (
199
+ target_exc
200
+ if hasattr(target_exc, "trailing_metadata")
201
+ else getattr(target_exc, "response", None)
202
+ )
203
+ if rpc_call is not None and hasattr(rpc_call, "trailing_metadata"):
204
+ try:
205
+ from google.api_core import exceptions
206
+
207
+ _, error_info = exceptions._parse_grpc_error_details(rpc_call)
208
+ except Exception:
209
+ pass
210
+
211
+ # 4. Unified attribute extraction: extract domain, reason, and metadata from ErrorInfo or exception attributes
212
+ source = error_info or target_exc
213
+ domain = getattr(source, "domain", None)
214
+ if domain:
215
+ attrs["gcp.errors.domain"] = domain
216
+ reason = getattr(source, "reason", None)
217
+ if reason:
218
+ attrs["error.type"] = reason
219
+ metadata = getattr(source, "metadata", None)
220
+ if metadata:
221
+ for k, v in metadata.items():
222
+ attrs[f"gcp.errors.metadata.{k}"] = str(v)
223
+
224
+ return attrs
225
+
226
+
107
227
  class _GapicCallable(object):
108
228
  """Callable that applies retry, timeout, and metadata logic.
109
229
 
@@ -123,6 +243,18 @@ class _GapicCallable(object):
123
243
  provided to the RPC method on every invocation. This is merged with
124
244
  any metadata specified during invocation. If ``None``, no
125
245
  additional metadata will be passed to the RPC method.
246
+ client_options
247
+ (Optional[google.api_core.client_options.ClientOptions]):
248
+ Client options used to configure client-level behavior, such as
249
+ custom OpenTelemetry tracer providers. Defaults to None.
250
+ method_name (Optional[str]): The optional explicit full RPC method name
251
+ (e.g. "/google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion").
252
+ is_streaming (bool): Whether the RPC method is streaming. Defaults to False.
253
+ Note: Streaming methods do not currently generate Tier 3 observability spans.
254
+ client_info (Optional[google.api_core.gapic_v1.client_info.ClientInfo]):
255
+ Client information used for metadata headers. Defaults to None.
256
+ kind (str): The transport kind for the RPC method. Defaults to "grpc".
257
+ Allowed values for OpenTelemetry method tracing are "grpc" and "grpc_asyncio".
126
258
  """
127
259
 
128
260
  def __init__(
@@ -132,22 +264,65 @@ class _GapicCallable(object):
132
264
  timeout,
133
265
  compression,
134
266
  metadata=None,
267
+ client_options=None,
268
+ method_name=None,
269
+ is_streaming=False,
270
+ client_info=None,
271
+ kind="grpc",
135
272
  ):
136
273
  self._target = target
137
274
  self._retry = retry
138
275
  self._timeout = timeout
139
276
  self._compression = compression
277
+
140
278
  # Pre-extract the x-goog-api-client header from the initialized metadata.
141
279
  self._x_goog_api_client, remaining = _extract_metrics_header(metadata)
142
280
  self._static_metadata = tuple(remaining)
143
281
  if self._x_goog_api_client:
144
282
  self._default_metadata = (
145
- (client_info.METRICS_METADATA_KEY, self._x_goog_api_client),
283
+ (METRICS_METADATA_KEY, self._x_goog_api_client),
146
284
  *self._static_metadata,
147
285
  )
148
286
  else:
149
287
  self._default_metadata = self._static_metadata
150
288
 
289
+ # Configure the OpenTelemetry span factory once at initialization.
290
+ # For now, method tracing is gated to non-streaming gRPC calls where an explicit method_name is provided.
291
+ self._start_span_fn = None
292
+ if (
293
+ not is_streaming
294
+ and kind == "grpc"
295
+ and method_name is not None
296
+ and _observability.is_otel_capabilities_enabled(client_options)
297
+ ):
298
+ try:
299
+ from opentelemetry import trace
300
+
301
+ tracer_provider = None
302
+ if isinstance(client_options, dict):
303
+ tracer_provider = client_options.get("tracer_provider")
304
+ elif client_options is not None:
305
+ tracer_provider = getattr(client_options, "tracer_provider", None)
306
+ if tracer_provider is not None:
307
+ tracer = tracer_provider.get_tracer("google.api_core")
308
+ else:
309
+ tracer = trace.get_tracer("google.api_core")
310
+
311
+ span_name, _, _ = _extract_rpc_identity(method_name)
312
+ span_attributes = {
313
+ "rpc.system.name": "grpc",
314
+ "rpc.method": span_name,
315
+ }
316
+ self._start_span_fn = functools.partial(
317
+ tracer.start_as_current_span,
318
+ span_name,
319
+ kind=trace.SpanKind.CLIENT,
320
+ attributes=span_attributes,
321
+ )
322
+ except (ImportError, AttributeError, TypeError):
323
+ # Gracefully disable tracing if OpenTelemetry or custom provider fails
324
+ self._start_span_fn = None
325
+
151
326
  def __call__(
152
327
  self, *args, timeout=DEFAULT, retry=DEFAULT, compression=DEFAULT, **kwargs
153
328
  ):
@@ -177,7 +352,7 @@ class _GapicCallable(object):
177
352
  self._x_goog_api_client, user_x_goog
178
353
  )
179
354
  if merged_header:
180
- final_metadata.append((client_info.METRICS_METADATA_KEY, merged_header))
355
+ final_metadata.append((METRICS_METADATA_KEY, merged_header))
181
356
  final_metadata.extend(remaining)
182
357
  kwargs["metadata"] = final_metadata
183
358
  elif self._default_metadata:
@@ -186,7 +361,27 @@ class _GapicCallable(object):
186
361
  if self._compression is not None:
187
362
  kwargs["compression"] = compression
188
363
 
189
- return wrapped_func(*args, **kwargs)
364
+ span_cm = contextlib.nullcontext()
365
+ if self._start_span_fn is not None:
366
+ try:
367
+ span_cm = self._start_span_fn()
368
+ except Exception:
369
+ span_cm = contextlib.nullcontext()
370
+
371
+ with span_cm as span:
372
+ try:
373
+ result = wrapped_func(*args, **kwargs)
374
+ if span is not None and hasattr(span, "set_attribute"):
375
+ span.set_attribute("rpc.response.status_code", "OK")
376
+ return result
377
+ except Exception as exc:
378
+ if span is not None and hasattr(span, "set_attribute"):
379
+ span.set_attribute(
380
+ "rpc.response.status_code", _extract_status_code(exc)
381
+ )
382
+ for k, v in _extract_error_attributes(exc).items():
383
+ span.set_attribute(k, v)
384
+ raise
190
385
 
191
386
 
192
387
  def wrap_method(
@@ -197,6 +392,10 @@ def wrap_method(
197
392
  client_info=client_info.DEFAULT_CLIENT_INFO,
198
393
  *,
199
394
  with_call=False,
395
+ client_options=None,
396
+ method_name=None,
397
+ is_streaming=False,
398
+ kind="grpc",
200
399
  ):
201
400
  """Wrap an RPC method with common behavior.
202
401
 
@@ -280,6 +479,18 @@ def wrap_method(
280
479
  return a tuple of (response, grpc.Call) instead of just the response.
281
480
  This is useful for extracting trailing metadata from unary calls.
282
481
  Defaults to False.
482
+ client_options
483
+ (Optional[google.api_core.client_options.ClientOptions]):
484
+ Client options used to configure client-level behavior, such as
485
+ custom OpenTelemetry tracer providers. Defaults to None.
486
+ method_name (Optional[str]): Optional explicit full RPC method name
487
+ (e.g. "/google.cloud.secretmanager.v1.SecretManagerService/AccessSecretVersion").
488
+ Used to identify the RPC for observability.
489
+ is_streaming (bool): Whether the RPC method is streaming. Defaults to False.
490
+ Streaming methods are currently gated and do not generate Tier 3 spans.
491
+ kind (str): The transport kind for the RPC method. Defaults to "grpc".
492
+ Non-gRPC transports (e.g. "rest") are currently gated and do not generate
493
+ Tier 3 method spans.
283
494
 
284
495
  Returns:
285
496
  Callable: A new callable that takes optional ``retry``, ``timeout``,
@@ -307,5 +518,10 @@ def wrap_method(
307
518
  default_timeout,
308
519
  default_compression,
309
520
  metadata=user_agent_metadata,
521
+ client_options=client_options,
522
+ method_name=method_name,
523
+ is_streaming=is_streaming,
524
+ client_info=client_info,
525
+ kind=kind,
310
526
  )
311
527
  )
@@ -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.36.0"
15
+ __version__ = "2.37.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-api-core
3
- Version: 2.36.0
3
+ Version: 2.37.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
@@ -0,0 +1,66 @@
1
+ # Copyright 2026 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import os
16
+ import sys
17
+ import types
18
+ from unittest import mock
19
+
20
+ import pytest
21
+
22
+
23
+ @pytest.fixture(scope="session", autouse=True)
24
+ def mock_mtls_env():
25
+ """Autouse session-scoped fixture to isolate unit tests from workstation mTLS environments."""
26
+ with mock.patch.dict(
27
+ os.environ,
28
+ {
29
+ "GOOGLE_API_USE_CLIENT_CERTIFICATE": "false",
30
+ "CLOUDSDK_CONTEXT_AWARE_USE_CLIENT_CERTIFICATE": "false",
31
+ },
32
+ ):
33
+ yield
34
+
35
+
36
+ @pytest.fixture
37
+ def mock_otel(monkeypatch):
38
+ """Provides a mocked OpenTelemetry environment with tracing enabled."""
39
+ monkeypatch.setenv("GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED", "true")
40
+ mock_span = mock.MagicMock()
41
+ mock_tracer = mock.MagicMock()
42
+ mock_tracer.start_as_current_span.return_value.__enter__.return_value = mock_span
43
+
44
+ mock_trace = mock.Mock()
45
+ mock_trace.get_tracer.return_value = mock_tracer
46
+ mock_trace.SpanKind.CLIENT = "CLIENT"
47
+ mock_trace.StatusCode.ERROR = "ERROR"
48
+
49
+ with (
50
+ mock.patch(
51
+ "google.api_core._observability.is_otel_capabilities_enabled",
52
+ return_value=True,
53
+ ),
54
+ mock.patch.dict(
55
+ sys.modules,
56
+ {
57
+ "opentelemetry": mock.Mock(trace=mock_trace),
58
+ "opentelemetry.trace": mock_trace,
59
+ },
60
+ ),
61
+ ):
62
+ yield types.SimpleNamespace(
63
+ trace=mock_trace,
64
+ tracer=mock_tracer,
65
+ span=mock_span,
66
+ )
@@ -78,3 +78,28 @@ warn_deprecated_credentials_file = functools.partial(
78
78
  DeprecationWarning,
79
79
  match="argument is deprecated because of a potential security risk",
80
80
  )
81
+
82
+
83
+ def assert_uninstrumented_gapic_callable(
84
+ wrapped,
85
+ result,
86
+ mock_target,
87
+ mock_trace=None,
88
+ expected_result="success",
89
+ ):
90
+ """Verifies that an uninstrumented RPC callable succeeds without tracing overhead.
91
+
92
+ 1. Proves the RPC executed successfully with the expected return value.
93
+ 2. Proves the OpenTelemetry API was never invoked.
94
+ 3. Proves the callable holds no tracer or span configuration.
95
+ """
96
+ # 1. Prove the RPC executed successfully
97
+ assert result == expected_result
98
+ mock_target.assert_called_once()
99
+
100
+ # 2. Prove the OpenTelemetry API was never invoked
101
+ if mock_trace is not None:
102
+ mock_trace.get_tracer.assert_not_called()
103
+
104
+ # 3. Prove the callable holds no span factory
105
+ assert getattr(wrapped, "_start_span_fn", None) is None