Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions gcloud/bigtable/row.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ class Row(object):
the row, otherwise the :data:`False` state will be.
"""

ALL_COLUMNS = object()
"""Sentinel value used to indicate all columns in a column family."""

def __init__(self, row_key, table, filter_=None):
self._row_key = _to_bytes(row_key)
self._table = table
Expand Down Expand Up @@ -125,20 +128,26 @@ def append_cell_value(self, column_family_id, column, value):

def increment_cell_value(self, column_family_id, column, int_value):
"""Increments a value in an existing cell.

Assumes the value in the cell is stored as a 64 bit integer
serialized to bytes.

.. note::

This method adds a read-modify rule protobuf to the accumulated
read-modify rules on this :class:`Row`, but does not make an API
request. To actually send an API request (with the rules) to the
Google Cloud Bigtable API, call :meth:`commit_modifications`.

:type column_family_id: str
:param column_family_id: The column family that contains the column.
Must be of the form
``[_a-zA-Z0-9][-_.a-zA-Z0-9]*``.

:type column: bytes
:param column: The column within the column family where the cell
is located.

:type int_value: int
:param int_value: The value to increment the existing value in the cell
by. If the targeted cell is unset, it will be treated
Expand All @@ -153,6 +162,119 @@ def increment_cell_value(self, column_family_id, column, int_value):
increment_amount=int_value)
self._rule_pb_list.append(rule_pb)

def delete(self, state=None):
"""Deletes this row from the table.

.. note::

This method adds a mutation to the accumulated mutations on this
:class:`Row`, but does not make an API request. To actually
send an API request (with the mutations) to the Google Cloud
Bigtable API, call :meth:`commit`.

:type state: bool
:param state: (Optional) The state that the mutation should be
applied in. Unset if the mutation is not conditional,
otherwise :data:`True` or :data:`False`.
"""
mutation_val = data_pb2.Mutation.DeleteFromRow()
mutation_pb = data_pb2.Mutation(delete_from_row=mutation_val)
self._get_mutations(state).append(mutation_pb)

def delete_cell(self, column_family_id, column, time_range=None,
state=None):
"""Deletes cell in this row.

.. note::

This method adds a mutation to the accumulated mutations on this
:class:`Row`, but does not make an API request. To actually
send an API request (with the mutations) to the Google Cloud
Bigtable API, call :meth:`commit`.

:type column_family_id: str
:param column_family_id: The column family that contains the column
or columns with cells being deleted. Must be
of the form ``[_a-zA-Z0-9][-_.a-zA-Z0-9]*``.

:type column: bytes
:param column: The column within the column family that will have a
cell deleted.

:type time_range: :class:`TimestampRange`
:param time_range: (Optional) The range of time within which cells
should be deleted.

:type state: bool
:param state: (Optional) The state that the mutation should be
applied in. Unset if the mutation is not conditional,
otherwise :data:`True` or :data:`False`.
"""
self.delete_cells(column_family_id, [column], time_range=time_range,
state=state)

def delete_cells(self, column_family_id, columns, time_range=None,
state=None):
"""Deletes cells in this row.

.. note::

This method adds a mutation to the accumulated mutations on this
:class:`Row`, but does not make an API request. To actually
send an API request (with the mutations) to the Google Cloud
Bigtable API, call :meth:`commit`.

:type column_family_id: str
:param column_family_id: The column family that contains the column
or columns with cells being deleted. Must be
of the form ``[_a-zA-Z0-9][-_.a-zA-Z0-9]*``.

:type columns: :class:`list` of :class:`str` /
:func:`unicode <unicode>`, or :class:`object`
:param columns: The columns within the column family that will have
cells deleted. If :attr:`Row.ALL_COLUMNS` is used then
the entire column family will be deleted from the row.

:type time_range: :class:`TimestampRange`
:param time_range: (Optional) The range of time within which cells
should be deleted.

:type state: bool
:param state: (Optional) The state that the mutation should be
applied in. Unset if the mutation is not conditional,
otherwise :data:`True` or :data:`False`.
"""
mutations_list = self._get_mutations(state)
if columns is self.ALL_COLUMNS:
mutation_val = data_pb2.Mutation.DeleteFromFamily(
family_name=column_family_id,
)
mutation_pb = data_pb2.Mutation(delete_from_family=mutation_val)
mutations_list.append(mutation_pb)
else:
delete_kwargs = {}
if time_range is not None:
delete_kwargs['time_range'] = time_range.to_pb()

to_append = []
for column in columns:
column = _to_bytes(column)
# time_range will never change if present, but the rest of
# delete_kwargs will
delete_kwargs.update(
family_name=column_family_id,
column_qualifier=column,
)
mutation_val = data_pb2.Mutation.DeleteFromColumn(
**delete_kwargs)
mutation_pb = data_pb2.Mutation(
delete_from_column=mutation_val)
to_append.append(mutation_pb)

# We don't add the mutations until all columns have been
# processed without error.
mutations_list.extend(to_append)


