diff --git a/muranoapi/api/v1/services.py b/muranoapi/api/v1/services.py index 65b96a7d..9b2476f5 100644 --- a/muranoapi/api/v1/services.py +++ b/muranoapi/api/v1/services.py @@ -39,7 +39,8 @@ def normalize_path(f): class Controller(object): @normalize_path def get(self, request, environment_id, path): - log.debug(_('Services:Get '.format(environment_id))) + log.debug(_('Services:Get '.format(environment_id, path))) session_id = None if hasattr(request, 'context') and request.context.session: @@ -54,8 +55,8 @@ class Controller(object): @utils.verify_session @normalize_path def post(self, request, environment_id, path, body): - log.debug(_('Services:Post '.format(environment_id, body))) + log.debug(_('Services:Post '.format(environment_id, body, path))) post_data = CoreServices.post_data session_id = request.context.session @@ -68,8 +69,8 @@ class Controller(object): @utils.verify_session @normalize_path def put(self, request, environment_id, path, body): - log.debug(_('Services:Put '.format(environment_id, body))) + log.debug(_('Services:Put '.format(environment_id, body, path))) put_data = CoreServices.put_data session_id = request.context.session @@ -80,6 +81,20 @@ class Controller(object): raise HTTPNotFound return result + @utils.verify_session + @normalize_path + def delete(self, request, environment_id, path): + log.debug(_('Services:Put '.format(environment_id, path))) + + delete_data = CoreServices.delete_data + session_id = request.context.session + + try: + delete_data(environment_id, session_id, path) + except (KeyError, ValueError): + raise HTTPNotFound + def create_resource(): return wsgi.Resource(Controller()) diff --git a/muranoapi/common/utils.py b/muranoapi/common/utils.py index bf3be030..90296559 100644 --- a/muranoapi/common/utils.py +++ b/muranoapi/common/utils.py @@ -20,16 +20,21 @@ from collections import deque from functools import wraps from muranoapi.openstack.common import log as logging + log = logging.getLogger(__name__) class TraverseHelper(object): + value_type = (types.StringTypes, types.IntType, types.FloatType, + types.BooleanType) + @staticmethod def get(path, source): """ Provides the ability to traverse a data source made up of any - combination of lists and dicts. Has simple rules for selecting item of + combination of lists and dicts. Has simple rules for selecting item of the list: + * each item should have id property * to select item from the list, specify id value @@ -49,24 +54,24 @@ class TraverseHelper(object): :return: object :raise: ValueError if object is malformed """ - if path.startswith('/'): - path = path[1:] - - queue = deque(path.split('/')) - obj = source + queue = deque(filter(lambda x: x, path.split('/'))) while len(queue): path = queue.popleft() - if isinstance(obj, types.ListType): - filtered = filter(lambda i: 'id' in i and i['id'] == path, obj) - obj = filtered[0] if filtered else None - elif isinstance(obj, types.DictionaryType): - obj = obj[path] if path else obj + if isinstance(source, types.ListType): + idx_source = source + source = next((i for i in source if i['id'] == path), None) + if source is None and path.isdigit(): + source = idx_source[int(path)] + elif isinstance(source, types.DictionaryType): + source = source[path] + elif isinstance(source, TraverseHelper.value_type): + break else: - raise ValueError('Object or path is malformed') + raise ValueError('Source object or path is malformed') - return obj + return source @staticmethod def update(path, value, source): @@ -91,11 +96,34 @@ class TraverseHelper(object): :param path: string with path to desired value :param value: value - :param source: python object (list or dict) + :param source: List """ node = TraverseHelper.get(path, source) node.append(value) + @staticmethod + def remove(path, source): + """ + Removes selected item from source. + + :param path: string with path to desired value + :param source: python object (list or dict) + """ + parent_path = '/'.join(path.split('/')[:-1]) + node = TraverseHelper.get(parent_path, source) + key = path[1:].split('/')[-1] + + if isinstance(node, types.ListType): + item = next((i for i in node if i['id'] == key), None) + if item is None and key.isdigit(): + del node[int(key)] + else: + node.remove(item) + elif isinstance(node, types.DictionaryType): + del node[key] + else: + raise ValueError('Source object or path is malformed') + def auto_id(value): if isinstance(value, types.DictionaryType): diff --git a/muranoapi/db/services/core_services.py b/muranoapi/db/services/core_services.py index 7874eccd..c6df98d0 100644 --- a/muranoapi/db/services/core_services.py +++ b/muranoapi/db/services/core_services.py @@ -67,3 +67,13 @@ class CoreServices(object): save_description(session_id, env_description) return data + + @staticmethod + def delete_data(environment_id, session_id, path): + get_description = EnvironmentServices.get_environment_description + save_description = EnvironmentServices.save_environment_description + + env_description = get_description(environment_id, session_id) + + TraverseHelper.remove(path, env_description) + save_description(session_id, env_description) diff --git a/muranoapi/tests/common/traverse_helper_tests.py b/muranoapi/tests/common/traverse_helper_tests.py index 7cc06eeb..f03757d3 100644 --- a/muranoapi/tests/common/traverse_helper_tests.py +++ b/muranoapi/tests/common/traverse_helper_tests.py @@ -11,27 +11,38 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + import unittest2 as unittest from muranoapi.common.utils import TraverseHelper class TraverseHelperTests(unittest.TestCase): - def test_simple_root_get(self): - source = {"attr": True} + def test_root_get_with_dict(self): + source = {'attr': True} value = TraverseHelper.get('/', source) - self.assertDictEqual(value, {"attr": True}) + self.assertDictEqual(value, source) - def test_simple_attribute_get(self): - source = {"attr": True} + def test_root_get_with_list(self): + source = [{'attr': True}] + value = TraverseHelper.get('/', source) + self.assertListEqual(value, source) + + def test_root_get_with_value_type(self): + source = 'source' + value = TraverseHelper.get('/', source) + self.assertEqual(value, source) + + def test_attribute_get(self): + source = {'attr': True} value = TraverseHelper.get('/attr', source) self.assertEqual(value, True) - def test_attribute_get(self): + def test_nested_attribute_get(self): source = {'obj': {'attr': True}} value = TraverseHelper.get('/obj/attr', source) self.assertEqual(value, True) - def test_list_item_attribute_get_(self): + def test_list_item_attribute_get(self): source = {'obj': [ {'id': '1', 'value': 1}, {'id': '2s', 'value': 2}, @@ -39,53 +50,64 @@ class TraverseHelperTests(unittest.TestCase): value = TraverseHelper.get('/obj/2s/value', source) self.assertEqual(value, 2) - def test_simple_attribute_set(self): - source = {"attr": True} + def test_list_item_attribute_get_by_index(self): + source = {'obj': [ + {'id': 'guid1', 'value': 1}, + {'id': 'guid2', 'value': 2}, + ]} + value = TraverseHelper.get('/obj/1/value', source) + self.assertEqual(value, 2) + + def test_attribute_set(self): + source = {'attr': True} TraverseHelper.update('/newAttr', False, source) value = TraverseHelper.get('/newAttr', source) self.assertEqual(value, False) - def test_simple_attribute_update(self): - source = {"attr": True} + def test_attribute_update(self): + source = {'attr': True} TraverseHelper.update('/attr', False, source) value = TraverseHelper.get('/attr', source) self.assertEqual(value, False) - def test_attribute_update(self): - source = {"obj": {"attr": True}} + def test_nested_attribute_update(self): + source = {'obj': {'attr': True}} TraverseHelper.update('/obj/attr', False, source) value = TraverseHelper.get('/obj/attr', source) self.assertEqual(value, False) - def test_simple_adding_item_to_list(self): - source = {"attr": [1, 2, 3]} + def test_adding_item_to_list(self): + source = {'attr': [1, 2, 3]} TraverseHelper.insert('/attr', 4, source) value = TraverseHelper.get('/attr', source) self.assertListEqual(value, [1, 2, 3, 4]) - def test_adding_item_to_list(self): - source = {"obj": {"attr": [1, 2, 3]}} + def test_nested_adding_item_to_list(self): + source = {'obj': {'attr': [1, 2, 3]}} TraverseHelper.insert('/obj/attr', 4, source) value = TraverseHelper.get('/obj/attr', source) self.assertListEqual(value, [1, 2, 3, 4]) - @unittest.skip - def test_simple_attribute_remove(self): - source = {"attr1": False, "attr2": True} + def test_attribute_remove_from_dict(self): + source = {'attr1': False, 'attr2': True} TraverseHelper.remove('/attr1', source) value = TraverseHelper.get('/', source) - self.assertEqual(value, {"attr2": True}) + self.assertDictEqual(value, {'attr2': True}) - @unittest.skip - def test_nested_attribute_remove_from_object(self): - source = {"obj": {"attr1": False, "attr2": True}} + def test_nested_attribute_remove_from_dict(self): + source = {'obj': {'attr1': False, 'attr2': True}} TraverseHelper.remove('/obj/attr1', source) value = TraverseHelper.get('/obj', source) - self.assertDictEqual(value, {"attr2": True}) + self.assertDictEqual(value, {'attr2': True}) - @unittest.skip - def test_nested_attribute_remove_from_list(self): - source = {"obj": [{"id": 'id1'}, {"id": 'id2'}]} + def test_nested_attribute_remove_from_list_by_id(self): + source = {'obj': [{'id': 'id1'}, {'id': 'id2'}]} TraverseHelper.remove('/obj/id1', source) - value = TraverseHelper.get('/', source) - self.assertListEqual(value, [{"id": 'id2'}]) + value = TraverseHelper.get('/obj', source) + self.assertListEqual(value, [{'id': 'id2'}]) + + def test_nested_attribute_remove_from_list_by_index(self): + source = {'obj': [{'id': 'id1'}, {'id': 'id2'}]} + TraverseHelper.remove('/obj/0', source) + value = TraverseHelper.get('/obj', source) + self.assertListEqual(value, [{'id': 'id2'}])