google-cloud-bigtable 2.30.0__py3-none-any.whl

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 (115) hide show
  1. google/cloud/bigtable/__init__.py +25 -0
  2. google/cloud/bigtable/app_profile.py +377 -0
  3. google/cloud/bigtable/backup.py +490 -0
  4. google/cloud/bigtable/batcher.py +414 -0
  5. google/cloud/bigtable/client.py +475 -0
  6. google/cloud/bigtable/cluster.py +541 -0
  7. google/cloud/bigtable/column_family.py +362 -0
  8. google/cloud/bigtable/data/__init__.py +103 -0
  9. google/cloud/bigtable/data/_async/__init__.py +25 -0
  10. google/cloud/bigtable/data/_async/_mutate_rows.py +226 -0
  11. google/cloud/bigtable/data/_async/_read_rows.py +363 -0
  12. google/cloud/bigtable/data/_async/client.py +1522 -0
  13. google/cloud/bigtable/data/_async/mutations_batcher.py +534 -0
  14. google/cloud/bigtable/data/_cross_sync/__init__.py +20 -0
  15. google/cloud/bigtable/data/_cross_sync/_decorators.py +441 -0
  16. google/cloud/bigtable/data/_cross_sync/_mapping_meta.py +64 -0
  17. google/cloud/bigtable/data/_cross_sync/cross_sync.py +334 -0
  18. google/cloud/bigtable/data/_helpers.py +249 -0
  19. google/cloud/bigtable/data/_sync_autogen/_mutate_rows.py +182 -0
  20. google/cloud/bigtable/data/_sync_autogen/_read_rows.py +304 -0
  21. google/cloud/bigtable/data/_sync_autogen/client.py +1293 -0
  22. google/cloud/bigtable/data/_sync_autogen/mutations_batcher.py +449 -0
  23. google/cloud/bigtable/data/exceptions.py +340 -0
  24. google/cloud/bigtable/data/execute_query/__init__.py +43 -0
  25. google/cloud/bigtable/data/execute_query/_async/__init__.py +13 -0
  26. google/cloud/bigtable/data/execute_query/_async/execute_query_iterator.py +291 -0
  27. google/cloud/bigtable/data/execute_query/_byte_cursor.py +123 -0
  28. google/cloud/bigtable/data/execute_query/_checksum.py +43 -0
  29. google/cloud/bigtable/data/execute_query/_parameters_formatting.py +155 -0
  30. google/cloud/bigtable/data/execute_query/_query_result_parsing_utils.py +134 -0
  31. google/cloud/bigtable/data/execute_query/_reader.py +122 -0
  32. google/cloud/bigtable/data/execute_query/_sync_autogen/execute_query_iterator.py +239 -0
  33. google/cloud/bigtable/data/execute_query/metadata.py +399 -0
  34. google/cloud/bigtable/data/execute_query/values.py +123 -0
  35. google/cloud/bigtable/data/mutations.py +380 -0
  36. google/cloud/bigtable/data/read_modify_write_rules.py +112 -0
  37. google/cloud/bigtable/data/read_rows_query.py +536 -0
  38. google/cloud/bigtable/data/row.py +535 -0
  39. google/cloud/bigtable/data/row_filters.py +968 -0
  40. google/cloud/bigtable/encryption_info.py +64 -0
  41. google/cloud/bigtable/enums.py +223 -0
  42. google/cloud/bigtable/error.py +64 -0
  43. google/cloud/bigtable/gapic_version.py +16 -0
  44. google/cloud/bigtable/helpers.py +31 -0
  45. google/cloud/bigtable/instance.py +789 -0
  46. google/cloud/bigtable/policy.py +255 -0
  47. google/cloud/bigtable/row.py +1267 -0
  48. google/cloud/bigtable/row_data.py +380 -0
  49. google/cloud/bigtable/row_filters.py +838 -0
  50. google/cloud/bigtable/row_merger.py +250 -0
  51. google/cloud/bigtable/row_set.py +213 -0
  52. google/cloud/bigtable/table.py +1409 -0
  53. google/cloud/bigtable_admin/__init__.py +410 -0
  54. google/cloud/bigtable_admin/gapic_version.py +16 -0
  55. google/cloud/bigtable_admin/py.typed +2 -0
  56. google/cloud/bigtable_admin_v2/__init__.py +250 -0
  57. google/cloud/bigtable_admin_v2/gapic_metadata.json +962 -0
  58. google/cloud/bigtable_admin_v2/gapic_version.py +16 -0
  59. google/cloud/bigtable_admin_v2/py.typed +2 -0
  60. google/cloud/bigtable_admin_v2/services/__init__.py +15 -0
  61. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/__init__.py +22 -0
  62. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/async_client.py +3465 -0
  63. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py +3944 -0
  64. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/pagers.py +681 -0
  65. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/__init__.py +40 -0
  66. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/base.py +751 -0
  67. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc.py +1249 -0
  68. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc_asyncio.py +1579 -0
  69. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest.py +6820 -0
  70. google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest_base.py +1746 -0
  71. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/__init__.py +22 -0
  72. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/async_client.py +3472 -0
  73. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py +3949 -0
  74. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/pagers.py +669 -0
  75. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/__init__.py +40 -0
  76. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/base.py +708 -0
  77. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc.py +1257 -0
  78. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc_asyncio.py +1549 -0
  79. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest.py +6609 -0
  80. google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest_base.py +1714 -0
  81. google/cloud/bigtable_admin_v2/types/__init__.py +248 -0
  82. google/cloud/bigtable_admin_v2/types/bigtable_instance_admin.py +1364 -0
  83. google/cloud/bigtable_admin_v2/types/bigtable_table_admin.py +1715 -0
  84. google/cloud/bigtable_admin_v2/types/common.py +81 -0
  85. google/cloud/bigtable_admin_v2/types/instance.py +819 -0
  86. google/cloud/bigtable_admin_v2/types/table.py +1028 -0
  87. google/cloud/bigtable_admin_v2/types/types.py +776 -0
  88. google/cloud/bigtable_v2/__init__.py +136 -0
  89. google/cloud/bigtable_v2/gapic_metadata.json +193 -0
  90. google/cloud/bigtable_v2/gapic_version.py +16 -0
  91. google/cloud/bigtable_v2/py.typed +2 -0
  92. google/cloud/bigtable_v2/services/__init__.py +15 -0
  93. google/cloud/bigtable_v2/services/bigtable/__init__.py +22 -0
  94. google/cloud/bigtable_v2/services/bigtable/async_client.py +1741 -0
  95. google/cloud/bigtable_v2/services/bigtable/client.py +2194 -0
  96. google/cloud/bigtable_v2/services/bigtable/transports/__init__.py +38 -0
  97. google/cloud/bigtable_v2/services/bigtable/transports/base.py +343 -0
  98. google/cloud/bigtable_v2/services/bigtable/transports/grpc.py +660 -0
  99. google/cloud/bigtable_v2/services/bigtable/transports/grpc_asyncio.py +762 -0
  100. google/cloud/bigtable_v2/services/bigtable/transports/rest.py +2489 -0
  101. google/cloud/bigtable_v2/services/bigtable/transports/rest_base.py +711 -0
  102. google/cloud/bigtable_v2/types/__init__.py +138 -0
  103. google/cloud/bigtable_v2/types/bigtable.py +1531 -0
  104. google/cloud/bigtable_v2/types/data.py +1612 -0
  105. google/cloud/bigtable_v2/types/feature_flags.py +119 -0
  106. google/cloud/bigtable_v2/types/request_stats.py +171 -0
  107. google/cloud/bigtable_v2/types/response_params.py +64 -0
  108. google/cloud/bigtable_v2/types/types.py +579 -0
  109. google_cloud_bigtable-2.30.0.data/scripts/fixup_bigtable_admin_v2_keywords.py +233 -0
  110. google_cloud_bigtable-2.30.0.data/scripts/fixup_bigtable_v2_keywords.py +186 -0
  111. google_cloud_bigtable-2.30.0.dist-info/LICENSE +202 -0
  112. google_cloud_bigtable-2.30.0.dist-info/METADATA +154 -0
  113. google_cloud_bigtable-2.30.0.dist-info/RECORD +115 -0
  114. google_cloud_bigtable-2.30.0.dist-info/WHEEL +5 -0
  115. google_cloud_bigtable-2.30.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,534 @@
