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.
Files changed (126) hide show
  1. {google_api_core-2.26.0/google_api_core.egg-info → google_api_core-2.27.0}/PKG-INFO +1 -1
  2. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/bidi.py +39 -63
  3. google_api_core-2.27.0/google/api_core/bidi_async.py +244 -0
  4. google_api_core-2.27.0/google/api_core/bidi_base.py +88 -0
  5. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/version.py +1 -1
  6. {google_api_core-2.26.0 → google_api_core-2.27.0/google_api_core.egg-info}/PKG-INFO +1 -1
  7. {google_api_core-2.26.0 → google_api_core-2.27.0}/google_api_core.egg-info/SOURCES.txt +3 -0
  8. google_api_core-2.27.0/tests/asyncio/test_bidi_async.py +305 -0
  9. {google_api_core-2.26.0 → google_api_core-2.27.0}/LICENSE +0 -0
  10. {google_api_core-2.26.0 → google_api_core-2.27.0}/MANIFEST.in +0 -0
  11. {google_api_core-2.26.0 → google_api_core-2.27.0}/README.rst +0 -0
  12. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/__init__.py +0 -0
  13. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/_rest_streaming_base.py +0 -0
  14. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/client_info.py +0 -0
  15. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/client_logging.py +0 -0
  16. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/client_options.py +0 -0
  17. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/datetime_helpers.py +0 -0
  18. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/exceptions.py +0 -0
  19. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/extended_operation.py +0 -0
  20. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/future/__init__.py +0 -0
  21. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/future/_helpers.py +0 -0
  22. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/future/async_future.py +0 -0
  23. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/future/base.py +0 -0
  24. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/future/polling.py +0 -0
  25. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/__init__.py +0 -0
  26. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/client_info.py +0 -0
  27. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/config.py +0 -0
  28. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/config_async.py +0 -0
  29. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/method.py +0 -0
  30. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/method_async.py +0 -0
  31. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/gapic_v1/routing_header.py +0 -0
  32. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/general_helpers.py +0 -0
  33. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/grpc_helpers.py +0 -0
  34. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/grpc_helpers_async.py +0 -0
  35. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/iam.py +0 -0
  36. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operation.py +0 -0
  37. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operation_async.py +0 -0
  38. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/__init__.py +0 -0
  39. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/abstract_operations_base_client.py +0 -0
  40. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/abstract_operations_client.py +0 -0
  41. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/operations_async_client.py +0 -0
  42. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/operations_client.py +0 -0
  43. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/operations_client_config.py +0 -0
  44. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/operations_rest_client_async.py +0 -0
  45. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/pagers.py +0 -0
  46. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/pagers_async.py +0 -0
  47. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/pagers_base.py +0 -0
  48. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/transports/__init__.py +0 -0
  49. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/transports/base.py +0 -0
  50. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/transports/rest.py +0 -0
  51. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/operations_v1/transports/rest_asyncio.py +0 -0
  52. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/page_iterator.py +0 -0
  53. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/page_iterator_async.py +0 -0
  54. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/path_template.py +0 -0
  55. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/protobuf_helpers.py +0 -0
  56. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/py.typed +0 -0
  57. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/rest_helpers.py +0 -0
  58. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/rest_streaming.py +0 -0
  59. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/rest_streaming_async.py +0 -0
  60. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/__init__.py +0 -0
  61. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/retry_base.py +0 -0
  62. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/retry_streaming.py +0 -0
  63. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/retry_streaming_async.py +0 -0
  64. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/retry_unary.py +0 -0
  65. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry/retry_unary_async.py +0 -0
  66. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/retry_async.py +0 -0
  67. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/timeout.py +0 -0
  68. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/universe.py +0 -0
  69. {google_api_core-2.26.0 → google_api_core-2.27.0}/google/api_core/version_header.py +0 -0
  70. {google_api_core-2.26.0 → google_api_core-2.27.0}/google_api_core.egg-info/dependency_links.txt +0 -0
  71. {google_api_core-2.26.0 → google_api_core-2.27.0}/google_api_core.egg-info/requires.txt +0 -0
  72. {google_api_core-2.26.0 → google_api_core-2.27.0}/google_api_core.egg-info/top_level.txt +0 -0
  73. {google_api_core-2.26.0 → google_api_core-2.27.0}/pyproject.toml +0 -0
  74. {google_api_core-2.26.0 → google_api_core-2.27.0}/setup.cfg +0 -0
  75. {google_api_core-2.26.0 → google_api_core-2.27.0}/setup.py +0 -0
  76. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/__init__.py +0 -0
  77. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/__init__.py +0 -0
  78. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/future/__init__.py +0 -0
  79. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/future/test_async_future.py +0 -0
  80. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/gapic/test_config_async.py +0 -0
  81. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/gapic/test_method_async.py +0 -0
  82. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/operations_v1/__init__.py +0 -0
  83. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/operations_v1/test_operations_async_client.py +0 -0
  84. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/retry/__init__.py +0 -0
  85. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/retry/test_retry_streaming_async.py +0 -0
  86. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/retry/test_retry_unary_async.py +0 -0
  87. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/test_grpc_helpers_async.py +0 -0
  88. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/test_operation_async.py +0 -0
  89. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/test_page_iterator_async.py +0 -0
  90. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/asyncio/test_rest_streaming_async.py +0 -0
  91. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/helpers.py +0 -0
  92. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/__init__.py +0 -0
  93. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/future/__init__.py +0 -0
  94. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/future/test__helpers.py +0 -0
  95. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/future/test_polling.py +0 -0
  96. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/gapic/test_client_info.py +0 -0
  97. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/gapic/test_config.py +0 -0
  98. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/gapic/test_method.py +0 -0
  99. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/gapic/test_routing_header.py +0 -0
  100. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/operations_v1/__init__.py +0 -0
  101. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/operations_v1/test_operations_client.py +0 -0
  102. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/operations_v1/test_operations_rest_client.py +0 -0
  103. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/retry/__init__.py +0 -0
  104. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/retry/test_retry_base.py +0 -0
  105. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/retry/test_retry_imports.py +0 -0
  106. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/retry/test_retry_streaming.py +0 -0
  107. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/retry/test_retry_unary.py +0 -0
  108. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_bidi.py +0 -0
  109. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_client_info.py +0 -0
  110. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_client_logging.py +0 -0
  111. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_client_options.py +0 -0
  112. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_datetime_helpers.py +0 -0
  113. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_exceptions.py +0 -0
  114. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_extended_operation.py +0 -0
  115. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_grpc_helpers.py +0 -0
  116. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_iam.py +0 -0
  117. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_operation.py +0 -0
  118. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_packaging.py +0 -0
  119. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_page_iterator.py +0 -0
  120. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_path_template.py +0 -0
  121. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_protobuf_helpers.py +0 -0
  122. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_rest_helpers.py +0 -0
  123. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_rest_streaming.py +0 -0
  124. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_timeout.py +0 -0
  125. {google_api_core-2.26.0 → google_api_core-2.27.0}/tests/unit/test_universe.py +0 -0
  126. {google_api_core-2.26.0 → google_api_core-2.27.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.26.0
3
+ Version: 2.27.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
@@ -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
- """Bi-directional streaming RPC helpers."""
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(object):
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 __init__(self, start_rpc, initial_request=None, metadata=None):
244
- self._start_rpc = start_rpc
245
- self._initial_request = initial_request
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("Can not open an already open stream.")
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("Can not send() on an RPC that has never been open()ed.")
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("Can not recv() on an RPC that has never been open()ed.")
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
- """bool: True if this stream is currently open and active."""
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("Can not send() on an RPC that has never been open()ed.")
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("Can not recv() on an RPC that has never been open()ed.")
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()
@@ -12,4 +12,4 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- __version__ = "2.26.0"
15
+ __version__ = "2.27.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-api-core
3
- Version: 2.26.0
3
+ Version: 2.27.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
@@ -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