class RowFilter(object):
"""Basic filter to apply to cells in a row.
Expand Down
171 changes: 171 additions & 0 deletions gcloud/bigtable/test_row.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,177 @@ def test_increment_cell_value(self):
increment_amount=int_value)
self.assertEqual(row._rule_pb_list, [expected_pb])

def test_delete(self):
from gcloud.bigtable._generated import bigtable_data_pb2 as data_pb2

row_key = b'row_key'
row = self._makeOne(row_key, object())
self.assertEqual(row._pb_mutations, [])
row.delete()

expected_pb = data_pb2.Mutation(
delete_from_row=data_pb2.Mutation.DeleteFromRow(),
)
self.assertEqual(row._pb_mutations, [expected_pb])

def test_delete_cell(self):
klass = self._getTargetClass()

class MockRow(klass):

def __init__(self, *args, **kwargs):
super(MockRow, self).__init__(*args, **kwargs)
self._args = []
self._kwargs = []

# Replace the called method with one that logs arguments.
def delete_cells(self, *args, **kwargs):
self._args.append(args)
self._kwargs.append(kwargs)

row_key = b'row_key'
column = b'column'
column_family_id = u'column_family_id'
table = object()

mock_row = MockRow(row_key, table)
# Make sure no values are set before calling the method.
self.assertEqual(mock_row._pb_mutations, [])
self.assertEqual(mock_row._args, [])
self.assertEqual(mock_row._kwargs, [])

# Actually make the request against the mock class.
time_range = object()
mock_row.delete_cell(column_family_id, column, time_range=time_range)
self.assertEqual(mock_row._pb_mutations, [])
self.assertEqual(mock_row._args, [(column_family_id, [column])])
self.assertEqual(mock_row._kwargs, [{
'state': None,
'time_range': time_range,
}])

def test_delete_cells_non_iterable(self):
row_key = b'row_key'
column_family_id = u'column_family_id'
table = object()

row = self._makeOne(row_key, table)
columns = object() # Not iterable
with self.assertRaises(TypeError):
row.delete_cells(column_family_id, columns)

def test_delete_cells_all_columns(self):
from gcloud.bigtable._generated import bigtable_data_pb2 as data_pb2

row_key = b'row_key'
column_family_id = u'column_family_id'
table = object()

row = self._makeOne(row_key, table)
klass = self._getTargetClass()
self.assertEqual(row._pb_mutations, [])
row.delete_cells(column_family_id, klass.ALL_COLUMNS)

expected_pb = data_pb2.Mutation(
delete_from_family=data_pb2.Mutation.DeleteFromFamily(
family_name=column_family_id,
),
)
self.assertEqual(row._pb_mutations, [expected_pb])

def test_delete_cells_no_columns(self):
row_key = b'row_key'
column_family_id = u'column_family_id'
table = object()

row = self._makeOne(row_key, table)
columns = []
self.assertEqual(row._pb_mutations, [])
row.delete_cells(column_family_id, columns)
self.assertEqual(row._pb_mutations, [])

def _delete_cells_helper(self, time_range=None):
from gcloud.bigtable._generated import bigtable_data_pb2 as data_pb2

row_key = b'row_key'
column = b'column'
column_family_id = u'column_family_id'
table = object()

row = self._makeOne(row_key, table)
columns = [column]
self.assertEqual(row._pb_mutations, [])
row.delete_cells(column_family_id, columns, time_range=time_range)

expected_pb = data_pb2.Mutation(
delete_from_column=data_pb2.Mutation.DeleteFromColumn(
family_name=column_family_id,
column_qualifier=column,
),
)
if time_range is not None:
expected_pb.delete_from_column.time_range.CopyFrom(
time_range.to_pb())
self.assertEqual(row._pb_mutations, [expected_pb])

def test_delete_cells_no_time_range(self):
self._delete_cells_helper()

def test_delete_cells_with_time_range(self):
import datetime
from gcloud._helpers import _EPOCH
from gcloud.bigtable.row import TimestampRange

microseconds = 30871000 # Makes sure already milliseconds granularity
start = _EPOCH + datetime.timedelta(microseconds=microseconds)
time_range = TimestampRange(start=start)
self._delete_cells_helper(time_range=time_range)

def test_delete_cells_with_bad_column(self):
# This makes sure a failure on one of the columns doesn't leave
# the row's mutations in a bad state.
row_key = b'row_key'
column = b'column'
column_family_id = u'column_family_id'
table = object()

row = self._makeOne(row_key, table)
columns = [column, object()]
self.assertEqual(row._pb_mutations, [])
with self.assertRaises(TypeError):
row.delete_cells(column_family_id, columns)
self.assertEqual(row._pb_mutations, [])

def test_delete_cells_with_string_columns(self):
from gcloud.bigtable._generated import bigtable_data_pb2 as data_pb2

row_key = b'row_key'
column_family_id = u'column_family_id'
column1 = u'column1'
column1_bytes = b'column1'
column2 = u'column2'
column2_bytes = b'column2'
table = object()

row = self._makeOne(row_key, table)
columns = [column1, column2]
self.assertEqual(row._pb_mutations, [])
row.delete_cells(column_family_id, columns)

expected_pb1 = data_pb2.Mutation(
delete_from_column=data_pb2.Mutation.DeleteFromColumn(
family_name=column_family_id,
column_qualifier=column1_bytes,
),
)
expected_pb2 = data_pb2.Mutation(
delete_from_column=data_pb2.Mutation.DeleteFromColumn(
family_name=column_family_id,
column_qualifier=column2_bytes,
),
)
self.assertEqual(row._pb_mutations, [expected_pb1, expected_pb2])


class Test_BoolFilter(unittest2.TestCase):

Expand Down