google-api-core 2.35.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.
- {google_api_core-2.35.0/google_api_core.egg-info → google_api_core-2.37.0}/PKG-INFO +6 -1
- google_api_core-2.37.0/google/api_core/_observability.py +135 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/client_options.py +8 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/exceptions.py +2 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/method.py +222 -6
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/grpc_helpers.py +68 -1
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/path_template.py +14 -4
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/rest_helpers.py +2 -1
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/version.py +1 -1
- {google_api_core-2.35.0 → google_api_core-2.37.0/google_api_core.egg-info}/PKG-INFO +6 -1
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google_api_core.egg-info/SOURCES.txt +2 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google_api_core.egg-info/requires.txt +7 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/pyproject.toml +7 -0
- google_api_core-2.37.0/tests/conftest.py +66 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/helpers.py +25 -0
- google_api_core-2.37.0/tests/unit/gapic/test_method.py +908 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_client_options.py +4 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_exceptions.py +25 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_feature_gating_helpers.py +1 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_grpc_helpers.py +101 -0
- google_api_core-2.37.0/tests/unit/test_observability.py +255 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_path_template.py +60 -29
- google_api_core-2.35.0/tests/conftest.py +0 -31
- google_api_core-2.35.0/tests/unit/gapic/test_method.py +0 -348
- {google_api_core-2.35.0 → google_api_core-2.37.0}/LICENSE +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/MANIFEST.in +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/README.rst +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/_feature_gating_helpers.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/_python_package_support.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/_python_version_support.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/_rest_streaming_base.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/bidi.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/bidi_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/bidi_base.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/client_info.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/client_logging.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/datetime_helpers.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/extended_operation.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/future/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/future/_helpers.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/future/async_future.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/future/base.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/future/polling.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/client_info.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/config.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/config_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/method_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/requests.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/gapic_v1/routing_header.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/general_helpers.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/grpc_helpers_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/iam.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operation.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operation_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/abstract_operations_base_client.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/abstract_operations_client.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/operations_async_client.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/operations_client.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/operations_client_config.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/operations_rest_client_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/pagers.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/pagers_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/pagers_base.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/transports/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/transports/base.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/transports/rest.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/operations_v1/transports/rest_asyncio.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/page_iterator.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/page_iterator_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/protobuf_helpers.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/py.typed +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/rest_streaming.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/rest_streaming_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/retry/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/retry/retry_base.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/retry/retry_streaming.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/retry/retry_streaming_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/retry/retry_unary.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/retry/retry_unary_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/retry_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/timeout.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/universe.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google/api_core/version_header.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google_api_core.egg-info/dependency_links.txt +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/google_api_core.egg-info/top_level.txt +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/setup.cfg +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/setup.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/future/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/future/test_async_future.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/gapic/test_config_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/gapic/test_method_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/operations_v1/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/operations_v1/test_operations_async_client.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/retry/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/retry/test_retry_streaming_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/retry/test_retry_unary_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/test_bidi_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/test_grpc_helpers_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/test_operation_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/test_page_iterator_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/asyncio/test_rest_streaming_async.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/future/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/future/test__helpers.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/future/test_polling.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/gapic/test_client_info.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/gapic/test_config.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/gapic/test_requests.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/gapic/test_routing_header.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/operations_v1/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/operations_v1/test_operations_client.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/operations_v1/test_operations_rest_client.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/retry/__init__.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/retry/test_retry_base.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/retry/test_retry_imports.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/retry/test_retry_streaming.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/retry/test_retry_unary.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_bidi.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_client_info.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_client_logging.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_datetime_helpers.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_extended_operation.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_iam.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_operation.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_packaging.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_page_iterator.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_protobuf_helpers.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_python_package_support.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_python_version_support.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_rest_helpers.py +1 -1
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_rest_streaming.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_timeout.py +0 -0
- {google_api_core-2.35.0 → google_api_core-2.37.0}/tests/unit/test_universe.py +0 -0
- {google_api_core-2.35.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.
|
|
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
|
|
@@ -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
|
+
)
|
|
@@ -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", ())
|
|
@@ -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 =
|
|
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
|
-
(
|
|
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((
|
|
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
|
-
|
|
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
|
)
|
|
@@ -17,7 +17,17 @@
|
|
|
17
17
|
import collections
|
|
18
18
|
import functools
|
|
19
19
|
import warnings
|
|
20
|
-
from typing import
|
|
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
|
)
|
|
@@ -123,7 +123,7 @@ def _expand_variable_match(positional_vars, named_vars, match):
|
|
|
123
123
|
try:
|
|
124
124
|
val = str(named_vars[name])
|
|
125
125
|
_extract_and_validate_wildcards(val, template, name)
|
|
126
|
-
return
|
|
126
|
+
return val
|
|
127
127
|
except KeyError:
|
|
128
128
|
raise ValueError(
|
|
129
129
|
"Named variable '{}' not specified and needed by template "
|
|
@@ -133,7 +133,7 @@ def _expand_variable_match(positional_vars, named_vars, match):
|
|
|
133
133
|
try:
|
|
134
134
|
val = str(positional_vars.pop(0))
|
|
135
135
|
_extract_and_validate_wildcards(val, positional, "positional variable")
|
|
136
|
-
return
|
|
136
|
+
return val
|
|
137
137
|
except IndexError:
|
|
138
138
|
raise ValueError(
|
|
139
139
|
"Positional variable not specified and needed by template "
|
|
@@ -215,12 +215,17 @@ def _generate_pattern_for_template(tmpl):
|
|
|
215
215
|
return _VARIABLE_RE.sub(_replace_variable_with_pattern, tmpl)
|
|
216
216
|
|
|
217
217
|
|
|
218
|
-
def get_field(request, field):
|
|
218
|
+
def get_field(request, field, encode=False):
|
|
219
219
|
"""Get the value of a field from a given dictionary.
|
|
220
220
|
|
|
221
221
|
Args:
|
|
222
222
|
request (dict | Message): A dictionary or a Message object.
|
|
223
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.
|
|
224
229
|
|
|
225
230
|
Returns:
|
|
226
231
|
The value of the field.
|
|
@@ -235,6 +240,8 @@ def get_field(request, field):
|
|
|
235
240
|
value = value.get(part)
|
|
236
241
|
if isinstance(value, dict):
|
|
237
242
|
return
|
|
243
|
+
if encode and value is not None:
|
|
244
|
+
return urllib.parse.quote(str(value), safe="/")
|
|
238
245
|
return value
|
|
239
246
|
|
|
240
247
|
|
|
@@ -327,7 +334,10 @@ def transcode(http_options, message=None, **request_kwargs):
|
|
|
327
334
|
]
|
|
328
335
|
bindings.append((uri_template, fields))
|
|
329
336
|
|
|
330
|
-
path_args = {
|
|
337
|
+
path_args = {
|
|
338
|
+
field: get_field(transcoded_value, field, encode=True)
|
|
339
|
+
for field, _ in fields
|
|
340
|
+
}
|
|
331
341
|
request["uri"] = expand(uri_template, **path_args)
|
|
332
342
|
|
|
333
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
|
|