Skip to content

Commit 7c56016

Browse files
committed
Merge remote-tracking branch 'origin/main' into feature/lazy-imports-google-auth
2 parents 5184b42 + 8ed6b71 commit 7c56016

9 files changed

Lines changed: 213 additions & 82 deletions

File tree

.github/workflows/import-profiler.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,7 @@ jobs:
3232
TARGET_BRANCH: ${{ github.base_ref || github.event.merge_group.base_ref }}
3333
TEST_TYPE: import_profile
3434
PY_VERSION: "3.15"
35+
# Workaround: Allows libcst to compile on Python 3.15+ while PyO3 catches up
36+
PYO3_USE_ABI3_FORWARD_COMPATIBILITY: "1"
3537
run: |
3638
ci/run_conditional_tests.sh

packages/google-api-core/google/api_core/operations_v1/__init__.py

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,68 @@
1414

1515
"""Package for interacting with the google.longrunning.operations meta-API."""
1616

17-
from google.api_core.operations_v1.abstract_operations_client import AbstractOperationsClient
18-
from google.api_core.operations_v1.operations_async_client import OperationsAsyncClient
19-
from google.api_core.operations_v1.operations_client import OperationsClient
20-
from google.api_core.operations_v1.transports.rest import OperationsRestTransport
17+
import importlib.util
18+
from typing import Set
19+
20+
try:
21+
_has_async_rest = (
22+
importlib.util.find_spec("google.auth.aio.transport.sessions") is not None
23+
)
24+
except ModuleNotFoundError:
25+
_has_async_rest = False
26+
27+
# PEP 0810: Explicit Lazy Imports
28+
# Python 3.15+ natively intercepts and defers these imports.
29+
# Developers can disable this behavior and force eager imports.
30+
# For more information, see:
31+
# https://docs.python.org/3.15/library/sys.html#sys.set_lazy_imports_filter
32+
# Older Python versions safely ignore this variable.
33+
# NOTE: We statically define all modules here (including async ones) to ensure
34+
# static analysis tools (mypy, pyright, Ruff) can easily parse them. If async
35+
# support is not present, the imports are ignored, making their presence safe.
36+
__lazy_modules__: Set[str] = {
37+
"google.api_core.operations_v1.abstract_operations_client",
38+
"google.api_core.operations_v1.operations_async_client",
39+
"google.api_core.operations_v1.operations_client",
40+
"google.api_core.operations_v1.transports.rest",
41+
"google.api_core.operations_v1.transports.rest_asyncio",
42+
"google.api_core.operations_v1.operations_rest_client_async",
43+
}
2144

2245
__all__ = [
2346
"AbstractOperationsClient",
2447
"OperationsAsyncClient",
2548
"OperationsClient",
26-
"OperationsRestTransport"
49+
"OperationsRestTransport",
2750
]
2851

29-
try:
30-
from google.api_core.operations_v1.transports.rest_asyncio import (
31-
AsyncOperationsRestTransport,
32-
)
33-
from google.api_core.operations_v1.operations_rest_client_async import AsyncOperationsRestClient
34-
35-
__all__ += ["AsyncOperationsRestClient", "AsyncOperationsRestTransport"]
36-
except ImportError:
37-
# This import requires the `async_rest` extra.
38-
# Don't raise an exception if `AsyncOperationsRestTransport` cannot be imported
39-
# as other transports are still available.
40-
pass
52+
53+
from google.api_core.operations_v1.abstract_operations_client import ( # noqa: E402
54+
AbstractOperationsClient,
55+
)
56+
from google.api_core.operations_v1.operations_async_client import ( # noqa: E402
57+
OperationsAsyncClient,
58+
)
59+
from google.api_core.operations_v1.operations_client import ( # noqa: E402
60+
OperationsClient,
61+
)
62+
from google.api_core.operations_v1.transports.rest import ( # noqa: E402
63+
OperationsRestTransport,
64+
)
65+
66+
if _has_async_rest:
67+
try:
68+
# On Python 3.15+, PEP 0810 lazy loading means these imports will succeed
69+
# instantly (returning a lazy proxy). Any actual ImportErrors (e.g., due to
70+
# missing aiohttp/auth dependencies) are deferred until the proxies are accessed.
71+
from google.api_core.operations_v1.transports.rest_asyncio import ( # noqa: E402, F401
72+
AsyncOperationsRestTransport,
73+
)
74+
from google.api_core.operations_v1.operations_rest_client_async import ( # noqa: E402, F401
75+
AsyncOperationsRestClient,
76+
)
77+
78+
__all__.extend(["AsyncOperationsRestClient", "AsyncOperationsRestTransport"])
79+
except ImportError:
80+
# Fallback for older python/environments when importlib find_spec succeeds but actual import fails
81+
pass

