google-api-core 2.37.0__tar.gz → 2.39.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.37.0/google_api_core.egg-info → google_api_core-2.39.0}/PKG-INFO +1 -1
- google_api_core-2.39.0/google/api_core/_observability.py +281 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/_python_package_support.py +1 -1
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/exceptions.py +35 -0
- google_api_core-2.39.0/google/api_core/resumable_transfer/__init__.py +49 -0
- google_api_core-2.39.0/google/api_core/resumable_transfer/common.py +191 -0
- google_api_core-2.39.0/google/api_core/resumable_transfer/upload.py +1092 -0
- google_api_core-2.39.0/google/api_core/resumable_transfer/upload_async.py +1143 -0
- google_api_core-2.39.0/google/api_core/resumable_transfer/upload_state.py +339 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/version.py +1 -1
- {google_api_core-2.37.0 → google_api_core-2.39.0/google_api_core.egg-info}/PKG-INFO +1 -1
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google_api_core.egg-info/SOURCES.txt +8 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/test_grpc_helpers_async.py +1 -1
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/test_rest_streaming_async.py +1 -1
- google_api_core-2.39.0/tests/asyncio/test_resumable_transfer_async.py +2294 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/helpers.py +1 -1
- google_api_core-2.39.0/tests/unit/test_observability.py +544 -0
- google_api_core-2.39.0/tests/unit/test_resumable_transfer.py +2003 -0
- google_api_core-2.39.0/tests/unit/test_resumable_transfer_scenarios.py +862 -0
- google_api_core-2.37.0/google/api_core/_observability.py +0 -135
- google_api_core-2.37.0/tests/unit/test_observability.py +0 -255
- {google_api_core-2.37.0 → google_api_core-2.39.0}/LICENSE +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/MANIFEST.in +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/README.rst +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/_feature_gating_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/_python_version_support.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/_rest_streaming_base.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/bidi.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/bidi_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/bidi_base.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/client_info.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/client_logging.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/client_options.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/datetime_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/extended_operation.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/future/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/future/_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/future/async_future.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/future/base.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/future/polling.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/gapic_v1/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/gapic_v1/client_info.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/gapic_v1/config.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/gapic_v1/config_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/gapic_v1/method.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/gapic_v1/method_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/gapic_v1/requests.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/gapic_v1/routing_header.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/general_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/grpc_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/grpc_helpers_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/iam.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operation.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operation_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/abstract_operations_base_client.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/abstract_operations_client.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/operations_async_client.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/operations_client.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/operations_client_config.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/operations_rest_client_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/pagers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/pagers_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/pagers_base.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/transports/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/transports/base.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/transports/rest.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/operations_v1/transports/rest_asyncio.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/page_iterator.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/page_iterator_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/path_template.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/protobuf_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/py.typed +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/rest_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/rest_streaming.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/rest_streaming_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/retry/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/retry/retry_base.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/retry/retry_streaming.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/retry/retry_streaming_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/retry/retry_unary.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/retry/retry_unary_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/retry_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/timeout.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/universe.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/version_header.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google_api_core.egg-info/dependency_links.txt +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google_api_core.egg-info/requires.txt +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/google_api_core.egg-info/top_level.txt +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/pyproject.toml +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/setup.cfg +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/setup.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/future/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/future/test_async_future.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/gapic/test_config_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/gapic/test_method_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/operations_v1/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/operations_v1/test_operations_async_client.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/retry/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/retry/test_retry_streaming_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/retry/test_retry_unary_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/test_bidi_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/test_operation_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/asyncio/test_page_iterator_async.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/conftest.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/future/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/future/test__helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/future/test_polling.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/gapic/test_client_info.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/gapic/test_config.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/gapic/test_method.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/gapic/test_requests.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/gapic/test_routing_header.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/operations_v1/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/operations_v1/test_operations_client.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/operations_v1/test_operations_rest_client.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/retry/__init__.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/retry/test_retry_base.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/retry/test_retry_imports.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/retry/test_retry_streaming.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/retry/test_retry_unary.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_bidi.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_client_info.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_client_logging.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_client_options.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_datetime_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_exceptions.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_extended_operation.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_feature_gating_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_grpc_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_iam.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_operation.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_packaging.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_page_iterator.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_path_template.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_protobuf_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_python_package_support.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_python_version_support.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_rest_helpers.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_rest_streaming.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_timeout.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_universe.py +0 -0
- {google_api_core-2.37.0 → google_api_core-2.39.0}/tests/unit/test_version_header.py +0 -0
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
import urllib.parse
|
|
22
|
+
from typing import TYPE_CHECKING, Any, Callable, Sequence
|
|
23
|
+
|
|
24
|
+
from google.api_core import _feature_gating_helpers
|
|
25
|
+
from google.api_core.client_options import ClientOptions
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
# flake8: grpc, trace, and ClientInterceptor are imported only for static analysis and type annotations
|
|
29
|
+
# The 'noqa: F401' comment avoids flake8 "imported but not used" errors.
|
|
30
|
+
import grpc # noqa: F401
|
|
31
|
+
import opentelemetry.trace # noqa: F401
|
|
32
|
+
|
|
33
|
+
from google.api_core.grpc_helpers import ClientInterceptor # noqa: F401
|
|
34
|
+
|
|
35
|
+
_TRACER_PROVIDER = "tracer_provider"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_otel_capabilities_enabled(
|
|
39
|
+
client_options: ClientOptions | dict[str, Any] | None = None,
|
|
40
|
+
env_var: str = "GOOGLE_SDK_EXPERIMENTAL_PYTHON_TRACING_ENABLED",
|
|
41
|
+
) -> bool:
|
|
42
|
+
"""Checks if OTel capabilities are enabled and installed.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
client_options: The client options object or dictionary.
|
|
46
|
+
env_var: The environment variable to check for enablement.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
bool: True if enabled and installed, False otherwise.
|
|
50
|
+
"""
|
|
51
|
+
is_tracing_enabled = _feature_gating_helpers.resolve_feature_flags(
|
|
52
|
+
env_var=env_var,
|
|
53
|
+
feature_key=_TRACER_PROVIDER,
|
|
54
|
+
configuration=client_options,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if is_tracing_enabled:
|
|
58
|
+
try:
|
|
59
|
+
import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found] # noqa: F401
|
|
60
|
+
|
|
61
|
+
return True
|
|
62
|
+
except ImportError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _extract_endpoint_attributes(
|
|
69
|
+
client_options: ClientOptions | dict[str, Any] | None = None,
|
|
70
|
+
) -> dict[str, Any]:
|
|
71
|
+
"""Extracts server.address, server.port (if non-default), and url.domain from client options if present.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
client_options: The client options object or dictionary.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
dict[str, Any]: A dictionary containing url.domain and, if an api_endpoint is configured,
|
|
78
|
+
server.address and non-default server.port.
|
|
79
|
+
"""
|
|
80
|
+
attrs: dict[str, Any] = {}
|
|
81
|
+
endpoint = None
|
|
82
|
+
universe_domain = None
|
|
83
|
+
|
|
84
|
+
if isinstance(client_options, dict):
|
|
85
|
+
endpoint = client_options.get("api_endpoint")
|
|
86
|
+
universe_domain = client_options.get("universe_domain")
|
|
87
|
+
elif client_options is not None:
|
|
88
|
+
endpoint = getattr(client_options, "api_endpoint", None)
|
|
89
|
+
universe_domain = getattr(client_options, "universe_domain", None)
|
|
90
|
+
|
|
91
|
+
attrs["url.domain"] = universe_domain or "googleapis.com"
|
|
92
|
+
|
|
93
|
+
if endpoint and isinstance(endpoint, str):
|
|
94
|
+
target = endpoint if "//" in endpoint else f"//{endpoint}"
|
|
95
|
+
parsed = None
|
|
96
|
+
hostname = None
|
|
97
|
+
port = None
|
|
98
|
+
try:
|
|
99
|
+
parsed = urllib.parse.urlsplit(target)
|
|
100
|
+
hostname = parsed.hostname
|
|
101
|
+
port = parsed.port
|
|
102
|
+
except ValueError:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
if hostname:
|
|
106
|
+
attrs["server.address"] = hostname
|
|
107
|
+
if port and parsed:
|
|
108
|
+
scheme = parsed.scheme.lower()
|
|
109
|
+
is_default_port = (port == 443 and scheme in ("https", "")) or (
|
|
110
|
+
port == 80 and scheme == "http"
|
|
111
|
+
)
|
|
112
|
+
if not is_default_port:
|
|
113
|
+
attrs["server.port"] = port
|
|
114
|
+
return attrs
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _make_grpc_client_request_hook(
|
|
118
|
+
endpoint_attrs: dict[str, Any] | None = None,
|
|
119
|
+
) -> Callable[[Any, Any], None]:
|
|
120
|
+
"""Creates an OpenTelemetry gRPC client request hook with optional endpoint attributes.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
endpoint_attrs: Optional static endpoint attributes to attach to every span.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Callable[[Any, Any], None]: The request hook callback.
|
|
127
|
+
"""
|
|
128
|
+
static_attrs = dict(endpoint_attrs) if endpoint_attrs else {}
|
|
129
|
+
|
|
130
|
+
def client_request_hook(span: Any, request: Any) -> None:
|
|
131
|
+
if span is None or not getattr(span, "is_recording", lambda: True)():
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
# Upstream opentelemetry-instrumentation-grpc may format span names with a
|
|
135
|
+
# leading slash (e.g. "/package.Service/Method"). Normalize the span name
|
|
136
|
+
# and ensure rpc.method is always captured as the clean, fully-qualified name.
|
|
137
|
+
span_name = getattr(span, "name", None)
|
|
138
|
+
if isinstance(span_name, str) and span_name:
|
|
139
|
+
clean_method_name = span_name.lstrip("/")
|
|
140
|
+
if span_name.startswith("/") and hasattr(span, "update_name"):
|
|
141
|
+
span.update_name(clean_method_name)
|
|
142
|
+
span.set_attribute("rpc.method", clean_method_name)
|
|
143
|
+
|
|
144
|
+
attrs: dict[str, Any] = {
|
|
145
|
+
"rpc.system.name": "grpc",
|
|
146
|
+
}
|
|
147
|
+
if static_attrs:
|
|
148
|
+
attrs.update(static_attrs)
|
|
149
|
+
for key, value in attrs.items():
|
|
150
|
+
span.set_attribute(key, value)
|
|
151
|
+
|
|
152
|
+
return client_request_hook
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
_grpc_client_request_hook = _make_grpc_client_request_hook()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _grpc_client_response_hook(span: Any, response: Any) -> None:
|
|
159
|
+
"""OpenTelemetry gRPC client response hook to record successful response status.
|
|
160
|
+
|
|
161
|
+
Upstream ``opentelemetry-instrumentation-grpc`` sets the integer status code
|
|
162
|
+
``rpc.grpc.status_code`` (e.g. 0), but does not record the modern string status
|
|
163
|
+
``rpc.response.status_code`` (e.g. "OK") required by Cloud Trace and current
|
|
164
|
+
OpenTelemetry semantic conventions (v1.27.0+).
|
|
165
|
+
|
|
166
|
+
This hook enriches successful RPC attempt spans with ``rpc.response.status_code = "OK"``.
|
|
167
|
+
Errors and non-OK statuses are handled at the Tier 3 method span layer or upstream.
|
|
168
|
+
|
|
169
|
+
Upstream handles synchronous and asynchronous invocations differently:
|
|
170
|
+
- **Synchronous gRPC**: Upstream only invokes the response hook when an RPC call
|
|
171
|
+
succeeds. On failure, the hook is bypassed entirely.
|
|
172
|
+
- **Asynchronous gRPC**: Upstream invokes the response hook unconditionally for
|
|
173
|
+
both successes and failures (passing exception details on error). However, it
|
|
174
|
+
always marks ``span.status`` with an error status before calling the hook.
|
|
175
|
+
|
|
176
|
+
Because of this disparity, this hook checks ``span.status`` to guard against
|
|
177
|
+
async failure callbacks while allowing synchronous and successful asynchronous
|
|
178
|
+
calls to be marked "OK".
|
|
179
|
+
|
|
180
|
+
Note:
|
|
181
|
+
If upstream ``opentelemetry-instrumentation-grpc`` adds native support for
|
|
182
|
+
modern ``rpc.response.status_code`` in future releases, this hook can be retired.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
span: The OpenTelemetry span.
|
|
186
|
+
response: The gRPC response object or details.
|
|
187
|
+
"""
|
|
188
|
+
if not span.is_recording():
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# Guard against upstream async calls that invoke this hook on failures.
|
|
192
|
+
status = getattr(span, "status", None)
|
|
193
|
+
status_code = getattr(status, "status_code", None)
|
|
194
|
+
if (
|
|
195
|
+
getattr(status_code, "name", None) == "ERROR"
|
|
196
|
+
or getattr(status_code, "value", None) == 2
|
|
197
|
+
):
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
span.set_attribute("rpc.response.status_code", "OK")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _get_tracer_provider(
|
|
204
|
+
client_options: ClientOptions | dict[str, Any] | None = None,
|
|
205
|
+
) -> opentelemetry.trace.TracerProvider | None:
|
|
206
|
+
"""Extracts the OpenTelemetry tracer provider from client options if present.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
client_options: The client options object or dictionary.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
opentelemetry.trace.TracerProvider | None: The tracer provider if present,
|
|
213
|
+
None otherwise.
|
|
214
|
+
"""
|
|
215
|
+
if isinstance(client_options, dict):
|
|
216
|
+
return client_options.get(_TRACER_PROVIDER)
|
|
217
|
+
elif client_options is not None:
|
|
218
|
+
return getattr(client_options, _TRACER_PROVIDER, None)
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def get_otel_interceptor(
|
|
223
|
+
client_options: ClientOptions | dict[str, Any] | None = None,
|
|
224
|
+
) -> Callable[[grpc.Channel], grpc.Channel] | None:
|
|
225
|
+
"""Returns an interceptor callable that wraps a sync gRPC channel with OpenTelemetry tracing.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
client_options: The client options object or dictionary used for feature gating
|
|
229
|
+
and extracting the tracer provider.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Callable[[grpc.Channel], grpc.Channel] | None: An interceptor callable if OpenTelemetry
|
|
233
|
+
tracing is enabled and installed, None otherwise.
|
|
234
|
+
"""
|
|
235
|
+
if not is_otel_capabilities_enabled(client_options):
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found]
|
|
239
|
+
|
|
240
|
+
endpoint_attrs = _extract_endpoint_attributes(client_options)
|
|
241
|
+
request_hook = _make_grpc_client_request_hook(endpoint_attrs)
|
|
242
|
+
|
|
243
|
+
interceptor: ClientInterceptor = otel_grpc.client_interceptor(
|
|
244
|
+
tracer_provider=_get_tracer_provider(client_options),
|
|
245
|
+
request_hook=request_hook,
|
|
246
|
+
response_hook=_grpc_client_response_hook,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def otel_interceptor(channel: grpc.Channel) -> grpc.Channel:
|
|
250
|
+
return otel_grpc.intercept_channel(channel, interceptor)
|
|
251
|
+
|
|
252
|
+
return otel_interceptor
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def get_otel_async_interceptor(
|
|
256
|
+
client_options: ClientOptions | dict[str, Any] | None = None,
|
|
257
|
+
) -> Sequence[grpc.aio.ClientInterceptor] | None:
|
|
258
|
+
"""Returns async gRPC client interceptors for OpenTelemetry tracing.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
client_options: The client options object or dictionary used for feature gating
|
|
262
|
+
and extracting the tracer provider.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Sequence[grpc.aio.ClientInterceptor] | None: Instantiated OpenTelemetry async
|
|
266
|
+
client interceptors if tracing is enabled and installed, None otherwise.
|
|
267
|
+
"""
|
|
268
|
+
if not is_otel_capabilities_enabled(client_options):
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
# Ignored by mypy: Optional dependency only loaded if early-return is skipped
|
|
272
|
+
import opentelemetry.instrumentation.grpc as otel_grpc # type: ignore[import-not-found]
|
|
273
|
+
|
|
274
|
+
endpoint_attrs = _extract_endpoint_attributes(client_options)
|
|
275
|
+
request_hook = _make_grpc_client_request_hook(endpoint_attrs)
|
|
276
|
+
|
|
277
|
+
return otel_grpc.aio_client_interceptors(
|
|
278
|
+
tracer_provider=_get_tracer_provider(client_options),
|
|
279
|
+
request_hook=request_hook,
|
|
280
|
+
response_hook=_grpc_client_response_hook,
|
|
281
|
+
)
|
{google_api_core-2.37.0 → google_api_core-2.39.0}/google/api_core/_python_package_support.py
RENAMED
|
@@ -42,7 +42,7 @@ DependencyConstraint = namedtuple(
|
|
|
42
42
|
PQC_GRPC_WARNING_TEMPLATE = (
|
|
43
43
|
"Package {consumer_package} depends on {dependency_package}, currently installed at version {version_used_string}. "
|
|
44
44
|
"grpcio < 1.83.0 does not support Post-Quantum Cryptography (PQC). "
|
|
45
|
-
"Support for non-PQC environments is deprecated. In
|
|
45
|
+
"Support for non-PQC environments is deprecated. In April 2027, "
|
|
46
46
|
"Google Cloud Python packages will raise their minimum requirements "
|
|
47
47
|
"(including google-api-core, grpcio, and grpcio-status) to enforce grpcio >= 1.83.0. "
|
|
48
48
|
"For more details on Google Cloud's post-quantum security migration, visit: "
|
|
@@ -446,6 +446,41 @@ class AsyncRestUnsupportedParameterError(NotImplementedError):
|
|
|
446
446
|
pass
|
|
447
447
|
|
|
448
448
|
|
|
449
|
+
class ResumableTransferError(GoogleAPICallError):
|
|
450
|
+
"""Base class for resumable transfer errors."""
|
|
451
|
+
|
|
452
|
+
upload_url: Optional[str] = None
|
|
453
|
+
chunk_size: Optional[int] = None
|
|
454
|
+
|
|
455
|
+
def __init__(
|
|
456
|
+
self,
|
|
457
|
+
message: str,
|
|
458
|
+
*args,
|
|
459
|
+
upload_url: Optional[str] = None,
|
|
460
|
+
chunk_size: Optional[int] = None,
|
|
461
|
+
**kwargs,
|
|
462
|
+
) -> None:
|
|
463
|
+
super().__init__(message, *args, **kwargs)
|
|
464
|
+
self.upload_url = upload_url
|
|
465
|
+
self.chunk_size = chunk_size
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class TransferStalledError(ResumableTransferError):
|
|
469
|
+
"""Raised when upload throughput stays below minimum rate past stall timeout."""
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
class UnseekableStreamError(ResumableTransferError):
|
|
473
|
+
"""Raised when server recovery requires rewinding a non-seekable stream."""
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class UploadCancelledError(ResumableTransferError):
|
|
477
|
+
"""Raised when the upload is cancelled by the client or server."""
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class MissingStatusHeaderError(ResumableTransferError):
|
|
481
|
+
"""Raised when server response lacks the required X-Goog-Upload-Status header."""
|
|
482
|
+
|
|
483
|
+
|
|
449
484
|
def exception_class_for_http_status(status_code):
|
|
450
485
|
"""Return the exception class for a specific HTTP status code.
|
|
451
486
|
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
"""Resumable transfer library for Google APIs."""
|
|
16
|
+
|
|
17
|
+
from google.api_core.exceptions import (
|
|
18
|
+
MissingStatusHeaderError,
|
|
19
|
+
ResumableTransferError,
|
|
20
|
+
TransferStalledError,
|
|
21
|
+
UnseekableStreamError,
|
|
22
|
+
UploadCancelledError,
|
|
23
|
+
)
|
|
24
|
+
from google.api_core.resumable_transfer.common import (
|
|
25
|
+
DEFAULT_CHUNK_SIZE,
|
|
26
|
+
ProgressState,
|
|
27
|
+
ResumableUploadConfig,
|
|
28
|
+
UploadProgress,
|
|
29
|
+
)
|
|
30
|
+
from google.api_core.resumable_transfer.upload import ResumableUploadSession
|
|
31
|
+
from google.api_core.resumable_transfer.upload_async import (
|
|
32
|
+
AsyncResumableUploadSession,
|
|
33
|
+
AsyncUploadOperation,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"DEFAULT_CHUNK_SIZE",
|
|
38
|
+
"MissingStatusHeaderError",
|
|
39
|
+
"ProgressState",
|
|
40
|
+
"ResumableTransferError",
|
|
41
|
+
"TransferStalledError",
|
|
42
|
+
"UnseekableStreamError",
|
|
43
|
+
"UploadCancelledError",
|
|
44
|
+
"UploadProgress",
|
|
45
|
+
"ResumableUploadConfig",
|
|
46
|
+
"ResumableUploadSession",
|
|
47
|
+
"AsyncResumableUploadSession",
|
|
48
|
+
"AsyncUploadOperation",
|
|
49
|
+
]
|
|
@@ -0,0 +1,191 @@
|
|
|
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
|
+
"""Common constants, headers, and shared configuration for Resumable Upload protocol."""
|
|
16
|
+
|
|
17
|
+
import dataclasses
|
|
18
|
+
import datetime
|
|
19
|
+
import enum
|
|
20
|
+
from typing import Any, Mapping, Optional, Sequence, Tuple, Union
|
|
21
|
+
|
|
22
|
+
import google.protobuf.message
|
|
23
|
+
import proto
|
|
24
|
+
from google.protobuf import json_format
|
|
25
|
+
|
|
26
|
+
from google.api_core import exceptions
|
|
27
|
+
|
|
28
|
+
# Default chunk size: 10 MiB
|
|
29
|
+
DEFAULT_CHUNK_SIZE = 10 * 1024 * 1024
|
|
30
|
+
|
|
31
|
+
# Default timeout in seconds for the initial start request
|
|
32
|
+
DEFAULT_START_TIMEOUT = 60.0
|
|
33
|
+
|
|
34
|
+
# Protocol Headers
|
|
35
|
+
HEADER_PROTOCOL = "X-Goog-Upload-Protocol"
|
|
36
|
+
HEADER_COMMAND = "X-Goog-Upload-Command"
|
|
37
|
+
HEADER_STATUS = "X-Goog-Upload-Status"
|
|
38
|
+
HEADER_URL = "X-Goog-Upload-URL"
|
|
39
|
+
HEADER_OFFSET = "X-Goog-Upload-Offset"
|
|
40
|
+
HEADER_SIZE_RECEIVED = "X-Goog-Upload-Size-Received"
|
|
41
|
+
HEADER_CONTENT_TYPE = "X-Goog-Upload-Header-Content-Type"
|
|
42
|
+
HEADER_CONTENT_LENGTH = "X-Goog-Upload-Header-Content-Length"
|
|
43
|
+
HEADER_CHUNK_GRANULARITY = "X-Goog-Upload-Chunk-Granularity"
|
|
44
|
+
|
|
45
|
+
PROTOCOL_RESUMABLE = "resumable"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _Command(str, enum.Enum):
|
|
49
|
+
"""Protocol commands."""
|
|
50
|
+
|
|
51
|
+
START = "start"
|
|
52
|
+
UPLOAD = "upload"
|
|
53
|
+
FINALIZE = "finalize"
|
|
54
|
+
QUERY = "query"
|
|
55
|
+
CANCEL = "cancel"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class _Status(str, enum.Enum):
|
|
59
|
+
"""Server upload status values."""
|
|
60
|
+
|
|
61
|
+
ACTIVE = "active"
|
|
62
|
+
FINAL = "final"
|
|
63
|
+
CANCELLED = "cancelled"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ProgressState(str, enum.Enum):
|
|
67
|
+
"""Progress notification state values."""
|
|
68
|
+
|
|
69
|
+
STARTED = "started"
|
|
70
|
+
UPLOADING = "uploading"
|
|
71
|
+
RECOVERING = "recovering"
|
|
72
|
+
OFFSET_RECEIVED = "offset received"
|
|
73
|
+
FINALIZED = "finalized"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclasses.dataclass(frozen=True)
|
|
77
|
+
class UploadProgress:
|
|
78
|
+
"""Upload progress notification payload.
|
|
79
|
+
|
|
80
|
+
Attributes:
|
|
81
|
+
upload_url: The unique session URL for this upload.
|
|
82
|
+
chunk_size: The actual negotiated chunk size.
|
|
83
|
+
bytes_uploaded: The total confirmed bytes committed so far.
|
|
84
|
+
total_bytes: The total size of the stream in bytes, if known.
|
|
85
|
+
state: The current progress state.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
upload_url: str
|
|
89
|
+
chunk_size: int
|
|
90
|
+
bytes_uploaded: int
|
|
91
|
+
total_bytes: Optional[int]
|
|
92
|
+
state: ProgressState
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# HTTP status codes indicating transient retryable errors (Category 1)
|
|
96
|
+
RETRYABLE_STATUS_CODES = (408, 429, 500, 502, 503, 504)
|
|
97
|
+
|
|
98
|
+
# HTTP status codes indicating state consistency errors requiring recovery (Category 2)
|
|
99
|
+
RECOVERABLE_STATUS_CODES = (400, 412, 416)
|
|
100
|
+
|
|
101
|
+
# Exception types indicating unrecoverable terminal conditions (Category 3)
|
|
102
|
+
TERMINAL_ERRORS = (
|
|
103
|
+
exceptions.DeadlineExceeded,
|
|
104
|
+
exceptions.TransferStalledError,
|
|
105
|
+
exceptions.UploadCancelledError,
|
|
106
|
+
exceptions.UnseekableStreamError,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclasses.dataclass
|
|
111
|
+
class ResumableUploadConfig:
|
|
112
|
+
"""Configuration options for a resumable upload.
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
chunk_size: Size in bytes for each uploaded data chunk. Defaults to 10 MiB.
|
|
116
|
+
stall_minimum_rate: Minimum transfer rate in bytes per second. Defaults to 64 KiB/s.
|
|
117
|
+
stall_timeout: Stall duration threshold in seconds. Defaults to 120s.
|
|
118
|
+
headers: Additional HTTP headers dispatched exclusively with start request.
|
|
119
|
+
deadline: Optional overall wall-clock deadline for the entire upload process.
|
|
120
|
+
When set, each HTTP request timeout is trimmed to the remaining time before
|
|
121
|
+
the deadline, and DeadlineExceeded is raised immediately when the deadline
|
|
122
|
+
elapses (even on healthy streams). Timezone-aware datetimes (e.g.,
|
|
123
|
+
``datetime.now(timezone.utc) + timedelta(...)``) are recommended;
|
|
124
|
+
timezone-naive datetimes are assumed to be in local system time. When
|
|
125
|
+
None (default), transfer duration is governed by stall control
|
|
126
|
+
(stall_minimum_rate and stall_timeout), allowing healthy streams
|
|
127
|
+
transferring above the minimum rate to continue indefinitely.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
chunk_size: int = DEFAULT_CHUNK_SIZE
|
|
131
|
+
stall_minimum_rate: int = 64 * 1024
|
|
132
|
+
stall_timeout: float = 120.0
|
|
133
|
+
headers: Optional[Union[Mapping[str, str], Sequence[Tuple[str, str]]]] = None
|
|
134
|
+
deadline: Optional[datetime.datetime] = None
|
|
135
|
+
|
|
136
|
+
def __post_init__(self) -> None:
|
|
137
|
+
if self.deadline is not None:
|
|
138
|
+
self.deadline = self.deadline.astimezone(datetime.timezone.utc)
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def start_headers(self) -> Optional[Sequence[Tuple[str, str]]]:
|
|
142
|
+
"""Returns normalized additional headers for the start request."""
|
|
143
|
+
if self.headers is None:
|
|
144
|
+
return None
|
|
145
|
+
if isinstance(self.headers, Mapping):
|
|
146
|
+
return list(self.headers.items())
|
|
147
|
+
return list(self.headers)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _format_response_payload(
|
|
151
|
+
response: Union[Any, bytes],
|
|
152
|
+
response_type: Optional[Any],
|
|
153
|
+
) -> Any:
|
|
154
|
+
"""Formats raw response or bytes into protobuf or proto-plus message type if configured.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
response: Raw HTTP response object or response body bytes.
|
|
158
|
+
response_type: Deserializer callable, proto.Message class, or
|
|
159
|
+
google.protobuf.message.Message class or instance.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Deserialized protobuf message, or raw response bytes if
|
|
163
|
+
``response_type`` is ``None``.
|
|
164
|
+
"""
|
|
165
|
+
content: bytes
|
|
166
|
+
if isinstance(response, bytes):
|
|
167
|
+
content = response
|
|
168
|
+
elif hasattr(response, "content"):
|
|
169
|
+
content = response.content
|
|
170
|
+
else:
|
|
171
|
+
content = bytes(response)
|
|
172
|
+
|
|
173
|
+
if response_type is None:
|
|
174
|
+
return content
|
|
175
|
+
|
|
176
|
+
from_json_fn = getattr(response_type, "from_json", None)
|
|
177
|
+
if callable(from_json_fn):
|
|
178
|
+
if isinstance(response_type, type) and issubclass(response_type, proto.Message):
|
|
179
|
+
return from_json_fn(content, ignore_unknown_fields=True)
|
|
180
|
+
return from_json_fn(content)
|
|
181
|
+
if isinstance(response_type, type) and issubclass(
|
|
182
|
+
response_type, google.protobuf.message.Message
|
|
183
|
+
):
|
|
184
|
+
instance = response_type()
|
|
185
|
+
return json_format.Parse(content, instance, ignore_unknown_fields=True)
|
|
186
|
+
if isinstance(response_type, google.protobuf.message.Message):
|
|
187
|
+
return json_format.Parse(content, response_type, ignore_unknown_fields=True)
|
|
188
|
+
if callable(response_type):
|
|
189
|
+
return response_type(content)
|
|
190
|
+
|
|
191
|
+
return content
|