Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 6e57110

Browse files
authored
Merge branch 'main' into fix-function-test
2 parents ad28793 + 17a1ed9 commit 6e57110

37 files changed

+675
-188
lines changed

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@
44

55
[1]: https://pypi.org/project/bigframes/#history
66

7+
## [2.19.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v2.18.0...v2.19.0) (2025-09-09)
8+
9+
10+
### Features
11+
12+
* Add str.join method ([#2054](https://github.com/googleapis/python-bigquery-dataframes/issues/2054)) ([8804ada](https://github.com/googleapis/python-bigquery-dataframes/commit/8804adaf8ba23fdcad6e42a7bf034bd0a11c890f))
13+
* Support display.max_colwidth option ([#2053](https://github.com/googleapis/python-bigquery-dataframes/issues/2053)) ([5229e07](https://github.com/googleapis/python-bigquery-dataframes/commit/5229e07b4535c01b0cdbd731455ff225a373b5c8))
14+
* Support VPC egress setting in remote function ([#2059](https://github.com/googleapis/python-bigquery-dataframes/issues/2059)) ([5df779d](https://github.com/googleapis/python-bigquery-dataframes/commit/5df779d4f421d3ba777cfd928d99ca2e8a3f79ad))
15+
16+
17+
### Bug Fixes
18+
19+
* Fix issue mishandling chunked array while loading data ([#2051](https://github.com/googleapis/python-bigquery-dataframes/issues/2051)) ([873d0ee](https://github.com/googleapis/python-bigquery-dataframes/commit/873d0eee474ed34f1d5164c37383f2737dbec4db))
20+
* Remove warning for slot_millis_sum ([#2047](https://github.com/googleapis/python-bigquery-dataframes/issues/2047)) ([425a691](https://github.com/googleapis/python-bigquery-dataframes/commit/425a6917d5442eeb4df486c6eed1fd136bbcedfb))
21+
722
## [2.18.0](https://github.com/googleapis/python-bigquery-dataframes/compare/v2.17.0...v2.18.0) (2025-09-03)
823

924

bigframes/_config/auth.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import threading
18+
from typing import Optional
19+
20+
import google.auth.credentials
21+
import google.auth.transport.requests
22+
import pydata_google_auth
23+
24+
_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
25+
26+
# Put the lock here rather than in BigQueryOptions so that BigQueryOptions
27+
# remains deepcopy-able.
28+
_AUTH_LOCK = threading.Lock()
29+
_cached_credentials: Optional[google.auth.credentials.Credentials] = None
30+
_cached_project_default: Optional[str] = None
31+
32+
33+
def get_default_credentials_with_project() -> tuple[
34+
google.auth.credentials.Credentials, Optional[str]
35+
]:
36+
global _AUTH_LOCK, _cached_credentials, _cached_project_default
37+
38+
with _AUTH_LOCK:
39+
if _cached_credentials is not None:
40+
return _cached_credentials, _cached_project_default
41+
42+
_cached_credentials, _cached_project_default = pydata_google_auth.default(
43+
scopes=_SCOPES, use_local_webserver=False
44+
)
45+
46+
# Ensure an access token is available.
47+
_cached_credentials.refresh(google.auth.transport.requests.Request())
48+
49+
return _cached_credentials, _cached_project_default
50+
51+
52+
def reset_default_credentials_and_project():
53+
global _AUTH_LOCK, _cached_credentials, _cached_project_default
54+
55+
with _AUTH_LOCK:
56+
_cached_credentials = None
57+
_cached_project_default = None

bigframes/_config/bigquery_options.py

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import google.auth.credentials
2323
import requests.adapters
2424

25+
import bigframes._config.auth
2526
import bigframes._importing
2627
import bigframes.enums
2728
import bigframes.exceptions as bfe
@@ -37,6 +38,7 @@
3738

3839
def _get_validated_location(value: Optional[str]) -> Optional[str]:
3940
import bigframes._tools.strings
41+
import bigframes.constants
4042

4143
if value is None or value in bigframes.constants.ALL_BIGQUERY_LOCATIONS:
4244
return value
@@ -141,20 +143,52 @@ def application_name(self, value: Optional[str]):
141143
)
142144
self._application_name = value
143145

146+
def _try_set_default_credentials_and_project(
147+
self,
148+
) -> tuple[google.auth.credentials.Credentials, Optional[str]]:
149+
# Don't fetch credentials or project if credentials is already set.
150+
# If it's set, we've already authenticated, so if the user wants to
151+
# re-auth, they should explicitly reset the credentials.
152+
if self._credentials is not None:
153+
return self._credentials, self._project
154+
155+
(
156+
credentials,
157+
credentials_project,
158+
) = bigframes._config.auth.get_default_credentials_with_project()
159+
self._credentials = credentials
160+
161+
# Avoid overriding an explicitly set project with a default value.
162+
if self._project is None:
163+
self._project = credentials_project
164+
165+
return credentials, self._project
166+
144167
@property
145-
def credentials(self) -> Optional[google.auth.credentials.Credentials]:
168+
def credentials(self) -> google.auth.credentials.Credentials:
146169
"""The OAuth2 credentials to use for this client.
147170
171+
Set to None to force re-authentication.
172+
148173
Returns:
149174
None or google.auth.credentials.Credentials:
150175
google.auth.credentials.Credentials if exists; otherwise None.
151176
"""
152-
return self._credentials
177+
if self._credentials:
178+
return self._credentials
179+
180+
credentials, _ = self._try_set_default_credentials_and_project()
181+
return credentials
153182

154183
@credentials.setter
155184
def credentials(self, value: Optional[google.auth.credentials.Credentials]):
156185
if self._session_started and self._credentials is not value:
157186
raise ValueError(SESSION_STARTED_MESSAGE.format(attribute="credentials"))
187+
188+
if value is None:
189+
# The user has _explicitly_ asked that we re-authenticate.
190+
bigframes._config.auth.reset_default_credentials_and_project()
191+
158192
self._credentials = value
159193

160194
@property
@@ -183,7 +217,11 @@ def project(self) -> Optional[str]:
183217
None or str:
184218
Google Cloud project ID as a string; otherwise None.
185219
"""
186-
return self._project
220+
if self._project:
221+
return self._project
222+
223+
_, project = self._try_set_default_credentials_and_project()
224+
return project
187225

188226
@project.setter
189227
def project(self, value: Optional[str]):

bigframes/core/groupby/dataframe_group_by.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -461,23 +461,19 @@ def expanding(self, min_periods: int = 1) -> windows.Window:
461461

462462
def agg(self, func=None, **kwargs) -> typing.Union[df.DataFrame, series.Series]:
463463
if func:
464-
if isinstance(func, str):
465-
return self.size() if func == "size" else self._agg_string(func)
466-
elif utils.is_dict_like(func):
464+
if utils.is_dict_like(func):
467465
return self._agg_dict(func)
468466
elif utils.is_list_like(func):
469467
return self._agg_list(func)
470468
else:
471-
raise NotImplementedError(
472-
f"Aggregate with {func} not supported. {constants.FEEDBACK_LINK}"
473-
)
469+
return self.size() if func == "size" else self._agg_func(func)
474470
else:
475471
return self._agg_named(**kwargs)
476472

477-
def _agg_string(self, func: str) -> df.DataFrame:
473+
def _agg_func(self, func) -> df.DataFrame:
478474
ids, labels = self._aggregated_columns()
479475
aggregations = [
480-
aggs.agg(col_id, agg_ops.lookup_agg_func(func)) for col_id in ids
476+
aggs.agg(col_id, agg_ops.lookup_agg_func(func)[0]) for col_id in ids
481477
]
482478
agg_block, _ = self._block.aggregate(
483479
by_column_ids=self._by_col_ids,
@@ -500,7 +496,7 @@ def _agg_dict(self, func: typing.Mapping) -> df.DataFrame:
500496
funcs_for_id if utils.is_list_like(funcs_for_id) else [funcs_for_id]
501497
)
502498
for f in func_list:
503-
aggregations.append(aggs.agg(col_id, agg_ops.lookup_agg_func(f)))
499+
aggregations.append(aggs.agg(col_id, agg_ops.lookup_agg_func(f)[0]))
504500
column_labels.append(label)
505501
agg_block, _ = self._block.aggregate(
506502
by_column_ids=self._by_col_ids,
@@ -525,19 +521,23 @@ def _agg_dict(self, func: typing.Mapping) -> df.DataFrame:
525521
def _agg_list(self, func: typing.Sequence) -> df.DataFrame:
526522
ids, labels = self._aggregated_columns()
527523
aggregations = [
528-
aggs.agg(col_id, agg_ops.lookup_agg_func(f)) for col_id in ids for f in func
524+
aggs.agg(col_id, agg_ops.lookup_agg_func(f)[0])
525+
for col_id in ids
526+
for f in func
529527
]
530528

531529
if self._block.column_labels.nlevels > 1:
532530
# Restructure MultiIndex for proper format: (idx1, idx2, func)
533531
# rather than ((idx1, idx2), func).
534532
column_labels = [
535-
tuple(label) + (f,)
533+
tuple(label) + (agg_ops.lookup_agg_func(f)[1],)
536534
for label in labels.to_frame(index=False).to_numpy()
537535
for f in func
538536
]
539537
else: # Single-level index
540-
column_labels = [(label, f) for label in labels for f in func]
538+
column_labels = [
539+
(label, agg_ops.lookup_agg_func(f)[1]) for label in labels for f in func
540+
]
541541

542542
agg_block, _ = self._block.aggregate(
543543
by_column_ids=self._by_col_ids,
@@ -563,7 +563,7 @@ def _agg_named(self, **kwargs) -> df.DataFrame:
563563
if not isinstance(v, tuple) or (len(v) != 2):
564564
raise TypeError("kwargs values must be 2-tuples of column, aggfunc")
565565
col_id = self._resolve_label(v[0])
566-
aggregations.append(aggs.agg(col_id, agg_ops.lookup_agg_func(v[1])))
566+
aggregations.append(aggs.agg(col_id, agg_ops.lookup_agg_func(v[1])[0]))
567567
column_labels.append(k)
568568
agg_block, _ = self._block.aggregate(
569569
by_column_ids=self._by_col_ids,

bigframes/core/groupby/series_group_by.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -216,18 +216,17 @@ def prod(self, *args) -> series.Series:
216216

217217
def agg(self, func=None) -> typing.Union[df.DataFrame, series.Series]:
218218
column_names: list[str] = []
219-
if isinstance(func, str):
220-
aggregations = [aggs.agg(self._value_column, agg_ops.lookup_agg_func(func))]
221-
column_names = [func]
222-
elif utils.is_list_like(func):
223-
aggregations = [
224-
aggs.agg(self._value_column, agg_ops.lookup_agg_func(f)) for f in func
225-
]
226-
column_names = list(func)
227-
else:
219+
if utils.is_dict_like(func):
228220
raise NotImplementedError(
229221
f"Aggregate with {func} not supported. {constants.FEEDBACK_LINK}"
230222
)
223+
if not utils.is_list_like(func):
224+
func = [func]
225+
226+
aggregations = [
227+
aggs.agg(self._value_column, agg_ops.lookup_agg_func(f)[0]) for f in func
228+
]
229+
column_names = [agg_ops.lookup_agg_func(f)[1] for f in func]
231230

232231
agg_block, _ = self._block.aggregate(
233232
by_column_ids=self._by_col_ids,

bigframes/core/log_adapter.py

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -149,49 +149,61 @@ def wrap(cls):
149149
return wrap(decorated_cls)
150150

151151

152-
def method_logger(method, /, *, custom_base_name: Optional[str] = None):
152+
def method_logger(method=None, /, *, custom_base_name: Optional[str] = None):
153153
"""Decorator that adds logging functionality to a method."""
154154

155-
@functools.wraps(method)
156-
def wrapper(*args, **kwargs):
157-
api_method_name = getattr(method, LOG_OVERRIDE_NAME, method.__name__)
158-
if custom_base_name is None:
159-
qualname_parts = getattr(method, "__qualname__", method.__name__).split(".")
160-
class_name = qualname_parts[-2] if len(qualname_parts) > 1 else ""
161-
base_name = (
162-
class_name if class_name else "_".join(method.__module__.split(".")[1:])
163-
)
164-
else:
165-
base_name = custom_base_name
166-
167-
full_method_name = f"{base_name.lower()}-{api_method_name}"
168-
# Track directly called methods
169-
if len(_call_stack) == 0:
170-
add_api_method(full_method_name)
171-
172-
_call_stack.append(full_method_name)
173-
174-
try:
175-
return method(*args, **kwargs)
176-
except (NotImplementedError, TypeError) as e:
177-
# Log method parameters that are implemented in pandas but either missing (TypeError)
178-
# or not fully supported (NotImplementedError) in BigFrames.
179-
# Logging is currently supported only when we can access the bqclient through
180-
# _block.session.bqclient.
181-
if len(_call_stack) == 1:
182-
submit_pandas_labels(
183-
_get_bq_client(*args, **kwargs),
184-
base_name,
185-
api_method_name,
186-
args,
187-
kwargs,
188-
task=PANDAS_PARAM_TRACKING_TASK,
155+
def outer_wrapper(method):
156+
@functools.wraps(method)
157+
def wrapper(*args, **kwargs):
158+
api_method_name = getattr(method, LOG_OVERRIDE_NAME, method.__name__)
159+
if custom_base_name is None:
160+
qualname_parts = getattr(method, "__qualname__", method.__name__).split(
161+
"."
162+
)
163+
class_name = qualname_parts[-2] if len(qualname_parts) > 1 else ""
164+
base_name = (
165+
class_name
166+
if class_name
167+
else "_".join(method.__module__.split(".")[1:])
189168
)
190-
raise e
191-
finally:
192-
_call_stack.pop()
169+
else:
170+
base_name = custom_base_name
193171

194-
return wrapper
172+
full_method_name = f"{base_name.lower()}-{api_method_name}"
173+
# Track directly called methods
174+
if len(_call_stack) == 0:
175+
add_api_method(full_method_name)
176+
177+
_call_stack.append(full_method_name)
178+
179+
try:
180+
return method(*args, **kwargs)
181+
except (NotImplementedError, TypeError) as e:
182+
# Log method parameters that are implemented in pandas but either missing (TypeError)
183+
# or not fully supported (NotImplementedError) in BigFrames.
184+
# Logging is currently supported only when we can access the bqclient through
185+
# _block.session.bqclient.
186+
if len(_call_stack) == 1:
187+
submit_pandas_labels(
188+
_get_bq_client(*args, **kwargs),
189+
base_name,
190+
api_method_name,
191+
args,
192+
kwargs,
193+
task=PANDAS_PARAM_TRACKING_TASK,
194+
)
195+
raise e
196+
finally:
197+
_call_stack.pop()
198+
199+
return wrapper
200+
201+
if method is None:
202+
# Called with parentheses
203+
return outer_wrapper
204+
205+
# Called without parentheses
206+
return outer_wrapper(method)
195207

196208

197209
def property_logger(prop):

bigframes/core/nodes.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,15 @@ def remap_vars(
300300
def remap_refs(
301301
self, mappings: Mapping[identifiers.ColumnId, identifiers.ColumnId]
302302
) -> InNode:
303-
return dataclasses.replace(self, left_col=self.left_col.remap_column_refs(mappings, allow_partial_bindings=True), right_col=self.right_col.remap_column_refs(mappings, allow_partial_bindings=True)) # type: ignore
303+
return dataclasses.replace(
304+
self,
305+
left_col=self.left_col.remap_column_refs(
306+
mappings, allow_partial_bindings=True
307+
),
308+
right_col=self.right_col.remap_column_refs(
309+
mappings, allow_partial_bindings=True
310+
),
311+
) # type: ignore
304312

305313

306314
@dataclasses.dataclass(frozen=True, eq=False)

0 commit comments

Comments
 (0)