packages/google-cloud-bigtable/mypy.ini

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# We use the constraints file for the latest Python version
2+
# (currently this file) to check that the latest
3+
# major versions of dependencies are supported in setup.py.
4+
# List all library dependencies and extras in this file.
5+
# Require the latest major version be installed for each dependency.
6+
# e.g., if setup.py has "google-cloud-foo >= 1.14.0, < 2.0.0",
7+
# Then this file should have google-cloud-foo>=1
8+
google-api-core>=2
9+
google-auth>=2
10+
grpcio>=1
11+
proto-plus>=1
12+
protobuf>=7

packages/google-cloud-bigtable/tests/system/data/test_system_async.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ async def add_row(
6868
elif isinstance(value, int):
6969
value = value.to_bytes(8, byteorder="big", signed=True)
7070
request = {
71-
"table_name": self.target.table_name,
71+
**self.target._request_path,
7272
"row_key": row_key,
7373
"mutations": [
7474
{
@@ -88,7 +88,7 @@ async def add_aggregate_row(
8888
self, row_key, *, family=TEST_AGGREGATE_FAMILY, qualifier=b"q", input=0
8989
):
9090
request = {
91-
"table_name": self.target.table_name,
91+
**self.target._request_path,
9292
"row_key": row_key,
9393
"mutations": [
9494
{
@@ -107,14 +107,44 @@ async def add_aggregate_row(
107107
@CrossSync.convert
108108
async def delete_rows(self):
109109
if self.rows:
110-
request = {
111-
"table_name": self.target.table_name,
112-
"entries": [
113-
{"row_key": row, "mutations": [{"delete_from_row": {}}]}
114-
for row in self.rows
115-
],
116-
}
117-
await self.target.client._gapic_client.mutate_rows(request)
110+
# Chunk deletions to 5,000 rows. While Bigtable officially supports up to
111+
# 100,000 mutations per MutateRows RPC, sending massive batches may hit
112+
# the default gRPC 4MB client payload size limit due to metadata
113+
# serialization overhead. Keeping chunks at 5,000 ensures we stay safely
114+
# under 4MB and minimizes transient network timeouts on live connections.
115+
chunk_size = 5000
116+
rows_list = list(self.rows)
117+
118+
# Check if the test target is an Authorized View
119+
is_authorized_view = "authorized_view_name" in self.target._request_path
120+
121+
if is_authorized_view:
122+
# For Authorized Views, we cannot use delete_from_row because it attempts
123+
# to delete families outside the view's scope. We must delete explicitly
124+
# from the allowed families we wrote to.
125+
mutations = [
126+
{"delete_from_family": {"family_name": TEST_FAMILY}},
127+
{"delete_from_family": {"family_name": TEST_AGGREGATE_FAMILY}},
128+
]
129+
else:
130+
mutations = [{"delete_from_row": {}}]
131+
132+
for i in range(0, len(rows_list), chunk_size):
133+
chunk = rows_list[i : i + chunk_size]
134+
request = {
135+
**self.target._request_path,
136+
"entries": [
137+
{"row_key": row, "mutations": mutations} for row in chunk
138+
],
139+
}
140+
# Await and consume the gRPC stream to guarantee execution
141+
stream = await self.target.client._gapic_client.mutate_rows(request)
142+
async for response in stream:
143+
for entry in response.entries:
144+
if entry.status.code != 0:
145+
raise RuntimeError(
146+
f"Failed to delete row: {entry.status.message}"
147+
)
118148

119149
@CrossSync.convert
120150
async def retrieve_cell_value(self, target, row_key):

packages/google-cloud-bigtable/tests/system/data/test_system_autogen.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def add_row(
5757
elif isinstance(value, int):
5858
value = value.to_bytes(8, byteorder="big", signed=True)
5959
request = {
60-
"table_name": self.target.table_name,
60+
**self.target._request_path,
6161
"row_key": row_key,
6262
"mutations": [
6363
{
@@ -76,7 +76,7 @@ def add_aggregate_row(
7676
self, row_key, *, family=TEST_AGGREGATE_FAMILY, qualifier=b"q", input=0
7777
):
7878
request = {
79-
"table_name": self.target.table_name,
79+
**self.target._request_path,
8080
"row_key": row_key,
8181
"mutations": [
8282
{
@@ -94,14 +94,31 @@ def add_aggregate_row(
9494

9595
def delete_rows(self):
9696
if self.rows:
97-
request = {
98-
"table_name": self.target.table_name,
99-
"entries": [
100-
{"row_key": row, "mutations": [{"delete_from_row": {}}]}
101-
for row in self.rows
102-
],
103-
}
104-
self.target.client._gapic_client.mutate_rows(request)
97+
chunk_size = 5000
98+
rows_list = list(self.rows)
99+
is_authorized_view = "authorized_view_name" in self.target._request_path
100+
if is_authorized_view:
101+
mutations = [
102+
{"delete_from_family": {"family_name": TEST_FAMILY}},
103+
{"delete_from_family": {"family_name": TEST_AGGREGATE_FAMILY}},
104+
]
105+
else:
106+
mutations = [{"delete_from_row": {}}]
107+
for i in range(0, len(rows_list), chunk_size):
108+
chunk = rows_list[i : i + chunk_size]
109+
request = {
110+
**self.target._request_path,
111+
"entries": [
112+
{"row_key": row, "mutations": mutations} for row in chunk
113+
],
114+
}
115+
stream = self.target.client._gapic_client.mutate_rows(request)
116+
for response in stream:
117+
for entry in response.entries:
118+
if entry.status.code != 0:
119+
raise RuntimeError(
120+
f"Failed to delete row: {entry.status.message}"
121+
)
105122

106123
def retrieve_cell_value(self, target, row_key):
107124
"""Helper to read an individual row"""

packages/google-cloud-bigtable/tests/unit/data/_sync_autogen/test_client.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2998,15 +2998,11 @@ def test_execute_query_with_params(self, client, execute_query_mock, prepare_moc
29982998
def test_execute_query_with_view_parameters(
29992999
self, client, execute_query_mock, prepare_mock
30003000
):
3001-
values = [
3002-
*chunked_responses(2, str_val("test2"), int_val(9), token=b"r2"),
3003-
]
3001+
values = [*chunked_responses(2, str_val("test2"), int_val(9), token=b"r2")]
30043002
execute_query_mock.return_value = self._make_gapic_stream(values)
30053003
query_str = f"SELECT a, b FROM {self.TABLE_NAME} WHERE user_id = VIEW_PARAMETERS('user_id')"
30063004
result = client.execute_query(
3007-
query_str,
3008-
self.INSTANCE_NAME,
3009-
view_parameters={"user_id": "alice"},
3005+
query_str, self.INSTANCE_NAME, view_parameters={"user_id": "alice"}
30103006
)
30113007
results = [r for r in result]
30123008
assert len(results) == 1
@@ -3015,7 +3011,6 @@ def test_execute_query_with_view_parameters(
30153011
assert execute_query_mock.call_count == 1
30163012
assert prepare_mock.call_count == 1
30173013
assert prepare_mock.call_args[1]["request"]["query"] == query_str
3018-
30193014
request = execute_query_mock.call_args[0][0]
30203015
assert "user_id" in request.view_parameters
30213016
assert request.view_parameters["user_id"].string_value == "alice"

0 commit comments

Comments
 (0)