google-api-core 2.26.0__tar.gz → 2.27.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.26.0/google_api_core.egg-info → google_api_core-2.27.0}/PKG-INFO +1 -1
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/bidi.py +39 -63
- google_api_core-2.27.0/google/api_core/bidi_async.py +244 -0
- google_api_core-2.27.0/google/api_core/bidi_base.py +88 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/version.py +1 -1
- {google_api_core-2.26.0 → google_api_core-2.27.0/google_api_core.egg-info}/PKG-INFO +1 -1
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google_api_core.egg-info/SOURCES.txt +3 -0
- google_api_core-2.27.0/tests/asyncio/test_bidi_async.py +305 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/LICENSE +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/MANIFEST.in +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/README.rst +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/_rest_streaming_base.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/client_info.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/client_logging.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/client_options.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/datetime_helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/exceptions.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/extended_operation.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/future/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/future/_helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/future/async_future.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/future/base.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/future/polling.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/client_info.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/config.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/config_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/method.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/method_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/routing_header.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/general_helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/grpc_helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/grpc_helpers_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/iam.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operation.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operation_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/abstract_operations_base_client.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/abstract_operations_client.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/operations_async_client.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/operations_client.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/operations_client_config.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/operations_rest_client_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/pagers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/pagers_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/pagers_base.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/transports/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/transports/base.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/transports/rest.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/transports/rest_asyncio.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/page_iterator.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/page_iterator_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/path_template.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/protobuf_helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/py.typed +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/rest_helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/rest_streaming.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/rest_streaming_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/retry_base.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/retry_streaming.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/retry_streaming_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/retry_unary.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/retry_unary_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/timeout.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/universe.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/version_header.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google_api_core.egg-info/dependency_links.txt +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google_api_core.egg-info/requires.txt +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/google_api_core.egg-info/top_level.txt +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/pyproject.toml +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/setup.cfg +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/setup.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/future/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/future/test_async_future.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/gapic/test_config_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/gapic/test_method_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/operations_v1/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/operations_v1/test_operations_async_client.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/retry/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/retry/test_retry_streaming_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/retry/test_retry_unary_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/test_grpc_helpers_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/test_operation_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/test_page_iterator_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/test_rest_streaming_async.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/future/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/future/test__helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/future/test_polling.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/gapic/test_client_info.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/gapic/test_config.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/gapic/test_method.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/gapic/test_routing_header.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/operations_v1/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/operations_v1/test_operations_client.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/operations_v1/test_operations_rest_client.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/retry/__init__.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/retry/test_retry_base.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/retry/test_retry_imports.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/retry/test_retry_streaming.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/retry/test_retry_unary.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_bidi.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_client_info.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_client_logging.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_client_options.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_datetime_helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_exceptions.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_extended_operation.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_grpc_helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_iam.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_operation.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_packaging.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_page_iterator.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_path_template.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_protobuf_helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_rest_helpers.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_rest_streaming.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_timeout.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_universe.py +0 -0
- {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_version_header.py +0 -0
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
-
"""
|
|
15
|
+
"""Helpers for synchronous bidirectional streaming RPCs."""
|
|
16
16
|
|
|
17
17
|
import collections
|
|
18
18
|
import datetime
|
|
@@ -22,6 +22,7 @@ import threading
|
|
|
22
22
|
import time
|
|
23
23
|
|
|
24
24
|
from google.api_core import exceptions
|
|
25
|
+
from google.api_core.bidi_base import BidiRpcBase
|
|
25
26
|
|
|
26
27
|
_LOGGER = logging.getLogger(__name__)
|
|
27
28
|
_BIDIRECTIONAL_CONSUMER_NAME = "Thread-ConsumeBidirectionalStream"
|
|
@@ -36,21 +37,6 @@ class _RequestQueueGenerator(object):
|
|
|
36
37
|
otherwise open-ended set of requests to send through a request-streaming
|
|
37
38
|
(or bidirectional) RPC.
|
|
38
39
|
|
|
39
|
-
The reason this is necessary is because gRPC takes an iterator as the
|
|
40
|
-
request for request-streaming RPCs. gRPC consumes this iterator in another
|
|
41
|
-
thread to allow it to block while generating requests for the stream.
|
|
42
|
-
However, if the generator blocks indefinitely gRPC will not be able to
|
|
43
|
-
clean up the thread as it'll be blocked on `next(iterator)` and not be able
|
|
44
|
-
to check the channel status to stop iterating. This helper mitigates that
|
|
45
|
-
by waiting on the queue with a timeout and checking the RPC state before
|
|
46
|
-
yielding.
|
|
47
|
-
|
|
48
|
-
Finally, it allows for retrying without swapping queues because if it does
|
|
49
|
-
pull an item off the queue when the RPC is inactive, it'll immediately put
|
|
50
|
-
it back and then exit. This is necessary because yielding the item in this
|
|
51
|
-
case will cause gRPC to discard it. In practice, this means that the order
|
|
52
|
-
of messages is not guaranteed. If such a thing is necessary it would be
|
|
53
|
-
easy to use a priority queue.
|
|
54
40
|
|
|
55
41
|
Example::
|
|
56
42
|
|
|
@@ -62,12 +48,6 @@ class _RequestQueueGenerator(object):
|
|
|
62
48
|
print(response)
|
|
63
49
|
q.put(...)
|
|
64
50
|
|
|
65
|
-
Note that it is possible to accomplish this behavior without "spinning"
|
|
66
|
-
(using a queue timeout). One possible way would be to use more threads to
|
|
67
|
-
multiplex the grpc end event with the queue, another possible way is to
|
|
68
|
-
use selectors and a custom event/queue object. Both of these approaches
|
|
69
|
-
are significant from an engineering perspective for small benefit - the
|
|
70
|
-
CPU consumed by spinning is pretty minuscule.
|
|
71
51
|
|
|
72
52
|
Args:
|
|
73
53
|
queue (queue_module.Queue): The request queue.
|
|
@@ -96,6 +76,31 @@ class _RequestQueueGenerator(object):
|
|
|
96
76
|
return self.call is None or self.call.is_active()
|
|
97
77
|
|
|
98
78
|
def __iter__(self):
|
|
79
|
+
# The reason this is necessary is because gRPC takes an iterator as the
|
|
80
|
+
# request for request-streaming RPCs. gRPC consumes this iterator in
|
|
81
|
+
# another thread to allow it to block while generating requests for
|
|
82
|
+
# the stream. However, if the generator blocks indefinitely gRPC will
|
|
83
|
+
# not be able to clean up the thread as it'll be blocked on
|
|
84
|
+
# `next(iterator)` and not be able to check the channel status to stop
|
|
85
|
+
# iterating. This helper mitigates that by waiting on the queue with
|
|
86
|
+
# a timeout and checking the RPC state before yielding.
|
|
87
|
+
#
|
|
88
|
+
# Finally, it allows for retrying without swapping queues because if
|
|
89
|
+
# it does pull an item off the queue when the RPC is inactive, it'll
|
|
90
|
+
# immediately put it back and then exit. This is necessary because
|
|
91
|
+
# yielding the item in this case will cause gRPC to discard it. In
|
|
92
|
+
# practice, this means that the order of messages is not guaranteed.
|
|
93
|
+
# If such a thing is necessary it would be easy to use a priority
|
|
94
|
+
# queue.
|
|
95
|
+
#
|
|
96
|
+
# Note that it is possible to accomplish this behavior without
|
|
97
|
+
# "spinning" (using a queue timeout). One possible way would be to use
|
|
98
|
+
# more threads to multiplex the grpc end event with the queue, another
|
|
99
|
+
# possible way is to use selectors and a custom event/queue object.
|
|
100
|
+
# Both of these approaches are significant from an engineering
|
|
101
|
+
# perspective for small benefit - the CPU consumed by spinning is
|
|
102
|
+
# pretty minuscule.
|
|
103
|
+
|
|
99
104
|
if self._initial_request is not None:
|
|
100
105
|
if callable(self._initial_request):
|
|
101
106
|
yield self._initial_request()
|
|
@@ -201,7 +206,7 @@ class _Throttle(object):
|
|
|
201
206
|
)
|
|
202
207
|
|
|
203
208
|
|
|
204
|
-
class BidiRpc(
|
|
209
|
+
class BidiRpc(BidiRpcBase):
|
|
205
210
|
"""A helper for consuming a bi-directional streaming RPC.
|
|
206
211
|
|
|
207
212
|
This maps gRPC's built-in interface which uses a request iterator and a
|
|
@@ -227,6 +232,8 @@ class BidiRpc(object):
|
|
|
227
232
|
rpc.send(example_pb2.StreamingRpcRequest(
|
|
228
233
|
data='example'))
|
|
229
234
|
|
|
235
|
+
rpc.close()
|
|
236
|
+
|
|
230
237
|
This does *not* retry the stream on errors. See :class:`ResumableBidiRpc`.
|
|
231
238
|
|
|
232
239
|
Args:
|
|
@@ -240,40 +247,14 @@ class BidiRpc(object):
|
|
|
240
247
|
the request.
|
|
241
248
|
"""
|
|
242
249
|
|
|
243
|
-
def
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
self._rpc_metadata = metadata
|
|
247
|
-
self._request_queue = queue_module.Queue()
|
|
248
|
-
self._request_generator = None
|
|
249
|
-
self._is_active = False
|
|
250
|
-
self._callbacks = []
|
|
251
|
-
self.call = None
|
|
252
|
-
|
|
253
|
-
def add_done_callback(self, callback):
|
|
254
|
-
"""Adds a callback that will be called when the RPC terminates.
|
|
255
|
-
|
|
256
|
-
This occurs when the RPC errors or is successfully terminated.
|
|
257
|
-
|
|
258
|
-
Args:
|
|
259
|
-
callback (Callable[[grpc.Future], None]): The callback to execute.
|
|
260
|
-
It will be provided with the same gRPC future as the underlying
|
|
261
|
-
stream which will also be a :class:`grpc.Call`.
|
|
262
|
-
"""
|
|
263
|
-
self._callbacks.append(callback)
|
|
264
|
-
|
|
265
|
-
def _on_call_done(self, future):
|
|
266
|
-
# This occurs when the RPC errors or is successfully terminated.
|
|
267
|
-
# Note that grpc's "future" here can also be a grpc.RpcError.
|
|
268
|
-
# See note in https://github.com/grpc/grpc/issues/10885#issuecomment-302651331
|
|
269
|
-
# that `grpc.RpcError` is also `grpc.call`.
|
|
270
|
-
for callback in self._callbacks:
|
|
271
|
-
callback(future)
|
|
250
|
+
def _create_queue(self):
|
|
251
|
+
"""Create a queue for requests."""
|
|
252
|
+
return queue_module.Queue()
|
|
272
253
|
|
|
273
254
|
def open(self):
|
|
274
255
|
"""Opens the stream."""
|
|
275
256
|
if self.is_active:
|
|
276
|
-
raise ValueError("
|
|
257
|
+
raise ValueError("Cannot open an already open stream.")
|
|
277
258
|
|
|
278
259
|
request_generator = _RequestQueueGenerator(
|
|
279
260
|
self._request_queue, initial_request=self._initial_request
|
|
@@ -322,7 +303,7 @@ class BidiRpc(object):
|
|
|
322
303
|
request (protobuf.Message): The request to send.
|
|
323
304
|
"""
|
|
324
305
|
if self.call is None:
|
|
325
|
-
raise ValueError("
|
|
306
|
+
raise ValueError("Cannot send on an RPC stream that has never been opened.")
|
|
326
307
|
|
|
327
308
|
# Don't use self.is_active(), as ResumableBidiRpc will overload it
|
|
328
309
|
# to mean something semantically different.
|
|
@@ -343,20 +324,15 @@ class BidiRpc(object):
|
|
|
343
324
|
protobuf.Message: The received message.
|
|
344
325
|
"""
|
|
345
326
|
if self.call is None:
|
|
346
|
-
raise ValueError("
|
|
327
|
+
raise ValueError("Cannot recv on an RPC stream that has never been opened.")
|
|
347
328
|
|
|
348
329
|
return next(self.call)
|
|
349
330
|
|
|
350
331
|
@property
|
|
351
332
|
def is_active(self):
|
|
352
|
-
"""
|
|
333
|
+
"""True if this stream is currently open and active."""
|
|
353
334
|
return self.call is not None and self.call.is_active()
|
|
354
335
|
|
|
355
|
-
@property
|
|
356
|
-
def pending_requests(self):
|
|
357
|
-
"""int: Returns an estimate of the number of queued requests."""
|
|
358
|
-
return self._request_queue.qsize()
|
|
359
|
-
|
|
360
336
|
|
|
361
337
|
def _never_terminate(future_or_error):
|
|
362
338
|
"""By default, no errors cause BiDi termination."""
|
|
@@ -544,7 +520,7 @@ class ResumableBidiRpc(BidiRpc):
|
|
|
544
520
|
call = self.call
|
|
545
521
|
|
|
546
522
|
if call is None:
|
|
547
|
-
raise ValueError("
|
|
523
|
+
raise ValueError("Cannot send on an RPC that has never been opened.")
|
|
548
524
|
|
|
549
525
|
# Don't use self.is_active(), as ResumableBidiRpc will overload it
|
|
550
526
|
# to mean something semantically different.
|
|
@@ -563,7 +539,7 @@ class ResumableBidiRpc(BidiRpc):
|
|
|
563
539
|
call = self.call
|
|
564
540
|
|
|
565
541
|
if call is None:
|
|
566
|
-
raise ValueError("
|
|
542
|
+
raise ValueError("Cannot recv on an RPC that has never been opened.")
|
|
567
543
|
|
|
568
544
|
return next(call)
|
|
569
545
|
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Copyright 2025, 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
|
+
# https://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
|
+
"""Asynchronous bi-directional streaming RPC helpers."""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import logging
|
|
19
|
+
from typing import Callable, Optional, Union
|
|
20
|
+
|
|
21
|
+
from grpc import aio
|
|
22
|
+
|
|
23
|
+
from google.api_core import exceptions
|
|
24
|
+
from google.api_core.bidi_base import BidiRpcBase
|
|
25
|
+
|
|
26
|
+
from google.protobuf.message import Message as ProtobufMessage
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_LOGGER = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _AsyncRequestQueueGenerator:
|
|
33
|
+
"""_AsyncRequestQueueGenerator is a helper class for sending asynchronous
|
|
34
|
+
requests to a gRPC stream from a Queue.
|
|
35
|
+
|
|
36
|
+
This generator takes asynchronous requests off a given `asyncio.Queue` and
|
|
37
|
+
yields them to gRPC.
|
|
38
|
+
|
|
39
|
+
It's useful when you have an indeterminate, indefinite, or otherwise
|
|
40
|
+
open-ended set of requests to send through a request-streaming (or
|
|
41
|
+
bidirectional) RPC.
|
|
42
|
+
|
|
43
|
+
Example::
|
|
44
|
+
|
|
45
|
+
requests = _AsyncRequestQueueGenerator(q)
|
|
46
|
+
call = await stub.StreamingRequest(requests)
|
|
47
|
+
requests.call = call
|
|
48
|
+
|
|
49
|
+
async for response in call:
|
|
50
|
+
print(response)
|
|
51
|
+
await q.put(...)
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
queue (asyncio.Queue): The request queue.
|
|
55
|
+
initial_request (Union[ProtobufMessage,
|
|
56
|
+
Callable[[], ProtobufMessage]]): The initial request to
|
|
57
|
+
yield. This is done independently of the request queue to allow for
|
|
58
|
+
easily restarting streams that require some initial configuration
|
|
59
|
+
request.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
queue: asyncio.Queue,
|
|
65
|
+
initial_request: Optional[
|
|
66
|
+
Union[ProtobufMessage, Callable[[], ProtobufMessage]]
|
|
67
|
+
] = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
self._queue = queue
|
|
70
|
+
self._initial_request = initial_request
|
|
71
|
+
self.call: Optional[aio.Call] = None
|
|
72
|
+
|
|
73
|
+
def _is_active(self) -> bool:
|
|
74
|
+
"""Returns true if the call is not set or not completed."""
|
|
75
|
+
# Note: there is a possibility that this starts *before* the call
|
|
76
|
+
# property is set. So we have to check if self.call is set before
|
|
77
|
+
# seeing if it's active. We need to return True if self.call is None.
|
|
78
|
+
# See https://github.com/googleapis/python-api-core/issues/560.
|
|
79
|
+
return self.call is None or not self.call.done()
|
|
80
|
+
|
|
81
|
+
async def __aiter__(self):
|
|
82
|
+
# The reason this is necessary is because it lets the user have
|
|
83
|
+
# control on when they would want to send requests proto messages
|
|
84
|
+
# instead of sending all of them initially.
|
|
85
|
+
#
|
|
86
|
+
# This is achieved via asynchronous queue (asyncio.Queue),
|
|
87
|
+
# gRPC awaits until there's a message in the queue.
|
|
88
|
+
#
|
|
89
|
+
# Finally, it allows for retrying without swapping queues because if
|
|
90
|
+
# it does pull an item off the queue when the RPC is inactive, it'll
|
|
91
|
+
# immediately put it back and then exit. This is necessary because
|
|
92
|
+
# yielding the item in this case will cause gRPC to discard it. In
|
|
93
|
+
# practice, this means that the order of messages is not guaranteed.
|
|
94
|
+
# If preserving order is necessary it would be easy to use a priority
|
|
95
|
+
# queue.
|
|
96
|
+
if self._initial_request is not None:
|
|
97
|
+
if callable(self._initial_request):
|
|
98
|
+
yield self._initial_request()
|
|
99
|
+
else:
|
|
100
|
+
yield self._initial_request
|
|
101
|
+
|
|
102
|
+
while True:
|
|
103
|
+
item = await self._queue.get()
|
|
104
|
+
|
|
105
|
+
# The consumer explicitly sent "None", indicating that the request
|
|
106
|
+
# should end.
|
|
107
|
+
if item is None:
|
|
108
|
+
_LOGGER.debug("Cleanly exiting request generator.")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if not self._is_active():
|
|
112
|
+
# We have an item, but the call is closed. We should put the
|
|
113
|
+
# item back on the queue so that the next call can consume it.
|
|
114
|
+
await self._queue.put(item)
|
|
115
|
+
_LOGGER.debug(
|
|
116
|
+
"Inactive call, replacing item on queue and exiting "
|
|
117
|
+
"request generator."
|
|
118
|
+
)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
yield item
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class AsyncBidiRpc(BidiRpcBase):
|
|
125
|
+
"""A helper for consuming a async bi-directional streaming RPC.
|
|
126
|
+
|
|
127
|
+
This maps gRPC's built-in interface which uses a request iterator and a
|
|
128
|
+
response iterator into a socket-like :func:`send` and :func:`recv`. This
|
|
129
|
+
is a more useful pattern for long-running or asymmetric streams (streams
|
|
130
|
+
where there is not a direct correlation between the requests and
|
|
131
|
+
responses).
|
|
132
|
+
|
|
133
|
+
Example::
|
|
134
|
+
|
|
135
|
+
initial_request = example_pb2.StreamingRpcRequest(
|
|
136
|
+
setting='example')
|
|
137
|
+
rpc = AsyncBidiRpc(
|
|
138
|
+
stub.StreamingRpc,
|
|
139
|
+
initial_request=initial_request,
|
|
140
|
+
metadata=[('name', 'value')]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
await rpc.open()
|
|
144
|
+
|
|
145
|
+
while rpc.is_active:
|
|
146
|
+
print(await rpc.recv())
|
|
147
|
+
await rpc.send(example_pb2.StreamingRpcRequest(
|
|
148
|
+
data='example'))
|
|
149
|
+
|
|
150
|
+
await rpc.close()
|
|
151
|
+
|
|
152
|
+
This does *not* retry the stream on errors.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
start_rpc (grpc.aio.StreamStreamMultiCallable): The gRPC method used to
|
|
156
|
+
start the RPC.
|
|
157
|
+
initial_request (Union[ProtobufMessage,
|
|
158
|
+
Callable[[], ProtobufMessage]]): The initial request to
|
|
159
|
+
yield. This is useful if an initial request is needed to start the
|
|
160
|
+
stream.
|
|
161
|
+
metadata (Sequence[Tuple(str, str)]): RPC metadata to include in
|
|
162
|
+
the request.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def _create_queue(self) -> asyncio.Queue:
|
|
166
|
+
"""Create a queue for requests."""
|
|
167
|
+
return asyncio.Queue()
|
|
168
|
+
|
|
169
|
+
async def open(self) -> None:
|
|
170
|
+
"""Opens the stream."""
|
|
171
|
+
if self.is_active:
|
|
172
|
+
raise ValueError("Cannot open an already open stream.")
|
|
173
|
+
|
|
174
|
+
request_generator = _AsyncRequestQueueGenerator(
|
|
175
|
+
self._request_queue, initial_request=self._initial_request
|
|
176
|
+
)
|
|
177
|
+
try:
|
|
178
|
+
call = await self._start_rpc(request_generator, metadata=self._rpc_metadata)
|
|
179
|
+
except exceptions.GoogleAPICallError as exc:
|
|
180
|
+
# The original `grpc.aio.AioRpcError` (which is usually also a
|
|
181
|
+
# `grpc.aio.Call`) is available from the ``response`` property on
|
|
182
|
+
# the mapped exception.
|
|
183
|
+
self._on_call_done(exc.response)
|
|
184
|
+
raise
|
|
185
|
+
|
|
186
|
+
request_generator.call = call
|
|
187
|
+
|
|
188
|
+
# TODO: api_core should expose the future interface for wrapped
|
|
189
|
+
# callables as well.
|
|
190
|
+
if hasattr(call, "_wrapped"): # pragma: NO COVER
|
|
191
|
+
call._wrapped.add_done_callback(self._on_call_done)
|
|
192
|
+
else:
|
|
193
|
+
call.add_done_callback(self._on_call_done)
|
|
194
|
+
|
|
195
|
+
self._request_generator = request_generator
|
|
196
|
+
self.call = call
|
|
197
|
+
|
|
198
|
+
async def close(self) -> None:
|
|
199
|
+
"""Closes the stream."""
|
|
200
|
+
if self.call is None:
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
await self._request_queue.put(None)
|
|
204
|
+
self.call.cancel()
|
|
205
|
+
self._request_generator = None
|
|
206
|
+
self._initial_request = None
|
|
207
|
+
self._callbacks = []
|
|
208
|
+
# Don't set self.call to None. Keep it around so that send/recv can
|
|
209
|
+
# raise the error.
|
|
210
|
+
|
|
211
|
+
async def send(self, request: ProtobufMessage) -> None:
|
|
212
|
+
"""Queue a message to be sent on the stream.
|
|
213
|
+
|
|
214
|
+
If the underlying RPC has been closed, this will raise.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
request (ProtobufMessage): The request to send.
|
|
218
|
+
"""
|
|
219
|
+
if self.call is None:
|
|
220
|
+
raise ValueError("Cannot send on an RPC stream that has never been opened.")
|
|
221
|
+
|
|
222
|
+
if not self.call.done():
|
|
223
|
+
await self._request_queue.put(request)
|
|
224
|
+
else:
|
|
225
|
+
# calling read should cause the call to raise.
|
|
226
|
+
await self.call.read()
|
|
227
|
+
|
|
228
|
+
async def recv(self) -> ProtobufMessage:
|
|
229
|
+
"""Wait for a message to be returned from the stream.
|
|
230
|
+
|
|
231
|
+
If the underlying RPC has been closed, this will raise.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
ProtobufMessage: The received message.
|
|
235
|
+
"""
|
|
236
|
+
if self.call is None:
|
|
237
|
+
raise ValueError("Cannot recv on an RPC stream that has never been opened.")
|
|
238
|
+
|
|
239
|
+
return await self.call.read()
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def is_active(self) -> bool:
|
|
243
|
+
"""Whether the stream is currently open and active."""
|
|
244
|
+
return self.call is not None and not self.call.done()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Copyright 2025, Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
#
|
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
10
|
+
# See the License for the specific language governing permissions and
|
|
11
|
+
# limitations under the License.
|
|
12
|
+
|
|
13
|
+
"""Base class for bi-directional streaming RPC helpers."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BidiRpcBase:
|
|
17
|
+
"""A base class for consuming a bi-directional streaming RPC.
|
|
18
|
+
|
|
19
|
+
This maps gRPC's built-in interface which uses a request iterator and a
|
|
20
|
+
response iterator into a socket-like :func:`send` and :func:`recv`. This
|
|
21
|
+
is a more useful pattern for long-running or asymmetric streams (streams
|
|
22
|
+
where there is not a direct correlation between the requests and
|
|
23
|
+
responses).
|
|
24
|
+
|
|
25
|
+
This does *not* retry the stream on errors.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
start_rpc (Union[grpc.StreamStreamMultiCallable,
|
|
29
|
+
grpc.aio.StreamStreamMultiCallable]): The gRPC method used
|
|
30
|
+
to start the RPC.
|
|
31
|
+
initial_request (Union[protobuf.Message,
|
|
32
|
+
Callable[[], protobuf.Message]]): The initial request to
|
|
33
|
+
yield. This is useful if an initial request is needed to start the
|
|
34
|
+
stream.
|
|
35
|
+
metadata (Sequence[Tuple(str, str)]): RPC metadata to include in
|
|
36
|
+
the request.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, start_rpc, initial_request=None, metadata=None):
|
|
40
|
+
self._start_rpc = start_rpc
|
|
41
|
+
self._initial_request = initial_request
|
|
42
|
+
self._rpc_metadata = metadata
|
|
43
|
+
self._request_queue = self._create_queue()
|
|
44
|
+
self._request_generator = None
|
|
45
|
+
self._callbacks = []
|
|
46
|
+
self.call = None
|
|
47
|
+
|
|
48
|
+
def _create_queue(self):
|
|
49
|
+
"""Create a queue for requests."""
|
|
50
|
+
raise NotImplementedError("`_create_queue` is not implemented.")
|
|
51
|
+
|
|
52
|
+
def add_done_callback(self, callback):
|
|
53
|
+
"""Adds a callback that will be called when the RPC terminates.
|
|
54
|
+
|
|
55
|
+
This occurs when the RPC errors or is successfully terminated.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
callback (Union[Callable[[grpc.Future], None], Callable[[Any], None]]):
|
|
59
|
+
The callback to execute after gRPC call completed (success or
|
|
60
|
+
failure).
|
|
61
|
+
|
|
62
|
+
For sync streaming gRPC: Callable[[grpc.Future], None]
|
|
63
|
+
|
|
64
|
+
For async streaming gRPC: Callable[[Any], None]
|
|
65
|
+
"""
|
|
66
|
+
self._callbacks.append(callback)
|
|
67
|
+
|
|
68
|
+
def _on_call_done(self, future):
|
|
69
|
+
# This occurs when the RPC errors or is successfully terminated.
|
|
70
|
+
# Note that grpc's "future" here can also be a grpc.RpcError.
|
|
71
|
+
# See note in https://github.com/grpc/grpc/issues/10885#issuecomment-302651331
|
|
72
|
+
# that `grpc.RpcError` is also `grpc.Call`.
|
|
73
|
+
# for asynchronous gRPC call it would be `grpc.aio.AioRpcError`
|
|
74
|
+
|
|
75
|
+
# Note: sync callbacks can be limiting for async code, because you can't
|
|
76
|
+
# await anything in a sync callback.
|
|
77
|
+
for callback in self._callbacks:
|
|
78
|
+
callback(future)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def is_active(self):
|
|
82
|
+
"""True if the gRPC call is not done yet."""
|
|
83
|
+
raise NotImplementedError("`is_active` is not implemented.")
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def pending_requests(self):
|
|
87
|
+
"""Estimate of the number of queued requests."""
|
|
88
|
+
return self._request_queue.qsize()
|
|
@@ -7,6 +7,8 @@ setup.py
|
|
|
7
7
|
google/api_core/__init__.py
|
|
8
8
|
google/api_core/_rest_streaming_base.py
|
|
9
9
|
google/api_core/bidi.py
|
|
10
|
+
google/api_core/bidi_async.py
|
|
11
|
+
google/api_core/bidi_base.py
|
|
10
12
|
google/api_core/client_info.py
|
|
11
13
|
google/api_core/client_logging.py
|
|
12
14
|
google/api_core/client_options.py
|
|
@@ -72,6 +74,7 @@ google_api_core.egg-info/top_level.txt
|
|
|
72
74
|
tests/__init__.py
|
|
73
75
|
tests/helpers.py
|
|
74
76
|
tests/asyncio/__init__.py
|
|
77
|
+
tests/asyncio/test_bidi_async.py
|
|
75
78
|
tests/asyncio/test_grpc_helpers_async.py
|
|
76
79
|
tests/asyncio/test_operation_async.py
|
|
77
80
|
tests/asyncio/test_page_iterator_async.py
|