Skip to content

Commit 923e59f

Browse files
feat: Add BigQuery ObjectRef functions to bigframes.bigquery.obj
This change introduces support for BigQuery ObjectRef functions: - `OBJ.FETCH_METADATA` - `OBJ.GET_ACCESS_URL` - `OBJ.MAKE_REF` These are exposed via a new `bigframes.bigquery.obj` module. Changes: - Added `ObjMakeRefJson` operation in `bigframes/operations/blob_ops.py`. - Updated `ObjGetAccessUrl` operation to support optional duration. - Updated `bigframes/operations/__init__.py` to export new operations. - Updated `bigframes/core/compile/ibis_compiler/scalar_op_registry.py` and `bigframes/core/compile/sqlglot/expressions/blob_ops.py` to support new operations. - Created `bigframes/bigquery/_operations/obj.py` with the implementation of `fetch_metadata`, `get_access_url`, and `make_ref`. - Created `bigframes/bigquery/obj.py` to expose the functions. - Exposed `obj` module in `bigframes/bigquery/__init__.py`. - Added unit tests in `tests/unit/bigquery/test_obj.py`.
1 parent 775740b commit 923e59f

File tree

6 files changed

+44
-46
lines changed

6 files changed

+44
-46
lines changed

bigframes/bigquery/_operations/obj.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@
2121

2222
from __future__ import annotations
2323

24+
import datetime
2425
from typing import Optional, Union
2526

