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,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)