diff --git a/muranoapi/common/utils.py b/muranoapi/common/utils.py index d2761880..1923d71f 100644 --- a/muranoapi/common/utils.py +++ b/muranoapi/common/utils.py @@ -13,12 +13,88 @@ # under the License. import eventlet +import types +from collections import deque from functools import wraps from muranoapi.openstack.common import log as logging log = logging.getLogger(__name__) +class TraverseHelper(object): + @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 + the list: + * each item should have id property + * to select item from the list, specify id value + + Examples: + source = {'obj': {'attr': True}} + value = TraverseHelper.get('/obj/attr', source) + + source = {'obj': [ + {'id': '1', 'value': 1}, + {'id': '2s', 'value': 2}, + ]} + value = TraverseHelper.get('/obj/2s/value', source) + + + :param path: string with path to desired value + :param source: python object (list or dict) + :return: object + :raise: ValueError if object is malformed + """ + if path.startswith('/'): + path = path[1:] + + queue = deque(path.split('/')) + obj = source + + 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 + else: + raise ValueError('Object or path is malformed') + + return obj + + @staticmethod + def update(path, value, source): + """ + Updates value selected with specified path. + + Warning: Root object could not be updated + + :param path: string with path to desired value + :param value: 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] + node[key] = value + + @staticmethod + def insert(path, value, source): + """ + Inserts new item to selected list. + + :param path: string with path to desired value + :param value: value + :param source: python object (list or dict) + """ + node = TraverseHelper.get(path, source) + node.append(value) + + def retry(ExceptionToCheck, tries=4, delay=3, backoff=2): """Retry calling the decorated function using an exponential backoff. diff --git a/muranoapi/common/uuidutils.py b/muranoapi/common/uuidutils.py index 16cf60ea..cc7686b7 100644 --- a/muranoapi/common/uuidutils.py +++ b/muranoapi/common/uuidutils.py @@ -16,4 +16,4 @@ import uuid def generate_uuid(): - return str(uuid.uuid4()).replace('-', '') + return uuid.uuid4().hex diff --git a/muranoapi/tests/common/__init__.py b/muranoapi/tests/common/__init__.py new file mode 100644 index 00000000..7d93825c --- /dev/null +++ b/muranoapi/tests/common/__init__.py @@ -0,0 +1,13 @@ +# Copyright (c) 2013 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. diff --git a/muranoapi/tests/common/traverse_helper_tests.py b/muranoapi/tests/common/traverse_helper_tests.py new file mode 100644 index 00000000..f4c25a16 --- /dev/null +++ b/muranoapi/tests/common/traverse_helper_tests.py @@ -0,0 +1,70 @@ +# Copyright (c) 2013 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 unittest +from muranoapi.common.utils import TraverseHelper + + +class TraverseHelperTests(unittest.TestCase): + def test_simple_root_get(self): + source = {"attr": True} + value = TraverseHelper.get('/', source) + self.assertEqual(value, {"attr": True}) + + def test_simple_attribute_get(self): + source = {"attr": True} + value = TraverseHelper.get('/attr', source) + self.assertEqual(value, True) + + def test_attribute_get(self): + source = {'obj': {'attr': True}} + value = TraverseHelper.get('/obj/attr', source) + self.assertEqual(value, True) + + def test_list_item_attribute_get_(self): + source = {'obj': [ + {'id': '1', 'value': 1}, + {'id': '2s', 'value': 2}, + ]} + value = TraverseHelper.get('/obj/2s/value', source) + self.assertEqual(value, 2) + + def test_simple_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} + TraverseHelper.update('/attr', False, source) + value = TraverseHelper.get('/attr', source) + self.assertEqual(value, False) + + def test_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]} + TraverseHelper.insert('/attr', 4, source) + value = TraverseHelper.get('/attr', source) + self.assertEqual(value, [1, 2, 3, 4]) + + def test_adding_item_to_list(self): + source = {"obj": {"attr": [1, 2, 3]}} + TraverseHelper.insert('/obj/attr', 4, source) + value = TraverseHelper.get('/obj/attr', source) + self.assertEqual(value, [1, 2, 3, 4])