Skip to content
This repository has been archived by the owner on Feb 2, 2018. It is now read-only.

Commit

Permalink
StorageClient: Add get_many(), save_many(), delete_many().
Browse files Browse the repository at this point in the history
  • Loading branch information
mbarnes authored and ashcrow committed Mar 9, 2017
1 parent e017998 commit d5508c3
Show file tree
Hide file tree
Showing 2 changed files with 258 additions and 1 deletion.
111 changes: 111 additions & 0 deletions src/commissaire/storage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,43 @@ def get(self, model_instance):
model_instance.primary_key, error))
raise error

def get_many(self, list_of_model_instances):
"""
Similar to StorageClient.get(), but takes a list of model instances and
returns a list of model instances. The models must be of the same type
or else the function throws a TypeError.
:param list_of_model_instances: List of model instances with
identifying data
:type list_of_model_instances: [commissaire.models.Model, ...]
:returns: List of new model instances with stored data
:rtype: [commissaire.models.Model, ...]
:raises: TypeError, commissaire.bus.RemoteProcedureCallError
"""
# Handle the trivial case immediately
if len(list_of_model_instances) == 0:
return []

set_of_types = set([type(x) for x in list_of_model_instances])
if len(set_of_types) > 1:
raise TypeError('Model instances must be of identical type')
model_class = set_of_types.pop()

try:
model_json_data = [x.to_dict() for x in list_of_model_instances]
params = {
'model_type_name': model_class.__name__,
'model_json_data': model_json_data
}
response = self.bus_mixin.request('storage.get', params=params)
return [model_class.new(**x) for x in response['result']]
except RemoteProcedureCallError as error:
self.bus_mixin.logger.error(
'{}: Unable to get multiple {} records: {}'.format(
self.bus_mixin.__class__.__name__,
model_class.__name__, error))
raise error

def save(self, model_instance):
"""
Issues a "storage.save" request over the bus using data from
Expand Down Expand Up @@ -89,6 +126,46 @@ def save(self, model_instance):
model_instance.primary_key, error))
raise error

def save_many(self, list_of_model_instances):
"""
Similar to StorageClient.save(), but takes a list of model instances
and returns a list of model instances. The models must be of the same
type or else the function throws a TypeError.
:param list_of_model_instances: List of model instances with data to
save
:type list_of_model_instances: [commissaire.models.Model, ...]
:returns: List of new model instances with all saved fields
:rtype: [commissaire.models.Model, ...]
:raises: TypeError, commissaire.bus.RemoteProcedureCallError,
commissaire.models.ValidationError
"""
# Handle the trivial case immediately
if len(list_of_model_instances) == 0:
return []

set_of_types = set([type(x) for x in list_of_model_instances])
if len(set_of_types) > 1:
raise TypeError('Model instances must be of identical type')
model_class = set_of_types.pop()

try:
for model_instance in list_of_model_instances:
model_instance._validate()
model_json_data = [x.to_dict() for x in list_of_model_instances]
params = {
'model_type_name': model_class.__name__,
'model_json_data': model_json_data
}
response = self.bus_mixin.request('storage.save', params=params)
return [model_class.new(**x) for x in response['result']]
except (RemoteProcedureCallError, models.ValidationError) as error:
self.bus_mixin.logger.error(
'{}: Unable to save multiple {} records: {}'.format(
self.bus_mixin.__class__.__name__,
model_class.__name__, error))
raise error

def delete(self, model_instance):
"""
Issues a "storage.delete" request over the bus using identifying
Expand All @@ -112,6 +189,40 @@ def delete(self, model_instance):
model_instance.primary_key, error))
raise error

def delete_many(self, list_of_model_instances):
"""
Similar to StorageClient.delete(), but takes a list of model instances.
The models must be of the same type or else the function throws a
TypeError.
:param list_of_model_instances: List of model instances with
identifying data
:type list_of_model_instances: [commissaire.models.Model, ...]
:raises: TypeError, commissaire.bus.RemoteProcedureCallError
"""
# Handle the trivial case immediately
if len(list_of_model_instances) == 0:
return

set_of_types = set([type(x) for x in list_of_model_instances])
if len(set_of_types) > 1:
raise TypeError('Model instances must be of identical type')
model_class = set_of_types.pop()

try:
model_json_data = [x.to_dict() for x in list_of_model_instances]
params = {
'model_type_name': model_class.__name__,
'model_json_data': model_json_data
}
self.bus_mixin.request('storage.delete', params=params)
except RemoteProcedureCallError as error:
self.bus_mixin.logger.error(
'{}: Unable to delete multiple {} records: {}'.format(
self.bus_mixin.__class__.__name__,
model_class.__name__, error))
raise error

def list(self, model_class):
"""
Issues a "storage.list" request over the bus for the given model
Expand Down
148 changes: 147 additions & 1 deletion test/test_storage_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from . import TestCase

from commissaire.bus import BusMixin, RemoteProcedureCallError
from commissaire.models import Host, Hosts, ValidationError
from commissaire.models import Host, Hosts, Cluster, ValidationError
from commissaire.storage.client import StorageClient

#: Message ID
Expand All @@ -49,6 +49,13 @@
#: Full host instance
FULL_HOST = Host.new(**FULL_HOST_DICT)

#: Minimal cluster dictionary
MINI_CLUSTER_DICT = {
'name': 'honeynut'
}
#: Minimal cluster instance
MINI_CLUSTER = Cluster.new(**MINI_CLUSTER_DICT)


class TestCommissaireStorageClient(TestCase):
"""
Expand Down Expand Up @@ -91,6 +98,51 @@ def test_get_rpc_error(self):
}
)