1
+ # Copyright 2024 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
+ from __future__ import annotations
16
+
17
+ from typing import Sequence, TYPE_CHECKING, cast
18
+ import atexit
19
+ import warnings
20
+ from collections import deque
21
+ import concurrent.futures
22
+
23
+ from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup
24
+ from google.cloud.bigtable.data.exceptions import FailedMutationEntryError
25
+ from google.cloud.bigtable.data._helpers import _get_retryable_errors
26
+ from google.cloud.bigtable.data._helpers import _get_timeouts
27
+ from google.cloud.bigtable.data._helpers import TABLE_DEFAULT
28
+
29
+ from google.cloud.bigtable.data.mutations import (
30
+ _MUTATE_ROWS_REQUEST_MUTATION_LIMIT,
31
+ )
32
+ from google.cloud.bigtable.data.mutations import Mutation
33
+
34
+ from google.cloud.bigtable.data._cross_sync import CrossSync
35
+
36
+ if TYPE_CHECKING:
37
+ from google.cloud.bigtable.data.mutations import RowMutationEntry
38
+
39
+ if CrossSync.is_async:
40
+ from google.cloud.bigtable.data._async.client import TableAsync as TableType
41
+ else:
42
+ from google.cloud.bigtable.data._sync_autogen.client import Table as TableType # type: ignore
43
+
44
+ __CROSS_SYNC_OUTPUT__ = "google.cloud.bigtable.data._sync_autogen.mutations_batcher"
45
+
46
+ # used to make more readable default values
47
+ _MB_SIZE = 1024 * 1024
48
+
49
+
50
+ @CrossSync.convert_class(sync_name="_FlowControl", add_mapping_for_name="_FlowControl")
51
+ class _FlowControlAsync:
52
+ """
53
+ Manages flow control for batched mutations. Mutations are registered against
54
+ the FlowControl object before being sent, which will block if size or count
55
+ limits have reached capacity. As mutations completed, they are removed from
56
+ the FlowControl object, which will notify any blocked requests that there
57
+ is additional capacity.
58
+
59
+ Flow limits are not hard limits. If a single mutation exceeds the configured
60
+ limits, it will be allowed as a single batch when the capacity is available.
61
+
62
+ Args:
63
+ max_mutation_count: maximum number of mutations to send in a single rpc.
64
+ This corresponds to individual mutations in a single RowMutationEntry.
65
+ max_mutation_bytes: maximum number of bytes to send in a single rpc.
66
+ Raises:
67
+ ValueError: if max_mutation_count or max_mutation_bytes is less than 0
68
+ """
69
+
70
+ def __init__(
71
+ self,
72
+ max_mutation_count: int,
73
+ max_mutation_bytes: int,
74
+ ):
75
+ self._max_mutation_count = max_mutation_count
76
+ self._max_mutation_bytes = max_mutation_bytes
77
+ if self._max_mutation_count < 1:
78
+ raise ValueError("max_mutation_count must be greater than 0")
79
+ if self._max_mutation_bytes < 1:
80
+ raise ValueError("max_mutation_bytes must be greater than 0")
81
+ self._capacity_condition = CrossSync.Condition()
82
+ self._in_flight_mutation_count = 0
83
+ self._in_flight_mutation_bytes = 0
84
+
85
+ def _has_capacity(self, additional_count: int, additional_size: int) -> bool:
86
+ """
87
+ Checks if there is capacity to send a new entry with the given size and count
88
+
89
+ FlowControl limits are not hard limits. If a single mutation exceeds
90
+ the configured flow limits, it will be sent in a single batch when
91
+ previous batches have completed.
92
+
93
+ Args:
94
+ additional_count: number of mutations in the pending entry
95
+ additional_size: size of the pending entry
96
+ Returns:
97
+ bool: True if there is capacity to send the pending entry, False otherwise
98
+ """
99
+ # adjust limits to allow overly large mutations
100
+ acceptable_size = max(self._max_mutation_bytes, additional_size)
101
+ acceptable_count = max(self._max_mutation_count, additional_count)
102
+ # check if we have capacity for new mutation
103
+ new_size = self._in_flight_mutation_bytes + additional_size
104
+ new_count = self._in_flight_mutation_count + additional_count
105
+ return new_size <= acceptable_size and new_count <= acceptable_count
106
+
107
+ @CrossSync.convert
108
+ async def remove_from_flow(
109
+ self, mutations: RowMutationEntry | list[RowMutationEntry]
110
+ ) -> None:
111
+ """
112
+ Removes mutations from flow control. This method should be called once
113
+ for each mutation that was sent to add_to_flow, after the corresponding
114
+ operation is complete.
115
+
116
+ Args:
117
+ mutations: mutation or list of mutations to remove from flow control
118
+ """
119
+ if not isinstance(mutations, list):
120
+ mutations = [mutations]
121
+ total_count = sum(len(entry.mutations) for entry in mutations)
122
+ total_size = sum(entry.size() for entry in mutations)
123
+ self._in_flight_mutation_count -= total_count
124
+ self._in_flight_mutation_bytes -= total_size
125
+ # notify any blocked requests that there is additional capacity
126
+ async with self._capacity_condition:
127
+ self._capacity_condition.notify_all()
128
+
129
+ @CrossSync.convert
130
+ async def add_to_flow(self, mutations: RowMutationEntry | list[RowMutationEntry]):
131
+ """
132
+ Generator function that registers mutations with flow control. As mutations
133
+ are accepted into the flow control, they are yielded back to the caller,
134
+ to be sent in a batch. If the flow control is at capacity, the generator
135
+ will block until there is capacity available.
136
+
137
+ Args:
138
+ mutations: list mutations to break up into batches
139
+ Yields:
140
+ list[RowMutationEntry]:
141
+ list of mutations that have reserved space in the flow control.
142
+ Each batch contains at least one mutation.
143
+ """
144
+ if not isinstance(mutations, list):
145
+ mutations = [mutations]
146
+ start_idx = 0
147
+ end_idx = 0
148
+ while end_idx < len(mutations):
149
+ start_idx = end_idx
150
+ batch_mutation_count = 0
151
+ # fill up batch until we hit capacity
152
+ async with self._capacity_condition:
153
+ while end_idx < len(mutations):
154
+ next_entry = mutations[end_idx]
155
+ next_size = next_entry.size()
156
+ next_count = len(next_entry.mutations)
157
+ if (
158
+ self._has_capacity(next_count, next_size)
159
+ # make sure not to exceed per-request mutation count limits
160
+ and (batch_mutation_count + next_count)
161
+ <= _MUTATE_ROWS_REQUEST_MUTATION_LIMIT
162
+ ):
163
+ # room for new mutation; add to batch
164
+ end_idx += 1
165
+ batch_mutation_count += next_count
166
+ self._in_flight_mutation_bytes += next_size
167
+ self._in_flight_mutation_count += next_count
168
+ elif start_idx != end_idx:
169
+ # we have at least one mutation in the batch, so send it
170
+ break
171
+ else:
172
+ # batch is empty. Block until we have capacity
173
+ await self._capacity_condition.wait_for(
174
+ lambda: self._has_capacity(next_count, next_size)
175
+ )
176
+ yield mutations[start_idx:end_idx]
177
+
178
+
179
+ @CrossSync.convert_class(sync_name="MutationsBatcher")
180
+ class MutationsBatcherAsync:
181
+ """
182
+ Allows users to send batches using context manager API:
183
+
184
+ Runs mutate_row, mutate_rows, and check_and_mutate_row internally, combining
185
+ to use as few network requests as required
186
+
187
+ Will automatically flush the batcher:
188
+ - every flush_interval seconds
189
+ - after queue size reaches flush_limit_mutation_count
190
+ - after queue reaches flush_limit_bytes
191
+ - when batcher is closed or destroyed
192
+
193
+ Args:
194
+ table: Table to preform rpc calls
195
+ flush_interval: Automatically flush every flush_interval seconds.
196
+ If None, no time-based flushing is performed.
197
+ flush_limit_mutation_count: Flush immediately after flush_limit_mutation_count
198
+ mutations are added across all entries. If None, this limit is ignored.
199
+ flush_limit_bytes: Flush immediately after flush_limit_bytes bytes are added.
200
+ flow_control_max_mutation_count: Maximum number of inflight mutations.
201
+ flow_control_max_bytes: Maximum number of inflight bytes.
202
+ batch_operation_timeout: timeout for each mutate_rows operation, in seconds.
203
+ If TABLE_DEFAULT, defaults to the Table's default_mutate_rows_operation_timeout.
204
+ batch_attempt_timeout: timeout for each individual request, in seconds.
205
+ If TABLE_DEFAULT, defaults to the Table's default_mutate_rows_attempt_timeout.
206
+ If None, defaults to batch_operation_timeout.
207
+ batch_retryable_errors: a list of errors that will be retried if encountered.
208
+ Defaults to the Table's default_mutate_rows_retryable_errors.
209
+ """
210
+
211
+ def __init__(
212
+ self,
213
+ table: TableType,
214
+ *,
215
+ flush_interval: float | None = 5,
216
+ flush_limit_mutation_count: int | None = 1000,
217
+ flush_limit_bytes: int = 20 * _MB_SIZE,
218
+ flow_control_max_mutation_count: int = 100_000,
219
+ flow_control_max_bytes: int = 100 * _MB_SIZE,
220
+ batch_operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
221
+ batch_attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
222
+ batch_retryable_errors: Sequence[type[Exception]]
223
+ | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
224
+ ):
225
+ self._operation_timeout, self._attempt_timeout = _get_timeouts(
226
+ batch_operation_timeout, batch_attempt_timeout, table
227
+ )
228
+ self._retryable_errors: list[type[Exception]] = _get_retryable_errors(
229
+ batch_retryable_errors, table
230
+ )
231
+
232
+ self._closed = CrossSync.Event()
233
+ self._table = table
234
+ self._staged_entries: list[RowMutationEntry] = []
235
+ self._staged_count, self._staged_bytes = 0, 0
236
+ self._flow_control = CrossSync._FlowControl(
237
+ flow_control_max_mutation_count, flow_control_max_bytes
238
+ )
239
+ self._flush_limit_bytes = flush_limit_bytes
240
+ self._flush_limit_count = (
241
+ flush_limit_mutation_count
242
+ if flush_limit_mutation_count is not None
243
+ else float("inf")
244
+ )
245
+ # used by sync class to run mutate_rows operations
246
+ self._sync_rpc_executor = (
247
+ concurrent.futures.ThreadPoolExecutor(max_workers=8)
248
+ if not CrossSync.is_async
249
+ else None
250
+ )
251
+ # used by sync class to manage flush_internal tasks
252
+ self._sync_flush_executor = (
253
+ concurrent.futures.ThreadPoolExecutor(max_workers=4)
254
+ if not CrossSync.is_async
255
+ else None
256
+ )
257
+ self._flush_timer = CrossSync.create_task(
258
+ self._timer_routine, flush_interval, sync_executor=self._sync_flush_executor
259
+ )
260
+ self._flush_jobs: set[CrossSync.Future[None]] = set()
261
+ # MutationExceptionGroup reports number of successful entries along with failures
262
+ self._entries_processed_since_last_raise: int = 0
263
+ self._exceptions_since_last_raise: int = 0
264
+ # keep track of the first and last _exception_list_limit exceptions
265
+ self._exception_list_limit: int = 10
266
+ self._oldest_exceptions: list[Exception] = []
267
+ self._newest_exceptions: deque[Exception] = deque(
268
+ maxlen=self._exception_list_limit
269
+ )
270
+ # clean up on program exit
271
+ atexit.register(self._on_exit)
272
+
273
+ @CrossSync.convert
274
+ async def _timer_routine(self, interval: float | None) -> None:
275
+ """
276
+ Set up a background task to flush the batcher every interval seconds
277
+
278
+ If interval is None, an empty future is returned
279
+
280
+ Args:
281
+ flush_interval: Automatically flush every flush_interval seconds.
282
+ If None, no time-based flushing is performed.
283
+ """
284
+ if not interval or interval <= 0:
285
+ return None
286
+ while not self._closed.is_set():
287
+ # wait until interval has passed, or until closed
288
+ await CrossSync.event_wait(
289
+ self._closed, timeout=interval, async_break_early=False
290
+ )
291
+ if not self._closed.is_set() and self._staged_entries:
292
+ self._schedule_flush()
293
+
294
+ @CrossSync.convert
295
+ async def append(self, mutation_entry: RowMutationEntry):
296
+ """
297
+ Add a new set of mutations to the internal queue
298
+
299
+ Args:
300
+ mutation_entry: new entry to add to flush queue
301
+ Raises:
302
+ RuntimeError: if batcher is closed
303
+ ValueError: if an invalid mutation type is added
304
+ """
305
+ # TODO: return a future to track completion of this entry
306
+ if self._closed.is_set():
307
+ raise RuntimeError("Cannot append to closed MutationsBatcher")
308
+ if isinstance(cast(Mutation, mutation_entry), Mutation):
309
+ raise ValueError(
310
+ f"invalid mutation type: {type(mutation_entry).__name__}. Only RowMutationEntry objects are supported by batcher"
311
+ )
312
+ self._staged_entries.append(mutation_entry)
313
+ # start a new flush task if limits exceeded
314
+ self._staged_count += len(mutation_entry.mutations)
315
+ self._staged_bytes += mutation_entry.size()
316
+ if (
317
+ self._staged_count >= self._flush_limit_count
318
+ or self._staged_bytes >= self._flush_limit_bytes
319
+ ):
320
+ self._schedule_flush()
321
+ # yield to the event loop to allow flush to run
322
+ await CrossSync.yield_to_event_loop()
323
+
324
+ def _schedule_flush(self) -> CrossSync.Future[None] | None:
325
+ """
326
+ Update the flush task to include the latest staged entries
327
+
328
+ Returns:
329
+ Future[None] | None:
330
+ future representing the background task, if started
331
+ """
332
+ if self._staged_entries:
333
+ entries, self._staged_entries = self._staged_entries, []
334
+ self._staged_count, self._staged_bytes = 0, 0
335
+ new_task = CrossSync.create_task(
336
+ self._flush_internal, entries, sync_executor=self._sync_flush_executor
337
+ )
338
+ if not new_task.done():
339
+ self._flush_jobs.add(new_task)
340
+ new_task.add_done_callback(self._flush_jobs.remove)
341
+ return new_task
342
+ return None
343
+
344
+ @CrossSync.convert
345
+ async def _flush_internal(self, new_entries: list[RowMutationEntry]):
346
+ """
347
+ Flushes a set of mutations to the server, and updates internal state
348
+
349
+ Args:
350
+ new_entries list of RowMutationEntry objects to flush
351
+ """
352
+ # flush new entries
353
+ in_process_requests: list[CrossSync.Future[list[FailedMutationEntryError]]] = []
354
+ async for batch in self._flow_control.add_to_flow(new_entries):
355
+ batch_task = CrossSync.create_task(
356
+ self._execute_mutate_rows, batch, sync_executor=self._sync_rpc_executor
357
+ )
358
+ in_process_requests.append(batch_task)
359
+ # wait for all inflight requests to complete
360
+ found_exceptions = await self._wait_for_batch_results(*in_process_requests)
361
+ # update exception data to reflect any new errors
362
+ self._entries_processed_since_last_raise += len(new_entries)
363
+ self._add_exceptions(found_exceptions)
364
+
365
+ @CrossSync.convert
366
+ async def _execute_mutate_rows(
367
+ self, batch: list[RowMutationEntry]
368
+ ) -> list[FailedMutationEntryError]:
369
+ """
370
+ Helper to execute mutation operation on a batch
371
+
372
+ Args:
373
+ batch: list of RowMutationEntry objects to send to server
374
+ timeout: timeout in seconds. Used as operation_timeout and attempt_timeout.
375
+ If not given, will use table defaults
376
+ Returns:
377
+ list[FailedMutationEntryError]:
378
+ list of FailedMutationEntryError objects for mutations that failed.
379
+ FailedMutationEntryError objects will not contain index information
380
+ """
381
+ try:
382
+ operation = CrossSync._MutateRowsOperation(
383
+ self._table.client._gapic_client,
384
+ self._table,
385
+ batch,
386
+ operation_timeout=self._operation_timeout,
387
+ attempt_timeout=self._attempt_timeout,
388
+ retryable_exceptions=self._retryable_errors,
389
+ )
390
+ await operation.start()
391
+ except MutationsExceptionGroup as e:
392
+ # strip index information from exceptions, since it is not useful in a batch context
393
+ for subexc in e.exceptions:
394
+ subexc.index = None
395
+ return list(e.exceptions)
396
+ finally:
397
+ # mark batch as complete in flow control
398
+ await self._flow_control.remove_from_flow(batch)
399
+ return []
400
+
401
+ def _add_exceptions(self, excs: list[Exception]):
402
+ """
403
+ Add new list of exceptions to internal store. To avoid unbounded memory,
404
+ the batcher will store the first and last _exception_list_limit exceptions,
405
+ and discard any in between.
406
+
407
+ Args:
408
+ excs: list of exceptions to add to the internal store
409
+ """
410
+ self._exceptions_since_last_raise += len(excs)
411
+ if excs and len(self._oldest_exceptions) < self._exception_list_limit:
412
+ # populate oldest_exceptions with found_exceptions
413
+ addition_count = self._exception_list_limit - len(self._oldest_exceptions)
414
+ self._oldest_exceptions.extend(excs[:addition_count])
415
+ excs = excs[addition_count:]
416
+ if excs:
417
+ # populate newest_exceptions with remaining found_exceptions
418
+ self._newest_exceptions.extend(excs[-self._exception_list_limit :])
419
+
420
+ def _raise_exceptions(self):
421
+ """
422
+ Raise any unreported exceptions from background flush operations
423
+
424
+ Raises:
425
+ MutationsExceptionGroup: exception group with all unreported exceptions
426
+ """
427
+ if self._oldest_exceptions or self._newest_exceptions:
428
+ oldest, self._oldest_exceptions = self._oldest_exceptions, []
429
+ newest = list(self._newest_exceptions)
430
+ self._newest_exceptions.clear()
431
+ entry_count, self._entries_processed_since_last_raise = (
432
+ self._entries_processed_since_last_raise,
433
+ 0,
434
+ )
435
+ exc_count, self._exceptions_since_last_raise = (
436
+ self._exceptions_since_last_raise,
437
+ 0,
438
+ )
439
+ raise MutationsExceptionGroup.from_truncated_lists(
440
+ first_list=oldest,
441
+ last_list=newest,
442
+ total_excs=exc_count,
443
+ entry_count=entry_count,
444
+ )
445
+
446
+ @CrossSync.convert(sync_name="__enter__")
447
+ async def __aenter__(self):
448
+ """Allow use of context manager API"""
449
+ return self
450
+
451
+ @CrossSync.convert(sync_name="__exit__")
452
+ async def __aexit__(self, exc_type, exc, tb):
453
+ """
454
+ Allow use of context manager API.
455
+
456
+ Flushes the batcher and cleans up resources.
457
+ """
458
+ await self.close()
459
+
460
+ @property
461
+ def closed(self) -> bool:
462
+ """
463
+ Returns:
464
+ - True if the batcher is closed, False otherwise
465
+ """
466
+ return self._closed.is_set()
467
+
468
+ @CrossSync.convert
469
+ async def close(self):
470
+ """
471
+ Flush queue and clean up resources
472
+ """
473
+ self._closed.set()
474
+ self._flush_timer.cancel()
475
+ self._schedule_flush()
476
+ # shut down executors
477
+ if self._sync_flush_executor:
478
+ with self._sync_flush_executor:
479
+ self._sync_flush_executor.shutdown(wait=True)
480
+ if self._sync_rpc_executor:
481
+ with self._sync_rpc_executor:
482
+ self._sync_rpc_executor.shutdown(wait=True)
483
+ await CrossSync.wait([*self._flush_jobs, self._flush_timer])
484
+ atexit.unregister(self._on_exit)
485
+ # raise unreported exceptions
486
+ self._raise_exceptions()
487
+
488
+ def _on_exit(self):
489
+ """
490
+ Called when program is exited. Raises warning if unflushed mutations remain
491
+ """
492
+ if not self._closed.is_set() and self._staged_entries:
493
+ warnings.warn(
494
+ f"MutationsBatcher for table {self._table.table_name} was not closed. "
495
+ f"{len(self._staged_entries)} Unflushed mutations will not be sent to the server."
496
+ )
497
+
498
+ @staticmethod
499
+ @CrossSync.convert
500
+ async def _wait_for_batch_results(
501
+ *tasks: CrossSync.Future[list[FailedMutationEntryError]]
502
+ | CrossSync.Future[None],
503
+ ) -> list[Exception]:
504
+ """
505
+ Takes in a list of futures representing _execute_mutate_rows tasks,
506
+ waits for them to complete, and returns a list of errors encountered.
507
+
508
+ Args:
509
+ *tasks: futures representing _execute_mutate_rows or _flush_internal tasks
510
+ Returns:
511
+ list[Exception]:
512
+ list of Exceptions encountered by any of the tasks. Errors are expected
513
+ to be FailedMutationEntryError, representing a failed mutation operation.
514
+ If a task fails with a different exception, it will be included in the
515
+ output list. Successful tasks will not be represented in the output list.
516
+ """
517
+ if not tasks:
518
+ return []
519
+ exceptions: list[Exception] = []
520
+ for task in tasks:
521
+ if CrossSync.is_async:
522
+ # futures don't need to be awaited in sync mode
523
+ await task
524
+ try:
525
+ exc_list = task.result()
526
+ if exc_list:
527
+ # expect a list of FailedMutationEntryError objects
528
+ for exc in exc_list:
529
+ # strip index information
530
+ exc.index = None
531
+ exceptions.extend(exc_list)
532
+ except Exception as e:
533
+ exceptions.append(e)
534
+ return exceptions
@@ -0,0 +1,20 @@
1
+ # Copyright 2024 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
+ from .cross_sync import CrossSync
16
+
17
+
18
+ __all__ = [
19
+ "CrossSync",
20
+ ]