27+
import numpy as np
28+
import pandas as pd
29+
2630
import bigframes.core.utils as utils
2731
import bigframes.operations as ops
2832
import bigframes.series as series
@@ -48,7 +52,9 @@ def fetch_metadata(
4852
def get_access_url(
4953
objectref: series.Series,
5054
mode: str,
51-
duration: Optional[series.Series] = None,
55+
duration: Optional[
56+
Union[datetime.timedelta, pd.Timedelta, np.timedelta64]
57+
] = None,
5258
) -> series.Series:
5359
"""The OBJ.GET_ACCESS_URL function returns JSON that contains reference information for the input ObjectRef value, and also access URLs that you can use to read or modify the Cloud Storage object.
5460
@@ -59,17 +65,19 @@ def get_access_url(
5965
A STRING value that identifies the type of URL that you want to be returned. The following values are supported:
6066
'r': Returns a URL that lets you read the object.
6167
'rw': Returns two URLs, one that lets you read the object, and one that lets you modify the object.
62-
duration (bigframes.series.Series, optional):
68+
duration (Union[datetime.timedelta, pandas.Timedelta, numpy.timedelta64], optional):
6369
An optional INTERVAL value that specifies how long the generated access URLs remain valid. You can specify a value between 30 minutes and 6 hours. For example, you could specify INTERVAL 2 HOUR to generate URLs that expire after 2 hours. The default value is 6 hours.
6470
6571
Returns:
6672
bigframes.series.Series: A JSON value that contains the Cloud Storage object reference information from the input ObjectRef value, and also one or more URLs that you can use to access the Cloud Storage object.
6773
"""
74+
duration_micros = None
6875
if duration is not None:
69-
return objectref._apply_binary_op(
70-
duration, ops.ObjGetAccessUrlWithDuration(mode=mode)
71-
)
72-
return objectref._apply_unary_op(ops.ObjGetAccessUrl(mode=mode))
76+
duration_micros = utils.timedelta_to_micros(duration)
77+
78+
return objectref._apply_unary_op(
79+
ops.ObjGetAccessUrl(mode=mode, duration=duration_micros)
80+
)
7381

7482

7583
@utils.preview(name="The ObjectRef API `make_ref`")

bigframes/core/compile/ibis_compiler/scalar_op_registry.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,18 +1247,14 @@ def obj_fetch_metadata_op_impl(obj_ref: ibis_types.Value):
12471247

12481248
@scalar_op_compiler.register_unary_op(ops.ObjGetAccessUrl, pass_op=True)
12491249
def obj_get_access_url_op_impl(obj_ref: ibis_types.Value, op: ops.ObjGetAccessUrl):
1250+
if op.duration is not None:
1251+
duration_value = ibis_types.literal(op.duration).to_interval("us")
1252+
return obj_get_access_url_with_duration(
1253+
obj_ref=obj_ref, mode=op.mode, duration=duration_value
1254+
)
12501255
return obj_get_access_url(obj_ref=obj_ref, mode=op.mode)
12511256

12521257

1253-
@scalar_op_compiler.register_binary_op(ops.ObjGetAccessUrlWithDuration, pass_op=True)
1254-
def obj_get_access_url_with_duration_op_impl(
1255-
obj_ref: ibis_types.Value, duration: ibis_types.Value, op: ops.ObjGetAccessUrlWithDuration
1256-
):
1257-
return obj_get_access_url_with_duration(
1258-
obj_ref=obj_ref, mode=op.mode, duration=duration
1259-
)
1260-
1261-
12621258
### Binary Ops
12631259
def short_circuit_nulls(type_override: typing.Optional[ibis_dtypes.DataType] = None):
12641260
"""Wraps a binary operator to generate nulls of the expected type if either input is a null scalar."""

bigframes/core/compile/sqlglot/expressions/blob_ops.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,15 @@ def _(expr: TypedExpr) -> sge.Expression:
3131

3232
@register_unary_op(ops.ObjGetAccessUrl, pass_op=True)
3333
def _(expr: TypedExpr, op: ops.ObjGetAccessUrl) -> sge.Expression:
34-
return sge.func("OBJ.GET_ACCESS_URL", expr.expr, sge.Literal.string(op.mode))
35-
36-
37-
@register_binary_op(ops.ObjGetAccessUrlWithDuration, pass_op=True)
38-
def _(
39-
left: TypedExpr, right: TypedExpr, op: ops.ObjGetAccessUrlWithDuration
40-
) -> sge.Expression:
41-
return sge.func(
42-
"OBJ.GET_ACCESS_URL", left.expr, sge.Literal.string(op.mode), right.expr
43-
)
34+
args = [expr.expr, sge.Literal.string(op.mode)]
35+
if op.duration is not None:
36+
args.append(
37+
sge.Interval(
38+
this=sge.Literal.number(op.duration),
39+
unit=sge.Var(this="MICROSECOND"),
40+
)
41+
)
42+
return sge.func("OBJ.GET_ACCESS_URL", *args)
4443

4544

4645
@register_binary_op(ops.obj_make_ref_op)

bigframes/operations/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
obj_make_ref_json_op,
4444
obj_make_ref_op,
4545
ObjGetAccessUrl,
46-
ObjGetAccessUrlWithDuration,
4746
)
4847
from bigframes.operations.bool_ops import and_op, or_op, xor_op
4948
from bigframes.operations.comparison_ops import (
@@ -367,7 +366,6 @@
367366
"ArrayToStringOp",
368367
# Blob ops
369368
"ObjGetAccessUrl",
370-
"ObjGetAccessUrlWithDuration",
371369
"obj_make_ref_json_op",
372370
"obj_make_ref_op",
373371
"obj_fetch_metadata_op",

bigframes/operations/blob_ops.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,7 @@
2929
class ObjGetAccessUrl(base_ops.UnaryOp):
3030
name: typing.ClassVar[str] = "obj_get_access_url"
3131
mode: str # access mode, e.g. R read, W write, RW read & write
32-
33-
def output_type(self, *input_types):
34-
return dtypes.JSON_DTYPE
35-
36-
37-
@dataclasses.dataclass(frozen=True)
38-
class ObjGetAccessUrlWithDuration(base_ops.BinaryOp):
39-
name: typing.ClassVar[str] = "obj_get_access_url_with_duration"
40-
mode: str # access mode, e.g. R read, W write, RW read & write
32+
duration: typing.Optional[int] = None # duration in microseconds
4133

4234
def output_type(self, *input_types):
4335
return dtypes.JSON_DTYPE

tests/unit/bigquery/test_obj.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import datetime
1516
import pytest
16-
from unittest.mock import MagicMock
17+
from unittest.mock import MagicMock, patch
1718

1819
import bigframes.bigquery.obj as obj
1920
import bigframes.operations as ops
@@ -27,11 +28,13 @@ def test_get_access_url_op_structure():
2728
op = ops.ObjGetAccessUrl(mode="r")
2829
assert op.name == "obj_get_access_url"
2930
assert op.mode == "r"
31+
assert op.duration is None
3032

3133
def test_get_access_url_with_duration_op_structure():
32-
op = ops.ObjGetAccessUrlWithDuration(mode="rw")
33-
assert op.name == "obj_get_access_url_with_duration"
34+
op = ops.ObjGetAccessUrl(mode="rw", duration=3600000000)
35+
assert op.name == "obj_get_access_url"
3436
assert op.mode == "rw"
37+
assert op.duration == 3600000000
3538

3639
def test_make_ref_op_structure():
3740
op = ops.obj_make_ref_op
@@ -59,18 +62,20 @@ def test_get_access_url_calls_apply_unary_op_without_duration():
5962
args, _ = s._apply_unary_op.call_args
6063
assert isinstance(args[0], ops.ObjGetAccessUrl)
6164
assert args[0].mode == "r"
65+
assert args[0].duration is None
6266

63-
def test_get_access_url_calls_apply_binary_op_with_duration():
67+
def test_get_access_url_calls_apply_unary_op_with_duration():
6468
s = MagicMock(spec=series.Series)
65-
duration = MagicMock(spec=series.Series)
69+
duration = datetime.timedelta(hours=1)
6670

6771
obj.get_access_url(s, mode="rw", duration=duration)
6872

69-
s._apply_binary_op.assert_called_once()
70-
args, kwargs = s._apply_binary_op.call_args
71-
assert args[0] == duration
72-
assert isinstance(args[1], ops.ObjGetAccessUrlWithDuration)
73-
assert args[1].mode == "rw"
73+
s._apply_unary_op.assert_called_once()
74+
args, kwargs = s._apply_unary_op.call_args
75+
assert isinstance(args[0], ops.ObjGetAccessUrl)
76+
assert args[0].mode == "rw"
77+
# 1 hour = 3600 seconds = 3600 * 1000 * 1000 microseconds
78+
assert args[0].duration == 3600000000
7479

7580
def test_make_ref_calls_apply_binary_op_with_authorizer():
7681
uri = MagicMock(spec=series.Series)

0 commit comments

Comments
 (0)