Repository: googleapis/google-cloud-python

Issue:
Bigtable: TimestampRangeFilter rejects time. 
Bigtable supports millisecond granularity but using microsecond granularity in TimestampRange

https://googlecloudplatform.github.io/google-cloud-python/latest/_modules/google/cloud/bigtable/row_filters.html#TimestampRange

Therefore, it was the result in the following error:
```
Timestamp granularity mismatch. Expected a multiple of 1000 (millisecond granularity), but got 1519628106494933.
```

## Code

```python
end = datetime.datetime.now()
start = end - datetime.timedelta(minutes = 60)
timefilter = TimestampRangeFilter(TimestampRange(start=start, end=end))
row_data = table.read_rows(filter_=timefilter)
row_data.consume_all()
```

## Stacktrace

```
_RendezvousTraceback (most recent call last)
<ipython-input-69-b9d4b2239efa> in <module>()
      4 timefilter = TimestampRangeFilter(TimestampRange(start=start, end=end))
      5 row_data = table.read_rows(filter_=timefilter)
----> 6 row_data.consume_all()

/usr/local/envs/py2env/lib/python2.7/site-packages/google/cloud/bigtable/row_data.pyc in consume_all(self, max_loops)
    325             curr_loop += 1
    326             try:
--> 327                 self.consume_next()
    328             except StopIteration:
    329                 break

/usr/local/envs/py2env/lib/python2.7/site-packages/google/cloud/bigtable/row_data.pyc in consume_next(self)
    259         :attr:`_rows`. Rows are returned in order by row key.
    260         """
--> 261         response = six.next(self._response_iterator)
    262         self._counter += 1
    263 

/usr/local/envs/py2env/lib/python2.7/site-packages/grpc/_channel.pyc in next(self)
    348 
    349     def next(self):
--> 350         return self._next()
    351 
    352     def is_active(self):

/usr/local/envs/py2env/lib/python2.7/site-packages/grpc/_channel.pyc in _next(self)
    339                         raise StopIteration()
    340                     elif self._state.code is not None:
--> 341                         raise self
    342 
    343     def __iter__(self):

_Rendezvous: <_Rendezvous of RPC that terminated with (StatusCode.INVALID_ARGUMENT, Error in field 'timestamp_range_filter' : Timestamp granularity mismatch. Expected a multiple of 1000 (millisecond granularity), but got 1519628106494933.)>```




Discussion:

Resolving patch:
diff --git a/bigtable/google/cloud/bigtable/row_filters.py b/bigtable/google/cloud/bigtable/row_filters.py
--- a/bigtable/google/cloud/bigtable/row_filters.py
+++ b/bigtable/google/cloud/bigtable/row_filters.py
@@ -279,10 +279,12 @@ def to_pb(self):
         timestamp_range_kwargs = {}
         if self.start is not None:
             timestamp_range_kwargs['start_timestamp_micros'] = (
-                _microseconds_from_datetime(self.start))
+                _microseconds_from_datetime(self.start) // 1000 * 1000)
         if self.end is not None:
-            timestamp_range_kwargs['end_timestamp_micros'] = (
-                _microseconds_from_datetime(self.end))
+            end_time = _microseconds_from_datetime(self.end)
+            if end_time % 1000 != 0:
+                end_time = end_time // 1000 * 1000 + 1000
+            timestamp_range_kwargs['end_timestamp_micros'] = end_time
         return data_v2_pb2.TimestampRange(**timestamp_range_kwargs)
 
 


Test patch:
diff --git a/bigtable/tests/system.py b/bigtable/tests/system.py
--- a/bigtable/tests/system.py
+++ b/bigtable/tests/system.py
@@ -337,6 +337,16 @@ def _write_to_row(self, row1=None, row2=None, row3=None, row4=None):
         cell4 = Cell(CELL_VAL4, timestamp4_micros)
         return cell1, cell2, cell3, cell4
 
