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

Commit bab32d9

Browse files
authored
Merge branch 'main' into owl-bot-update-lock-3b3a31be60853477bc39ed8d9bac162cac3ba083724cecaad54eb81d4e4dae9c
2 parents e7b1061 + 7301667 commit bab32d9

8 files changed

Lines changed: 815 additions & 48 deletions

File tree

google/cloud/bigquery/_job_helpers.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@
3939
import functools
4040
import os
4141
import uuid
42+
import textwrap
4243
from typing import Any, Dict, Optional, TYPE_CHECKING, Union
44+
import warnings
4345

4446
import google.api_core.exceptions as core_exceptions
4547
from google.api_core import retry as retries
@@ -198,6 +200,44 @@ def _validate_job_config(request_body: Dict[str, Any], invalid_key: str):
198200
raise ValueError(f"got unexpected key {repr(invalid_key)} in job_config")
199201

200202

203+
def validate_job_retry(job_id: Optional[str], job_retry: Optional[retries.Retry]):
204+
"""Catch common mistakes, such as setting a job_id and job_retry at the same
205+
time.
206+
"""
207+
if job_id is not None and job_retry is not None:
208+
# TODO(tswast): To avoid breaking changes but still allow a default
209+
# query job retry, we currently only raise if they explicitly set a
210+
# job_retry other than the default. In a future version, we may want to
211+
# avoid this check for DEFAULT_JOB_RETRY and always raise.
212+
if job_retry is not google.cloud.bigquery.retry.DEFAULT_JOB_RETRY:
213+
raise TypeError(
214+
textwrap.dedent(
215+
"""
216+
`job_retry` was provided, but the returned job is
217+
not retryable, because a custom `job_id` was
218+
provided. To customize the job ID and allow for job
219+
retries, set job_id_prefix, instead.
220+
"""
221+
).strip()
222+
)
223+
else:
224+
warnings.warn(
225+
textwrap.dedent(
226+
"""
227+
job_retry must be explicitly set to None if job_id is set.
228+
BigQuery cannot retry a failed job by using the exact
229+
same ID. Setting job_id without explicitly disabling
230+
job_retry will raise an error in the future. To avoid this
231+
warning, either use job_id_prefix instead (preferred) or
232+
set job_retry=None.
233+
"""
234+
).strip(),
235+
category=FutureWarning,
236+
# user code -> client.query / client.query_and_wait -> validate_job_retry
237+
stacklevel=3,
238+
)
239+
240+
201241
def _to_query_request(
202242
job_config: Optional[job.QueryJobConfig] = None,
203243
*,
@@ -308,7 +348,7 @@ def query_jobs_query(
308348
project: str,
309349
retry: retries.Retry,
310350
timeout: Optional[float],
311-
job_retry: retries.Retry,
351+
job_retry: Optional[retries.Retry],
312352
) -> job.QueryJob:
313353
"""Initiate a query using jobs.query with jobCreationMode=JOB_CREATION_REQUIRED.
314354

google/cloud/bigquery/client.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3388,7 +3388,7 @@ def query(
33883388
project: Optional[str] = None,
33893389
retry: retries.Retry = DEFAULT_RETRY,
33903390
timeout: TimeoutType = DEFAULT_TIMEOUT,
3391-
job_retry: retries.Retry = DEFAULT_JOB_RETRY,
3391+
job_retry: Optional[retries.Retry] = DEFAULT_JOB_RETRY,
33923392
api_method: Union[str, enums.QueryApiMethod] = enums.QueryApiMethod.INSERT,
33933393
) -> job.QueryJob:
33943394
"""Run a SQL query.
@@ -3455,18 +3455,9 @@ def query(
34553455
class, or if both ``job_id`` and non-``None`` non-default
34563456
``job_retry`` are provided.
34573457
"""
3458-
job_id_given = job_id is not None
3459-
if (
3460-
job_id_given
3461-
and job_retry is not None
3462-
and job_retry is not DEFAULT_JOB_RETRY
3463-
):
3464-
raise TypeError(
3465-
"`job_retry` was provided, but the returned job is"
3466-
" not retryable, because a custom `job_id` was"
3467-
" provided."
3468-
)
3458+
_job_helpers.validate_job_retry(job_id, job_retry)
34693459

3460+
job_id_given = job_id is not None
34703461
if job_id_given and api_method == enums.QueryApiMethod.QUERY:
34713462
raise TypeError(
34723463
"`job_id` was provided, but the 'QUERY' `api_method` was requested."

google/cloud/bigquery/dataset.py

Lines changed: 110 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -298,12 +298,15 @@ def __init__(
298298
role: Optional[str] = None,
299299
entity_type: Optional[str] = None,
300300
entity_id: Optional[Union[Dict[str, Any], str]] = None,
301+
**kwargs,
301302
):
302-
self._properties = {}
303+
self._properties: Dict[str, Any] = {}
303304
if entity_type is not None:
304305
self._properties[entity_type] = entity_id
305306
self._properties["role"] = role
306-
self._entity_type = entity_type
307+
self._entity_type: Optional[str] = entity_type
308+
for prop, val in kwargs.items():
309+
setattr(self, prop, val)
307310

308311
@property
309312
def role(self) -> Optional[str]:
@@ -330,6 +333,9 @@ def dataset(self, value):
330333
if isinstance(value, str):
331334
value = DatasetReference.from_string(value).to_api_repr()
332335

336+
if isinstance(value, DatasetReference):
337+
value = value.to_api_repr()
338+
333339
if isinstance(value, (Dataset, DatasetListItem)):
334340
value = value.reference.to_api_repr()
335341

@@ -437,15 +443,65 @@ def special_group(self) -> Optional[str]:
437443
def special_group(self, value):
438444
self._properties["specialGroup"] = value
439445

446+
@property
447+
def condition(self) -> Optional["Condition"]:
448+
"""Optional[Condition]: The IAM condition associated with this entry."""
449+
value = typing.cast(Dict[str, Any], self._properties.get("condition"))
450+
return Condition.from_api_repr(value) if value else None
451+
452+
@condition.setter
453+
def condition(self, value: Union["Condition", dict, None]):
454+
"""Set the IAM condition for this entry."""
455+
if value is None:
456+
self._properties["condition"] = None
457+
elif isinstance(value, Condition):
458+
self._properties["condition"] = value.to_api_repr()
459+
elif isinstance(value, dict):
460+
self._properties["condition"] = value
461+
else:
462+
raise TypeError("condition must be a Condition object, dict, or None")
463+
440464
@property
441465
def entity_type(self) -> Optional[str]:
442466
"""The entity_type of the entry."""
467+
468+
# The api_repr for an AccessEntry object is expected to be a dict with
469+
# only a few keys. Two keys that may be present are role and condition.
470+
# Any additional key is going to have one of ~eight different names:
471+
# userByEmail, groupByEmail, domain, dataset, specialGroup, view,
472+
# routine, iamMember
473+
474+
# if self._entity_type is None, see if it needs setting
475+
# i.e. is there a key: value pair that should be associated with
476+
# entity_type and entity_id?
477+
if self._entity_type is None:
478+
resource = self._properties.copy()
479+
# we are empyting the dict to get to the last `key: value`` pair
480+
# so we don't keep these first entries
481+
_ = resource.pop("role", None)
482+
_ = resource.pop("condition", None)
483+
484+
try:
485+
# we only need entity_type, because entity_id gets set elsewhere.
486+
entity_type, _ = resource.popitem()
487+
except KeyError:
488+
entity_type = None
489+
490+
self._entity_type = entity_type
491+
443492
return self._entity_type
444493

445494
@property
446495
def entity_id(self) -> Optional[Union[Dict[str, Any], str]]:
447496
"""The entity_id of the entry."""
448-
return self._properties.get(self._entity_type) if self._entity_type else None
497+
if self.entity_type:
498+
entity_type = self.entity_type
499+
else:
500+
return None
501+
return typing.cast(
502+
Optional[Union[Dict[str, Any], str]],
503+
self._properties.get(entity_type, None),
504+
)
449505

450506
def __eq__(self, other):
451507
if not isinstance(other, AccessEntry):
@@ -464,7 +520,16 @@ def _key(self):
464520
Returns:
465521
Tuple: The contents of this :class:`~google.cloud.bigquery.dataset.AccessEntry`.
466522
"""
523+
467524
properties = self._properties.copy()
525+
526+
# Dicts are not hashable.
527+
# Convert condition to a hashable datatype(s)
528+
condition = properties.get("condition")
529+
if isinstance(condition, dict):
530+
condition_key = tuple(sorted(condition.items()))
531+
properties["condition"] = condition_key
532+
468533
prop_tup = tuple(sorted(properties.items()))
469534
return (self.role, self._entity_type, self.entity_id, prop_tup)
470535

@@ -491,19 +556,11 @@ def from_api_repr(cls, resource: dict) -> "AccessEntry":
491556
Returns:
492557
google.cloud.bigquery.dataset.AccessEntry:
493558
Access entry parsed from ``resource``.
494-
495-
Raises:
496-
ValueError:
497-
If the resource has more keys than ``role`` and one additional
498-
key.
499559
"""
500-
entry = resource.copy()
501-
role = entry.pop("role", None)
502-
entity_type, entity_id = entry.popitem()
503-
if len(entry) != 0:
504-
raise ValueError("Entry has unexpected keys remaining.", entry)
505560

506-
return cls(role, entity_type, entity_id)
561+
access_entry = cls()
562+
access_entry._properties = resource.copy()
563+
return access_entry
507564

508565

509566
class Dataset(object):
@@ -1160,6 +1217,43 @@ def from_api_repr(cls, resource: Dict[str, Any]) -> "Condition":
11601217

11611218
return cls(
11621219
expression=resource["expression"],
1163-
title=resource.get("title"),
1164-
description=resource.get("description"),
1220+
title=resource.get("title", None),
1221+
description=resource.get("description", None),
11651222
)
1223+
1224+
def __eq__(self, other: object) -> bool:
1225+
"""Check for equality based on expression, title, and description."""
1226+
if not isinstance(other, Condition):
1227+
return NotImplemented
1228+
return self._key() == other._key()
1229+
1230+
def _key(self):
1231+
"""A tuple key that uniquely describes this field.
1232+
Used to compute this instance's hashcode and evaluate equality.
1233+
Returns:
1234+
Tuple: The contents of this :class:`~google.cloud.bigquery.dataset.AccessEntry`.
1235+
"""
1236+
1237+
properties = self._properties.copy()
1238+
1239+
# Dicts are not hashable.
1240+
# Convert object to a hashable datatype(s)
1241+
prop_tup = tuple(sorted(properties.items()))
1242+
return prop_tup
1243+
1244+
def __ne__(self, other: object) -> bool:
1245+
"""Check for inequality."""
1246+
return not self == other
1247+
1248+
def __hash__(self) -> int:
1249+
"""Generate a hash based on expression, title, and description."""
1250+
return hash(self._key())
1251+
1252+
def __repr__(self) -> str:
1253+
"""Return a string representation of the Condition object."""
1254+
parts = [f"expression={self.expression!r}"]
1255+
if self.title is not None:
1256+
parts.append(f"title={self.title!r}")
1257+
if self.description is not None:
1258+
parts.append(f"description={self.description!r}")
1259+
return f"Condition({', '.join(parts)})"

google/cloud/bigquery/enums.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,3 +387,19 @@ def _generate_next_value_(name, start, count, last_values):
387387
ROUNDING_MODE_UNSPECIFIED = enum.auto()
388388
ROUND_HALF_AWAY_FROM_ZERO = enum.auto()
389389
ROUND_HALF_EVEN = enum.auto()
390+
391+
392+
class BigLakeFileFormat(object):
393+
FILE_FORMAT_UNSPECIFIED = "FILE_FORMAT_UNSPECIFIED"
394+
"""The default unspecified value."""
395+
396+
PARQUET = "PARQUET"
397+
"""Apache Parquet format."""
398+
399+
400+
class BigLakeTableFormat(object):
401+
TABLE_FORMAT_UNSPECIFIED = "TABLE_FORMAT_UNSPECIFIED"
402+
"""The default unspecified value."""
403+
404+
ICEBERG = "ICEBERG"
405+
"""Apache Iceberg format."""

0 commit comments

Comments
 (0)