def test_get_many(self):
"""
Verify StorageClient.get_many works as expected
"""
storage = StorageClient(mock.MagicMock())
storage.bus_mixin.logger = mock.MagicMock()

self.assertEqual(storage.get_many([]), [])
storage.bus_mixin.request.assert_not_called()

self.assertRaises(TypeError, storage.get_many, [MINI_HOST, MINI_CLUSTER])
storage.bus_mixin.request.assert_not_called()

storage.bus_mixin.request.return_value = {
'jsonrpc': '2.0',
'id': ID,
'result': [FULL_HOST_DICT, FULL_HOST_DICT]
}
input_list = [MINI_HOST, MINI_HOST]
output_list = storage.get_many(input_list)
storage.bus_mixin.request.assert_called_once_with(
'storage.get', params={
'model_type_name': MINI_HOST.__class__.__name__,
'model_json_data': [x.to_dict() for x in input_list]
}
)
self.assertEqual(
[x.to_dict_safe() for x in output_list],
[FULL_HOST_DICT, FULL_HOST_DICT])

def test_get_many_rpc_error(self):
"""
Verify StorageClient.get_many re-raises RemoteProcedureCallError
"""
storage = StorageClient(mock.MagicMock())
storage.bus_mixin.logger = mock.MagicMock()
storage.bus_mixin.request.side_effect = RemoteProcedureCallError('test')
self.assertRaises(RemoteProcedureCallError, storage.get_many, [MINI_HOST])
storage.bus_mixin.request.assert_called_once_with(
'storage.get', params={
'model_type_name': MINI_HOST.__class__.__name__,
'model_json_data': [MINI_HOST.to_dict()]
}
)

def test_save(self):
"""
Verify StorageClient.save with a valid model.
Expand Down Expand Up @@ -139,6 +191,63 @@ def test_save_invalid(self):
self.assertRaises(ValidationError, storage.save, bad_host)
storage.bus_mixin.request.assert_not_called()

def test_save_many(self):
"""
Verify StorageClient.save_many works as expected
"""
storage = StorageClient(mock.MagicMock())
storage.bus_mixin.logger = mock.MagicMock()

self.assertEqual(storage.save_many([]), [])
storage.bus_mixin.request.assert_not_called()

self.assertRaises(TypeError, storage.save_many, [MINI_HOST, MINI_CLUSTER])
storage.bus_mixin.request.assert_not_called()

storage.bus_mixin.request.return_value = {
'jsonrpc': '2.0',
'id': ID,
'result': [FULL_HOST_DICT, FULL_HOST_DICT]
}
input_list = [MINI_HOST, MINI_HOST]
output_list = storage.save_many(input_list)
storage.bus_mixin.request.assert_called_once_with(
'storage.save', params={
'model_type_name': MINI_HOST.__class__.__name__,
'model_json_data': [x.to_dict() for x in input_list]
}
)
self.assertEqual(
[x.to_dict_safe() for x in output_list],
[FULL_HOST_DICT, FULL_HOST_DICT])

def test_save_many_rpc_error(self):
"""
Verify StorageClient.save_many re-raises RemoteProcedureCallError
"""
storage = StorageClient(mock.MagicMock())
storage.bus_mixin.logger = mock.MagicMock()
storage.bus_mixin.request.side_effect = RemoteProcedureCallError('test')
self.assertRaises(RemoteProcedureCallError, storage.save_many, [FULL_HOST])
storage.bus_mixin.request.assert_called_once_with(
'storage.save', params={
'model_type_name': FULL_HOST.__class__.__name__,
'model_json_data': [FULL_HOST.to_dict()]
}
)

def test_save_many_invalid(self):
"""
Verify StorageClient.save_many rejects an invalid model
"""
storage = StorageClient(mock.MagicMock())
storage.bus_mixin.logger = mock.MagicMock()
storage.bus_mixin.request.side_effect = ValidationError('test')
bad_host = Host.new(**FULL_HOST_DICT)
bad_host.address = None
self.assertRaises(ValidationError, storage.save_many, [bad_host])
storage.bus_mixin.request.assert_not_called()

def test_delete(self):
"""
Verify StorageClient.delete with a valid model.
Expand Down Expand Up @@ -168,6 +277,43 @@ def test_delete_rpc_error(self):
}
)

def test_delete_many(self):
"""
Verify StorageClient.delete_many works as expected
"""
storage = StorageClient(mock.MagicMock())
storage.bus_mixin.logger = mock.MagicMock()

storage.delete_many([])
storage.bus_mixin.request.assert_not_called()

self.assertRaises(TypeError, storage.delete_many, [MINI_HOST, MINI_CLUSTER])
storage.bus_mixin.request.assert_not_called()

input_list = [MINI_HOST, MINI_HOST]
storage.delete_many(input_list)
storage.bus_mixin.request.assert_called_once_with(
'storage.delete', params={
'model_type_name': MINI_HOST.__class__.__name__,
'model_json_data': [x.to_dict() for x in input_list]
}
)

def test_delete_many_rpc_error(self):
"""
Verify StorageClient.delete_many re-raises RemoteProcedureCallError
"""
storage = StorageClient(mock.MagicMock())
storage.bus_mixin.logger = mock.MagicMock()
storage.bus_mixin.request.side_effect = RemoteProcedureCallError('test')
self.assertRaises(RemoteProcedureCallError, storage.delete_many, [MINI_HOST])
storage.bus_mixin.request.assert_called_once_with(
'storage.delete', params={
'model_type_name': MINI_HOST.__class__.__name__,
'model_json_data': [MINI_HOST.to_dict()]
}
)

def test_list(self):
"""
Verify StorageClient.list returns valid models.
Expand Down

0 comments on commit d5508c3

Please sign in to comment.