diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/mutations.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/mutations.py index 41c067f345b1..e0c9a8ac4706 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/mutations.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/mutations.py @@ -22,7 +22,10 @@ import google.cloud.bigtable_v2.types.bigtable as types_pb import google.cloud.bigtable_v2.types.data as data_pb -from google.cloud.bigtable.data.read_modify_write_rules import _MAX_INCREMENT_VALUE +from google.cloud.bigtable.data.read_modify_write_rules import ( + _MAX_INCREMENT_VALUE, + _MIN_INCREMENT_VALUE, +) # special value for SetCell mutation timestamps. If set, server will assign a timestamp _SERVER_SIDE_TIMESTAMP = -1 @@ -169,12 +172,14 @@ def __init__( qualifier = qualifier.encode() if isinstance(qualifier, str) else qualifier if not isinstance(qualifier, bytes): raise TypeError("qualifier must be bytes or str") + if isinstance(new_value, bool): + raise TypeError("new_value must be bytes, str, or int") if isinstance(new_value, str): new_value = new_value.encode() elif isinstance(new_value, int): - if abs(new_value) > _MAX_INCREMENT_VALUE: + if not (_MIN_INCREMENT_VALUE <= new_value <= _MAX_INCREMENT_VALUE): raise ValueError( - "int values must be between -2**63 and 2**63 (64-bit signed int)" + "int values must be between -2**63 and 2**63 - 1 (64-bit signed int)" ) new_value = new_value.to_bytes(8, "big", signed=True) if not isinstance(new_value, bytes): @@ -223,13 +228,17 @@ class DeleteRangeFromColumn(Mutation): """ family: str - qualifier: bytes + qualifier: bytes | str # None represents 0 start_timestamp_micros: int | None = None # None represents infinity end_timestamp_micros: int | None = None def __post_init__(self): + if isinstance(self.qualifier, str): + self.qualifier = self.qualifier.encode() + if not isinstance(self.qualifier, bytes): + raise TypeError("qualifier must be bytes or str") if ( self.start_timestamp_micros is not None and self.end_timestamp_micros is not None @@ -321,13 +330,13 @@ def __init__( qualifier = qualifier.encode() if isinstance(qualifier, str) else qualifier if not isinstance(qualifier, bytes): raise TypeError("qualifier must be bytes or str") - if not isinstance(value, int): + if isinstance(value, bool) or not isinstance(value, int): raise TypeError("value must be int") - if not isinstance(timestamp_micros, int): + if isinstance(timestamp_micros, bool) or not isinstance(timestamp_micros, int): raise TypeError("timestamp_micros must be int") - if abs(value) > _MAX_INCREMENT_VALUE: + if not (_MIN_INCREMENT_VALUE <= value <= _MAX_INCREMENT_VALUE): raise ValueError( - "int values must be between -2**63 and 2**63 (64-bit signed int)" + "int values must be between -2**63 and 2**63 - 1 (64-bit signed int)" ) if timestamp_micros < 0: diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/read_modify_write_rules.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/read_modify_write_rules.py index e4446f755c00..62e7744bfc0a 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/read_modify_write_rules.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/read_modify_write_rules.py @@ -19,6 +19,7 @@ import google.cloud.bigtable_v2.types.data as data_pb # value must fit in 64-bit signed integer +_MIN_INCREMENT_VALUE = -(1 << 63) _MAX_INCREMENT_VALUE = (1 << 63) - 1 @@ -61,11 +62,11 @@ class IncrementRule(ReadModifyWriteRule): """ def __init__(self, family: str, qualifier: bytes | str, increment_amount: int = 1): - if not isinstance(increment_amount, int): + if isinstance(increment_amount, bool) or not isinstance(increment_amount, int): raise TypeError("increment_amount must be an integer") - if abs(increment_amount) > _MAX_INCREMENT_VALUE: + if not (_MIN_INCREMENT_VALUE <= increment_amount <= _MAX_INCREMENT_VALUE): raise ValueError( - "increment_amount must be between -2**63 and 2**63 (64-bit signed int)" + "increment_amount must be between -2**63 and 2**63 - 1 (64-bit signed int)" ) super().__init__(family, qualifier) self.increment_amount = increment_amount diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py b/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py index 9a6511c40818..d42e58363125 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/data/row_filters.py @@ -144,7 +144,7 @@ def __init__(self, regex: str | bytes): self.regex: bytes = _to_bytes(regex) def __eq__(self, other): - if not isinstance(other, self.__class__): + if type(other) is not type(self): return NotImplemented return other.regex == self.regex @@ -302,21 +302,37 @@ def _to_dict(self) -> dict[str, int]: return timestamp_range_kwargs def __repr__(self) -> str: - return f"{self.__class__.__name__}(start={self.start}, end={self.end})" + return f"{self.__class__.__name__}(start={self.start!r}, end={self.end!r})" class TimestampRangeFilter(RowFilter): """Row filter that limits cells to a range of time. - :type range_: :class:`TimestampRange` - :param range_: Range of time that cells should match against. + :type start: :class:`datetime.datetime` or :class:`TimestampRange` + :param start: (Optional) The (inclusive) lower bound of the timestamp + range, or a :class:`TimestampRange` instance. + + :type end: :class:`datetime.datetime` + :param end: (Optional) The (exclusive) upper bound of the timestamp + range. """ - def __init__(self, start: "datetime" | None = None, end: "datetime" | None = None): - self.range_: TimestampRange = TimestampRange(start, end) + def __init__( + self, + start: "datetime" | TimestampRange | None = None, + end: "datetime" | None = None, + ): + if isinstance(start, TimestampRange): + if end is not None: + raise ValueError( + "Cannot specify both a TimestampRange and an end datetime." + ) + self.range_: TimestampRange = start + else: + self.range_ = TimestampRange(start, end) def __eq__(self, other): - if not isinstance(other, self.__class__): + if not isinstance(other, TimestampRangeFilter): return NotImplemented return other.range_ == self.range_ @@ -408,7 +424,7 @@ def __init__( self.inclusive_end = inclusive_end def __eq__(self, other): - if not isinstance(other, self.__class__): + if not isinstance(other, ColumnRangeFilter): return NotImplemented return ( other.family_id == self.family_id @@ -946,7 +962,7 @@ def __init__( self.false_filter = false_filter def __eq__(self, other): - if not isinstance(other, self.__class__): + if not isinstance(other, ConditionalRowFilter): return NotImplemented return ( other.predicate_filter == self.predicate_filter diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/row.py b/packages/google-cloud-bigtable/google/cloud/bigtable/row.py index 1925c73e2ee5..c04d47a83b87 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/row.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/row.py @@ -220,6 +220,10 @@ def _delete_cells(self, column_family_id, columns, time_range=None, state=None): mutations.DeleteAllFromFamily(family_to_delete=column_family_id) ) else: + if isinstance(columns, (str, bytes)): + raise TypeError( + "columns must be an iterable of column names, not str or bytes" + ) timestamps = time_range._to_dict() if time_range else {} start_timestamp_micros = timestamps.get("start_timestamp_micros") end_timestamp_micros = timestamps.get("end_timestamp_micros") diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/row_filters.py b/packages/google-cloud-bigtable/google/cloud/bigtable/row_filters.py index 433ac7c04ef5..7f72c117f8b7 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/row_filters.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/row_filters.py @@ -89,6 +89,9 @@ class TimestampRangeFilter(BaseTimestampRangeFilter): def __init__(self, range_): self.range_ = range_ + def __repr__(self) -> str: + return f"{self.__class__.__name__}(range_={self.range_!r})" + class ExactValueFilter(ValueRegexFilter): """Row filter for an exact value. diff --git a/packages/google-cloud-bigtable/google/cloud/bigtable/row_set.py b/packages/google-cloud-bigtable/google/cloud/bigtable/row_set.py index 6e041e3ed7b8..2aab495edb70 100644 --- a/packages/google-cloud-bigtable/google/cloud/bigtable/row_set.py +++ b/packages/google-cloud-bigtable/google/cloud/bigtable/row_set.py @@ -48,7 +48,10 @@ def row_keys(self): @property def row_ranges(self): - return self._read_rows_query.row_ranges + return [ + r if isinstance(r, RowRange) else RowRange._from_pb(r._to_pb()) + for r in self._read_rows_query.row_ranges + ] def add_row_key(self, row_key): """Add row key to row_keys list. @@ -157,6 +160,31 @@ class RowRange(_MappableAttributesMixin, BaseRowRange): "end_inclusive": "end_is_inclusive", } + def __init__(self, *args, **kwargs): + start_inc = kwargs.get( + "start_inclusive", + kwargs.get("start_is_inclusive", args[2] if len(args) > 2 else None), + ) + end_inc = kwargs.get( + "end_inclusive", + kwargs.get("end_is_inclusive", args[3] if len(args) > 3 else None), + ) + super().__init__(*args, **kwargs) + self._start_inclusive = True if start_inc is None else bool(start_inc) + self._end_inclusive = False if end_inc is None else bool(end_inc) + + @property + def start_inclusive(self) -> bool: + if self.start_key is not None: + return self.start_is_inclusive + return getattr(self, "_start_inclusive", True) + + @property + def end_inclusive(self) -> bool: + if self.end_key is not None: + return self.end_is_inclusive + return getattr(self, "_end_inclusive", False) + def get_range_kwargs(self): """Convert row range object to dict which can be passed to google.bigtable.v2.RowRange add method. diff --git a/packages/google-cloud-bigtable/tests/unit/data/test_mutations.py b/packages/google-cloud-bigtable/tests/unit/data/test_mutations.py index 54baaf780674..8addfdc2c2d3 100644 --- a/packages/google-cloud-bigtable/tests/unit/data/test_mutations.py +++ b/packages/google-cloud-bigtable/tests/unit/data/test_mutations.py @@ -188,12 +188,18 @@ def _target_class(self): def _make_one(self, *args, **kwargs): return self._target_class()(*args, **kwargs) - @pytest.mark.parametrize("input_val", [2**64, -(2**64)]) + @pytest.mark.parametrize("input_val", [2**63, -(2**63) - 1, 2**64, -(2**64)]) def test_ctor_large_int(self, input_val): with pytest.raises(ValueError) as e: self._make_one(family="f", qualifier=b"b", new_value=input_val) assert "int values must be between" in str(e.value) + @pytest.mark.parametrize("input_val", [True, False]) + def test_ctor_bool_value(self, input_val): + with pytest.raises(TypeError) as e: + self._make_one(family="f", qualifier=b"b", new_value=input_val) + assert "new_value must be bytes, str, or int" in str(e.value) + @pytest.mark.parametrize("input_val", ["", "a", "abc", "hello world!"]) def test_ctor_str_value(self, input_val): found = self._make_one(family="f", qualifier=b"b", new_value=input_val) @@ -223,7 +229,9 @@ def test_ctor_str_inputs(self): assert instance.qualifier == expected_qualifier assert instance.new_value == expected_value - @pytest.mark.parametrize("input_val", [-20, -1, 0, 1, 100, int(2**60)]) + @pytest.mark.parametrize( + "input_val", [-(2**63), -20, -1, 0, 1, 100, int(2**60), 2**63 - 1] + ) def test_ctor_int_value(self, input_val): found = self._make_one(family="f", qualifier=b"b", new_value=input_val) assert found.new_value == input_val.to_bytes(8, "big", signed=True) @@ -231,6 +239,7 @@ def test_ctor_int_value(self, input_val): @pytest.mark.parametrize( "int_value,expected_bytes", [ + (-(2**63), b"\x80\x00\x00\x00\x00\x00\x00\x00"), (-42, b"\xff\xff\xff\xff\xff\xff\xff\xd6"), (-2, b"\xff\xff\xff\xff\xff\xff\xff\xfe"), (-1, b"\xff\xff\xff\xff\xff\xff\xff\xff"), @@ -238,6 +247,7 @@ def test_ctor_int_value(self, input_val): (1, b"\x00\x00\x00\x00\x00\x00\x00\x01"), (2, b"\x00\x00\x00\x00\x00\x00\x00\x02"), (100, b"\x00\x00\x00\x00\x00\x00\x00d"), + (2**63 - 1, b"\x7f\xff\xff\xff\xff\xff\xff\xff"), ], ) def test_ctor_int_value_bytes(self, int_value, expected_bytes): @@ -409,6 +419,18 @@ def test_ctor_no_timestamps(self): assert instance.start_timestamp_micros is None assert instance.end_timestamp_micros is None + @pytest.mark.parametrize("qualifier_str", ["", "a", "col", "my_column", "qual_str"]) + def test_ctor_str_qualifier(self, qualifier_str): + instance = self._make_one("test-family", qualifier_str) + expected_bytes = qualifier_str.encode("utf-8") + assert instance.qualifier == expected_bytes + assert instance._to_pb().delete_from_column.column_qualifier == expected_bytes + + @pytest.mark.parametrize("bad_qualifier", [123, None, object()]) + def test_ctor_invalid_qualifier(self, bad_qualifier): + with pytest.raises(TypeError, match="qualifier must be bytes or str"): + self._make_one("test-family", bad_qualifier) + def test_ctor_timestamps_out_of_order(self): expected_family = "test-family" expected_qualifier = b"test-qualifier" @@ -725,7 +747,7 @@ def _target_class(self): def _make_one(self, *args, **kwargs): return self._target_class()(*args, **kwargs) - @pytest.mark.parametrize("input_val", [2**64, -(2**64)]) + @pytest.mark.parametrize("input_val", [2**63, -(2**63) - 1, 2**64, -(2**64)]) def test_ctor_large_int(self, input_val): with pytest.raises(ValueError) as e: self._make_one( @@ -733,6 +755,30 @@ def test_ctor_large_int(self, input_val): ) assert "int values must be between" in str(e.value) + @pytest.mark.parametrize("input_val", [-(2**63), 2**63 - 1]) + def test_ctor_int64_bounds(self, input_val): + instance = self._make_one( + family="f", qualifier=b"b", value=input_val, timestamp_micros=123 + ) + assert instance.value == input_val + assert instance._to_pb().add_to_cell.input.int_value == input_val + + @pytest.mark.parametrize("input_val", [True, False]) + def test_ctor_bool_value(self, input_val): + with pytest.raises(TypeError) as e: + self._make_one( + family="f", qualifier=b"b", value=input_val, timestamp_micros=123 + ) + assert "value must be int" in str(e.value) + + @pytest.mark.parametrize("input_ts", [True, False]) + def test_ctor_bool_timestamp(self, input_ts): + with pytest.raises(TypeError) as e: + self._make_one( + family="f", qualifier=b"b", value=123, timestamp_micros=input_ts + ) + assert "timestamp_micros must be int" in str(e.value) + @pytest.mark.parametrize("input_val", ["", "a", "abc", "hello world!"]) def test_ctor_str_value(self, input_val): with pytest.raises(TypeError) as e: diff --git a/packages/google-cloud-bigtable/tests/unit/data/test_read_modify_write_rules.py b/packages/google-cloud-bigtable/tests/unit/data/test_read_modify_write_rules.py index 8c029c743b36..16aa78c281ca 100644 --- a/packages/google-cloud-bigtable/tests/unit/data/test_read_modify_write_rules.py +++ b/packages/google-cloud-bigtable/tests/unit/data/test_read_modify_write_rules.py @@ -50,6 +50,8 @@ def _target_class(self): [ (("fam", b"qual", 1), ("fam", b"qual", 1)), (("fam", b"qual", -12), ("fam", b"qual", -12)), + (("fam", b"qual", -(2**63)), ("fam", b"qual", -(2**63))), + (("fam", b"qual", 2**63 - 1), ("fam", b"qual", 2**63 - 1)), (("fam", "qual", 1), ("fam", b"qual", 1)), (("fam", "qual", 0), ("fam", b"qual", 0)), (("", "", 0), ("", b"", 0)), @@ -62,17 +64,21 @@ def test_ctor(self, args, expected): assert instance.qualifier == expected[1] assert instance.increment_amount == expected[2] - @pytest.mark.parametrize("input_amount", [1.1, None, "1", object(), "", b"", b"1"]) + @pytest.mark.parametrize( + "input_amount", [True, False, 1.1, None, "1", object(), "", b"", b"1"] + ) def test_ctor_bad_input(self, input_amount): with pytest.raises(TypeError) as e: self._target_class()("fam", b"qual", input_amount) assert "increment_amount must be an integer" in str(e.value) - @pytest.mark.parametrize("large_value", [2**64, 2**64 + 1, -(2**64), -(2**64) - 1]) + @pytest.mark.parametrize( + "large_value", [2**63, -(2**63) - 1, 2**64, 2**64 + 1, -(2**64), -(2**64) - 1] + ) def test_ctor_large_values(self, large_value): with pytest.raises(ValueError) as e: self._target_class()("fam", b"qual", large_value) - assert "too large" in str(e.value) + assert "increment_amount must be between -2**63 and 2**63" in str(e.value) @pytest.mark.parametrize( "args,expected", diff --git a/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py b/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py index 6c7bd84bed80..c15f43e157b0 100644 --- a/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py +++ b/packages/google-cloud-bigtable/tests/unit/data/test_row_filters.py @@ -831,6 +831,18 @@ def test_value_regex_filter___repr__(): assert eval(repr(row_filter)) == row_filter +def test_value_regex_filter_not_equal_to_literal_value_filter(): + from google.cloud.bigtable.data.row_filters import ( + LiteralValueFilter, + ValueRegexFilter, + ) + + vrf = ValueRegexFilter(b"abc") + lvf = LiteralValueFilter(b"abc") + assert vrf != lvf + assert lvf != vrf + + def test_literal_value_filter_to_pb_w_bytes(): from google.cloud.bigtable.data.row_filters import LiteralValueFilter diff --git a/packages/google-cloud-bigtable/tests/unit/v2_client/test_row.py b/packages/google-cloud-bigtable/tests/unit/v2_client/test_row.py index b0100db89f07..f53468f2d50f 100644 --- a/packages/google-cloud-bigtable/tests/unit/v2_client/test_row.py +++ b/packages/google-cloud-bigtable/tests/unit/v2_client/test_row.py @@ -181,6 +181,17 @@ def test_direct_row_set_cell_with_integer_value(): _set_cell_helper(value=value) +def test_direct_row_set_cell_with_min_int64_value(): + _set_cell_helper(value=-(2**63)) + + +@pytest.mark.parametrize("bool_val", [True, False]) +def test_direct_row_set_cell_with_bool_value(bool_val): + row = _make_direct_row(b"row_key", object()) + with pytest.raises(TypeError): + row.set_cell("column_family_id", b"column", bool_val) + + def test_direct_row_set_cell_with_non_bytes_value(): row_key = b"row_key" column = b"column" @@ -260,6 +271,22 @@ def test_direct_row_delete_cells_non_iterable(): row.delete_cells(column_family_id, columns) +@pytest.mark.parametrize("columns", ["col_abc", b"col_abc"]) +def test_direct_row_delete_cells_rejects_str_and_bytes(columns): + row = _make_direct_row(b"row_key", object()) + with pytest.raises(TypeError, match="columns must be an iterable of column names"): + row.delete_cells("column_family_id", columns) + assert row._mutations == [] + + +@pytest.mark.parametrize("columns", ["col_abc", b"col_abc"]) +def test_conditional_row_delete_cells_rejects_str_and_bytes(columns): + row = _make_conditional_row(b"row_key", object(), filter_=object()) + with pytest.raises(TypeError, match="columns must be an iterable of column names"): + row.delete_cells("column_family_id", columns, state=True) + assert row._true_pb_mutations == [] + + def test_direct_row_delete_cells_all_columns(): from google.cloud.bigtable.data.mutations import DeleteAllFromFamily from google.cloud.bigtable.row import DirectRow diff --git a/packages/google-cloud-bigtable/tests/unit/v2_client/test_row_filters.py b/packages/google-cloud-bigtable/tests/unit/v2_client/test_row_filters.py index 6f6de3c00899..649983c7f266 100644 --- a/packages/google-cloud-bigtable/tests/unit/v2_client/test_row_filters.py +++ b/packages/google-cloud-bigtable/tests/unit/v2_client/test_row_filters.py @@ -1243,3 +1243,66 @@ def test_timestamp_range_to_pb_backwards_compatibility(): assert hasattr(tr, "to_pb") with pytest.deprecated_call(): assert tr.to_pb() == tr._to_pb() + + +def test_timestamp_range_filter_repr_and_cross_layer_eq(): + from datetime import datetime, timezone + + from google.cloud.bigtable import row_filters as classic_rf + from google.cloud.bigtable.data import row_filters as data_rf + + t1 = datetime(2023, 1, 1, tzinfo=timezone.utc) + t2 = datetime(2023, 1, 2, tzinfo=timezone.utc) + tr = classic_rf.TimestampRange(start=t1, end=t2) + classic_trf = classic_rf.TimestampRangeFilter(tr) + + assert repr(classic_trf) == f"TimestampRangeFilter(range_={tr!r})" + assert ( + eval( + repr(classic_trf), + { + "TimestampRangeFilter": classic_rf.TimestampRangeFilter, + "TimestampRange": classic_rf.TimestampRange, + "datetime": __import__("datetime"), + }, + ) + == classic_trf + ) + + empty_trf = classic_rf.TimestampRangeFilter(None) + assert repr(empty_trf) == "TimestampRangeFilter(range_=None)" + + data_trf = data_rf.TimestampRangeFilter(start=t1, end=t2) + data_trf_from_range = data_rf.TimestampRangeFilter(tr) + assert data_trf_from_range._to_pb() == classic_trf._to_pb() + assert classic_trf == data_trf + assert data_trf == classic_trf + assert classic_trf.__eq__(data_trf) is True + assert data_trf.__eq__(classic_trf) is True + + classic_crf = classic_rf.ColumnRangeFilter( + "cf1", start_column=b"c1", end_column=b"c2" + ) + data_crf = data_rf.ColumnRangeFilter( + "cf1", start_qualifier=b"c1", end_qualifier=b"c2" + ) + assert classic_crf == data_crf + assert data_crf == classic_crf + assert classic_crf.__eq__(data_crf) is True + assert data_crf.__eq__(classic_crf) is True + + classic_cond = classic_rf.ConditionalRowFilter( + classic_rf.PassAllFilter(True), + true_filter=classic_rf.StripValueTransformerFilter(True), + ) + data_cond = data_rf.ConditionalRowFilter( + data_rf.PassAllFilter(True), + true_filter=data_rf.StripValueTransformerFilter(True), + ) + assert classic_cond == data_cond + assert data_cond == classic_cond + assert classic_cond.__eq__(data_cond) is True + assert data_cond.__eq__(classic_cond) is True + + assert classic_rf.ValueRegexFilter(b"abc") != classic_rf.ExactValueFilter(b"abc") + assert classic_rf.ExactValueFilter(b"abc") != classic_rf.ValueRegexFilter(b"abc") diff --git a/packages/google-cloud-bigtable/tests/unit/v2_client/test_row_set.py b/packages/google-cloud-bigtable/tests/unit/v2_client/test_row_set.py index e76d1f15135c..53ba19dd332e 100644 --- a/packages/google-cloud-bigtable/tests/unit/v2_client/test_row_set.py +++ b/packages/google-cloud-bigtable/tests/unit/v2_client/test_row_set.py @@ -156,11 +156,34 @@ def test_row_set_add_row_range_from_keys(): def test_row_set_add_row_range_with_prefix(): - from google.cloud.bigtable.row_set import RowSet + from google.cloud.bigtable.row_set import RowRange, RowSet row_set = RowSet() row_set.add_row_range_with_prefix("row") + assert isinstance(row_set.row_ranges[0], RowRange) assert row_set.row_ranges[0].end_key == b"rox" + assert row_set.row_ranges[0].start_inclusive is True + assert row_set.row_ranges[0].end_inclusive is False + assert row_set.row_ranges[0].get_range_kwargs() == { + "start_key_closed": b"row", + "end_key_open": b"rox", + } + + +def test_row_range_unbounded_end_inclusive_default(): + from google.cloud.bigtable.row_set import RowRange, RowSet + + rr = RowRange(start_key=b"a", end_key=None) + assert rr.start_inclusive is True + assert rr.end_inclusive is False + assert rr.get_range_kwargs() == {"start_key_closed": b"a"} + + rs = RowSet() + rs.add_row_range_from_keys(start_key=b"a", end_key=b"z") + returned = rs.row_ranges[0] + assert isinstance(returned, RowRange) + assert returned == RowRange(start_key=b"a", end_key=b"z") + assert hash(returned) == hash(RowRange(start_key=b"a", end_key=b"z")) def test_row_range_constructor():