+    def test_timestamp_filter_millisecond_granularity(self):
+        from google.cloud.bigtable import row_filters
+
+        end = datetime.datetime.now()
+        start = end - datetime.timedelta(minutes=60)
+        timestamp_range = row_filters.TimestampRange(start=start, end=end)
+        timefilter = row_filters.TimestampRangeFilter(timestamp_range)
+        row_data = self._table.read_rows(filter_=timefilter)
+        row_data.consume_all()
+
     def test_mutate_rows(self):
         row1 = self._table.row(ROW_KEY)
         row1.set_cell(COLUMN_FAMILY_ID1, COL_NAME1, CELL_VAL1)
diff --git a/bigtable/tests/unit/test_row_filters.py b/bigtable/tests/unit/test_row_filters.py
--- a/bigtable/tests/unit/test_row_filters.py
+++ b/bigtable/tests/unit/test_row_filters.py
@@ -285,41 +285,53 @@ def test___ne__same_value(self):
         comparison_val = (time_range1 != time_range2)
         self.assertFalse(comparison_val)
 
-    def _to_pb_helper(self, start_micros=None, end_micros=None):
+    def _to_pb_helper(self, pb_kwargs, start=None, end=None):
         import datetime
         from google.cloud._helpers import _EPOCH
-
-        pb_kwargs = {}
-
-        start = None
-        if start_micros is not None:
-            start = _EPOCH + datetime.timedelta(microseconds=start_micros)
-            pb_kwargs['start_timestamp_micros'] = start_micros
-        end = None
-        if end_micros is not None:
-            end = _EPOCH + datetime.timedelta(microseconds=end_micros)
-            pb_kwargs['end_timestamp_micros'] = end_micros
+        if start is not None:
+            start = _EPOCH + datetime.timedelta(microseconds=start)
+        if end is not None:
+            end = _EPOCH + datetime.timedelta(microseconds=end)
         time_range = self._make_one(start=start, end=end)
-
         expected_pb = _TimestampRangePB(**pb_kwargs)
-        self.assertEqual(time_range.to_pb(), expected_pb)
+        time_pb = time_range.to_pb()
+        self.assertEqual(
+            time_pb.start_timestamp_micros,
+            expected_pb.start_timestamp_micros)
+        self.assertEqual(
+            time_pb.end_timestamp_micros,
+            expected_pb.end_timestamp_micros)
+        self.assertEqual(time_pb, expected_pb)
 
     def test_to_pb(self):
-        # Makes sure already milliseconds granularity
-        start_micros = 30871000
-        end_micros = 12939371000
-        self._to_pb_helper(start_micros=start_micros,
-                           end_micros=end_micros)
+        start_micros = 30871234
+        end_micros = 12939371234
+        start_millis = start_micros // 1000 * 1000
+        self.assertEqual(start_millis, 30871000)
+        end_millis = end_micros // 1000 * 1000 + 1000
+        self.assertEqual(end_millis, 12939372000)
+        pb_kwargs = {}
+        pb_kwargs['start_timestamp_micros'] = start_millis
+        pb_kwargs['end_timestamp_micros'] = end_millis
+        self._to_pb_helper(pb_kwargs, start=start_micros, end=end_micros)
 
     def test_to_pb_start_only(self):
         # Makes sure already milliseconds granularity
         start_micros = 30871000
-        self._to_pb_helper(start_micros=start_micros)
+        start_millis = start_micros // 1000 * 1000
+        self.assertEqual(start_millis, 30871000)
+        pb_kwargs = {}
+        pb_kwargs['start_timestamp_micros'] = start_millis
+        self._to_pb_helper(pb_kwargs, start=start_micros, end=None)
 
     def test_to_pb_end_only(self):
         # Makes sure already milliseconds granularity
         end_micros = 12939371000
-        self._to_pb_helper(end_micros=end_micros)
+        end_millis = end_micros // 1000 * 1000
+        self.assertEqual(end_millis, 12939371000)
+        pb_kwargs = {}
+        pb_kwargs['end_timestamp_micros'] = end_millis
+        self._to_pb_helper(pb_kwargs, start=None, end=end_micros)
 
 
 class TestTimestampRangeFilter(unittest.TestCase):