Skip to content

Commit f143da2

Browse files
committed
Merge pull request #1290 from dhermes/bigtable-add-gc-rule
Adding primitive Bigtable garbage collection rule classes.
2 parents c150f8e + d11980f commit f143da2

File tree

8 files changed

+307
-5
lines changed

8 files changed

+307
-5
lines changed

gcloud/_helpers.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import os
2222
from threading import local as Local
2323
import socket
24+
import sys
2425

2526
import six
2627
from six.moves.http_client import HTTPConnection # pylint: disable=F0401
@@ -260,6 +261,36 @@ def _millis_from_datetime(value):
260261
return _millis(value)
261262

262263

264+
def _total_seconds_backport(offset):
265+
"""Backport of timedelta.total_seconds() from python 2.7+.
266+
267+
:type offset: :class:`datetime.timedelta`
268+
:param offset: A timedelta object.
269+
270+
:rtype: int
271+
:returns: The total seconds (including microseconds) in the
272+
duration.
273+
"""
274+
seconds = offset.days * 24 * 60 * 60 + offset.seconds
275+
return seconds + offset.microseconds * 1e-6
276+
277+
278+
def _total_seconds(offset):
279+
"""Version independent total seconds for a time delta.
280+
281+
:type offset: :class:`datetime.timedelta`
282+
:param offset: A timedelta object.
283+
284+
:rtype: int
285+
:returns: The total seconds (including microseconds) in the
286+
duration.
287+
"""
288+
if sys.version_info[:2] < (2, 7): # pragma: NO COVER
289+
return _total_seconds_backport(offset)
290+
else:
291+
return offset.total_seconds()
292+
293+
263294
def _to_bytes(value, encoding='ascii'):
264295
"""Converts a string value to bytes, if necessary.
265296

gcloud/bigtable/cluster.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def finished(self):
213213
operation_name = ('operations/' + self._cluster.name +
214214
'/operations/%d' % (self.op_id,))
215215
request_pb = operations_pb2.GetOperationRequest(name=operation_name)
216-
# We expact a `._generated.operations_pb2.Operation`.
216+
# We expect a `._generated.operations_pb2.Operation`.
217217
operation_pb = self._cluster._client._operations_stub.GetOperation(
218218
request_pb, self._cluster._client.timeout_seconds)
219219

@@ -258,7 +258,7 @@ class Cluster(object):
258258
259259
:type serve_nodes: int
260260
:param serve_nodes: (Optional) The number of nodes in the cluster.
261-
Defaults to 3.
261+
Defaults to 3 (``_DEFAULT_SERVE_NODES``).
262262
"""
263263

