diff --git a/google/cloud/bigtable/row_filters.py b/google/cloud/bigtable/row_filters.py index e8a70a9f4..973ba9565 100644 --- a/google/cloud/bigtable/row_filters.py +++ b/google/cloud/bigtable/row_filters.py @@ -14,11 +14,15 @@ """Filters for Google Cloud Bigtable Row classes.""" +import struct + from google.cloud._helpers import _microseconds_from_datetime from google.cloud._helpers import _to_bytes from google.cloud.bigtable_v2.proto import data_pb2 as data_v2_pb2 +_PACK_I64 = struct.Struct(">q").pack + class RowFilter(object): """Basic filter to apply to cells in a row. @@ -115,7 +119,9 @@ class _RegexFilter(RowFilter): .. _RE2 reference: https://github.com/google/re2/wiki/Syntax :type regex: bytes or str - :param regex: A regular expression (RE2) for some row filter. + :param regex: + A regular expression (RE2) for some row filter. String values + will be encoded as ASCII. """ def __init__(self, regex): @@ -439,9 +445,9 @@ class ValueRegexFilter(_RegexFilter): character will not match the new line character ``\\n``, which may be present in a binary value. - :type regex: bytes + :type regex: bytes or str :param regex: A regular expression (RE2) to match cells with values that - match this regex. + match this regex. String values will be encoded as ASCII. """ def to_pb(self): @@ -453,6 +459,22 @@ def to_pb(self): return data_v2_pb2.RowFilter(value_regex_filter=self.regex) +class ExactValueFilter(ValueRegexFilter): + """Row filter for an exact value. + + + :type value: bytes or str or int + :param value: + a literal string encodable as ASCII, or the + equivalent bytes, or an integer (which will be packed into 8-bytes). + """ + + def __init__(self, value): + if isinstance(value, int): + value = _PACK_I64(value) + super(ExactValueFilter, self).__init__(value) + + class ValueRangeFilter(RowFilter): """A range of values to restrict to in a row filter. @@ -496,6 +518,8 @@ def __init__( raise ValueError( "Inclusive start was specified but no " "start value was given." ) + if isinstance(start_value, int): + start_value = _PACK_I64(start_value) self.start_value = start_value self.inclusive_start = inclusive_start @@ -505,6 +529,8 @@ def __init__( raise ValueError( "Inclusive end was specified but no " "end value was given." ) + if isinstance(end_value, int): + end_value = _PACK_I64(end_value) self.end_value = end_value self.inclusive_end = inclusive_end diff --git a/tests/unit/test_row_filters.py b/tests/unit/test_row_filters.py index 1c51651d8..02a912318 100644 --- a/tests/unit/test_row_filters.py +++ b/tests/unit/test_row_filters.py @@ -498,9 +498,53 @@ def _get_target_class(): def _make_one(self, *args, **kwargs): return self._get_target_class()(*args, **kwargs) - def test_to_pb(self): - regex = b"value-regex" - row_filter = self._make_one(regex) + def test_to_pb_w_bytes(self): + value = regex = b"value-regex" + row_filter = self._make_one(value) + pb_val = row_filter.to_pb() + expected_pb = _RowFilterPB(value_regex_filter=regex) + self.assertEqual(pb_val, expected_pb) + + def test_to_pb_w_str(self): + value = u"value-regex" + regex = value.encode("ascii") + row_filter = self._make_one(value) + pb_val = row_filter.to_pb() + expected_pb = _RowFilterPB(value_regex_filter=regex) + self.assertEqual(pb_val, expected_pb) + + +class TestExactValueFilter(unittest.TestCase): + @staticmethod + def _get_target_class(): + from google.cloud.bigtable.row_filters import ExactValueFilter + + return ExactValueFilter + + def _make_one(self, *args, **kwargs): + return self._get_target_class()(*args, **kwargs) + + def test_to_pb_w_bytes(self): + value = regex = b"value-regex" + row_filter = self._make_one(value) + pb_val = row_filter.to_pb() + expected_pb = _RowFilterPB(value_regex_filter=regex) + self.assertEqual(pb_val, expected_pb) + + def test_to_pb_w_str(self): + value = u"value-regex" + regex = value.encode("ascii") + row_filter = self._make_one(value) + pb_val = row_filter.to_pb() + expected_pb = _RowFilterPB(value_regex_filter=regex) + self.assertEqual(pb_val, expected_pb) + + def test_to_pb_w_int(self): + import struct + + value = 1 + regex = struct.Struct(">q").pack(value) + row_filter = self._make_one(value) pb_val = row_filter.to_pb() expected_pb = _RowFilterPB(value_regex_filter=regex) self.assertEqual(pb_val, expected_pb) @@ -518,6 +562,7 @@ def _make_one(self, *args, **kwargs): def test_constructor_defaults(self): row_filter = self._make_one() + self.assertIsNone(row_filter.start_value) self.assertIsNone(row_filter.end_value) self.assertTrue(row_filter.inclusive_start) @@ -528,22 +573,42 @@ def test_constructor_explicit(self): end_value = object() inclusive_start = object() inclusive_end = object() + row_filter = self._make_one( start_value=start_value, end_value=end_value, inclusive_start=inclusive_start, inclusive_end=inclusive_end, ) + self.assertIs(row_filter.start_value, start_value) self.assertIs(row_filter.end_value, end_value) self.assertIs(row_filter.inclusive_start, inclusive_start) self.assertIs(row_filter.inclusive_end, inclusive_end) + def test_constructor_w_int_values(self): + import struct + + start_value = 1 + end_value = 10 + + row_filter = self._make_one(start_value=start_value, end_value=end_value) + + expected_start_value = struct.Struct(">q").pack(start_value) + expected_end_value = struct.Struct(">q").pack(end_value) + + self.assertEqual(row_filter.start_value, expected_start_value) + self.assertEqual(row_filter.end_value, expected_end_value) + self.assertTrue(row_filter.inclusive_start) + self.assertTrue(row_filter.inclusive_end) + def test_constructor_bad_start(self): - self.assertRaises(ValueError, self._make_one, inclusive_start=True) + with self.assertRaises(ValueError): + self._make_one(inclusive_start=True) def test_constructor_bad_end(self): - self.assertRaises(ValueError, self._make_one, inclusive_end=True) + with self.assertRaises(ValueError): + self._make_one(inclusive_end=True) def test___eq__(self): start_value = object()