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,1522 @@
|
|
|
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 (
|
|
19
|
+
cast,
|
|
20
|
+
Any,
|
|
21
|
+
AsyncIterable,
|
|
22
|
+
Optional,
|
|
23
|
+
Set,
|
|
24
|
+
Sequence,
|
|
25
|
+
TYPE_CHECKING,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
import time
|
|
29
|
+
import warnings
|
|
30
|
+
import random
|
|
31
|
+
import os
|
|
32
|
+
import concurrent.futures
|
|
33
|
+
|
|
34
|
+
from functools import partial
|
|
35
|
+
from grpc import Channel
|
|
36
|
+
|
|
37
|
+
from google.cloud.bigtable.data.execute_query.values import ExecuteQueryValueType
|
|
38
|
+
from google.cloud.bigtable.data.execute_query.metadata import (
|
|
39
|
+
SqlType,
|
|
40
|
+
_pb_metadata_to_metadata_types,
|
|
41
|
+
)
|
|
42
|
+
from google.cloud.bigtable.data.execute_query._parameters_formatting import (
|
|
43
|
+
_format_execute_query_params,
|
|
44
|
+
_to_param_types,
|
|
45
|
+
)
|
|
46
|
+
from google.cloud.bigtable_v2.services.bigtable.transports.base import (
|
|
47
|
+
DEFAULT_CLIENT_INFO,
|
|
48
|
+
)
|
|
49
|
+
from google.cloud.bigtable_v2.types.bigtable import PingAndWarmRequest
|
|
50
|
+
from google.cloud.client import ClientWithProject
|
|
51
|
+
from google.cloud.environment_vars import BIGTABLE_EMULATOR # type: ignore
|
|
52
|
+
from google.api_core import retry as retries
|
|
53
|
+
from google.api_core.exceptions import DeadlineExceeded
|
|
54
|
+
from google.api_core.exceptions import ServiceUnavailable
|
|
55
|
+
from google.api_core.exceptions import Aborted
|
|
56
|
+
|
|
57
|
+
import google.auth.credentials
|
|
58
|
+
import google.auth._default
|
|
59
|
+
from google.api_core import client_options as client_options_lib
|
|
60
|
+
from google.cloud.bigtable.client import _DEFAULT_BIGTABLE_EMULATOR_CLIENT
|
|
61
|
+
from google.cloud.bigtable.data.row import Row
|
|
62
|
+
from google.cloud.bigtable.data.read_rows_query import ReadRowsQuery
|
|
63
|
+
from google.cloud.bigtable.data.exceptions import FailedQueryShardError
|
|
64
|
+
from google.cloud.bigtable.data.exceptions import ShardedReadRowsExceptionGroup
|
|
65
|
+
|
|
66
|
+
from google.cloud.bigtable.data._helpers import TABLE_DEFAULT, _align_timeouts
|
|
67
|
+
from google.cloud.bigtable.data._helpers import _WarmedInstanceKey
|
|
68
|
+
from google.cloud.bigtable.data._helpers import _CONCURRENCY_LIMIT
|
|
69
|
+
from google.cloud.bigtable.data._helpers import _retry_exception_factory
|
|
70
|
+
from google.cloud.bigtable.data._helpers import _validate_timeouts
|
|
71
|
+
from google.cloud.bigtable.data._helpers import _get_error_type
|
|
72
|
+
from google.cloud.bigtable.data._helpers import _get_retryable_errors
|
|
73
|
+
from google.cloud.bigtable.data._helpers import _get_timeouts
|
|
74
|
+
from google.cloud.bigtable.data._helpers import _attempt_timeout_generator
|
|
75
|
+
from google.cloud.bigtable.data.mutations import Mutation, RowMutationEntry
|
|
76
|
+
|
|
77
|
+
from google.cloud.bigtable.data.read_modify_write_rules import ReadModifyWriteRule
|
|
78
|
+
from google.cloud.bigtable.data.row_filters import RowFilter
|
|
79
|
+
from google.cloud.bigtable.data.row_filters import StripValueTransformerFilter
|
|
80
|
+
from google.cloud.bigtable.data.row_filters import CellsRowLimitFilter
|
|
81
|
+
from google.cloud.bigtable.data.row_filters import RowFilterChain
|
|
82
|
+
|
|
83
|
+
from google.cloud.bigtable.data._cross_sync import CrossSync
|
|
84
|
+
|
|
85
|
+
if CrossSync.is_async:
|
|
86
|
+
from grpc.aio import insecure_channel
|
|
87
|
+
from google.cloud.bigtable_v2.services.bigtable.transports import (
|
|
88
|
+
BigtableGrpcAsyncIOTransport as TransportType,
|
|
89
|
+
)
|
|
90
|
+
from google.cloud.bigtable.data._async.mutations_batcher import _MB_SIZE
|
|
91
|
+
else:
|
|
92
|
+
from typing import Iterable # noqa: F401
|
|
93
|
+
from grpc import insecure_channel
|
|
94
|
+
from grpc import intercept_channel
|
|
95
|
+
from google.cloud.bigtable_v2.services.bigtable.transports import BigtableGrpcTransport as TransportType # type: ignore
|
|
96
|
+
from google.cloud.bigtable.data._sync_autogen.mutations_batcher import _MB_SIZE
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
if TYPE_CHECKING:
|
|
100
|
+
from google.cloud.bigtable.data._helpers import RowKeySamples
|
|
101
|
+
from google.cloud.bigtable.data._helpers import ShardedQuery
|
|
102
|
+
|
|
103
|
+
if CrossSync.is_async:
|
|
104
|
+
from google.cloud.bigtable.data._async.mutations_batcher import (
|
|
105
|
+
MutationsBatcherAsync,
|
|
106
|
+
)
|
|
107
|
+
from google.cloud.bigtable.data.execute_query._async.execute_query_iterator import (
|
|
108
|
+
ExecuteQueryIteratorAsync,
|
|
109
|
+
)
|
|
110
|
+
else:
|
|
111
|
+
from google.cloud.bigtable.data._sync_autogen.mutations_batcher import ( # noqa: F401
|
|
112
|
+
MutationsBatcher,
|
|
113
|
+
)
|
|
114
|
+
from google.cloud.bigtable.data.execute_query._sync_autogen.execute_query_iterator import ( # noqa: F401
|
|
115
|
+
ExecuteQueryIterator,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__CROSS_SYNC_OUTPUT__ = "google.cloud.bigtable.data._sync_autogen.client"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@CrossSync.convert_class(
|
|
123
|
+
sync_name="BigtableDataClient",
|
|
124
|
+
add_mapping_for_name="DataClient",
|
|
125
|
+
)
|
|
126
|
+
class BigtableDataClientAsync(ClientWithProject):
|
|
127
|
+
@CrossSync.convert(
|
|
128
|
+
docstring_format_vars={
|
|
129
|
+
"LOOP_MESSAGE": (
|
|
130
|
+
"Client should be created within an async context (running event loop)",
|
|
131
|
+
None,
|
|
132
|
+
),
|
|
133
|
+
"RAISE_NO_LOOP": (
|
|
134
|
+
"RuntimeError: if called outside of an async context (no running event loop)",
|
|
135
|
+
None,
|
|
136
|
+
),
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
*,
|
|
142
|
+
project: str | None = None,
|
|
143
|
+
credentials: google.auth.credentials.Credentials | None = None,
|
|
144
|
+
client_options: dict[str, Any]
|
|
145
|
+
| "google.api_core.client_options.ClientOptions"
|
|
146
|
+
| None = None,
|
|
147
|
+
**kwargs,
|
|
148
|
+
):
|
|
149
|
+
"""
|
|
150
|
+
Create a client instance for the Bigtable Data API
|
|
151
|
+
|
|
152
|
+
{LOOP_MESSAGE}
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
project: the project which the client acts on behalf of.
|
|
156
|
+
If not passed, falls back to the default inferred
|
|
157
|
+
from the environment.
|
|
158
|
+
credentials:
|
|
159
|
+
Thehe OAuth2 Credentials to use for this
|
|
160
|
+
client. If not passed (and if no ``_http`` object is
|
|
161
|
+
passed), falls back to the default inferred from the
|
|
162
|
+
environment.
|
|
163
|
+
client_options:
|
|
164
|
+
Client options used to set user options
|
|
165
|
+
on the client. API Endpoint should be set through client_options.
|
|
166
|
+
Raises:
|
|
167
|
+
{RAISE_NO_LOOP}
|
|
168
|
+
"""
|
|
169
|
+
if "pool_size" in kwargs:
|
|
170
|
+
warnings.warn("pool_size no longer supported")
|
|
171
|
+
# set up client info headers for veneer library
|
|
172
|
+
self.client_info = DEFAULT_CLIENT_INFO
|
|
173
|
+
self.client_info.client_library_version = self._client_version()
|
|
174
|
+
# parse client options
|
|
175
|
+
if type(client_options) is dict:
|
|
176
|
+
client_options = client_options_lib.from_dict(client_options)
|
|
177
|
+
client_options = cast(
|
|
178
|
+
Optional[client_options_lib.ClientOptions], client_options
|
|
179
|
+
)
|
|
180
|
+
custom_channel = None
|
|
181
|
+
self._emulator_host = os.getenv(BIGTABLE_EMULATOR)
|
|
182
|
+
if self._emulator_host is not None:
|
|
183
|
+
warnings.warn(
|
|
184
|
+
"Connecting to Bigtable emulator at {}".format(self._emulator_host),
|
|
185
|
+
RuntimeWarning,
|
|
186
|
+
stacklevel=2,
|
|
187
|
+
)
|
|
188
|
+
# use insecure channel if emulator is set
|
|
189
|
+
custom_channel = insecure_channel(self._emulator_host)
|
|
190
|
+
if credentials is None:
|
|
191
|
+
credentials = google.auth.credentials.AnonymousCredentials()
|
|
192
|
+
if project is None:
|
|
193
|
+
project = _DEFAULT_BIGTABLE_EMULATOR_CLIENT
|
|
194
|
+
# initialize client
|
|
195
|
+
ClientWithProject.__init__(
|
|
196
|
+
self,
|
|
197
|
+
credentials=credentials,
|
|
198
|
+
project=project,
|
|
199
|
+
client_options=client_options,
|
|
200
|
+
)
|
|
201
|
+
self._gapic_client = CrossSync.GapicClient(
|
|
202
|
+
credentials=credentials,
|
|
203
|
+
client_options=client_options,
|
|
204
|
+
client_info=self.client_info,
|
|
205
|
+
transport=lambda *args, **kwargs: TransportType(
|
|
206
|
+
*args, **kwargs, channel=custom_channel
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
self._is_closed = CrossSync.Event()
|
|
210
|
+
self.transport = cast(TransportType, self._gapic_client.transport)
|
|
211
|
+
# keep track of active instances to for warmup on channel refresh
|
|
212
|
+
self._active_instances: Set[_WarmedInstanceKey] = set()
|
|
213
|
+
# keep track of table objects associated with each instance
|
|
214
|
+
# only remove instance from _active_instances when all associated tables remove it
|
|
215
|
+
self._instance_owners: dict[_WarmedInstanceKey, Set[int]] = {}
|
|
216
|
+
self._channel_init_time = time.monotonic()
|
|
217
|
+
self._channel_refresh_task: CrossSync.Task[None] | None = None
|
|
218
|
+
self._executor = (
|
|
219
|
+
concurrent.futures.ThreadPoolExecutor() if not CrossSync.is_async else None
|
|
220
|
+
)
|
|
221
|
+
if self._emulator_host is None:
|
|
222
|
+
# attempt to start background channel refresh tasks
|
|
223
|
+
try:
|
|
224
|
+
self._start_background_channel_refresh()
|
|
225
|
+
except RuntimeError:
|
|
226
|
+
warnings.warn(
|
|
227
|
+
f"{self.__class__.__name__} should be started in an "
|
|
228
|
+
"asyncio event loop. Channel refresh will not be started",
|
|
229
|
+
RuntimeWarning,
|
|
230
|
+
stacklevel=2,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def _client_version() -> str:
|
|
235
|
+
"""
|
|
236
|
+
Helper function to return the client version string for this client
|
|
237
|
+
"""
|
|
238
|
+
version_str = f"{google.cloud.bigtable.__version__}-data"
|
|
239
|
+
if CrossSync.is_async:
|
|
240
|
+
version_str += "-async"
|
|
241
|
+
return version_str
|
|
242
|
+
|
|
243
|
+
@CrossSync.convert(
|
|
244
|
+
docstring_format_vars={
|
|
245
|
+
"RAISE_NO_LOOP": (
|
|
246
|
+
"RuntimeError: if not called in an asyncio event loop",
|
|
247
|
+
"None",
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
def _start_background_channel_refresh(self) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Starts a background task to ping and warm grpc channel
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
{RAISE_NO_LOOP}
|
|
257
|
+
"""
|
|
258
|
+
if (
|
|
259
|
+
not self._channel_refresh_task
|
|
260
|
+
and not self._emulator_host
|
|
261
|
+
and not self._is_closed.is_set()
|
|
262
|
+
):
|
|
263
|
+
# raise error if not in an event loop in async client
|
|
264
|
+
CrossSync.verify_async_event_loop()
|
|
265
|
+
self._channel_refresh_task = CrossSync.create_task(
|
|
266
|
+
self._manage_channel,
|
|
267
|
+
sync_executor=self._executor,
|
|
268
|
+
task_name=f"{self.__class__.__name__} channel refresh",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
@CrossSync.convert
|
|
272
|
+
async def close(self, timeout: float | None = 2.0):
|
|
273
|
+
"""
|
|
274
|
+
Cancel all background tasks
|
|
275
|
+
"""
|
|
276
|
+
self._is_closed.set()
|
|
277
|
+
if self._channel_refresh_task is not None:
|
|
278
|
+
self._channel_refresh_task.cancel()
|
|
279
|
+
await CrossSync.wait([self._channel_refresh_task], timeout=timeout)
|
|
280
|
+
await self.transport.close()
|
|
281
|
+
if self._executor:
|
|
282
|
+
self._executor.shutdown(wait=False)
|
|
283
|
+
self._channel_refresh_task = None
|
|
284
|
+
|
|
285
|
+
@CrossSync.convert
|
|
286
|
+
async def _ping_and_warm_instances(
|
|
287
|
+
self,
|
|
288
|
+
instance_key: _WarmedInstanceKey | None = None,
|
|
289
|
+
channel: Channel | None = None,
|
|
290
|
+
) -> list[BaseException | None]:
|
|
291
|
+
"""
|
|
292
|
+
Prepares the backend for requests on a channel
|
|
293
|
+
|
|
294
|
+
Pings each Bigtable instance registered in `_active_instances` on the client
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
instance_key: if provided, only warm the instance associated with the key
|
|
298
|
+
channel: grpc channel to warm. If none, warms `self.transport.grpc_channel`
|
|
299
|
+
Returns:
|
|
300
|
+
list[BaseException | None]: sequence of results or exceptions from the ping requests
|
|
301
|
+
"""
|
|
302
|
+
channel = channel or self.transport.grpc_channel
|
|
303
|
+
instance_list = (
|
|
304
|
+
[instance_key] if instance_key is not None else self._active_instances
|
|
305
|
+
)
|
|
306
|
+
ping_rpc = channel.unary_unary(
|
|
307
|
+
"/google.bigtable.v2.Bigtable/PingAndWarm",
|
|
308
|
+
request_serializer=PingAndWarmRequest.serialize,
|
|
309
|
+
)
|
|
310
|
+
# prepare list of coroutines to run
|
|
311
|
+
partial_list = [
|
|
312
|
+
partial(
|
|
313
|
+
ping_rpc,
|
|
314
|
+
request={"name": instance_name, "app_profile_id": app_profile_id},
|
|
315
|
+
metadata=[
|
|
316
|
+
(
|
|
317
|
+
"x-goog-request-params",
|
|
318
|
+
f"name={instance_name}&app_profile_id={app_profile_id}",
|
|
319
|
+
)
|
|
320
|
+
],
|
|
321
|
+
wait_for_ready=True,
|
|
322
|
+
)
|
|
323
|
+
for (instance_name, table_name, app_profile_id) in instance_list
|
|
324
|
+
]
|
|
325
|
+
result_list = await CrossSync.gather_partials(
|
|
326
|
+
partial_list, return_exceptions=True, sync_executor=self._executor
|
|
327
|
+
)
|
|
328
|
+
return [r or None for r in result_list]
|
|
329
|
+
|
|
330
|
+
@CrossSync.convert
|
|
331
|
+
async def _manage_channel(
|
|
332
|
+
self,
|
|
333
|
+
refresh_interval_min: float = 60 * 35,
|
|
334
|
+
refresh_interval_max: float = 60 * 45,
|
|
335
|
+
grace_period: float = 60 * 10,
|
|
336
|
+
) -> None:
|
|
337
|
+
"""
|
|
338
|
+
Background task that periodically refreshes and warms a grpc channel
|
|
339
|
+
|
|
340
|
+
The backend will automatically close channels after 60 minutes, so
|
|
341
|
+
`refresh_interval` + `grace_period` should be < 60 minutes
|
|
342
|
+
|
|
343
|
+
Runs continuously until the client is closed
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
refresh_interval_min: minimum interval before initiating refresh
|
|
347
|
+
process in seconds. Actual interval will be a random value
|
|
348
|
+
between `refresh_interval_min` and `refresh_interval_max`
|
|
349
|
+
refresh_interval_max: maximum interval before initiating refresh
|
|
350
|
+
process in seconds. Actual interval will be a random value
|
|
351
|
+
between `refresh_interval_min` and `refresh_interval_max`
|
|
352
|
+
grace_period: time to allow previous channel to serve existing
|
|
353
|
+
requests before closing, in seconds
|
|
354
|
+
"""
|
|
355
|
+
first_refresh = self._channel_init_time + random.uniform(
|
|
356
|
+
refresh_interval_min, refresh_interval_max
|
|
357
|
+
)
|
|
358
|
+
next_sleep = max(first_refresh - time.monotonic(), 0)
|
|
359
|
+
if next_sleep > 0:
|
|
360
|
+
# warm the current channel immediately
|
|
361
|
+
await self._ping_and_warm_instances(channel=self.transport.grpc_channel)
|
|
362
|
+
# continuously refresh the channel every `refresh_interval` seconds
|
|
363
|
+
while not self._is_closed.is_set():
|
|
364
|
+
await CrossSync.event_wait(
|
|
365
|
+
self._is_closed,
|
|
366
|
+
next_sleep,
|
|
367
|
+
async_break_early=False, # no need to interrupt sleep. Task will be cancelled on close
|
|
368
|
+
)
|
|
369
|
+
if self._is_closed.is_set():
|
|
370
|
+
# don't refresh if client is closed
|
|
371
|
+
break
|
|
372
|
+
start_timestamp = time.monotonic()
|
|
373
|
+
# prepare new channel for use
|
|
374
|
+
# TODO: refactor to avoid using internal references: https://github.com/googleapis/python-bigtable/issues/1094
|
|
375
|
+
old_channel = self.transport.grpc_channel
|
|
376
|
+
new_channel = self.transport.create_channel()
|
|
377
|
+
if CrossSync.is_async:
|
|
378
|
+
new_channel._unary_unary_interceptors.append(
|
|
379
|
+
self.transport._interceptor
|
|
380
|
+
)
|
|
381
|
+
else:
|
|
382
|
+
new_channel = intercept_channel(
|
|
383
|
+
new_channel, self.transport._interceptor
|
|
384
|
+
)
|
|
385
|
+
await self._ping_and_warm_instances(channel=new_channel)
|
|
386
|
+
# cycle channel out of use, with long grace window before closure
|
|
387
|
+
self.transport._grpc_channel = new_channel
|
|
388
|
+
self.transport._logged_channel = new_channel
|
|
389
|
+
# invalidate caches
|
|
390
|
+
self.transport._stubs = {}
|
|
391
|
+
self.transport._prep_wrapped_messages(self.client_info)
|
|
392
|
+
# give old_channel a chance to complete existing rpcs
|
|
393
|
+
if CrossSync.is_async:
|
|
394
|
+
await old_channel.close(grace_period)
|
|
395
|
+
else:
|
|
396
|
+
if grace_period:
|
|
397
|
+
self._is_closed.wait(grace_period) # type: ignore
|
|
398
|
+
old_channel.close() # type: ignore
|
|
399
|
+
# subtract thed time spent waiting for the channel to be replaced
|
|
400
|
+
next_refresh = random.uniform(refresh_interval_min, refresh_interval_max)
|
|
401
|
+
next_sleep = max(next_refresh - (time.monotonic() - start_timestamp), 0)
|
|
402
|
+
|
|
403
|
+
@CrossSync.convert(
|
|
404
|
+
replace_symbols={
|
|
405
|
+
"TableAsync": "Table",
|
|
406
|
+
"ExecuteQueryIteratorAsync": "ExecuteQueryIterator",
|
|
407
|
+
}
|
|
408
|
+
)
|
|
409
|
+
async def _register_instance(
|
|
410
|
+
self, instance_id: str, owner: TableAsync | ExecuteQueryIteratorAsync
|
|
411
|
+
) -> None:
|
|
412
|
+
"""
|
|
413
|
+
Registers an instance with the client, and warms the channel for the instance
|
|
414
|
+
The client will periodically refresh grpc channel used to make
|
|
415
|
+
requests, and new channels will be warmed for each registered instance
|
|
416
|
+
Channels will not be refreshed unless at least one instance is registered
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
instance_id: id of the instance to register.
|
|
420
|
+
owner: table that owns the instance. Owners will be tracked in
|
|
421
|
+
_instance_owners, and instances will only be unregistered when all
|
|
422
|
+
owners call _remove_instance_registration
|
|
423
|
+
"""
|
|
424
|
+
instance_name = self._gapic_client.instance_path(self.project, instance_id)
|
|
425
|
+
instance_key = _WarmedInstanceKey(
|
|
426
|
+
instance_name, owner.table_name, owner.app_profile_id
|
|
427
|
+
)
|
|
428
|
+
self._instance_owners.setdefault(instance_key, set()).add(id(owner))
|
|
429
|
+
if instance_key not in self._active_instances:
|
|
430
|
+
self._active_instances.add(instance_key)
|
|
431
|
+
if self._channel_refresh_task:
|
|
432
|
+
# refresh tasks already running
|
|
433
|
+
# call ping and warm on all existing channels
|
|
434
|
+
await self._ping_and_warm_instances(instance_key)
|
|
435
|
+
else:
|
|
436
|
+
# refresh tasks aren't active. start them as background tasks
|
|
437
|
+
self._start_background_channel_refresh()
|
|
438
|
+
|
|
439
|
+
@CrossSync.convert(
|
|
440
|
+
replace_symbols={
|
|
441
|
+
"TableAsync": "Table",
|
|
442
|
+
"ExecuteQueryIteratorAsync": "ExecuteQueryIterator",
|
|
443
|
+
}
|
|
444
|
+
)
|
|
445
|
+
async def _remove_instance_registration(
|
|
446
|
+
self, instance_id: str, owner: TableAsync | "ExecuteQueryIteratorAsync"
|
|
447
|
+
) -> bool:
|
|
448
|
+
"""
|
|
449
|
+
Removes an instance from the client's registered instances, to prevent
|
|
450
|
+
warming new channels for the instance
|
|
451
|
+
|
|
452
|
+
If instance_id is not registered, or is still in use by other tables, returns False
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
instance_id: id of the instance to remove
|
|
456
|
+
owner: table that owns the instance. Owners will be tracked in
|
|
457
|
+
_instance_owners, and instances will only be unregistered when all
|
|
458
|
+
owners call _remove_instance_registration
|
|
459
|
+
Returns:
|
|
460
|
+
bool: True if instance was removed, else False
|
|
461
|
+
"""
|
|
462
|
+
instance_name = self._gapic_client.instance_path(self.project, instance_id)
|
|
463
|
+
instance_key = _WarmedInstanceKey(
|
|
464
|
+
instance_name, owner.table_name, owner.app_profile_id
|
|
465
|
+
)
|
|
466
|
+
owner_list = self._instance_owners.get(instance_key, set())
|
|
467
|
+
try:
|
|
468
|
+
owner_list.remove(id(owner))
|
|
469
|
+
if len(owner_list) == 0:
|
|
470
|
+
self._active_instances.remove(instance_key)
|
|
471
|
+
return True
|
|
472
|
+
except KeyError:
|
|
473
|
+
return False
|
|
474
|
+
|
|
475
|
+
@CrossSync.convert(
|
|
476
|
+
replace_symbols={"TableAsync": "Table"},
|
|
477
|
+
docstring_format_vars={
|
|
478
|
+
"LOOP_MESSAGE": (
|
|
479
|
+
"Must be created within an async context (running event loop)",
|
|
480
|
+
"",
|
|
481
|
+
),
|
|
482
|
+
"RAISE_NO_LOOP": (
|
|
483
|
+
"RuntimeError: if called outside of an async context (no running event loop)",
|
|
484
|
+
"None",
|
|
485
|
+
),
|
|
486
|
+
},
|
|
487
|
+
)
|
|
488
|
+
def get_table(self, instance_id: str, table_id: str, *args, **kwargs) -> TableAsync:
|
|
489
|
+
"""
|
|
490
|
+
Returns a table instance for making data API requests. All arguments are passed
|
|
491
|
+
directly to the TableAsync constructor.
|
|
492
|
+
|
|
493
|
+
{LOOP_MESSAGE}
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
instance_id: The Bigtable instance ID to associate with this client.
|
|
497
|
+
instance_id is combined with the client's project to fully
|
|
498
|
+
specify the instance
|
|
499
|
+
table_id: The ID of the table. table_id is combined with the
|
|
500
|
+
instance_id and the client's project to fully specify the table
|
|
501
|
+
app_profile_id: The app profile to associate with requests.
|
|
502
|
+
https://cloud.google.com/bigtable/docs/app-profiles
|
|
503
|
+
default_read_rows_operation_timeout: The default timeout for read rows
|
|
504
|
+
operations, in seconds. If not set, defaults to 600 seconds (10 minutes)
|
|
505
|
+
default_read_rows_attempt_timeout: The default timeout for individual
|
|
506
|
+
read rows rpc requests, in seconds. If not set, defaults to 20 seconds
|
|
507
|
+
default_mutate_rows_operation_timeout: The default timeout for mutate rows
|
|
508
|
+
operations, in seconds. If not set, defaults to 600 seconds (10 minutes)
|
|
509
|
+
default_mutate_rows_attempt_timeout: The default timeout for individual
|
|
510
|
+
mutate rows rpc requests, in seconds. If not set, defaults to 60 seconds
|
|
511
|
+
default_operation_timeout: The default timeout for all other operations, in
|
|
512
|
+
seconds. If not set, defaults to 60 seconds
|
|
513
|
+
default_attempt_timeout: The default timeout for all other individual rpc
|
|
514
|
+
requests, in seconds. If not set, defaults to 20 seconds
|
|
515
|
+
default_read_rows_retryable_errors: a list of errors that will be retried
|
|
516
|
+
if encountered during read_rows and related operations.
|
|
517
|
+
Defaults to 4 (DeadlineExceeded), 14 (ServiceUnavailable), and 10 (Aborted)
|
|
518
|
+
default_mutate_rows_retryable_errors: a list of errors that will be retried
|
|
519
|
+
if encountered during mutate_rows and related operations.
|
|
520
|
+
Defaults to 4 (DeadlineExceeded) and 14 (ServiceUnavailable)
|
|
521
|
+
default_retryable_errors: a list of errors that will be retried if
|
|
522
|
+
encountered during all other operations.
|
|
523
|
+
Defaults to 4 (DeadlineExceeded) and 14 (ServiceUnavailable)
|
|
524
|
+
Returns:
|
|
525
|
+
TableAsync: a table instance for making data API requests
|
|
526
|
+
Raises:
|
|
527
|
+
{RAISE_NO_LOOP}
|
|
528
|
+
"""
|
|
529
|
+
return TableAsync(self, instance_id, table_id, *args, **kwargs)
|
|
530
|
+
|
|
531
|
+
@CrossSync.convert(
|
|
532
|
+
replace_symbols={"ExecuteQueryIteratorAsync": "ExecuteQueryIterator"}
|
|
533
|
+
)
|
|
534
|
+
async def execute_query(
|
|
535
|
+
self,
|
|
536
|
+
query: str,
|
|
537
|
+
instance_id: str,
|
|
538
|
+
*,
|
|
539
|
+
parameters: dict[str, ExecuteQueryValueType] | None = None,
|
|
540
|
+
parameter_types: dict[str, SqlType.Type] | None = None,
|
|
541
|
+
app_profile_id: str | None = None,
|
|
542
|
+
operation_timeout: float = 600,
|
|
543
|
+
attempt_timeout: float | None = 20,
|
|
544
|
+
retryable_errors: Sequence[type[Exception]] = (
|
|
545
|
+
DeadlineExceeded,
|
|
546
|
+
ServiceUnavailable,
|
|
547
|
+
Aborted,
|
|
548
|
+
),
|
|
549
|
+
prepare_operation_timeout: float = 60,
|
|
550
|
+
prepare_attempt_timeout: float | None = 20,
|
|
551
|
+
prepare_retryable_errors: Sequence[type[Exception]] = (
|
|
552
|
+
DeadlineExceeded,
|
|
553
|
+
ServiceUnavailable,
|
|
554
|
+
),
|
|
555
|
+
) -> "ExecuteQueryIteratorAsync":
|
|
556
|
+
"""
|
|
557
|
+
Executes an SQL query on an instance.
|
|
558
|
+
Returns an iterator to asynchronously stream back columns from selected rows.
|
|
559
|
+
|
|
560
|
+
Failed requests within operation_timeout will be retried based on the
|
|
561
|
+
retryable_errors list until operation_timeout is reached.
|
|
562
|
+
|
|
563
|
+
Note that this makes two requests, one to ``PrepareQuery`` and one to ``ExecuteQuery``.
|
|
564
|
+
These have separate retry configurations. ``ExecuteQuery`` is where the bulk of the
|
|
565
|
+
work happens.
|
|
566
|
+
|
|
567
|
+
Args:
|
|
568
|
+
query: Query to be run on Bigtable instance. The query can use ``@param``
|
|
569
|
+
placeholders to use parameter interpolation on the server. Values for all
|
|
570
|
+
parameters should be provided in ``parameters``. Types of parameters are
|
|
571
|
+
inferred but should be provided in ``parameter_types`` if the inference is
|
|
572
|
+
not possible (i.e. when value can be None, an empty list or an empty dict).
|
|
573
|
+
instance_id: The Bigtable instance ID to perform the query on.
|
|
574
|
+
instance_id is combined with the client's project to fully
|
|
575
|
+
specify the instance.
|
|
576
|
+
parameters: Dictionary with values for all parameters used in the ``query``.
|
|
577
|
+
parameter_types: Dictionary with types of parameters used in the ``query``.
|
|
578
|
+
Required to contain entries only for parameters whose type cannot be
|
|
579
|
+
detected automatically (i.e. the value can be None, an empty list or
|
|
580
|
+
an empty dict).
|
|
581
|
+
app_profile_id: The app profile to associate with requests.
|
|
582
|
+
https://cloud.google.com/bigtable/docs/app-profiles
|
|
583
|
+
operation_timeout: the time budget for the entire executeQuery operation, in seconds.
|
|
584
|
+
Failed requests will be retried within the budget.
|
|
585
|
+
Defaults to 600 seconds.
|
|
586
|
+
attempt_timeout: the time budget for an individual executeQuery network request, in seconds.
|
|
587
|
+
If it takes longer than this time to complete, the request will be cancelled with
|
|
588
|
+
a DeadlineExceeded exception, and a retry will be attempted.
|
|
589
|
+
Defaults to the 20 seconds.
|
|
590
|
+
If None, defaults to operation_timeout.
|
|
591
|
+
retryable_errors: a list of errors that will be retried if encountered during executeQuery.
|
|
592
|
+
Defaults to 4 (DeadlineExceeded), 14 (ServiceUnavailable), and 10 (Aborted)
|
|
593
|
+
prepare_operation_timeout: the time budget for the entire prepareQuery operation, in seconds.
|
|
594
|
+
Failed requests will be retried within the budget.
|
|
595
|
+
Defaults to 60 seconds.
|
|
596
|
+
prepare_attempt_timeout: the time budget for an individual prepareQuery network request, in seconds.
|
|
597
|
+
If it takes longer than this time to complete, the request will be cancelled with
|
|
598
|
+
a DeadlineExceeded exception, and a retry will be attempted.
|
|
599
|
+
Defaults to the 20 seconds.
|
|
600
|
+
If None, defaults to prepare_operation_timeout.
|
|
601
|
+
prepare_retryable_errors: a list of errors that will be retried if encountered during prepareQuery.
|
|
602
|
+
Defaults to 4 (DeadlineExceeded) and 14 (ServiceUnavailable)
|
|
603
|
+
Returns:
|
|
604
|
+
ExecuteQueryIteratorAsync: an asynchronous iterator that yields rows returned by the query
|
|
605
|
+
Raises:
|
|
606
|
+
google.api_core.exceptions.DeadlineExceeded: raised after operation timeout
|
|
607
|
+
will be chained with a RetryExceptionGroup containing GoogleAPIError exceptions
|
|
608
|
+
from any retries that failed
|
|
609
|
+
google.api_core.exceptions.GoogleAPIError: raised if the request encounters an unrecoverable error
|
|
610
|
+
google.cloud.bigtable.data.exceptions.ParameterTypeInferenceFailed: Raised if
|
|
611
|
+
a parameter is passed without an explicit type, and the type cannot be infered
|
|
612
|
+
"""
|
|
613
|
+
instance_name = self._gapic_client.instance_path(self.project, instance_id)
|
|
614
|
+
converted_param_types = _to_param_types(parameters, parameter_types)
|
|
615
|
+
prepare_request = {
|
|
616
|
+
"instance_name": instance_name,
|
|
617
|
+
"query": query,
|
|
618
|
+
"app_profile_id": app_profile_id,
|
|
619
|
+
"param_types": converted_param_types,
|
|
620
|
+
"proto_format": {},
|
|
621
|
+
}
|
|
622
|
+
prepare_predicate = retries.if_exception_type(
|
|
623
|
+
*[_get_error_type(e) for e in prepare_retryable_errors]
|
|
624
|
+
)
|
|
625
|
+
prepare_operation_timeout, prepare_attempt_timeout = _align_timeouts(
|
|
626
|
+
prepare_operation_timeout, prepare_attempt_timeout
|
|
627
|
+
)
|
|
628
|
+
prepare_sleep_generator = retries.exponential_sleep_generator(0.01, 2, 60)
|
|
629
|
+
|
|
630
|
+
target = partial(
|
|
631
|
+
self._gapic_client.prepare_query,
|
|
632
|
+
request=prepare_request,
|
|
633
|
+
timeout=prepare_attempt_timeout,
|
|
634
|
+
retry=None,
|
|
635
|
+
)
|
|
636
|
+
prepare_result = await CrossSync.retry_target(
|
|
637
|
+
target,
|
|
638
|
+
prepare_predicate,
|
|
639
|
+
prepare_sleep_generator,
|
|
640
|
+
prepare_operation_timeout,
|
|
641
|
+
exception_factory=_retry_exception_factory,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
prepare_metadata = _pb_metadata_to_metadata_types(prepare_result.metadata)
|
|
645
|
+
|
|
646
|
+
retryable_excs = [_get_error_type(e) for e in retryable_errors]
|
|
647
|
+
|
|
648
|
+
pb_params = _format_execute_query_params(parameters, parameter_types)
|
|
649
|
+
|
|
650
|
+
request_body = {
|
|
651
|
+
"instance_name": instance_name,
|
|
652
|
+
"app_profile_id": app_profile_id,
|
|
653
|
+
"prepared_query": prepare_result.prepared_query,
|
|
654
|
+
"params": pb_params,
|
|
655
|
+
}
|
|
656
|
+
operation_timeout, attempt_timeout = _align_timeouts(
|
|
657
|
+
operation_timeout, attempt_timeout
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
return CrossSync.ExecuteQueryIterator(
|
|
661
|
+
self,
|
|
662
|
+
instance_id,
|
|
663
|
+
app_profile_id,
|
|
664
|
+
request_body,
|
|
665
|
+
prepare_metadata,
|
|
666
|
+
attempt_timeout,
|
|
667
|
+
operation_timeout,
|
|
668
|
+
retryable_excs=retryable_excs,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
@CrossSync.convert(sync_name="__enter__")
|
|
672
|
+
async def __aenter__(self):
|
|
673
|
+
self._start_background_channel_refresh()
|
|
674
|
+
return self
|
|
675
|
+
|
|
676
|
+
@CrossSync.convert(sync_name="__exit__", replace_symbols={"__aexit__": "__exit__"})
|
|
677
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
678
|
+
await self.close()
|
|
679
|
+
await self._gapic_client.__aexit__(exc_type, exc_val, exc_tb)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@CrossSync.convert_class(sync_name="Table", add_mapping_for_name="Table")
|
|
683
|
+
class TableAsync:
|
|
684
|
+
"""
|
|
685
|
+
Main Data API surface
|
|
686
|
+
|
|
687
|
+
Table object maintains table_id, and app_profile_id context, and passes them with
|
|
688
|
+
each call
|
|
689
|
+
"""
|
|
690
|
+
|
|
691
|
+
@CrossSync.convert(
|
|
692
|
+
replace_symbols={"BigtableDataClientAsync": "BigtableDataClient"},
|
|
693
|
+
docstring_format_vars={
|
|
694
|
+
"LOOP_MESSAGE": (
|
|
695
|
+
"Must be created within an async context (running event loop)",
|
|
696
|
+
"",
|
|
697
|
+
),
|
|
698
|
+
"RAISE_NO_LOOP": (
|
|
699
|
+
"RuntimeError: if called outside of an async context (no running event loop)",
|
|
700
|
+
"None",
|
|
701
|
+
),
|
|
702
|
+
},
|
|
703
|
+
)
|
|
704
|
+
def __init__(
|
|
705
|
+
self,
|
|
706
|
+
client: BigtableDataClientAsync,
|
|
707
|
+
instance_id: str,
|
|
708
|
+
table_id: str,
|
|
709
|
+
app_profile_id: str | None = None,
|
|
710
|
+
*,
|
|
711
|
+
default_read_rows_operation_timeout: float = 600,
|
|
712
|
+
default_read_rows_attempt_timeout: float | None = 20,
|
|
713
|
+
default_mutate_rows_operation_timeout: float = 600,
|
|
714
|
+
default_mutate_rows_attempt_timeout: float | None = 60,
|
|
715
|
+
default_operation_timeout: float = 60,
|
|
716
|
+
default_attempt_timeout: float | None = 20,
|
|
717
|
+
default_read_rows_retryable_errors: Sequence[type[Exception]] = (
|
|
718
|
+
DeadlineExceeded,
|
|
719
|
+
ServiceUnavailable,
|
|
720
|
+
Aborted,
|
|
721
|
+
),
|
|
722
|
+
default_mutate_rows_retryable_errors: Sequence[type[Exception]] = (
|
|
723
|
+
DeadlineExceeded,
|
|
724
|
+
ServiceUnavailable,
|
|
725
|
+
),
|
|
726
|
+
default_retryable_errors: Sequence[type[Exception]] = (
|
|
727
|
+
DeadlineExceeded,
|
|
728
|
+
ServiceUnavailable,
|
|
729
|
+
),
|
|
730
|
+
):
|
|
731
|
+
"""
|
|
732
|
+
Initialize a Table instance
|
|
733
|
+
|
|
734
|
+
{LOOP_MESSAGE}
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
instance_id: The Bigtable instance ID to associate with this client.
|
|
738
|
+
instance_id is combined with the client's project to fully
|
|
739
|
+
specify the instance
|
|
740
|
+
table_id: The ID of the table. table_id is combined with the
|
|
741
|
+
instance_id and the client's project to fully specify the table
|
|
742
|
+
app_profile_id: The app profile to associate with requests.
|
|
743
|
+
https://cloud.google.com/bigtable/docs/app-profiles
|
|
744
|
+
default_read_rows_operation_timeout: The default timeout for read rows
|
|
745
|
+
operations, in seconds. If not set, defaults to 600 seconds (10 minutes)
|
|
746
|
+
default_read_rows_attempt_timeout: The default timeout for individual
|
|
747
|
+
read rows rpc requests, in seconds. If not set, defaults to 20 seconds
|
|
748
|
+
default_mutate_rows_operation_timeout: The default timeout for mutate rows
|
|
749
|
+
operations, in seconds. If not set, defaults to 600 seconds (10 minutes)
|
|
750
|
+
default_mutate_rows_attempt_timeout: The default timeout for individual
|
|
751
|
+
mutate rows rpc requests, in seconds. If not set, defaults to 60 seconds
|
|
752
|
+
default_operation_timeout: The default timeout for all other operations, in
|
|
753
|
+
seconds. If not set, defaults to 60 seconds
|
|
754
|
+
default_attempt_timeout: The default timeout for all other individual rpc
|
|
755
|
+
requests, in seconds. If not set, defaults to 20 seconds
|
|
756
|
+
default_read_rows_retryable_errors: a list of errors that will be retried
|
|
757
|
+
if encountered during read_rows and related operations.
|
|
758
|
+
Defaults to 4 (DeadlineExceeded), 14 (ServiceUnavailable), and 10 (Aborted)
|
|
759
|
+
default_mutate_rows_retryable_errors: a list of errors that will be retried
|
|
760
|
+
if encountered during mutate_rows and related operations.
|
|
761
|
+
Defaults to 4 (DeadlineExceeded) and 14 (ServiceUnavailable)
|
|
762
|
+
default_retryable_errors: a list of errors that will be retried if
|
|
763
|
+
encountered during all other operations.
|
|
764
|
+
Defaults to 4 (DeadlineExceeded) and 14 (ServiceUnavailable)
|
|
765
|
+
Raises:
|
|
766
|
+
{RAISE_NO_LOOP}
|
|
767
|
+
"""
|
|
768
|
+
# NOTE: any changes to the signature of this method should also be reflected
|
|
769
|
+
# in client.get_table()
|
|
770
|
+
# validate timeouts
|
|
771
|
+
_validate_timeouts(
|
|
772
|
+
default_operation_timeout, default_attempt_timeout, allow_none=True
|
|
773
|
+
)
|
|
774
|
+
_validate_timeouts(
|
|
775
|
+
default_read_rows_operation_timeout,
|
|
776
|
+
default_read_rows_attempt_timeout,
|
|
777
|
+
allow_none=True,
|
|
778
|
+
)
|
|
779
|
+
_validate_timeouts(
|
|
780
|
+
default_mutate_rows_operation_timeout,
|
|
781
|
+
default_mutate_rows_attempt_timeout,
|
|
782
|
+
allow_none=True,
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
self.client = client
|
|
786
|
+
self.instance_id = instance_id
|
|
787
|
+
self.instance_name = self.client._gapic_client.instance_path(
|
|
788
|
+
self.client.project, instance_id
|
|
789
|
+
)
|
|
790
|
+
self.table_id = table_id
|
|
791
|
+
self.table_name = self.client._gapic_client.table_path(
|
|
792
|
+
self.client.project, instance_id, table_id
|
|
793
|
+
)
|
|
794
|
+
self.app_profile_id = app_profile_id
|
|
795
|
+
|
|
796
|
+
self.default_operation_timeout = default_operation_timeout
|
|
797
|
+
self.default_attempt_timeout = default_attempt_timeout
|
|
798
|
+
self.default_read_rows_operation_timeout = default_read_rows_operation_timeout
|
|
799
|
+
self.default_read_rows_attempt_timeout = default_read_rows_attempt_timeout
|
|
800
|
+
self.default_mutate_rows_operation_timeout = (
|
|
801
|
+
default_mutate_rows_operation_timeout
|
|
802
|
+
)
|
|
803
|
+
self.default_mutate_rows_attempt_timeout = default_mutate_rows_attempt_timeout
|
|
804
|
+
|
|
805
|
+
self.default_read_rows_retryable_errors = (
|
|
806
|
+
default_read_rows_retryable_errors or ()
|
|
807
|
+
)
|
|
808
|
+
self.default_mutate_rows_retryable_errors = (
|
|
809
|
+
default_mutate_rows_retryable_errors or ()
|
|
810
|
+
)
|
|
811
|
+
self.default_retryable_errors = default_retryable_errors or ()
|
|
812
|
+
try:
|
|
813
|
+
self._register_instance_future = CrossSync.create_task(
|
|
814
|
+
self.client._register_instance,
|
|
815
|
+
self.instance_id,
|
|
816
|
+
self,
|
|
817
|
+
sync_executor=self.client._executor,
|
|
818
|
+
)
|
|
819
|
+
except RuntimeError as e:
|
|
820
|
+
raise RuntimeError(
|
|
821
|
+
f"{self.__class__.__name__} must be created within an async event loop context."
|
|
822
|
+
) from e
|
|
823
|
+
|
|
824
|
+
@CrossSync.convert(replace_symbols={"AsyncIterable": "Iterable"})
|
|
825
|
+
async def read_rows_stream(
|
|
826
|
+
self,
|
|
827
|
+
query: ReadRowsQuery,
|
|
828
|
+
*,
|
|
829
|
+
operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
830
|
+
attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
831
|
+
retryable_errors: Sequence[type[Exception]]
|
|
832
|
+
| TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
833
|
+
) -> AsyncIterable[Row]:
|
|
834
|
+
"""
|
|
835
|
+
Read a set of rows from the table, based on the specified query.
|
|
836
|
+
Returns an iterator to asynchronously stream back row data.
|
|
837
|
+
|
|
838
|
+
Failed requests within operation_timeout will be retried based on the
|
|
839
|
+
retryable_errors list until operation_timeout is reached.
|
|
840
|
+
|
|
841
|
+
Args:
|
|
842
|
+
query: contains details about which rows to return
|
|
843
|
+
operation_timeout: the time budget for the entire operation, in seconds.
|
|
844
|
+
Failed requests will be retried within the budget.
|
|
845
|
+
Defaults to the Table's default_read_rows_operation_timeout
|
|
846
|
+
attempt_timeout: the time budget for an individual network request, in seconds.
|
|
847
|
+
If it takes longer than this time to complete, the request will be cancelled with
|
|
848
|
+
a DeadlineExceeded exception, and a retry will be attempted.
|
|
849
|
+
Defaults to the Table's default_read_rows_attempt_timeout.
|
|
850
|
+
If None, defaults to operation_timeout.
|
|
851
|
+
retryable_errors: a list of errors that will be retried if encountered.
|
|
852
|
+
Defaults to the Table's default_read_rows_retryable_errors
|
|
853
|
+
Returns:
|
|
854
|
+
AsyncIterable[Row]: an asynchronous iterator that yields rows returned by the query
|
|
855
|
+
Raises:
|
|
856
|
+
google.api_core.exceptions.DeadlineExceeded: raised after operation timeout
|
|
857
|
+
will be chained with a RetryExceptionGroup containing GoogleAPIError exceptions
|
|
858
|
+
from any retries that failed
|
|
859
|
+
google.api_core.exceptions.GoogleAPIError: raised if the request encounters an unrecoverable error
|
|
860
|
+
"""
|
|
861
|
+
operation_timeout, attempt_timeout = _get_timeouts(
|
|
862
|
+
operation_timeout, attempt_timeout, self
|
|
863
|
+
)
|
|
864
|
+
retryable_excs = _get_retryable_errors(retryable_errors, self)
|
|
865
|
+
|
|
866
|
+
row_merger = CrossSync._ReadRowsOperation(
|
|
867
|
+
query,
|
|
868
|
+
self,
|
|
869
|
+
operation_timeout=operation_timeout,
|
|
870
|
+
attempt_timeout=attempt_timeout,
|
|
871
|
+
retryable_exceptions=retryable_excs,
|
|
872
|
+
)
|
|
873
|
+
return row_merger.start_operation()
|
|
874
|
+
|
|
875
|
+
@CrossSync.convert
|
|
876
|
+
async def read_rows(
|
|
877
|
+
self,
|
|
878
|
+
query: ReadRowsQuery,
|
|
879
|
+
*,
|
|
880
|
+
operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
881
|
+
attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
882
|
+
retryable_errors: Sequence[type[Exception]]
|
|
883
|
+
| TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
884
|
+
) -> list[Row]:
|
|
885
|
+
"""
|
|
886
|
+
Read a set of rows from the table, based on the specified query.
|
|
887
|
+
Retruns results as a list of Row objects when the request is complete.
|
|
888
|
+
For streamed results, use read_rows_stream.
|
|
889
|
+
|
|
890
|
+
Failed requests within operation_timeout will be retried based on the
|
|
891
|
+
retryable_errors list until operation_timeout is reached.
|
|
892
|
+
|
|
893
|
+
Args:
|
|
894
|
+
query: contains details about which rows to return
|
|
895
|
+
operation_timeout: the time budget for the entire operation, in seconds.
|
|
896
|
+
Failed requests will be retried within the budget.
|
|
897
|
+
Defaults to the Table's default_read_rows_operation_timeout
|
|
898
|
+
attempt_timeout: the time budget for an individual network request, in seconds.
|
|
899
|
+
If it takes longer than this time to complete, the request will be cancelled with
|
|
900
|
+
a DeadlineExceeded exception, and a retry will be attempted.
|
|
901
|
+
Defaults to the Table's default_read_rows_attempt_timeout.
|
|
902
|
+
If None, defaults to operation_timeout.
|
|
903
|
+
If None, defaults to the Table's default_read_rows_attempt_timeout,
|
|
904
|
+
or the operation_timeout if that is also None.
|
|
905
|
+
retryable_errors: a list of errors that will be retried if encountered.
|
|
906
|
+
Defaults to the Table's default_read_rows_retryable_errors.
|
|
907
|
+
Returns:
|
|
908
|
+
list[Row]: a list of Rows returned by the query
|
|
909
|
+
Raises:
|
|
910
|
+
google.api_core.exceptions.DeadlineExceeded: raised after operation timeout
|
|
911
|
+
will be chained with a RetryExceptionGroup containing GoogleAPIError exceptions
|
|
912
|
+
from any retries that failed
|
|
913
|
+
google.api_core.exceptions.GoogleAPIError: raised if the request encounters an unrecoverable error
|
|
914
|
+
"""
|
|
915
|
+
row_generator = await self.read_rows_stream(
|
|
916
|
+
query,
|
|
917
|
+
operation_timeout=operation_timeout,
|
|
918
|
+
attempt_timeout=attempt_timeout,
|
|
919
|
+
retryable_errors=retryable_errors,
|
|
920
|
+
)
|
|
921
|
+
return [row async for row in row_generator]
|
|
922
|
+
|
|
923
|
+
@CrossSync.convert
|
|
924
|
+
async def read_row(
|
|
925
|
+
self,
|
|
926
|
+
row_key: str | bytes,
|
|
927
|
+
*,
|
|
928
|
+
row_filter: RowFilter | None = None,
|
|
929
|
+
operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
930
|
+
attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
931
|
+
retryable_errors: Sequence[type[Exception]]
|
|
932
|
+
| TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
933
|
+
) -> Row | None:
|
|
934
|
+
"""
|
|
935
|
+
Read a single row from the table, based on the specified key.
|
|
936
|
+
|
|
937
|
+
Failed requests within operation_timeout will be retried based on the
|
|
938
|
+
retryable_errors list until operation_timeout is reached.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
query: contains details about which rows to return
|
|
942
|
+
operation_timeout: the time budget for the entire operation, in seconds.
|
|
943
|
+
Failed requests will be retried within the budget.
|
|
944
|
+
Defaults to the Table's default_read_rows_operation_timeout
|
|
945
|
+
attempt_timeout: the time budget for an individual network request, in seconds.
|
|
946
|
+
If it takes longer than this time to complete, the request will be cancelled with
|
|
947
|
+
a DeadlineExceeded exception, and a retry will be attempted.
|
|
948
|
+
Defaults to the Table's default_read_rows_attempt_timeout.
|
|
949
|
+
If None, defaults to operation_timeout.
|
|
950
|
+
retryable_errors: a list of errors that will be retried if encountered.
|
|
951
|
+
Defaults to the Table's default_read_rows_retryable_errors.
|
|
952
|
+
Returns:
|
|
953
|
+
Row | None: a Row object if the row exists, otherwise None
|
|
954
|
+
Raises:
|
|
955
|
+
google.api_core.exceptions.DeadlineExceeded: raised after operation timeout
|
|
956
|
+
will be chained with a RetryExceptionGroup containing GoogleAPIError exceptions
|
|
957
|
+
from any retries that failed
|
|
958
|
+
google.api_core.exceptions.GoogleAPIError: raised if the request encounters an unrecoverable error
|
|
959
|
+
"""
|
|
960
|
+
if row_key is None:
|
|
961
|
+
raise ValueError("row_key must be string or bytes")
|
|
962
|
+
query = ReadRowsQuery(row_keys=row_key, row_filter=row_filter, limit=1)
|
|
963
|
+
results = await self.read_rows(
|
|
964
|
+
query,
|
|
965
|
+
operation_timeout=operation_timeout,
|
|
966
|
+
attempt_timeout=attempt_timeout,
|
|
967
|
+
retryable_errors=retryable_errors,
|
|
968
|
+
)
|
|
969
|
+
if len(results) == 0:
|
|
970
|
+
return None
|
|
971
|
+
return results[0]
|
|
972
|
+
|
|
973
|
+
@CrossSync.convert
|
|
974
|
+
async def read_rows_sharded(
|
|
975
|
+
self,
|
|
976
|
+
sharded_query: ShardedQuery,
|
|
977
|
+
*,
|
|
978
|
+
operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
979
|
+
attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
980
|
+
retryable_errors: Sequence[type[Exception]]
|
|
981
|
+
| TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
982
|
+
) -> list[Row]:
|
|
983
|
+
"""
|
|
984
|
+
Runs a sharded query in parallel, then return the results in a single list.
|
|
985
|
+
Results will be returned in the order of the input queries.
|
|
986
|
+
|
|
987
|
+
This function is intended to be run on the results on a query.shard() call.
|
|
988
|
+
For example::
|
|
989
|
+
|
|
990
|
+
table_shard_keys = await table.sample_row_keys()
|
|
991
|
+
query = ReadRowsQuery(...)
|
|
992
|
+
shard_queries = query.shard(table_shard_keys)
|
|
993
|
+
results = await table.read_rows_sharded(shard_queries)
|
|
994
|
+
|
|
995
|
+
Args:
|
|
996
|
+
sharded_query: a sharded query to execute
|
|
997
|
+
operation_timeout: the time budget for the entire operation, in seconds.
|
|
998
|
+
Failed requests will be retried within the budget.
|
|
999
|
+
Defaults to the Table's default_read_rows_operation_timeout
|
|
1000
|
+
attempt_timeout: the time budget for an individual network request, in seconds.
|
|
1001
|
+
If it takes longer than this time to complete, the request will be cancelled with
|
|
1002
|
+
a DeadlineExceeded exception, and a retry will be attempted.
|
|
1003
|
+
Defaults to the Table's default_read_rows_attempt_timeout.
|
|
1004
|
+
If None, defaults to operation_timeout.
|
|
1005
|
+
retryable_errors: a list of errors that will be retried if encountered.
|
|
1006
|
+
Defaults to the Table's default_read_rows_retryable_errors.
|
|
1007
|
+
Returns:
|
|
1008
|
+
list[Row]: a list of Rows returned by the query
|
|
1009
|
+
Raises:
|
|
1010
|
+
ShardedReadRowsExceptionGroup: if any of the queries failed
|
|
1011
|
+
ValueError: if the query_list is empty
|
|
1012
|
+
"""
|
|
1013
|
+
if not sharded_query:
|
|
1014
|
+
raise ValueError("empty sharded_query")
|
|
1015
|
+
operation_timeout, attempt_timeout = _get_timeouts(
|
|
1016
|
+
operation_timeout, attempt_timeout, self
|
|
1017
|
+
)
|
|
1018
|
+
# make sure each rpc stays within overall operation timeout
|
|
1019
|
+
rpc_timeout_generator = _attempt_timeout_generator(
|
|
1020
|
+
operation_timeout, operation_timeout
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
# limit the number of concurrent requests using a semaphore
|
|
1024
|
+
concurrency_sem = CrossSync.Semaphore(_CONCURRENCY_LIMIT)
|
|
1025
|
+
|
|
1026
|
+
@CrossSync.convert
|
|
1027
|
+
async def read_rows_with_semaphore(query):
|
|
1028
|
+
async with concurrency_sem:
|
|
1029
|
+
# calculate new timeout based on time left in overall operation
|
|
1030
|
+
shard_timeout = next(rpc_timeout_generator)
|
|
1031
|
+
if shard_timeout <= 0:
|
|
1032
|
+
raise DeadlineExceeded(
|
|
1033
|
+
"Operation timeout exceeded before starting query"
|
|
1034
|
+
)
|
|
1035
|
+
return await self.read_rows(
|
|
1036
|
+
query,
|
|
1037
|
+
operation_timeout=shard_timeout,
|
|
1038
|
+
attempt_timeout=min(attempt_timeout, shard_timeout),
|
|
1039
|
+
retryable_errors=retryable_errors,
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
routine_list = [
|
|
1043
|
+
partial(read_rows_with_semaphore, query) for query in sharded_query
|
|
1044
|
+
]
|
|
1045
|
+
batch_result = await CrossSync.gather_partials(
|
|
1046
|
+
routine_list,
|
|
1047
|
+
return_exceptions=True,
|
|
1048
|
+
sync_executor=self.client._executor,
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
# collect results and errors
|
|
1052
|
+
error_dict = {}
|
|
1053
|
+
shard_idx = 0
|
|
1054
|
+
results_list = []
|
|
1055
|
+
for result in batch_result:
|
|
1056
|
+
if isinstance(result, Exception):
|
|
1057
|
+
error_dict[shard_idx] = result
|
|
1058
|
+
elif isinstance(result, BaseException):
|
|
1059
|
+
# BaseException not expected; raise immediately
|
|
1060
|
+
raise result
|
|
1061
|
+
else:
|
|
1062
|
+
results_list.extend(result)
|
|
1063
|
+
shard_idx += 1
|
|
1064
|
+
if error_dict:
|
|
1065
|
+
# if any sub-request failed, raise an exception instead of returning results
|
|
1066
|
+
raise ShardedReadRowsExceptionGroup(
|
|
1067
|
+
[
|
|
1068
|
+
FailedQueryShardError(idx, sharded_query[idx], e)
|
|
1069
|
+
for idx, e in error_dict.items()
|
|
1070
|
+
],
|
|
1071
|
+
results_list,
|
|
1072
|
+
len(sharded_query),
|
|
1073
|
+
)
|
|
1074
|
+
return results_list
|
|
1075
|
+
|
|
1076
|
+
@CrossSync.convert
|
|
1077
|
+
async def row_exists(
|
|
1078
|
+
self,
|
|
1079
|
+
row_key: str | bytes,
|
|
1080
|
+
*,
|
|
1081
|
+
operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
1082
|
+
attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
1083
|
+
retryable_errors: Sequence[type[Exception]]
|
|
1084
|
+
| TABLE_DEFAULT = TABLE_DEFAULT.READ_ROWS,
|
|
1085
|
+
) -> bool:
|
|
1086
|
+
"""
|
|
1087
|
+
Return a boolean indicating whether the specified row exists in the table.
|
|
1088
|
+
uses the filters: chain(limit cells per row = 1, strip value)
|
|
1089
|
+
|
|
1090
|
+
Args:
|
|
1091
|
+
row_key: the key of the row to check
|
|
1092
|
+
operation_timeout: the time budget for the entire operation, in seconds.
|
|
1093
|
+
Failed requests will be retried within the budget.
|
|
1094
|
+
Defaults to the Table's default_read_rows_operation_timeout
|
|
1095
|
+
attempt_timeout: the time budget for an individual network request, in seconds.
|
|
1096
|
+
If it takes longer than this time to complete, the request will be cancelled with
|
|
1097
|
+
a DeadlineExceeded exception, and a retry will be attempted.
|
|
1098
|
+
Defaults to the Table's default_read_rows_attempt_timeout.
|
|
1099
|
+
If None, defaults to operation_timeout.
|
|
1100
|
+
retryable_errors: a list of errors that will be retried if encountered.
|
|
1101
|
+
Defaults to the Table's default_read_rows_retryable_errors.
|
|
1102
|
+
Returns:
|
|
1103
|
+
bool: a bool indicating whether the row exists
|
|
1104
|
+
Raises:
|
|
1105
|
+
google.api_core.exceptions.DeadlineExceeded: raised after operation timeout
|
|
1106
|
+
will be chained with a RetryExceptionGroup containing GoogleAPIError exceptions
|
|
1107
|
+
from any retries that failed
|
|
1108
|
+
google.api_core.exceptions.GoogleAPIError: raised if the request encounters an unrecoverable error
|
|
1109
|
+
"""
|
|
1110
|
+
if row_key is None:
|
|
1111
|
+
raise ValueError("row_key must be string or bytes")
|
|
1112
|
+
|
|
1113
|
+
strip_filter = StripValueTransformerFilter(flag=True)
|
|
1114
|
+
limit_filter = CellsRowLimitFilter(1)
|
|
1115
|
+
chain_filter = RowFilterChain(filters=[limit_filter, strip_filter])
|
|
1116
|
+
query = ReadRowsQuery(row_keys=row_key, limit=1, row_filter=chain_filter)
|
|
1117
|
+
results = await self.read_rows(
|
|
1118
|
+
query,
|
|
1119
|
+
operation_timeout=operation_timeout,
|
|
1120
|
+
attempt_timeout=attempt_timeout,
|
|
1121
|
+
retryable_errors=retryable_errors,
|
|
1122
|
+
)
|
|
1123
|
+
return len(results) > 0
|
|
1124
|
+
|
|
1125
|
+
@CrossSync.convert
|
|
1126
|
+
async def sample_row_keys(
|
|
1127
|
+
self,
|
|
1128
|
+
*,
|
|
1129
|
+
operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.DEFAULT,
|
|
1130
|
+
attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.DEFAULT,
|
|
1131
|
+
retryable_errors: Sequence[type[Exception]]
|
|
1132
|
+
| TABLE_DEFAULT = TABLE_DEFAULT.DEFAULT,
|
|
1133
|
+
) -> RowKeySamples:
|
|
1134
|
+
"""
|
|
1135
|
+
Return a set of RowKeySamples that delimit contiguous sections of the table of
|
|
1136
|
+
approximately equal size
|
|
1137
|
+
|
|
1138
|
+
RowKeySamples output can be used with ReadRowsQuery.shard() to create a sharded query that
|
|
1139
|
+
can be parallelized across multiple backend nodes read_rows and read_rows_stream
|
|
1140
|
+
requests will call sample_row_keys internally for this purpose when sharding is enabled
|
|
1141
|
+
|
|
1142
|
+
RowKeySamples is simply a type alias for list[tuple[bytes, int]]; a list of
|
|
1143
|
+
row_keys, along with offset positions in the table
|
|
1144
|
+
|
|
1145
|
+
Args:
|
|
1146
|
+
operation_timeout: the time budget for the entire operation, in seconds.
|
|
1147
|
+
Failed requests will be retried within the budget.i
|
|
1148
|
+
Defaults to the Table's default_operation_timeout
|
|
1149
|
+
attempt_timeout: the time budget for an individual network request, in seconds.
|
|
1150
|
+
If it takes longer than this time to complete, the request will be cancelled with
|
|
1151
|
+
a DeadlineExceeded exception, and a retry will be attempted.
|
|
1152
|
+
Defaults to the Table's default_attempt_timeout.
|
|
1153
|
+
If None, defaults to operation_timeout.
|
|
1154
|
+
retryable_errors: a list of errors that will be retried if encountered.
|
|
1155
|
+
Defaults to the Table's default_retryable_errors.
|
|
1156
|
+
Returns:
|
|
1157
|
+
RowKeySamples: a set of RowKeySamples the delimit contiguous sections of the table
|
|
1158
|
+
Raises:
|
|
1159
|
+
google.api_core.exceptions.DeadlineExceeded: raised after operation timeout
|
|
1160
|
+
will be chained with a RetryExceptionGroup containing GoogleAPIError exceptions
|
|
1161
|
+
from any retries that failed
|
|
1162
|
+
google.api_core.exceptions.GoogleAPIError: raised if the request encounters an unrecoverable error
|
|
1163
|
+
"""
|
|
1164
|
+
# prepare timeouts
|
|
1165
|
+
operation_timeout, attempt_timeout = _get_timeouts(
|
|
1166
|
+
operation_timeout, attempt_timeout, self
|
|
1167
|
+
)
|
|
1168
|
+
attempt_timeout_gen = _attempt_timeout_generator(
|
|
1169
|
+
attempt_timeout, operation_timeout
|
|
1170
|
+
)
|
|
1171
|
+
# prepare retryable
|
|
1172
|
+
retryable_excs = _get_retryable_errors(retryable_errors, self)
|
|
1173
|
+
predicate = retries.if_exception_type(*retryable_excs)
|
|
1174
|
+
|
|
1175
|
+
sleep_generator = retries.exponential_sleep_generator(0.01, 2, 60)
|
|
1176
|
+
|
|
1177
|
+
@CrossSync.convert
|
|
1178
|
+
async def execute_rpc():
|
|
1179
|
+
results = await self.client._gapic_client.sample_row_keys(
|
|
1180
|
+
table_name=self.table_name,
|
|
1181
|
+
app_profile_id=self.app_profile_id,
|
|
1182
|
+
timeout=next(attempt_timeout_gen),
|
|
1183
|
+
retry=None,
|
|
1184
|
+
)
|
|
1185
|
+
return [(s.row_key, s.offset_bytes) async for s in results]
|
|
1186
|
+
|
|
1187
|
+
return await CrossSync.retry_target(
|
|
1188
|
+
execute_rpc,
|
|
1189
|
+
predicate,
|
|
1190
|
+
sleep_generator,
|
|
1191
|
+
operation_timeout,
|
|
1192
|
+
exception_factory=_retry_exception_factory,
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
@CrossSync.convert(replace_symbols={"MutationsBatcherAsync": "MutationsBatcher"})
|
|
1196
|
+
def mutations_batcher(
|
|
1197
|
+
self,
|
|
1198
|
+
*,
|
|
1199
|
+
flush_interval: float | None = 5,
|
|
1200
|
+
flush_limit_mutation_count: int | None = 1000,
|
|
1201
|
+
flush_limit_bytes: int = 20 * _MB_SIZE,
|
|
1202
|
+
flow_control_max_mutation_count: int = 100_000,
|
|
1203
|
+
flow_control_max_bytes: int = 100 * _MB_SIZE,
|
|
1204
|
+
batch_operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
1205
|
+
batch_attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
1206
|
+
batch_retryable_errors: Sequence[type[Exception]]
|
|
1207
|
+
| TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
1208
|
+
) -> "MutationsBatcherAsync":
|
|
1209
|
+
"""
|
|
1210
|
+
Returns a new mutations batcher instance.
|
|
1211
|
+
|
|
1212
|
+
Can be used to iteratively add mutations that are flushed as a group,
|
|
1213
|
+
to avoid excess network calls
|
|
1214
|
+
|
|
1215
|
+
Args:
|
|
1216
|
+
flush_interval: Automatically flush every flush_interval seconds. If None,
|
|
1217
|
+
a table default will be used
|
|
1218
|
+
flush_limit_mutation_count: Flush immediately after flush_limit_mutation_count
|
|
1219
|
+
mutations are added across all entries. If None, this limit is ignored.
|
|
1220
|
+
flush_limit_bytes: Flush immediately after flush_limit_bytes bytes are added.
|
|
1221
|
+
flow_control_max_mutation_count: Maximum number of inflight mutations.
|
|
1222
|
+
flow_control_max_bytes: Maximum number of inflight bytes.
|
|
1223
|
+
batch_operation_timeout: timeout for each mutate_rows operation, in seconds.
|
|
1224
|
+
Defaults to the Table's default_mutate_rows_operation_timeout
|
|
1225
|
+
batch_attempt_timeout: timeout for each individual request, in seconds.
|
|
1226
|
+
Defaults to the Table's default_mutate_rows_attempt_timeout.
|
|
1227
|
+
If None, defaults to batch_operation_timeout.
|
|
1228
|
+
batch_retryable_errors: a list of errors that will be retried if encountered.
|
|
1229
|
+
Defaults to the Table's default_mutate_rows_retryable_errors.
|
|
1230
|
+
Returns:
|
|
1231
|
+
MutationsBatcherAsync: a MutationsBatcherAsync context manager that can batch requests
|
|
1232
|
+
"""
|
|
1233
|
+
return CrossSync.MutationsBatcher(
|
|
1234
|
+
self,
|
|
1235
|
+
flush_interval=flush_interval,
|
|
1236
|
+
flush_limit_mutation_count=flush_limit_mutation_count,
|
|
1237
|
+
flush_limit_bytes=flush_limit_bytes,
|
|
1238
|
+
flow_control_max_mutation_count=flow_control_max_mutation_count,
|
|
1239
|
+
flow_control_max_bytes=flow_control_max_bytes,
|
|
1240
|
+
batch_operation_timeout=batch_operation_timeout,
|
|
1241
|
+
batch_attempt_timeout=batch_attempt_timeout,
|
|
1242
|
+
batch_retryable_errors=batch_retryable_errors,
|
|
1243
|
+
)
|
|
1244
|
+
|
|
1245
|
+
@CrossSync.convert
|
|
1246
|
+
async def mutate_row(
|
|
1247
|
+
self,
|
|
1248
|
+
row_key: str | bytes,
|
|
1249
|
+
mutations: list[Mutation] | Mutation,
|
|
1250
|
+
*,
|
|
1251
|
+
operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.DEFAULT,
|
|
1252
|
+
attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.DEFAULT,
|
|
1253
|
+
retryable_errors: Sequence[type[Exception]]
|
|
1254
|
+
| TABLE_DEFAULT = TABLE_DEFAULT.DEFAULT,
|
|
1255
|
+
):
|
|
1256
|
+
"""
|
|
1257
|
+
Mutates a row atomically.
|
|
1258
|
+
|
|
1259
|
+
Cells already present in the row are left unchanged unless explicitly changed
|
|
1260
|
+
by ``mutation``.
|
|
1261
|
+
|
|
1262
|
+
Idempotent operations (i.e, all mutations have an explicit timestamp) will be
|
|
1263
|
+
retried on server failure. Non-idempotent operations will not.
|
|
1264
|
+
|
|
1265
|
+
Args:
|
|
1266
|
+
row_key: the row to apply mutations to
|
|
1267
|
+
mutations: the set of mutations to apply to the row
|
|
1268
|
+
operation_timeout: the time budget for the entire operation, in seconds.
|
|
1269
|
+
Failed requests will be retried within the budget.
|
|
1270
|
+
Defaults to the Table's default_operation_timeout
|
|
1271
|
+
attempt_timeout: the time budget for an individual network request, in seconds.
|
|
1272
|
+
If it takes longer than this time to complete, the request will be cancelled with
|
|
1273
|
+
a DeadlineExceeded exception, and a retry will be attempted.
|
|
1274
|
+
Defaults to the Table's default_attempt_timeout.
|
|
1275
|
+
If None, defaults to operation_timeout.
|
|
1276
|
+
retryable_errors: a list of errors that will be retried if encountered.
|
|
1277
|
+
Only idempotent mutations will be retried. Defaults to the Table's
|
|
1278
|
+
default_retryable_errors.
|
|
1279
|
+
Raises:
|
|
1280
|
+
google.api_core.exceptions.DeadlineExceeded: raised after operation timeout
|
|
1281
|
+
will be chained with a RetryExceptionGroup containing all
|
|
1282
|
+
GoogleAPIError exceptions from any retries that failed
|
|
1283
|
+
google.api_core.exceptions.GoogleAPIError: raised on non-idempotent operations that cannot be
|
|
1284
|
+
safely retried.
|
|
1285
|
+
ValueError: if invalid arguments are provided
|
|
1286
|
+
"""
|
|
1287
|
+
operation_timeout, attempt_timeout = _get_timeouts(
|
|
1288
|
+
operation_timeout, attempt_timeout, self
|
|
1289
|
+
)
|
|
1290
|
+
|
|
1291
|
+
if not mutations:
|
|
1292
|
+
raise ValueError("No mutations provided")
|
|
1293
|
+
mutations_list = mutations if isinstance(mutations, list) else [mutations]
|
|
1294
|
+
|
|
1295
|
+
if all(mutation.is_idempotent() for mutation in mutations_list):
|
|
1296
|
+
# mutations are all idempotent and safe to retry
|
|
1297
|
+
predicate = retries.if_exception_type(
|
|
1298
|
+
*_get_retryable_errors(retryable_errors, self)
|
|
1299
|
+
)
|
|
1300
|
+
else:
|
|
1301
|
+
# mutations should not be retried
|
|
1302
|
+
predicate = retries.if_exception_type()
|
|
1303
|
+
|
|
1304
|
+
sleep_generator = retries.exponential_sleep_generator(0.01, 2, 60)
|
|
1305
|
+
|
|
1306
|
+
target = partial(
|
|
1307
|
+
self.client._gapic_client.mutate_row,
|
|
1308
|
+
row_key=row_key.encode("utf-8") if isinstance(row_key, str) else row_key,
|
|
1309
|
+
mutations=[mutation._to_pb() for mutation in mutations_list],
|
|
1310
|
+
table_name=self.table_name,
|
|
1311
|
+
app_profile_id=self.app_profile_id,
|
|
1312
|
+
timeout=attempt_timeout,
|
|
1313
|
+
retry=None,
|
|
1314
|
+
)
|
|
1315
|
+
return await CrossSync.retry_target(
|
|
1316
|
+
target,
|
|
1317
|
+
predicate,
|
|
1318
|
+
sleep_generator,
|
|
1319
|
+
operation_timeout,
|
|
1320
|
+
exception_factory=_retry_exception_factory,
|
|
1321
|
+
)
|
|
1322
|
+
|
|
1323
|
+
@CrossSync.convert
|
|
1324
|
+
async def bulk_mutate_rows(
|
|
1325
|
+
self,
|
|
1326
|
+
mutation_entries: list[RowMutationEntry],
|
|
1327
|
+
*,
|
|
1328
|
+
operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
1329
|
+
attempt_timeout: float | None | TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
1330
|
+
retryable_errors: Sequence[type[Exception]]
|
|
1331
|
+
| TABLE_DEFAULT = TABLE_DEFAULT.MUTATE_ROWS,
|
|
1332
|
+
):
|
|
1333
|
+
"""
|
|
1334
|
+
Applies mutations for multiple rows in a single batched request.
|
|
1335
|
+
|
|
1336
|
+
Each individual RowMutationEntry is applied atomically, but separate entries
|
|
1337
|
+
may be applied in arbitrary order (even for entries targetting the same row)
|
|
1338
|
+
In total, the row_mutations can contain at most 100000 individual mutations
|
|
1339
|
+
across all entries
|
|
1340
|
+
|
|
1341
|
+
Idempotent entries (i.e., entries with mutations with explicit timestamps)
|
|
1342
|
+
will be retried on failure. Non-idempotent will not, and will reported in a
|
|
1343
|
+
raised exception group
|
|
1344
|
+
|
|
1345
|
+
Args:
|
|
1346
|
+
mutation_entries: the batches of mutations to apply
|
|
1347
|
+
Each entry will be applied atomically, but entries will be applied
|
|
1348
|
+
in arbitrary order
|
|
1349
|
+
operation_timeout: the time budget for the entire operation, in seconds.
|
|
1350
|
+
Failed requests will be retried within the budget.
|
|
1351
|
+
Defaults to the Table's default_mutate_rows_operation_timeout
|
|
1352
|
+
attempt_timeout: the time budget for an individual network request, in seconds.
|
|
1353
|
+
If it takes longer than this time to complete, the request will be cancelled with
|
|
1354
|
+
a DeadlineExceeded exception, and a retry will be attempted.
|
|
1355
|
+
Defaults to the Table's default_mutate_rows_attempt_timeout.
|
|
1356
|
+
If None, defaults to operation_timeout.
|
|
1357
|
+
retryable_errors: a list of errors that will be retried if encountered.
|
|
1358
|
+
Defaults to the Table's default_mutate_rows_retryable_errors
|
|
1359
|
+
Raises:
|
|
1360
|
+
MutationsExceptionGroup: if one or more mutations fails
|
|
1361
|
+
Contains details about any failed entries in .exceptions
|
|
1362
|
+
ValueError: if invalid arguments are provided
|
|
1363
|
+
"""
|
|
1364
|
+
operation_timeout, attempt_timeout = _get_timeouts(
|
|
1365
|
+
operation_timeout, attempt_timeout, self
|
|
1366
|
+
)
|
|
1367
|
+
retryable_excs = _get_retryable_errors(retryable_errors, self)
|
|
1368
|
+
|
|
1369
|
+
operation = CrossSync._MutateRowsOperation(
|
|
1370
|
+
self.client._gapic_client,
|
|
1371
|
+
self,
|
|
1372
|
+
mutation_entries,
|
|
1373
|
+
operation_timeout,
|
|
1374
|
+
attempt_timeout,
|
|
1375
|
+
retryable_exceptions=retryable_excs,
|
|
1376
|
+
)
|
|
1377
|
+
await operation.start()
|
|
1378
|
+
|
|
1379
|
+
@CrossSync.convert
|
|
1380
|
+
async def check_and_mutate_row(
|
|
1381
|
+
self,
|
|
1382
|
+
row_key: str | bytes,
|
|
1383
|
+
predicate: RowFilter | None,
|
|
1384
|
+
*,
|
|
1385
|
+
true_case_mutations: Mutation | list[Mutation] | None = None,
|
|
1386
|
+
false_case_mutations: Mutation | list[Mutation] | None = None,
|
|
1387
|
+
operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.DEFAULT,
|
|
1388
|
+
) -> bool:
|
|
1389
|
+
"""
|
|
1390
|
+
Mutates a row atomically based on the output of a predicate filter
|
|
1391
|
+
|
|
1392
|
+
Non-idempotent operation: will not be retried
|
|
1393
|
+
|
|
1394
|
+
Args:
|
|
1395
|
+
row_key: the key of the row to mutate
|
|
1396
|
+
predicate: the filter to be applied to the contents of the specified row.
|
|
1397
|
+
Depending on whether or not any results are yielded,
|
|
1398
|
+
either true_case_mutations or false_case_mutations will be executed.
|
|
1399
|
+
If None, checks that the row contains any values at all.
|
|
1400
|
+
true_case_mutations:
|
|
1401
|
+
Changes to be atomically applied to the specified row if
|
|
1402
|
+
predicate yields at least one cell when
|
|
1403
|
+
applied to row_key. Entries are applied in order,
|
|
1404
|
+
meaning that earlier mutations can be masked by later
|
|
1405
|
+
ones. Must contain at least one entry if
|
|
1406
|
+
false_case_mutations is empty, and at most 100000.
|
|
1407
|
+
false_case_mutations:
|
|
1408
|
+
Changes to be atomically applied to the specified row if
|
|
1409
|
+
predicate_filter does not yield any cells when
|
|
1410
|
+
applied to row_key. Entries are applied in order,
|
|
1411
|
+
meaning that earlier mutations can be masked by later
|
|
1412
|
+
ones. Must contain at least one entry if
|
|
1413
|
+
`true_case_mutations` is empty, and at most 100000.
|
|
1414
|
+
operation_timeout: the time budget for the entire operation, in seconds.
|
|
1415
|
+
Failed requests will not be retried. Defaults to the Table's default_operation_timeout
|
|
1416
|
+
Returns:
|
|
1417
|
+
bool indicating whether the predicate was true or false
|
|
1418
|
+
Raises:
|
|
1419
|
+
google.api_core.exceptions.GoogleAPIError: exceptions from grpc call
|
|
1420
|
+
"""
|
|
1421
|
+
operation_timeout, _ = _get_timeouts(operation_timeout, None, self)
|
|
1422
|
+
if true_case_mutations is not None and not isinstance(
|
|
1423
|
+
true_case_mutations, list
|
|
1424
|
+
):
|
|
1425
|
+
true_case_mutations = [true_case_mutations]
|
|
1426
|
+
true_case_list = [m._to_pb() for m in true_case_mutations or []]
|
|
1427
|
+
if false_case_mutations is not None and not isinstance(
|
|
1428
|
+
false_case_mutations, list
|
|
1429
|
+
):
|
|
1430
|
+
false_case_mutations = [false_case_mutations]
|
|
1431
|
+
false_case_list = [m._to_pb() for m in false_case_mutations or []]
|
|
1432
|
+
result = await self.client._gapic_client.check_and_mutate_row(
|
|
1433
|
+
true_mutations=true_case_list,
|
|
1434
|
+
false_mutations=false_case_list,
|
|
1435
|
+
predicate_filter=predicate._to_pb() if predicate is not None else None,
|
|
1436
|
+
row_key=row_key.encode("utf-8") if isinstance(row_key, str) else row_key,
|
|
1437
|
+
table_name=self.table_name,
|
|
1438
|
+
app_profile_id=self.app_profile_id,
|
|
1439
|
+
timeout=operation_timeout,
|
|
1440
|
+
retry=None,
|
|
1441
|
+
)
|
|
1442
|
+
return result.predicate_matched
|
|
1443
|
+
|
|
1444
|
+
@CrossSync.convert
|
|
1445
|
+
async def read_modify_write_row(
|
|
1446
|
+
self,
|
|
1447
|
+
row_key: str | bytes,
|
|
1448
|
+
rules: ReadModifyWriteRule | list[ReadModifyWriteRule],
|
|
1449
|
+
*,
|
|
1450
|
+
operation_timeout: float | TABLE_DEFAULT = TABLE_DEFAULT.DEFAULT,
|
|
1451
|
+
) -> Row:
|
|
1452
|
+
"""
|
|
1453
|
+
Reads and modifies a row atomically according to input ReadModifyWriteRules,
|
|
1454
|
+
and returns the contents of all modified cells
|
|
1455
|
+
|
|
1456
|
+
The new value for the timestamp is the greater of the existing timestamp or
|
|
1457
|
+
the current server time.
|
|
1458
|
+
|
|
1459
|
+
Non-idempotent operation: will not be retried
|
|
1460
|
+
|
|
1461
|
+
Args:
|
|
1462
|
+
row_key: the key of the row to apply read/modify/write rules to
|
|
1463
|
+
rules: A rule or set of rules to apply to the row.
|
|
1464
|
+
Rules are applied in order, meaning that earlier rules will affect the
|
|
1465
|
+
results of later ones.
|
|
1466
|
+
operation_timeout: the time budget for the entire operation, in seconds.
|
|
1467
|
+
Failed requests will not be retried.
|
|
1468
|
+
Defaults to the Table's default_operation_timeout.
|
|
1469
|
+
Returns:
|
|
1470
|
+
Row: a Row containing cell data that was modified as part of the operation
|
|
1471
|
+
Raises:
|
|
1472
|
+
google.api_core.exceptions.GoogleAPIError: exceptions from grpc call
|
|
1473
|
+
ValueError: if invalid arguments are provided
|
|
1474
|
+
"""
|
|
1475
|
+
operation_timeout, _ = _get_timeouts(operation_timeout, None, self)
|
|
1476
|
+
if operation_timeout <= 0:
|
|
1477
|
+
raise ValueError("operation_timeout must be greater than 0")
|
|
1478
|
+
if rules is not None and not isinstance(rules, list):
|
|
1479
|
+
rules = [rules]
|
|
1480
|
+
if not rules:
|
|
1481
|
+
raise ValueError("rules must contain at least one item")
|
|
1482
|
+
result = await self.client._gapic_client.read_modify_write_row(
|
|
1483
|
+
rules=[rule._to_pb() for rule in rules],
|
|
1484
|
+
row_key=row_key.encode("utf-8") if isinstance(row_key, str) else row_key,
|
|
1485
|
+
table_name=self.table_name,
|
|
1486
|
+
app_profile_id=self.app_profile_id,
|
|
1487
|
+
timeout=operation_timeout,
|
|
1488
|
+
retry=None,
|
|
1489
|
+
)
|
|
1490
|
+
# construct Row from result
|
|
1491
|
+
return Row._from_pb(result.row)
|
|
1492
|
+
|
|
1493
|
+
@CrossSync.convert
|
|
1494
|
+
async def close(self):
|
|
1495
|
+
"""
|
|
1496
|
+
Called to close the Table instance and release any resources held by it.
|
|
1497
|
+
"""
|
|
1498
|
+
if self._register_instance_future:
|
|
1499
|
+
self._register_instance_future.cancel()
|
|
1500
|
+
await self.client._remove_instance_registration(self.instance_id, self)
|
|
1501
|
+
|
|
1502
|
+
@CrossSync.convert(sync_name="__enter__")
|
|
1503
|
+
async def __aenter__(self):
|
|
1504
|
+
"""
|
|
1505
|
+
Implement async context manager protocol
|
|
1506
|
+
|
|
1507
|
+
Ensure registration task has time to run, so that
|
|
1508
|
+
grpc channels will be warmed for the specified instance
|
|
1509
|
+
"""
|
|
1510
|
+
if self._register_instance_future:
|
|
1511
|
+
await self._register_instance_future
|
|
1512
|
+
return self
|
|
1513
|
+
|
|
1514
|
+
@CrossSync.convert(sync_name="__exit__")
|
|
1515
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
1516
|
+
"""
|
|
1517
|
+
Implement async context manager protocol
|
|
1518
|
+
|
|
1519
|
+
Unregister this instance with the client, so that
|
|
1520
|
+
grpc channels will no longer be warmed
|
|
1521
|
+
"""
|
|
1522
|
+
await self.close()
|