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,449 @@
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
+
16
+ # This file is automatically generated by CrossSync. Do not edit manually.
17
+
18
+ from __future__ import annotations
19
+ from typing import Sequence, TYPE_CHECKING, cast
20
+ import atexit
21
+ import warnings
22
+ from collections import deque
23
+ import concurrent.futures
24
+ from google.cloud.bigtable.data.exceptions import MutationsExceptionGroup
25
+ from google.cloud.bigtable.data.exceptions import FailedMutationEntryError
26
+ from google.cloud.bigtable.data._helpers import _get_retryable_errors
27
+ from google.cloud.bigtable.data._helpers import _get_timeouts
28
+ from google.cloud.bigtable.data._helpers import TABLE_DEFAULT
29
+ from google.cloud.bigtable.data.mutations import _MUTATE_ROWS_REQUEST_MUTATION_LIMIT
30
+ from google.cloud.bigtable.data.mutations import Mutation
31
+ from google.cloud.bigtable.data._cross_sync import CrossSync
32
+
33
+ if TYPE_CHECKING:
34
+ from google.cloud.bigtable.data.mutations import RowMutationEntry
35
+ from google.cloud.bigtable.data._sync_autogen.client import Table as TableType
36
+ _MB_SIZE = 1024 * 1024
37
+
38
+
39
+ @CrossSync._Sync_Impl.add_mapping_decorator("_FlowControl")
40
+ class _FlowControl:
41
+ """
42
+ Manages flow control for batched mutations. Mutations are registered against
43
+ the FlowControl object before being sent, which will block if size or count
44
+ limits have reached capacity. As mutations completed, they are removed from
45
+ the FlowControl object, which will notify any blocked requests that there
46
+ is additional capacity.
47
+
48
+ Flow limits are not hard limits. If a single mutation exceeds the configured
49
+ limits, it will be allowed as a single batch when the capacity is available.
50
+
51
+ Args:
52
+ max_mutation_count: maximum number of mutations to send in a single rpc.
53
+ This corresponds to individual mutations in a single RowMutationEntry.
54
+ max_mutation_bytes: maximum number of bytes to send in a single rpc.
55
+ Raises:
56
+ ValueError: if max_mutation_count or max_mutation_bytes is less than 0
57
+ """
58
+
59
+ def __init__(self, max_mutation_count: int, max_mutation_bytes: int):
60
+ self._max_mutation_count = max_mutation_count
61
+ self._max_mutation_bytes = max_mutation_bytes
62
+ if self._max_mutation_count < 1:
63
+ raise ValueError("max_mutation_count must be greater than 0")
64
+ if self._max_mutation_bytes < 1:
65
+ raise ValueError("max_mutation_bytes must be greater than 0")
66
+ self._capacity_condition = CrossSync._Sync_Impl.Condition()
67
+ self._in_flight_mutation_count = 0
68
+ self._in_flight_mutation_bytes = 0
69
+
70
+ def _has_capacity(self, additional_count: int, additional_size: int) -> bool:
71
+ """Checks if there is capacity to send a new entry with the given size and count
72
+
73
+ FlowControl limits are not hard limits. If a single mutation exceeds
74
+ the configured flow limits, it will be sent in a single batch when
75
+ previous batches have completed.
76
+
77
+ Args:
78
+ additional_count: number of mutations in the pending entry
79
+ additional_size: size of the pending entry
80
+ Returns:
81
+ bool: True if there is capacity to send the pending entry, False otherwise
82
+ """
83
+ acceptable_size = max(self._max_mutation_bytes, additional_size)
84
+ acceptable_count = max(self._max_mutation_count, additional_count)
85
+ new_size = self._in_flight_mutation_bytes + additional_size
86
+ new_count = self._in_flight_mutation_count + additional_count
87
+ return new_size <= acceptable_size and new_count <= acceptable_count
88
+
89
+ def remove_from_flow(
90
+ self, mutations: RowMutationEntry | list[RowMutationEntry]
91
+ ) -> None:
92
+ """Removes mutations from flow control. This method should be called once
93
+ for each mutation that was sent to add_to_flow, after the corresponding
94
+ operation is complete.
95
+
96
+ Args:
97
+ mutations: mutation or list of mutations to remove from flow control"""
98
+ if not isinstance(mutations, list):
99
+ mutations = [mutations]
100
+ total_count = sum((len(entry.mutations) for entry in mutations))
101
+ total_size = sum((entry.size() for entry in mutations))
102
+ self._in_flight_mutation_count -= total_count
103
+ self._in_flight_mutation_bytes -= total_size
104
+ with self._capacity_condition:
105
+ self._capacity_condition.notify_all()
106
+
107
+ def add_to_flow(self, mutations: RowMutationEntry | list[RowMutationEntry]):
108
+ """Generator function that registers mutations with flow control. As mutations
109
+ are accepted into the flow control, they are yielded back to the caller,
110
+ to be sent in a batch. If the flow control is at capacity, the generator
111
+ will block until there is capacity available.
112
+
113
+ Args:
114
+ mutations: list mutations to break up into batches
115
+ Yields:
116
+ list[RowMutationEntry]:
117
+ list of mutations that have reserved space in the flow control.
118
+ Each batch contains at least one mutation."""
119
+ if not isinstance(mutations, list):
120
+ mutations = [mutations]
121
+ start_idx = 0
122
+ end_idx = 0
123
+ while end_idx < len(mutations):
124
+ start_idx = end_idx
125
+ batch_mutation_count = 0
126
+ with self._capacity_condition:
127
+ while end_idx < len(mutations):
128
+ next_entry = mutations[end_idx]
129
+ next_size = next_entry.size()
130
+ next_count = len(next_entry.mutations)
131
+ if (
132
+ self._has_capacity(next_count, next_size)
133
+ and batch_mutation_count + next_count
134
+ <= _MUTATE_ROWS_REQUEST_MUTATION_LIMIT
135
+ ):
136
+ end_idx += 1
137
+ batch_mutation_count += next_count
138
+ self._in_flight_mutation_bytes += next_size
139
+ self._in_flight_mutation_count += next_count
140
+ elif start_idx != end_idx:
141
+ break
142
+ else:
143
+ self._capacity_condition.wait_for(
144
+ lambda: self._has_capacity(next_count, next_size)
145
+ )
146
+ yield mutations[start_idx:end_idx]
147
+
148
+
149
+ class MutationsBatcher:
150
+ """
151
+ Allows users to send batches using context manager API:
152
+
153
+ Runs mutate_row, mutate_rows, and check_and_mutate_row internally, combining
154
+ to use as few network requests as required
155
+
156
+ Will automatically flush the batcher:
157
+ - every flush_interval seconds
158
+ - after queue size reaches flush_limit_mutation_count
159
+ - after queue reaches flush_limit_bytes
160
+ - when batcher is closed or destroyed
161
+
162
+ Args:
163
+ table: Table to preform rpc calls
164
+ flush_interval: Automatically flush every flush_interval seconds.
165
+ If None, no time-based flushing is performed.
166
+ flush_limit_mutation_count: Flush immediately after flush_limit_mutation_count
167
+ mutations are added across all entries. If None, this limit is ignored.
168
+ flush_limit_bytes: Flush immediately after flush_limit_bytes bytes are added.
169
+ flow_control_max_mutation_count: Maximum number of inflight mutations.
170
+ flow_control_max_bytes: Maximum number of inflight bytes.
171
+ batch_operation_timeout: timeout for each mutate_rows operation, in seconds.
172
+ If TABLE_DEFAULT, defaults to the Table's default_mutate_rows_operation_timeout.
173
+ batch_attempt_timeout: timeout for each individual request, in seconds.
174
+ If TABLE_DEFAULT, defaults to the Table's default_mutate_rows_attempt_timeout.
175
+ If None, defaults to batch_operation_timeout.
176
+ batch_retryable_errors: a list of errors that will be retried if encountered.
177
+ Defaults to the Table's default_mutate_rows_retryable_errors.
178
+ """
179
+
180
+ def __init__(
181
+ self,
182
+ table: TableType,
183
+ *,
184
+ flush_interval: float | None = 5,
185
+ flush_limit_mutation_count: int | None = 1000,
186
+ flush_limit_bytes: int = 20 * _MB_SIZE,
187
+ flow_control_max_mutation_count: int = 100000,
188
+ flow_control_max_bytes: int = 100 * _MB_SIZE,
189
+ batch_operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
190
+ batch_attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
191
+ batch_retryable_errors: Sequence[type[Exception]]
192
+ | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
193
+ ):
194
+ (self._operation_timeout, self._attempt_timeout) = _get_timeouts(
195
+ batch_operation_timeout, batch_attempt_timeout, table
196
+ )
197
+ self._retryable_errors: list[type[Exception]] = _get_retryable_errors(
198
+ batch_retryable_errors, table
199
+ )
200
+ self._closed = CrossSync._Sync_Impl.Event()
201
+ self._table = table
202
+ self._staged_entries: list[RowMutationEntry] = []
203
+ (self._staged_count, self._staged_bytes) = (0, 0)
204
+ self._flow_control = CrossSync._Sync_Impl._FlowControl(
205
+ flow_control_max_mutation_count, flow_control_max_bytes
206
+ )
207
+ self._flush_limit_bytes = flush_limit_bytes
208
+ self._flush_limit_count = (
209
+ flush_limit_mutation_count
210
+ if flush_limit_mutation_count is not None
211
+ else float("inf")
212
+ )
213
+ self._sync_rpc_executor = (
214
+ concurrent.futures.ThreadPoolExecutor(max_workers=8)
215
+ if not CrossSync._Sync_Impl.is_async
216
+ else None
217
+ )
218
+ self._sync_flush_executor = (
219
+ concurrent.futures.ThreadPoolExecutor(max_workers=4)
220
+ if not CrossSync._Sync_Impl.is_async
221
+ else None
222
+ )
223
+ self._flush_timer = CrossSync._Sync_Impl.create_task(
224
+ self._timer_routine, flush_interval, sync_executor=self._sync_flush_executor
225
+ )
226
+ self._flush_jobs: set[CrossSync._Sync_Impl.Future[None]] = set()
227
+ self._entries_processed_since_last_raise: int = 0
228
+ self._exceptions_since_last_raise: int = 0
229
+ self._exception_list_limit: int = 10
230
+ self._oldest_exceptions: list[Exception] = []
231
+ self._newest_exceptions: deque[Exception] = deque(
232
+ maxlen=self._exception_list_limit
233
+ )
234
+ atexit.register(self._on_exit)
235
+
236
+ def _timer_routine(self, interval: float | None) -> None:
237
+ """Set up a background task to flush the batcher every interval seconds
238
+
239
+ If interval is None, an empty future is returned
240
+
241
+ Args:
242
+ flush_interval: Automatically flush every flush_interval seconds.
243
+ If None, no time-based flushing is performed."""
244
+ if not interval or interval <= 0:
245
+ return None
246
+ while not self._closed.is_set():
247
+ CrossSync._Sync_Impl.event_wait(
248
+ self._closed, timeout=interval, async_break_early=False
249
+ )
250
+ if not self._closed.is_set() and self._staged_entries:
251
+ self._schedule_flush()
252
+
253
+ def append(self, mutation_entry: RowMutationEntry):
254
+ """Add a new set of mutations to the internal queue
255
+
256
+ Args:
257
+ mutation_entry: new entry to add to flush queue
258
+ Raises:
259
+ RuntimeError: if batcher is closed
260
+ ValueError: if an invalid mutation type is added"""
261
+ if self._closed.is_set():
262
+ raise RuntimeError("Cannot append to closed MutationsBatcher")
263
+ if isinstance(cast(Mutation, mutation_entry), Mutation):
264
+ raise ValueError(
265
+ f"invalid mutation type: {type(mutation_entry).__name__}. Only RowMutationEntry objects are supported by batcher"
266
+ )
267
+ self._staged_entries.append(mutation_entry)
268
+ self._staged_count += len(mutation_entry.mutations)
269
+ self._staged_bytes += mutation_entry.size()
270
+ if (
271
+ self._staged_count >= self._flush_limit_count
272
+ or self._staged_bytes >= self._flush_limit_bytes
273
+ ):
274
+ self._schedule_flush()
275
+ CrossSync._Sync_Impl.yield_to_event_loop()
276
+
277
+ def _schedule_flush(self) -> CrossSync._Sync_Impl.Future[None] | None:
278
+ """Update the flush task to include the latest staged entries
279
+
280
+ Returns:
281
+ Future[None] | None:
282
+ future representing the background task, if started"""
283
+ if self._staged_entries:
284
+ (entries, self._staged_entries) = (self._staged_entries, [])
285
+ (self._staged_count, self._staged_bytes) = (0, 0)
286
+ new_task = CrossSync._Sync_Impl.create_task(
287
+ self._flush_internal, entries, sync_executor=self._sync_flush_executor
288
+ )
289
+ if not new_task.done():
290
+ self._flush_jobs.add(new_task)
291
+ new_task.add_done_callback(self._flush_jobs.remove)
292
+ return new_task
293
+ return None
294
+
295
+ def _flush_internal(self, new_entries: list[RowMutationEntry]):
296
+ """Flushes a set of mutations to the server, and updates internal state
297
+
298
+ Args:
299
+ new_entries list of RowMutationEntry objects to flush"""
300
+ in_process_requests: list[
301
+ CrossSync._Sync_Impl.Future[list[FailedMutationEntryError]]
302
+ ] = []
303
+ for batch in self._flow_control.add_to_flow(new_entries):
304
+ batch_task = CrossSync._Sync_Impl.create_task(
305
+ self._execute_mutate_rows, batch, sync_executor=self._sync_rpc_executor
306
+ )
307
+ in_process_requests.append(batch_task)
308
+ found_exceptions = self._wait_for_batch_results(*in_process_requests)
309
+ self._entries_processed_since_last_raise += len(new_entries)
310
+ self._add_exceptions(found_exceptions)
311
+
312
+ def _execute_mutate_rows(
313
+ self, batch: list[RowMutationEntry]
314
+ ) -> list[FailedMutationEntryError]:
315
+ """Helper to execute mutation operation on a batch
316
+
317
+ Args:
318
+ batch: list of RowMutationEntry objects to send to server
319
+ timeout: timeout in seconds. Used as operation_timeout and attempt_timeout.
320
+ If not given, will use table defaults
321
+ Returns:
322
+ list[FailedMutationEntryError]:
323
+ list of FailedMutationEntryError objects for mutations that failed.
324
+ FailedMutationEntryError objects will not contain index information"""
325
+ try:
326
+ operation = CrossSync._Sync_Impl._MutateRowsOperation(
327
+ self._table.client._gapic_client,
328
+ self._table,
329
+ batch,
330
+ operation_timeout=self._operation_timeout,
331
+ attempt_timeout=self._attempt_timeout,
332
+ retryable_exceptions=self._retryable_errors,
333
+ )
334
+ operation.start()
335
+ except MutationsExceptionGroup as e:
336
+ for subexc in e.exceptions:
337
+ subexc.index = None
338
+ return list(e.exceptions)
339
+ finally:
340
+ self._flow_control.remove_from_flow(batch)
341
+ return []
342
+
343
+ def _add_exceptions(self, excs: list[Exception]):
344
+ """Add new list of exceptions to internal store. To avoid unbounded memory,
345
+ the batcher will store the first and last _exception_list_limit exceptions,
346
+ and discard any in between.
347
+
348
+ Args:
349
+ excs: list of exceptions to add to the internal store"""
350
+ self._exceptions_since_last_raise += len(excs)
351
+ if excs and len(self._oldest_exceptions) < self._exception_list_limit:
352
+ addition_count = self._exception_list_limit - len(self._oldest_exceptions)
353
+ self._oldest_exceptions.extend(excs[:addition_count])
354
+ excs = excs[addition_count:]
355
+ if excs:
356
+ self._newest_exceptions.extend(excs[-self._exception_list_limit :])
357
+
358
+ def _raise_exceptions(self):
359
+ """Raise any unreported exceptions from background flush operations
360
+
361
+ Raises:
362
+ MutationsExceptionGroup: exception group with all unreported exceptions"""
363
+ if self._oldest_exceptions or self._newest_exceptions:
364
+ (oldest, self._oldest_exceptions) = (self._oldest_exceptions, [])
365
+ newest = list(self._newest_exceptions)
366
+ self._newest_exceptions.clear()
367
+ (entry_count, self._entries_processed_since_last_raise) = (
368
+ self._entries_processed_since_last_raise,
369
+ 0,
370
+ )
371
+ (exc_count, self._exceptions_since_last_raise) = (
372
+ self._exceptions_since_last_raise,
373
+ 0,
374
+ )
375
+ raise MutationsExceptionGroup.from_truncated_lists(
376
+ first_list=oldest,
377
+ last_list=newest,
378
+ total_excs=exc_count,
379
+ entry_count=entry_count,
380
+ )
381
+
382
+ def __enter__(self):
383
+ """Allow use of context manager API"""
384
+ return self
385
+
386
+ def __exit__(self, exc_type, exc, tb):
387
+ """Allow use of context manager API.
388
+
389
+ Flushes the batcher and cleans up resources."""
390
+ self.close()
391
+
392
+ @property
393
+ def closed(self) -> bool:
394
+ """Returns:
395
+ - True if the batcher is closed, False otherwise"""
396
+ return self._closed.is_set()
397
+
398
+ def close(self):
399
+ """Flush queue and clean up resources"""
400
+ self._closed.set()
401
+ self._flush_timer.cancel()
402
+ self._schedule_flush()
403
+ if self._sync_flush_executor:
404
+ with self._sync_flush_executor:
405
+ self._sync_flush_executor.shutdown(wait=True)
406
+ if self._sync_rpc_executor:
407
+ with self._sync_rpc_executor:
408
+ self._sync_rpc_executor.shutdown(wait=True)
409
+ CrossSync._Sync_Impl.wait([*self._flush_jobs, self._flush_timer])
410
+ atexit.unregister(self._on_exit)
411
+ self._raise_exceptions()
412
+
413
+ def _on_exit(self):
414
+ """Called when program is exited. Raises warning if unflushed mutations remain"""
415
+ if not self._closed.is_set() and self._staged_entries:
416
+ warnings.warn(
417
+ f"MutationsBatcher for table {self._table.table_name} was not closed. {len(self._staged_entries)} Unflushed mutations will not be sent to the server."
418
+ )
419
+
420
+ @staticmethod
421
+ def _wait_for_batch_results(
422
+ *tasks: CrossSync._Sync_Impl.Future[list[FailedMutationEntryError]]
423
+ | CrossSync._Sync_Impl.Future[None],
424
+ ) -> list[Exception]:
425
+ """Takes in a list of futures representing _execute_mutate_rows tasks,
426
+ waits for them to complete, and returns a list of errors encountered.
427
+
428
+ Args:
429
+ *tasks: futures representing _execute_mutate_rows or _flush_internal tasks
430
+ Returns:
431
+ list[Exception]:
432
+ list of Exceptions encountered by any of the tasks. Errors are expected
433
+ to be FailedMutationEntryError, representing a failed mutation operation.
434
+ If a task fails with a different exception, it will be included in the
435
+ output list. Successful tasks will not be represented in the output list.
436
+ """
437
+ if not tasks:
438
+ return []
439
+ exceptions: list[Exception] = []
440
+ for task in tasks:
441
+ try:
442
+ exc_list = task.result()
443
+ if exc_list:
444
+ for exc in exc_list:
445
+ exc.index = None
446
+ exceptions.extend(exc_list)
447
+ except Exception as e:
448
+ exceptions.append(e)
449
+ return exceptions