Skip to content

Commit

Permalink
feat: support filtering on incrementable values (#178)
Browse files Browse the repository at this point in the history
Document that 'regex' may be either bytes or text, and add an explicit test for that.

Add 'ExactValueFilter' shortcut, wrapping 'ValueRegexFilter', but also convertin integer values
to the equivalent packed 8-octet bytes.

Allow integer values for 'ValueRangeFilter', converting them to the equivalent packed 8-octet bytes values.

Closes #177.
  • Loading branch information
tseaver committed Jan 27, 2021
1 parent fdb17cd commit e221352
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 8 deletions.
32 changes: 29 additions & 3 deletions google/cloud/bigtable/row_filters.py
Expand Up @@ -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.
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
75 changes: 70 additions & 5 deletions tests/unit/test_row_filters.py
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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()
Expand Down

0 comments on commit e221352

Please sign in to comment.