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.
- google/cloud/bigtable/__init__.py +25 -0
- google/cloud/bigtable/app_profile.py +377 -0
- google/cloud/bigtable/backup.py +490 -0
- google/cloud/bigtable/batcher.py +414 -0
- google/cloud/bigtable/client.py +475 -0
- google/cloud/bigtable/cluster.py +541 -0
- google/cloud/bigtable/column_family.py +362 -0
- google/cloud/bigtable/data/__init__.py +103 -0
- google/cloud/bigtable/data/_async/__init__.py +25 -0
- google/cloud/bigtable/data/_async/_mutate_rows.py +226 -0
- google/cloud/bigtable/data/_async/_read_rows.py +363 -0
- google/cloud/bigtable/data/_async/client.py +1522 -0
- google/cloud/bigtable/data/_async/mutations_batcher.py +534 -0
- google/cloud/bigtable/data/_cross_sync/__init__.py +20 -0
- google/cloud/bigtable/data/_cross_sync/_decorators.py +441 -0
- google/cloud/bigtable/data/_cross_sync/_mapping_meta.py +64 -0
- google/cloud/bigtable/data/_cross_sync/cross_sync.py +334 -0
- google/cloud/bigtable/data/_helpers.py +249 -0
- google/cloud/bigtable/data/_sync_autogen/_mutate_rows.py +182 -0
- google/cloud/bigtable/data/_sync_autogen/_read_rows.py +304 -0
- google/cloud/bigtable/data/_sync_autogen/client.py +1293 -0
- google/cloud/bigtable/data/_sync_autogen/mutations_batcher.py +449 -0
- google/cloud/bigtable/data/exceptions.py +340 -0
- google/cloud/bigtable/data/execute_query/__init__.py +43 -0
- google/cloud/bigtable/data/execute_query/_async/__init__.py +13 -0
- google/cloud/bigtable/data/execute_query/_async/execute_query_iterator.py +291 -0
- google/cloud/bigtable/data/execute_query/_byte_cursor.py +123 -0
- google/cloud/bigtable/data/execute_query/_checksum.py +43 -0
- google/cloud/bigtable/data/execute_query/_parameters_formatting.py +155 -0
- google/cloud/bigtable/data/execute_query/_query_result_parsing_utils.py +134 -0
- google/cloud/bigtable/data/execute_query/_reader.py +122 -0
- google/cloud/bigtable/data/execute_query/_sync_autogen/execute_query_iterator.py +239 -0
- google/cloud/bigtable/data/execute_query/metadata.py +399 -0
- google/cloud/bigtable/data/execute_query/values.py +123 -0
- google/cloud/bigtable/data/mutations.py +380 -0
- google/cloud/bigtable/data/read_modify_write_rules.py +112 -0
- google/cloud/bigtable/data/read_rows_query.py +536 -0
- google/cloud/bigtable/data/row.py +535 -0
- google/cloud/bigtable/data/row_filters.py +968 -0
- google/cloud/bigtable/encryption_info.py +64 -0
- google/cloud/bigtable/enums.py +223 -0
- google/cloud/bigtable/error.py +64 -0
- google/cloud/bigtable/gapic_version.py +16 -0
- google/cloud/bigtable/helpers.py +31 -0
- google/cloud/bigtable/instance.py +789 -0
- google/cloud/bigtable/policy.py +255 -0
- google/cloud/bigtable/row.py +1267 -0
- google/cloud/bigtable/row_data.py +380 -0
- google/cloud/bigtable/row_filters.py +838 -0
- google/cloud/bigtable/row_merger.py +250 -0
- google/cloud/bigtable/row_set.py +213 -0
- google/cloud/bigtable/table.py +1409 -0
- google/cloud/bigtable_admin/__init__.py +410 -0
- google/cloud/bigtable_admin/gapic_version.py +16 -0
- google/cloud/bigtable_admin/py.typed +2 -0
- google/cloud/bigtable_admin_v2/__init__.py +250 -0
- google/cloud/bigtable_admin_v2/gapic_metadata.json +962 -0
- google/cloud/bigtable_admin_v2/gapic_version.py +16 -0
- google/cloud/bigtable_admin_v2/py.typed +2 -0
- google/cloud/bigtable_admin_v2/services/__init__.py +15 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/__init__.py +22 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/async_client.py +3465 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/client.py +3944 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/pagers.py +681 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/__init__.py +40 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/base.py +751 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc.py +1249 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/grpc_asyncio.py +1579 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest.py +6820 -0
- google/cloud/bigtable_admin_v2/services/bigtable_instance_admin/transports/rest_base.py +1746 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/__init__.py +22 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/async_client.py +3472 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/client.py +3949 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/pagers.py +669 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/__init__.py +40 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/base.py +708 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc.py +1257 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/grpc_asyncio.py +1549 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest.py +6609 -0
- google/cloud/bigtable_admin_v2/services/bigtable_table_admin/transports/rest_base.py +1714 -0
- google/cloud/bigtable_admin_v2/types/__init__.py +248 -0
- google/cloud/bigtable_admin_v2/types/bigtable_instance_admin.py +1364 -0
- google/cloud/bigtable_admin_v2/types/bigtable_table_admin.py +1715 -0
- google/cloud/bigtable_admin_v2/types/common.py +81 -0
- google/cloud/bigtable_admin_v2/types/instance.py +819 -0
- google/cloud/bigtable_admin_v2/types/table.py +1028 -0
- google/cloud/bigtable_admin_v2/types/types.py +776 -0
- google/cloud/bigtable_v2/__init__.py +136 -0
- google/cloud/bigtable_v2/gapic_metadata.json +193 -0
- google/cloud/bigtable_v2/gapic_version.py +16 -0
- google/cloud/bigtable_v2/py.typed +2 -0
- google/cloud/bigtable_v2/services/__init__.py +15 -0
- google/cloud/bigtable_v2/services/bigtable/__init__.py +22 -0
- google/cloud/bigtable_v2/services/bigtable/async_client.py +1741 -0
- google/cloud/bigtable_v2/services/bigtable/client.py +2194 -0
- google/cloud/bigtable_v2/services/bigtable/transports/__init__.py +38 -0
- google/cloud/bigtable_v2/services/bigtable/transports/base.py +343 -0
- google/cloud/bigtable_v2/services/bigtable/transports/grpc.py +660 -0
- google/cloud/bigtable_v2/services/bigtable/transports/grpc_asyncio.py +762 -0
- google/cloud/bigtable_v2/services/bigtable/transports/rest.py +2489 -0
- google/cloud/bigtable_v2/services/bigtable/transports/rest_base.py +711 -0
- google/cloud/bigtable_v2/types/__init__.py +138 -0
- google/cloud/bigtable_v2/types/bigtable.py +1531 -0
- google/cloud/bigtable_v2/types/data.py +1612 -0
- google/cloud/bigtable_v2/types/feature_flags.py +119 -0
- google/cloud/bigtable_v2/types/request_stats.py +171 -0
- google/cloud/bigtable_v2/types/response_params.py +64 -0
- google/cloud/bigtable_v2/types/types.py +579 -0
- google_cloud_bigtable-2.30.0.data/scripts/fixup_bigtable_admin_v2_keywords.py +233 -0
- google_cloud_bigtable-2.30.0.data/scripts/fixup_bigtable_v2_keywords.py +186 -0
- google_cloud_bigtable-2.30.0.dist-info/LICENSE +202 -0
- google_cloud_bigtable-2.30.0.dist-info/METADATA +154 -0
- google_cloud_bigtable-2.30.0.dist-info/RECORD +115 -0
- google_cloud_bigtable-2.30.0.dist-info/WHEEL +5 -0
- google_cloud_bigtable-2.30.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,226 @@
|
|
|
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
|
|
18
|
+
import functools
|
|
19
|
+
|
|
20
|
+
from google.api_core import exceptions as core_exceptions
|
|
21
|
+
from google.api_core import retry as retries
|
|
22
|
+
import google.cloud.bigtable.data.exceptions as bt_exceptions
|
|
23
|
+
from google.cloud.bigtable.data._helpers import _attempt_timeout_generator
|
|
24
|
+
from google.cloud.bigtable.data._helpers import _retry_exception_factory
|
|
25
|
+
|
|
26
|
+
# mutate_rows requests are limited to this number of mutations
|
|
27
|
+
from google.cloud.bigtable.data.mutations import _MUTATE_ROWS_REQUEST_MUTATION_LIMIT
|
|
28
|
+
from google.cloud.bigtable.data.mutations import _EntryWithProto
|
|
29
|
+
|
|
30
|
+
from google.cloud.bigtable.data._cross_sync import CrossSync
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from google.cloud.bigtable.data.mutations import RowMutationEntry
|
|
34
|
+
|
|
35
|
+
if CrossSync.is_async:
|
|
36
|
+
from google.cloud.bigtable_v2.services.bigtable.async_client import (
|
|
37
|
+
BigtableAsyncClient as GapicClientType,
|
|
38
|
+
)
|
|
39
|
+
from google.cloud.bigtable.data._async.client import TableAsync as TableType
|
|
40
|
+
else:
|
|
41
|
+
from google.cloud.bigtable_v2.services.bigtable.client import ( # type: ignore
|
|
42
|
+
BigtableClient as GapicClientType,
|
|
43
|
+
)
|
|
44
|
+
from google.cloud.bigtable.data._sync_autogen.client import Table as TableType # type: ignore
|
|
45
|
+
|
|
46
|
+
__CROSS_SYNC_OUTPUT__ = "google.cloud.bigtable.data._sync_autogen._mutate_rows"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@CrossSync.convert_class("_MutateRowsOperation")
|
|
50
|
+
class _MutateRowsOperationAsync:
|
|
51
|
+
"""
|
|
52
|
+
MutateRowsOperation manages the logic of sending a set of row mutations,
|
|
53
|
+
and retrying on failed entries. It manages this using the _run_attempt
|
|
54
|
+
function, which attempts to mutate all outstanding entries, and raises
|
|
55
|
+
_MutateRowsIncomplete if any retryable errors are encountered.
|
|
56
|
+
|
|
57
|
+
Errors are exposed as a MutationsExceptionGroup, which contains a list of
|
|
58
|
+
exceptions organized by the related failed mutation entries.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
gapic_client: the client to use for the mutate_rows call
|
|
62
|
+
table: the table associated with the request
|
|
63
|
+
mutation_entries: a list of RowMutationEntry objects to send to the server
|
|
64
|
+
operation_timeout: the timeout to use for the entire operation, in seconds.
|
|
65
|
+
attempt_timeout: the timeout to use for each mutate_rows attempt, in seconds.
|
|
66
|
+
If not specified, the request will run until operation_timeout is reached.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
@CrossSync.convert
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
gapic_client: GapicClientType,
|
|
73
|
+
table: TableType,
|
|
74
|
+
mutation_entries: list["RowMutationEntry"],
|
|
75
|
+
operation_timeout: float,
|
|
76
|
+
attempt_timeout: float | None,
|
|
77
|
+
retryable_exceptions: Sequence[type[Exception]] = (),
|
|
78
|
+
):
|
|
79
|
+
# check that mutations are within limits
|
|
80
|
+
total_mutations = sum(len(entry.mutations) for entry in mutation_entries)
|
|
81
|
+
if total_mutations > _MUTATE_ROWS_REQUEST_MUTATION_LIMIT:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
"mutate_rows requests can contain at most "
|
|
84
|
+
f"{_MUTATE_ROWS_REQUEST_MUTATION_LIMIT} mutations across "
|
|
85
|
+
f"all entries. Found {total_mutations}."
|
|
86
|
+
)
|
|
87
|
+
# create partial function to pass to trigger rpc call
|
|
88
|
+
self._gapic_fn = functools.partial(
|
|
89
|
+
gapic_client.mutate_rows,
|
|
90
|
+
table_name=table.table_name,
|
|
91
|
+
app_profile_id=table.app_profile_id,
|
|
92
|
+
retry=None,
|
|
93
|
+
)
|
|
94
|
+
# create predicate for determining which errors are retryable
|
|
95
|
+
self.is_retryable = retries.if_exception_type(
|
|
96
|
+
# RPC level errors
|
|
97
|
+
*retryable_exceptions,
|
|
98
|
+
# Entry level errors
|
|
99
|
+
bt_exceptions._MutateRowsIncomplete,
|
|
100
|
+
)
|
|
101
|
+
sleep_generator = retries.exponential_sleep_generator(0.01, 2, 60)
|
|
102
|
+
self._operation = lambda: CrossSync.retry_target(
|
|
103
|
+
self._run_attempt,
|
|
104
|
+
self.is_retryable,
|
|
105
|
+
sleep_generator,
|
|
106
|
+
operation_timeout,
|
|
107
|
+
exception_factory=_retry_exception_factory,
|
|
108
|
+
)
|
|
109
|
+
# initialize state
|
|
110
|
+
self.timeout_generator = _attempt_timeout_generator(
|
|
111
|
+
attempt_timeout, operation_timeout
|
|
112
|
+
)
|
|
113
|
+
self.mutations = [_EntryWithProto(m, m._to_pb()) for m in mutation_entries]
|
|
114
|
+
self.remaining_indices = list(range(len(self.mutations)))
|
|
115
|
+
self.errors: dict[int, list[Exception]] = {}
|
|
116
|
+
|
|
117
|
+
@CrossSync.convert
|
|
118
|
+
async def start(self):
|
|
119
|
+
"""
|
|
120
|
+
Start the operation, and run until completion
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
MutationsExceptionGroup: if any mutations failed
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
# trigger mutate_rows
|
|
127
|
+
await self._operation()
|
|
128
|
+
except Exception as exc:
|
|
129
|
+
# exceptions raised by retryable are added to the list of exceptions for all unfinalized mutations
|
|
130
|
+
incomplete_indices = self.remaining_indices.copy()
|
|
131
|
+
for idx in incomplete_indices:
|
|
132
|
+
self._handle_entry_error(idx, exc)
|
|
133
|
+
finally:
|
|
134
|
+
# raise exception detailing incomplete mutations
|
|
135
|
+
all_errors: list[Exception] = []
|
|
136
|
+
for idx, exc_list in self.errors.items():
|
|
137
|
+
if len(exc_list) == 0:
|
|
138
|
+
raise core_exceptions.ClientError(
|
|
139
|
+
f"Mutation {idx} failed with no associated errors"
|
|
140
|
+
)
|
|
141
|
+
elif len(exc_list) == 1:
|
|
142
|
+
cause_exc = exc_list[0]
|
|
143
|
+
else:
|
|
144
|
+
cause_exc = bt_exceptions.RetryExceptionGroup(exc_list)
|
|
145
|
+
entry = self.mutations[idx].entry
|
|
146
|
+
all_errors.append(
|
|
147
|
+
bt_exceptions.FailedMutationEntryError(idx, entry, cause_exc)
|
|
148
|
+
)
|
|
149
|
+
if all_errors:
|
|
150
|
+
raise bt_exceptions.MutationsExceptionGroup(
|
|
151
|
+
all_errors, len(self.mutations)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@CrossSync.convert
|
|
155
|
+
async def _run_attempt(self):
|
|
156
|
+
"""
|
|
157
|
+
Run a single attempt of the mutate_rows rpc.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
_MutateRowsIncomplete: if there are failed mutations eligible for
|
|
161
|
+
retry after the attempt is complete
|
|
162
|
+
GoogleAPICallError: if the gapic rpc fails
|
|
163
|
+
"""
|
|
164
|
+
request_entries = [self.mutations[idx].proto for idx in self.remaining_indices]
|
|
165
|
+
# track mutations in this request that have not been finalized yet
|
|
166
|
+
active_request_indices = {
|
|
167
|
+
req_idx: orig_idx for req_idx, orig_idx in enumerate(self.remaining_indices)
|
|
168
|
+
}
|
|
169
|
+
self.remaining_indices = []
|
|
170
|
+
if not request_entries:
|
|
171
|
+
# no more mutations. return early
|
|
172
|
+
return
|
|
173
|
+
# make gapic request
|
|
174
|
+
try:
|
|
175
|
+
result_generator = await self._gapic_fn(
|
|
176
|
+
timeout=next(self.timeout_generator),
|
|
177
|
+
entries=request_entries,
|
|
178
|
+
retry=None,
|
|
179
|
+
)
|
|
180
|
+
async for result_list in result_generator:
|
|
181
|
+
for result in result_list.entries:
|
|
182
|
+
# convert sub-request index to global index
|
|
183
|
+
orig_idx = active_request_indices[result.index]
|
|
184
|
+
entry_error = core_exceptions.from_grpc_status(
|
|
185
|
+
result.status.code,
|
|
186
|
+
result.status.message,
|
|
187
|
+
details=result.status.details,
|
|
188
|
+
)
|
|
189
|
+
if result.status.code != 0:
|
|
190
|
+
# mutation failed; update error list (and remaining_indices if retryable)
|
|
191
|
+
self._handle_entry_error(orig_idx, entry_error)
|
|
192
|
+
elif orig_idx in self.errors:
|
|
193
|
+
# mutation succeeded; remove from error list
|
|
194
|
+
del self.errors[orig_idx]
|
|
195
|
+
# remove processed entry from active list
|
|
196
|
+
del active_request_indices[result.index]
|
|
197
|
+
except Exception as exc:
|
|
198
|
+
# add this exception to list for each mutation that wasn't
|
|
199
|
+
# already handled, and update remaining_indices if mutation is retryable
|
|
200
|
+
for idx in active_request_indices.values():
|
|
201
|
+
self._handle_entry_error(idx, exc)
|
|
202
|
+
# bubble up exception to be handled by retry wrapper
|
|
203
|
+
raise
|
|
204
|
+
# check if attempt succeeded, or needs to be retried
|
|
205
|
+
if self.remaining_indices:
|
|
206
|
+
# unfinished work; raise exception to trigger retry
|
|
207
|
+
raise bt_exceptions._MutateRowsIncomplete
|
|
208
|
+
|
|
209
|
+
def _handle_entry_error(self, idx: int, exc: Exception):
|
|
210
|
+
"""
|
|
211
|
+
Add an exception to the list of exceptions for a given mutation index,
|
|
212
|
+
and add the index to the list of remaining indices if the exception is
|
|
213
|
+
retryable.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
idx: the index of the mutation that failed
|
|
217
|
+
exc: the exception to add to the list
|
|
218
|
+
"""
|
|
219
|
+
entry = self.mutations[idx].entry
|
|
220
|
+
self.errors.setdefault(idx, []).append(exc)
|
|
221
|
+
if (
|
|
222
|
+
entry.is_idempotent()
|
|
223
|
+
and self.is_retryable(exc)
|
|
224
|
+
and idx not in self.remaining_indices
|
|
225
|
+
):
|
|
226
|
+
self.remaining_indices.append(idx)
|
|
@@ -0,0 +1,363 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Sequence, TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from google.cloud.bigtable_v2.types import ReadRowsRequest as ReadRowsRequestPB
|
|
21
|
+
from google.cloud.bigtable_v2.types import ReadRowsResponse as ReadRowsResponsePB
|
|
22
|
+
from google.cloud.bigtable_v2.types import RowSet as RowSetPB
|
|
23
|
+
from google.cloud.bigtable_v2.types import RowRange as RowRangePB
|
|
24
|
+
|
|
25
|
+
from google.cloud.bigtable.data.row import Row, Cell
|
|
26
|
+
from google.cloud.bigtable.data.read_rows_query import ReadRowsQuery
|
|
27
|
+
from google.cloud.bigtable.data.exceptions import InvalidChunk
|
|
28
|
+
from google.cloud.bigtable.data.exceptions import _RowSetComplete
|
|
29
|
+
from google.cloud.bigtable.data.exceptions import _ResetRow
|
|
30
|
+
from google.cloud.bigtable.data._helpers import _attempt_timeout_generator
|
|
31
|
+
from google.cloud.bigtable.data._helpers import _retry_exception_factory
|
|
32
|
+
|
|
33
|
+
from google.api_core import retry as retries
|
|
34
|
+
from google.api_core.retry import exponential_sleep_generator
|
|
35
|
+
|
|
36
|
+
from google.cloud.bigtable.data._cross_sync import CrossSync
|
|
37
|
+
|
|
38
|
+
if TYPE_CHECKING:
|
|
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._read_rows"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@CrossSync.convert_class("_ReadRowsOperation")
|
|
48
|
+
class _ReadRowsOperationAsync:
|
|
49
|
+
"""
|
|
50
|
+
ReadRowsOperation handles the logic of merging chunks from a ReadRowsResponse stream
|
|
51
|
+
into a stream of Row objects.
|
|
52
|
+
|
|
53
|
+
ReadRowsOperation.merge_row_response_stream takes in a stream of ReadRowsResponse
|
|
54
|
+
and turns them into a stream of Row objects using an internal
|
|
55
|
+
StateMachine.
|
|
56
|
+
|
|
57
|
+
ReadRowsOperation(request, client) handles row merging logic end-to-end, including
|
|
58
|
+
performing retries on stream errors.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
query: The query to execute
|
|
62
|
+
table: The table to send the request to
|
|
63
|
+
operation_timeout: The total time to allow for the operation, in seconds
|
|
64
|
+
attempt_timeout: The time to allow for each individual attempt, in seconds
|
|
65
|
+
retryable_exceptions: A list of exceptions that should trigger a retry
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
__slots__ = (
|
|
69
|
+
"attempt_timeout_gen",
|
|
70
|
+
"operation_timeout",
|
|
71
|
+
"request",
|
|
72
|
+
"table",
|
|
73
|
+
"_predicate",
|
|
74
|
+
"_last_yielded_row_key",
|
|
75
|
+
"_remaining_count",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
query: ReadRowsQuery,
|
|
81
|
+
table: TableType,
|
|
82
|
+
operation_timeout: float,
|
|
83
|
+
attempt_timeout: float,
|
|
84
|
+
retryable_exceptions: Sequence[type[Exception]] = (),
|
|
85
|
+
):
|
|
86
|
+
self.attempt_timeout_gen = _attempt_timeout_generator(
|
|
87
|
+
attempt_timeout, operation_timeout
|
|
88
|
+
)
|
|
89
|
+
self.operation_timeout = operation_timeout
|
|
90
|
+
if isinstance(query, dict):
|
|
91
|
+
self.request = ReadRowsRequestPB(
|
|
92
|
+
**query,
|
|
93
|
+
table_name=table.table_name,
|
|
94
|
+
app_profile_id=table.app_profile_id,
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
self.request = query._to_pb(table)
|
|
98
|
+
self.table = table
|
|
99
|
+
self._predicate = retries.if_exception_type(*retryable_exceptions)
|
|
100
|
+
self._last_yielded_row_key: bytes | None = None
|
|
101
|
+
self._remaining_count: int | None = self.request.rows_limit or None
|
|
102
|
+
|
|
103
|
+
def start_operation(self) -> CrossSync.Iterable[Row]:
|
|
104
|
+
"""
|
|
105
|
+
Start the read_rows operation, retrying on retryable errors.
|
|
106
|
+
|
|
107
|
+
Yields:
|
|
108
|
+
Row: The next row in the stream
|
|
109
|
+
"""
|
|
110
|
+
return CrossSync.retry_target_stream(
|
|
111
|
+
self._read_rows_attempt,
|
|
112
|
+
self._predicate,
|
|
113
|
+
exponential_sleep_generator(0.01, 60, multiplier=2),
|
|
114
|
+
self.operation_timeout,
|
|
115
|
+
exception_factory=_retry_exception_factory,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _read_rows_attempt(self) -> CrossSync.Iterable[Row]:
|
|
119
|
+
"""
|
|
120
|
+
Attempt a single read_rows rpc call.
|
|
121
|
+
This function is intended to be wrapped by retry logic,
|
|
122
|
+
which will call this function until it succeeds or
|
|
123
|
+
a non-retryable error is raised.
|
|
124
|
+
|
|
125
|
+
Yields:
|
|
126
|
+
Row: The next row in the stream
|
|
127
|
+
"""
|
|
128
|
+
# revise request keys and ranges between attempts
|
|
129
|
+
if self._last_yielded_row_key is not None:
|
|
130
|
+
# if this is a retry, try to trim down the request to avoid ones we've already processed
|
|
131
|
+
try:
|
|
132
|
+
self.request.rows = self._revise_request_rowset(
|
|
133
|
+
row_set=self.request.rows,
|
|
134
|
+
last_seen_row_key=self._last_yielded_row_key,
|
|
135
|
+
)
|
|
136
|
+
except _RowSetComplete:
|
|
137
|
+
# if we've already seen all the rows, we're done
|
|
138
|
+
return self.merge_rows(None)
|
|
139
|
+
# revise the limit based on number of rows already yielded
|
|
140
|
+
if self._remaining_count is not None:
|
|
141
|
+
self.request.rows_limit = self._remaining_count
|
|
142
|
+
if self._remaining_count == 0:
|
|
143
|
+
return self.merge_rows(None)
|
|
144
|
+
# create and return a new row merger
|
|
145
|
+
gapic_stream = self.table.client._gapic_client.read_rows(
|
|
146
|
+
self.request,
|
|
147
|
+
timeout=next(self.attempt_timeout_gen),
|
|
148
|
+
retry=None,
|
|
149
|
+
)
|
|
150
|
+
chunked_stream = self.chunk_stream(gapic_stream)
|
|
151
|
+
return self.merge_rows(chunked_stream)
|
|
152
|
+
|
|
153
|
+
@CrossSync.convert()
|
|
154
|
+
async def chunk_stream(
|
|
155
|
+
self, stream: CrossSync.Awaitable[CrossSync.Iterable[ReadRowsResponsePB]]
|
|
156
|
+
) -> CrossSync.Iterable[ReadRowsResponsePB.CellChunk]:
|
|
157
|
+
"""
|
|
158
|
+
process chunks out of raw read_rows stream
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
stream: the raw read_rows stream from the gapic client
|
|
162
|
+
Yields:
|
|
163
|
+
ReadRowsResponsePB.CellChunk: the next chunk in the stream
|
|
164
|
+
"""
|
|
165
|
+
async for resp in await stream:
|
|
166
|
+
# extract proto from proto-plus wrapper
|
|
167
|
+
resp = resp._pb
|
|
168
|
+
|
|
169
|
+
# handle last_scanned_row_key packets, sent when server
|
|
170
|
+
# has scanned past the end of the row range
|
|
171
|
+
if resp.last_scanned_row_key:
|
|
172
|
+
if (
|
|
173
|
+
self._last_yielded_row_key is not None
|
|
174
|
+
and resp.last_scanned_row_key <= self._last_yielded_row_key
|
|
175
|
+
):
|
|
176
|
+
raise InvalidChunk("last scanned out of order")
|
|
177
|
+
self._last_yielded_row_key = resp.last_scanned_row_key
|
|
178
|
+
|
|
179
|
+
current_key = None
|
|
180
|
+
# process each chunk in the response
|
|
181
|
+
for c in resp.chunks:
|
|
182
|
+
if current_key is None:
|
|
183
|
+
current_key = c.row_key
|
|
184
|
+
if current_key is None:
|
|
185
|
+
raise InvalidChunk("first chunk is missing a row key")
|
|
186
|
+
elif (
|
|
187
|
+
self._last_yielded_row_key
|
|
188
|
+
and current_key <= self._last_yielded_row_key
|
|
189
|
+
):
|
|
190
|
+
raise InvalidChunk("row keys should be strictly increasing")
|
|
191
|
+
|
|
192
|
+
yield c
|
|
193
|
+
|
|
194
|
+
if c.reset_row:
|
|
195
|
+
current_key = None
|
|
196
|
+
elif c.commit_row:
|
|
197
|
+
# update row state after each commit
|
|
198
|
+
self._last_yielded_row_key = current_key
|
|
199
|
+
if self._remaining_count is not None:
|
|
200
|
+
self._remaining_count -= 1
|
|
201
|
+
if self._remaining_count < 0:
|
|
202
|
+
raise InvalidChunk("emit count exceeds row limit")
|
|
203
|
+
current_key = None
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
@CrossSync.convert(
|
|
207
|
+
replace_symbols={"__aiter__": "__iter__", "__anext__": "__next__"},
|
|
208
|
+
)
|
|
209
|
+
async def merge_rows(
|
|
210
|
+
chunks: CrossSync.Iterable[ReadRowsResponsePB.CellChunk] | None,
|
|
211
|
+
) -> CrossSync.Iterable[Row]:
|
|
212
|
+
"""
|
|
213
|
+
Merge chunks into rows
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
chunks: the chunk stream to merge
|
|
217
|
+
Yields:
|
|
218
|
+
Row: the next row in the stream
|
|
219
|
+
"""
|
|
220
|
+
if chunks is None:
|
|
221
|
+
return
|
|
222
|
+
it = chunks.__aiter__()
|
|
223
|
+
# For each row
|
|
224
|
+
while True:
|
|
225
|
+
try:
|
|
226
|
+
c = await it.__anext__()
|
|
227
|
+
except CrossSync.StopIteration:
|
|
228
|
+
# stream complete
|
|
229
|
+
return
|
|
230
|
+
row_key = c.row_key
|
|
231
|
+
|
|
232
|
+
if not row_key:
|
|
233
|
+
raise InvalidChunk("first row chunk is missing key")
|
|
234
|
+
|
|
235
|
+
cells = []
|
|
236
|
+
|
|
237
|
+
# shared per cell storage
|
|
238
|
+
family: str | None = None
|
|
239
|
+
qualifier: bytes | None = None
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
# for each cell
|
|
243
|
+
while True:
|
|
244
|
+
if c.reset_row:
|
|
245
|
+
raise _ResetRow(c)
|
|
246
|
+
k = c.row_key
|
|
247
|
+
f = c.family_name.value
|
|
248
|
+
q = c.qualifier.value if c.HasField("qualifier") else None
|
|
249
|
+
if k and k != row_key:
|
|
250
|
+
raise InvalidChunk("unexpected new row key")
|
|
251
|
+
if f:
|
|
252
|
+
family = f
|
|
253
|
+
if q is not None:
|
|
254
|
+
qualifier = q
|
|
255
|
+
else:
|
|
256
|
+
raise InvalidChunk("new family without qualifier")
|
|
257
|
+
elif family is None:
|
|
258
|
+
raise InvalidChunk("missing family")
|
|
259
|
+
elif q is not None:
|
|
260
|
+
if family is None:
|
|
261
|
+
raise InvalidChunk("new qualifier without family")
|
|
262
|
+
qualifier = q
|
|
263
|
+
elif qualifier is None:
|
|
264
|
+
raise InvalidChunk("missing qualifier")
|
|
265
|
+
|
|
266
|
+
ts = c.timestamp_micros
|
|
267
|
+
labels = c.labels if c.labels else []
|
|
268
|
+
value = c.value
|
|
269
|
+
|
|
270
|
+
# merge split cells
|
|
271
|
+
if c.value_size > 0:
|
|
272
|
+
buffer = [value]
|
|
273
|
+
while c.value_size > 0:
|
|
274
|
+
# throws when premature end
|
|
275
|
+
c = await it.__anext__()
|
|
276
|
+
|
|
277
|
+
t = c.timestamp_micros
|
|
278
|
+
cl = c.labels
|
|
279
|
+
k = c.row_key
|
|
280
|
+
if (
|
|
281
|
+
c.HasField("family_name")
|
|
282
|
+
and c.family_name.value != family
|
|
283
|
+
):
|
|
284
|
+
raise InvalidChunk("family changed mid cell")
|
|
285
|
+
if (
|
|
286
|
+
c.HasField("qualifier")
|
|
287
|
+
and c.qualifier.value != qualifier
|
|
288
|
+
):
|
|
289
|
+
raise InvalidChunk("qualifier changed mid cell")
|
|
290
|
+
if t and t != ts:
|
|
291
|
+
raise InvalidChunk("timestamp changed mid cell")
|
|
292
|
+
if cl and cl != labels:
|
|
293
|
+
raise InvalidChunk("labels changed mid cell")
|
|
294
|
+
if k and k != row_key:
|
|
295
|
+
raise InvalidChunk("row key changed mid cell")
|
|
296
|
+
|
|
297
|
+
if c.reset_row:
|
|
298
|
+
raise _ResetRow(c)
|
|
299
|
+
buffer.append(c.value)
|
|
300
|
+
value = b"".join(buffer)
|
|
301
|
+
cells.append(
|
|
302
|
+
Cell(value, row_key, family, qualifier, ts, list(labels))
|
|
303
|
+
)
|
|
304
|
+
if c.commit_row:
|
|
305
|
+
yield Row(row_key, cells)
|
|
306
|
+
break
|
|
307
|
+
c = await it.__anext__()
|
|
308
|
+
except _ResetRow as e:
|
|
309
|
+
c = e.chunk
|
|
310
|
+
if (
|
|
311
|
+
c.row_key
|
|
312
|
+
or c.HasField("family_name")
|
|
313
|
+
or c.HasField("qualifier")
|
|
314
|
+
or c.timestamp_micros
|
|
315
|
+
or c.labels
|
|
316
|
+
or c.value
|
|
317
|
+
):
|
|
318
|
+
raise InvalidChunk("reset row with data")
|
|
319
|
+
continue
|
|
320
|
+
except CrossSync.StopIteration:
|
|
321
|
+
raise InvalidChunk("premature end of stream")
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def _revise_request_rowset(
|
|
325
|
+
row_set: RowSetPB,
|
|
326
|
+
last_seen_row_key: bytes,
|
|
327
|
+
) -> RowSetPB:
|
|
328
|
+
"""
|
|
329
|
+
Revise the rows in the request to avoid ones we've already processed.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
row_set: the row set from the request
|
|
333
|
+
last_seen_row_key: the last row key encountered
|
|
334
|
+
Returns:
|
|
335
|
+
RowSetPB: the new rowset after adusting for the last seen key
|
|
336
|
+
Raises:
|
|
337
|
+
_RowSetComplete: if there are no rows left to process after the revision
|
|
338
|
+
"""
|
|
339
|
+
# if user is doing a whole table scan, start a new one with the last seen key
|
|
340
|
+
if row_set is None or (not row_set.row_ranges and not row_set.row_keys):
|
|
341
|
+
last_seen = last_seen_row_key
|
|
342
|
+
return RowSetPB(row_ranges=[RowRangePB(start_key_open=last_seen)])
|
|
343
|
+
# remove seen keys from user-specific key list
|
|
344
|
+
adjusted_keys: list[bytes] = [
|
|
345
|
+
k for k in row_set.row_keys if k > last_seen_row_key
|
|
346
|
+
]
|
|
347
|
+
# adjust ranges to ignore keys before last seen
|
|
348
|
+
adjusted_ranges: list[RowRangePB] = []
|
|
349
|
+
for row_range in row_set.row_ranges:
|
|
350
|
+
end_key = row_range.end_key_closed or row_range.end_key_open or None
|
|
351
|
+
if end_key is None or end_key > last_seen_row_key:
|
|
352
|
+
# end range is after last seen key
|
|
353
|
+
new_range = RowRangePB(row_range)
|
|
354
|
+
start_key = row_range.start_key_closed or row_range.start_key_open
|
|
355
|
+
if start_key is None or start_key <= last_seen_row_key:
|
|
356
|
+
# replace start key with last seen
|
|
357
|
+
new_range.start_key_open = last_seen_row_key
|
|
358
|
+
adjusted_ranges.append(new_range)
|
|
359
|
+
if len(adjusted_keys) == 0 and len(adjusted_ranges) == 0:
|
|
360
|
+
# if the query is empty after revision, raise an exception
|
|
361
|
+
# this will avoid an unwanted full table scan
|
|
362
|
+
raise _RowSetComplete()
|
|
363
|
+
return RowSetPB(row_keys=adjusted_keys, row_ranges=adjusted_ranges)
|