264264
def __init__(self, zone, cluster_id, client,

gcloud/bigtable/column_family.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,115 @@
1515
"""User friendly container for Google Cloud Bigtable Column Family."""
1616

1717

18+
from gcloud._helpers import _total_seconds
19+
from gcloud.bigtable._generated import bigtable_table_data_pb2 as data_pb2
20+
from gcloud.bigtable._generated import duration_pb2
21+
22+
23+
def _timedelta_to_duration_pb(timedelta_val):
24+
"""Convert a Python timedelta object to a duration protobuf.
25+
26+
.. note::
27+
28+
The Python timedelta has a granularity of microseconds while
29+
the protobuf duration type has a duration of nanoseconds.
30+
31+
:type timedelta_val: :class:`datetime.timedelta`
32+
:param timedelta_val: A timedelta object.
33+
34+
:rtype: :class:`duration_pb2.Duration`
35+
:returns: A duration object equivalent to the time delta.
36+
"""
37+
seconds_decimal = _total_seconds(timedelta_val)
38+
# Truncate the parts other than the integer.
39+
seconds = int(seconds_decimal)
40+
if seconds_decimal < 0:
41+
signed_micros = timedelta_val.microseconds - 10**6
42+
else:
43+
signed_micros = timedelta_val.microseconds
44+
# Convert nanoseconds to microseconds.
45+
nanos = 1000 * signed_micros
46+
return duration_pb2.Duration(seconds=seconds, nanos=nanos)
47+
48+
49+
class GarbageCollectionRule(object):
50+
"""Garbage collection rule for column families within a table.
51+
52+
Cells in the column family (within a table) fitting the rule will be
53+
deleted during garbage collection.
54+
55+
.. note::
56+
57+
This class is a do-nothing base class for all GC rules.
58+
59+
.. note::
60+
61+
A string ``gc_expression`` can also be used with API requests, but
62+
that value would be superceded by a ``gc_rule``. As a result, we
63+
don't support that feature and instead support via native classes.
64+
"""
65+
66+
def to_pb(self):
67+
"""Converts the :class:`GarbageCollectionRule` to a protobuf.
68+
69+
:raises: :class:`NotImplementedError <exceptions.NotImplementedError>`
70+
always since a virtual class.
71+
"""
72+
raise NotImplementedError
73+
74+
def __ne__(self, other):
75+
return not self.__eq__(other)
76+
77+
78+
class MaxVersionsGCRule(GarbageCollectionRule):
79+
"""Garbage collection limiting the number of versions of a cell.
80+
81+
:type max_num_versions: int
82+
:param max_num_versions: The maximum number of versions
83+
"""
84+
85+
def __init__(self, max_num_versions):
86+
self.max_num_versions = max_num_versions
87+
88+
def __eq__(self, other):
89+
if not isinstance(other, self.__class__):
90+
return False
91+
return other.max_num_versions == self.max_num_versions
92+
93+
def to_pb(self):
94+
"""Converts the garbage collection rule to a protobuf.
95+
96+
:rtype: :class:`.data_pb2.GcRule`
97+
:returns: The converted current object.
98+
"""
99+
return data_pb2.GcRule(max_num_versions=self.max_num_versions)
100+
101+
102+
class MaxAgeGCRule(GarbageCollectionRule):
103+
"""Garbage collection limiting the age of a cell.
104+
105+
:type max_age: :class:`datetime.timedelta`
106+
:param max_age: The maximum age allowed for a cell in the table.
107+
"""
108+
109+
def __init__(self, max_age):
110+
self.max_age = max_age
111+
112+
def __eq__(self, other):
113+
if not isinstance(other, self.__class__):
114+
return False
115+
return other.max_age == self.max_age
116+
117+
def to_pb(self):
118+
"""Converts the garbage collection rule to a protobuf.
119+
120+
:rtype: :class:`.data_pb2.GcRule`
121+
:returns: The converted current object.
122+
"""
123+
max_age = _timedelta_to_duration_pb(self.max_age)
124+
return data_pb2.GcRule(max_age=max_age)
125+
126+
18127
class ColumnFamily(object):
19128
"""Representation of a Google Cloud Bigtable Column Family.
20129

gcloud/bigtable/table.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class Table(object):
4141
We can use a :class:`Table` to:
4242
4343
* :meth:`create` the table
44+
* :meth:`rename` the table
4445
* :meth:`delete` the table
4546
4647
:type table_id: str

gcloud/bigtable/test_column_family.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,141 @@
1616
import unittest2
1717

1818

19+
class Test__timedelta_to_duration_pb(unittest2.TestCase):
20+
21+
def _callFUT(self, *args, **kwargs):
22+
from gcloud.bigtable.column_family import _timedelta_to_duration_pb
23+
return _timedelta_to_duration_pb(*args, **kwargs)
24+
25+
def test_it(self):
26+
import datetime
27+
from gcloud.bigtable._generated import duration_pb2
28+
29+
seconds = microseconds = 1
30+
timedelta_val = datetime.timedelta(seconds=seconds,
31+
microseconds=microseconds)
32+
result = self._callFUT(timedelta_val)
33+
self.assertTrue(isinstance(result, duration_pb2.Duration))
34+
self.assertEqual(result.seconds, seconds)
35+
self.assertEqual(result.nanos, 1000 * microseconds)
36+
37+
def test_with_negative_microseconds(self):
38+
import datetime
39+
from gcloud.bigtable._generated import duration_pb2
40+
41+
seconds = 1
42+
microseconds = -5
43+
timedelta_val = datetime.timedelta(seconds=seconds,
44+
microseconds=microseconds)
45+
result = self._callFUT(timedelta_val)
46+
self.assertTrue(isinstance(result, duration_pb2.Duration))
47+
self.assertEqual(result.seconds, seconds - 1)
48+
self.assertEqual(result.nanos, 10**9 + 1000 * microseconds)
49+
50+
def test_with_negative_seconds(self):
51+
import datetime
52+
from gcloud.bigtable._generated import duration_pb2
53+
54+
seconds = -1
55+
microseconds = 5
56+
timedelta_val = datetime.timedelta(seconds=seconds,
57+
microseconds=microseconds)
58+
result = self._callFUT(timedelta_val)
59+
self.assertTrue(isinstance(result, duration_pb2.Duration))
60+
self.assertEqual(result.seconds, seconds + 1)
61+
self.assertEqual(result.nanos, -(10**9 - 1000 * microseconds))
62+
63+
64+
class TestGarbageCollectionRule(unittest2.TestCase):
65+
66+
def _getTargetClass(self):
67+
from gcloud.bigtable.column_family import GarbageCollectionRule
68+
return GarbageCollectionRule
69+
70+
def _makeOne(self, *args, **kwargs):
71+
return self._getTargetClass()(*args, **kwargs)
72+
73+
def test_to_pb_virtual(self):
74+
gc_rule = self._makeOne()
75+
self.assertRaises(NotImplementedError, gc_rule.to_pb)
76+
77+
78+
class TestMaxVersionsGCRule(unittest2.TestCase):
79+
80+
def _getTargetClass(self):
81+
from gcloud.bigtable.column_family import MaxVersionsGCRule
82+
return MaxVersionsGCRule
83+
84+
def _makeOne(self, *args, **kwargs):
85+
return self._getTargetClass()(*args, **kwargs)
86+
87+
def test___eq__max_num_versions(self):
88+
gc_rule1 = self._makeOne(2)
89+
gc_rule2 = self._makeOne(2)
90+
self.assertEqual(gc_rule1, gc_rule2)
91+
92+
def test___eq__type_differ(self):
93+
gc_rule1 = self._makeOne(10)
94+
gc_rule2 = object()
95+
self.assertNotEqual(gc_rule1, gc_rule2)
96+
97+
def test___ne__same_value(self):
98+
gc_rule1 = self._makeOne(99)
99+
gc_rule2 = self._makeOne(99)
100+
comparison_val = (gc_rule1 != gc_rule2)
101+
self.assertFalse(comparison_val)
102+
103+
def test_to_pb(self):
104+
from gcloud.bigtable._generated import (
105+
bigtable_table_data_pb2 as data_pb2)
106+
max_num_versions = 1337
107+
gc_rule = self._makeOne(max_num_versions=max_num_versions)
108+
pb_val = gc_rule.to_pb()
109+
self.assertEqual(pb_val,
110+
data_pb2.GcRule(max_num_versions=max_num_versions))
111+
112+
113+
class TestMaxAgeGCRule(unittest2.TestCase):
114+
115+
def _getTargetClass(self):
116+
from gcloud.bigtable.column_family import MaxAgeGCRule
117+
return MaxAgeGCRule
118+
119+
def _makeOne(self, *args, **kwargs):
120+
return self._getTargetClass()(*args, **kwargs)
121+
122+
def test___eq__max_age(self):
123+
max_age = object()
124+
gc_rule1 = self._makeOne(max_age=max_age)
125+
gc_rule2 = self._makeOne(max_age=max_age)
126+
self.assertEqual(gc_rule1, gc_rule2)
127+
128+
def test___eq__type_differ(self):
129+
max_age = object()
130+
gc_rule1 = self._makeOne(max_age=max_age)
131+
gc_rule2 = object()
132+
self.assertNotEqual(gc_rule1, gc_rule2)
133+
134+
def test___ne__same_value(self):
135+
max_age = object()
136+
gc_rule1 = self._makeOne(max_age=max_age)
137+
gc_rule2 = self._makeOne(max_age=max_age)
138+
comparison_val = (gc_rule1 != gc_rule2)
139+
self.assertFalse(comparison_val)
140+
141+
def test_to_pb(self):
142+
import datetime
143+
from gcloud.bigtable._generated import (
144+
bigtable_table_data_pb2 as data_pb2)
145+
from gcloud.bigtable._generated import duration_pb2
146+
147+
max_age = datetime.timedelta(seconds=1)
148+
duration = duration_pb2.Duration(seconds=1)
149+
gc_rule = self._makeOne(max_age=max_age)
150+
pb_val = gc_rule.to_pb()
151+
self.assertEqual(pb_val, data_pb2.GcRule(max_age=duration))
152+
153+
19154
class TestColumnFamily(unittest2.TestCase):
20155

21156
def _getTargetClass(self):

gcloud/test__helpers.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,34 @@ def test_it(self):
354354
self.assertEqual(self._callFUT(NOW_MICROS), NOW)
355355

356356

357+
class Test__total_seconds_backport(unittest2.TestCase):
358+
359+
def _callFUT(self, *args, **kwargs):
360+
from gcloud._helpers import _total_seconds_backport
361+
return _total_seconds_backport(*args, **kwargs)
362+
363+
def test_it(self):
364+
import datetime
365+
offset = datetime.timedelta(seconds=3,
366+
microseconds=140000)
367+
result = self._callFUT(offset)
368+
self.assertEqual(result, 3.14)
369+
370+
371+
class Test__total_seconds(unittest2.TestCase):
372+
373+
def _callFUT(self, *args, **kwargs):
374+
from gcloud._helpers import _total_seconds
375+
return _total_seconds(*args, **kwargs)
376+
377+
def test_it(self):
378+
import datetime
379+
offset = datetime.timedelta(seconds=1,
380+
microseconds=414000)
381+
result = self._callFUT(offset)
382+
self.assertEqual(result, 1.414)
383+
384+
357385
class Test__to_bytes(unittest2.TestCase):
358386

359387
def _callFUT(self, *args, **kwargs):

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
REQUIREMENTS = [
1515
'httplib2 >= 0.9.1',
1616
'oauth2client >= 1.4.6',
17-
'protobuf >= 3.0.0a3',
17+
'protobuf == 3.0.0a3',
1818
'pycrypto',
1919
'six',
2020
]

tox.ini

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ commands =
1010
deps =
1111
nose
1212
unittest2
13-
protobuf>=3.0.0a3
1413
setenv =
1514
PYTHONPATH = {toxinidir}/_testing
1615
covercmd =
@@ -77,7 +76,6 @@ deps =
7776
pep8
7877
pylint
7978
unittest2
80-
protobuf==3.0.0-alpha-1
8179
passenv = {[testenv:system-tests]passenv}
8280

8381
[testenv:system-tests]

0 commit comments

Comments
 (0)