Update of resource_definitions content implemented
Now it is able to add, update, delete keys in resource definition content. Change-Id: I9624f97bf35eae15d6a7ddc2d5d42768292db4f0
This commit is contained in:
parent
819fb9125a
commit
b2a878e536
@ -29,6 +29,7 @@ from tuning_box.middleware import keystone
|
||||
api_errors = {
|
||||
'IntegrityError': {'status': 409}, # sqlalchemy IntegrityError
|
||||
'TuningboxIntegrityError': {'status': 409},
|
||||
'KeysOperationError': {'status': 409},
|
||||
'TuningboxNotFound': {'status': 404}
|
||||
}
|
||||
api = flask_restful.Api(errors=api_errors)
|
||||
@ -46,6 +47,11 @@ api.add_resource(
|
||||
resource_definitions.ResourceDefinition,
|
||||
'/resource_definition/<int:resource_definition_id>'
|
||||
)
|
||||
api.add_resource(
|
||||
resource_definitions.ResourceDefinitionKeys,
|
||||
'/resource_definition/<int:resource_definition_id>/'
|
||||
'keys/<keys_operation:operation>'
|
||||
)
|
||||
|
||||
# Resource values
|
||||
api.add_resource(
|
||||
@ -81,6 +87,12 @@ def handle_object_not_found(exc):
|
||||
return response
|
||||
|
||||
|
||||
def handle_keys_operation_error(exc):
|
||||
response = flask.jsonify(msg=exc.args[0])
|
||||
response.status_code = 409
|
||||
return response
|
||||
|
||||
|
||||
def build_app(configure_logging=True, with_keystone=True):
|
||||
app = flask.Flask(__name__)
|
||||
app.url_map.converters.update(converters.ALL)
|
||||
@ -94,6 +106,8 @@ def build_app(configure_logging=True, with_keystone=True):
|
||||
handle_integrity_error)
|
||||
app.register_error_handler(errors.TuningboxNotFound,
|
||||
handle_object_not_found)
|
||||
app.register_error_handler(errors.KeysOperationError,
|
||||
handle_keys_operation_error)
|
||||
db.db.init_app(app)
|
||||
if configure_logging:
|
||||
log_level = app.config.get('LOG_LEVEL', 'INFO')
|
||||
|
@ -15,6 +15,8 @@ import itertools
|
||||
from werkzeug import routing
|
||||
from werkzeug import urls
|
||||
|
||||
from tuning_box.library import resource_keys_operation
|
||||
|
||||
|
||||
class Levels(routing.BaseConverter):
|
||||
"""Converter that maps nested levels to list of tuples.
|
||||
@ -56,7 +58,20 @@ class IdOrName(routing.BaseConverter):
|
||||
def to_url(self, value):
|
||||
return super(IdOrName, self).to_url(str(value))
|
||||
|
||||
|
||||
class KeysOperation(routing.BaseConverter):
|
||||
"""Converter that matches keys operations
|
||||
|
||||
Allowed operations: add, delete, erase
|
||||
"""
|
||||
|
||||
regex = '(' + ')|('.join(
|
||||
resource_keys_operation.KeysOperationMixin.OPERATIONS
|
||||
) + ')'
|
||||
|
||||
|
||||
ALL = {
|
||||
'levels': Levels,
|
||||
'id_or_name': IdOrName,
|
||||
'keys_operation': KeysOperation
|
||||
}
|
||||
|
@ -21,3 +21,23 @@ class TuningboxIntegrityError(BaseTuningboxError):
|
||||
|
||||
class TuningboxNotFound(BaseTuningboxError):
|
||||
pass
|
||||
|
||||
|
||||
class KeysOperationError(BaseTuningboxError):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownKeysOperation(KeysOperationError):
|
||||
pass
|
||||
|
||||
|
||||
class KeysPathNotExisted(KeysOperationError):
|
||||
pass
|
||||
|
||||
|
||||
class KeysPathInvalid(KeysOperationError):
|
||||
pass
|
||||
|
||||
|
||||
class KeysPathUnreachable(KeysOperationError):
|
||||
pass
|
||||
|
@ -10,12 +10,12 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import flask
|
||||
import flask_restful
|
||||
from flask_restful import fields
|
||||
|
||||
from tuning_box import db
|
||||
from tuning_box.library import resource_keys_operation
|
||||
|
||||
resource_definition_fields = {
|
||||
'id': fields.Integer,
|
||||
@ -87,3 +87,22 @@ class ResourceDefinition(flask_restful.Resource):
|
||||
resource_definition_id)
|
||||
db.db.session.delete(res_definition)
|
||||
return None, 204
|
||||
|
||||
|
||||
class ResourceDefinitionKeys(flask_restful.Resource,
|
||||
resource_keys_operation.KeysOperationMixin):
|
||||
|
||||
@db.with_transaction
|
||||
def _do_update(self, resource_definition_id, operation):
|
||||
res_definition = db.ResourceDefinition.query.get_or_404(
|
||||
resource_definition_id)
|
||||
result = self.perform_operation(operation, res_definition.content,
|
||||
flask.request.json)
|
||||
res_definition.content = result
|
||||
|
||||
def put(self, resource_definition_id, operation):
|
||||
return self.patch(resource_definition_id, operation)
|
||||
|
||||
def patch(self, resource_definition_id, operation):
|
||||
self._do_update(resource_definition_id, operation)
|
||||
return None, 204
|
||||
|
118
tuning_box/library/resource_keys_operation.py
Normal file
118
tuning_box/library/resource_keys_operation.py
Normal file
@ -0,0 +1,118 @@
|
||||
# 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 copy
|
||||
|
||||
from tuning_box import errors
|
||||
|
||||
|
||||
class KeysOperationMixin(object):
|
||||
|
||||
OPERATION_SET = 'set'
|
||||
OPERATION_DELETE = 'delete'
|
||||
|
||||
OPERATIONS = (OPERATION_SET, OPERATION_DELETE)
|
||||
|
||||
def _check_out_of_index(self, cur_point, key, keys_path):
|
||||
if isinstance(cur_point, (list, tuple)) and key >= len(cur_point):
|
||||
raise errors.KeysPathNotExisted(
|
||||
"Keys path doesn't exist {0}. "
|
||||
"Failed on the key {1}".format(keys_path, key)
|
||||
)
|
||||
|
||||
def _check_key_existed(self, cur_point, key, keys_path):
|
||||
if isinstance(cur_point, dict) and key not in cur_point:
|
||||
raise errors.KeysPathNotExisted(
|
||||
"Keys path doesn't exist {0}. "
|
||||
"Failed on the key {1}".format(keys_path, key)
|
||||
)
|
||||
|
||||
def _check_path_is_reachable(self, cur_point, key, keys_path):
|
||||
if not isinstance(cur_point, (list, tuple, dict)):
|
||||
raise errors.KeysPathUnreachable(
|
||||
"Leaf value {0} found on key {1} "
|
||||
"in keys path {2}".format(cur_point, key, keys_path)
|
||||
)
|
||||
|
||||
def do_set(self, storage, keys_paths):
|
||||
"""Sets values from keys paths to storage.
|
||||
|
||||
Keys path is list of keys paths. If we have keys_paths
|
||||
[['a', 'b', 'val']], then storage['a']['b'] will be set to 'val'.
|
||||
Last value in the keys path is value to be set.
|
||||
|
||||
:param storage: original data
|
||||
:param keys_paths: lists of keys paths to be set
|
||||
:returns: result of merging keys_paths and storage
|
||||
"""
|
||||
|
||||
storage_copy = copy.deepcopy(storage)
|
||||
for keys_path in keys_paths:
|
||||
cur_point = storage_copy
|
||||
if len(keys_path) < 2:
|
||||
raise errors.KeysPathInvalid(
|
||||
"Keys path {0} invalid. Keys path should contain "
|
||||
"at least one key and value".format(keys_path)
|
||||
)
|
||||
|
||||
for key in keys_path[:-2]:
|
||||
self._check_path_is_reachable(cur_point, key, keys_path)
|
||||
self._check_out_of_index(cur_point, key, keys_path)
|
||||
self._check_key_existed(cur_point, key, keys_path)
|
||||
cur_point = cur_point[key]
|
||||
|
||||
assign_to = keys_path[-2]
|
||||
self._check_out_of_index(cur_point, assign_to, keys_path)
|
||||
cur_point[assign_to] = keys_path[-1]
|
||||
|
||||
return storage_copy
|
||||
|
||||
def do_delete(self, storage, keys_paths):
|
||||
"""Deletes keys paths from storage.
|
||||
|
||||
Keys path is list of keys paths. If we have keys_paths
|
||||
[['a', 'b']], then storage['a']['b'] will be removed.
|
||||
|
||||
:param storage: data
|
||||
:param keys_paths: lists of keys paths to be deleted
|
||||
:returns: result of keys_paths deletion from storage
|
||||
"""
|
||||
|
||||
storage_copy = copy.deepcopy(storage)
|
||||
for keys_path in keys_paths:
|
||||
cur_point = storage_copy
|
||||
if not keys_path:
|
||||
continue
|
||||
|
||||
try:
|
||||
for key in keys_path[:-1]:
|
||||
cur_point = cur_point[key]
|
||||
key = keys_path[-1]
|
||||
self._check_path_is_reachable(cur_point, key, keys_path)
|
||||
del cur_point[key]
|
||||
except (KeyError, IndexError):
|
||||
raise errors.KeysPathNotExisted(
|
||||
"Keys path doesn't exist {0}. "
|
||||
"Failed on the key {1}".format(keys_path, key)
|
||||
)
|
||||
return storage_copy
|
||||
|
||||
def perform_operation(self, operation, storage, keys_paths):
|
||||
if operation == self.OPERATION_SET:
|
||||
return self.do_set(storage, keys_paths)
|
||||
elif operation == self.OPERATION_DELETE:
|
||||
return self.do_delete(storage, keys_paths)
|
||||
else:
|
||||
raise errors.UnknownKeysOperation(
|
||||
"Unknown operation: {0}. "
|
||||
"Allowed operations: {1}".format(operation, self.OPERATIONS)
|
||||
)
|
@ -19,6 +19,7 @@ class TestResourceDefinitions(BaseTest):
|
||||
|
||||
collection_url = '/resource_definitions'
|
||||
object_url = '/resource_definition/{0}'
|
||||
object_keys_url = object_url + '/keys/{1}'
|
||||
|
||||
@property
|
||||
def _resource_json(self):
|
||||
@ -150,3 +151,63 @@ class TestResourceDefinitions(BaseTest):
|
||||
self.assertEqual(204, res.status_code)
|
||||
actual_res_def = self.client.get(self.object_url.format(res_id)).json
|
||||
self.assertEqual(self._resource_json, actual_res_def)
|
||||
|
||||
def test_put_resource_definition_set_operation_error(self):
|
||||
self.app.config["PROPAGATE_EXCEPTIONS"] = True
|
||||
self._fixture()
|
||||
res_id = self._resource_json['id']
|
||||
|
||||
data = [['a', 'b', 'c', 'value']]
|
||||
res = self.client.put(self.object_keys_url.format(res_id, 'set'),
|
||||
data=data)
|
||||
self.assertEqual(409, res.status_code)
|
||||
|
||||
def test_put_resource_definition_set(self):
|
||||
self._fixture()
|
||||
res_id = self._resource_json['id']
|
||||
|
||||
data = [['key', 'key_value'], ['key_x', 'key_x_value']]
|
||||
res = self.client.put(
|
||||
self.object_keys_url.format(res_id, 'set'),
|
||||
data=data
|
||||
)
|
||||
self.assertEqual(204, res.status_code)
|
||||
|
||||
res = self.client.get(self.object_url.format(res_id))
|
||||
self.assertEqual(200, res.status_code)
|
||||
actual = res.json
|
||||
self.assertEqual({'key': 'key_value', 'key_x': 'key_x_value'},
|
||||
actual['content'])
|
||||
|
||||
def test_put_resource_definition_delete(self):
|
||||
self._fixture()
|
||||
res_id = self._resource_json['id']
|
||||
|
||||
data = [['key']]
|
||||
res = self.client.put(
|
||||
self.object_keys_url.format(res_id, 'delete'),
|
||||
data=data
|
||||
)
|
||||
self.assertEqual(204, res.status_code)
|
||||
|
||||
res = self.client.get(self.object_url.format(res_id))
|
||||
self.assertEqual(200, res.status_code)
|
||||
actual = res.json
|
||||
self.assertEqual({}, actual['content'])
|
||||
|
||||
def test_put_resource_definition_delete_no_key(self):
|
||||
self.app.config["PROPAGATE_EXCEPTIONS"] = True
|
||||
self._fixture()
|
||||
res_id = self._resource_json['id']
|
||||
|
||||
data = [['fake_key']]
|
||||
res = self.client.put(
|
||||
self.object_keys_url.format(res_id, 'delete'),
|
||||
data=data
|
||||
)
|
||||
self.assertEqual(409, res.status_code)
|
||||
|
||||
res = self.client.get(self.object_url.format(res_id))
|
||||
self.assertEqual(200, res.status_code)
|
||||
actual = res.json
|
||||
self.assertEqual(self._resource_json['content'], actual['content'])
|
||||
|
132
tuning_box/tests/library/test_resource_key_operations.py
Normal file
132
tuning_box/tests/library/test_resource_key_operations.py
Normal file
@ -0,0 +1,132 @@
|
||||
# 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.
|
||||
|
||||
from tuning_box import errors
|
||||
from tuning_box.library import resource_keys_operation
|
||||
from tuning_box.tests.test_app import BaseTest
|
||||
|
||||
|
||||
class TestResourceDefinitions(BaseTest):
|
||||
|
||||
processor = resource_keys_operation.KeysOperationMixin()
|
||||
|
||||
def test_unknown_operation(self):
|
||||
self.assertRaises(errors.UnknownKeysOperation,
|
||||
self.processor.perform_operation,
|
||||
'fake_operation', {}, [])
|
||||
|
||||
def test_set_new(self):
|
||||
keys = [['a', {}]]
|
||||
data = {}
|
||||
result = self.processor.do_set(data, keys)
|
||||
self.assertEqual({'a': {}}, result)
|
||||
|
||||
keys = [['a', {}], ['a', 'b', []]]
|
||||
data = {}
|
||||
result = self.processor.do_set(data, keys)
|
||||
self.assertEqual({'a': {'b': []}}, result)
|
||||
|
||||
keys = [['a', 0, 'b', 'c_updated']]
|
||||
data = {'a': [{'b': 'c'}]}
|
||||
result = self.processor.do_set(data, keys)
|
||||
self.assertEqual({'a': [{'b': 'c_updated'}]}, result)
|
||||
|
||||
keys = [['a', 'b']]
|
||||
data = {'a': {'b': 'c'}}
|
||||
result = self.processor.do_set(data, keys)
|
||||
self.assertEqual({'a': 'b'}, result)
|
||||
|
||||
def test_set_empty(self):
|
||||
keys = [['a', 'b', '']]
|
||||
data = {'a': {'b': 'value'}}
|
||||
result = self.processor.do_set(data, keys)
|
||||
self.assertEqual({'a': {'b': ''}}, result)
|
||||
|
||||
def test_set_not_modifies_storage(self):
|
||||
keys = [['a', 'c', 'value_c']]
|
||||
data = {'a': {'b': 'value_b'}}
|
||||
result = self.processor.do_set(data, keys)
|
||||
self.assertEqual({'a': {'b': 'value_b'}}, data)
|
||||
self.assertEqual({'a': {'c': 'value_c', 'b': 'value_b'}}, result)
|
||||
|
||||
def test_set_invalid_keys_path(self):
|
||||
self.assertRaises(errors.KeysPathInvalid, self.processor.do_set,
|
||||
{}, [[]])
|
||||
self.assertRaises(errors.KeysPathInvalid, self.processor.do_set,
|
||||
{}, [['a']])
|
||||
|
||||
def test_set_key_path_not_existed(self):
|
||||
keys = [['a', 'b', 'c']]
|
||||
data = {}
|
||||
self.assertRaises(errors.KeysPathNotExisted, self.processor.do_set,
|
||||
data, keys)
|
||||
|
||||
keys = [['a', 1, 'b']]
|
||||
data = {'a': [{'b': 'c'}]}
|
||||
self.assertRaises(errors.KeysPathNotExisted, self.processor.do_set,
|
||||
data, keys)
|
||||
|
||||
def test_set_key_path_unreachable(self):
|
||||
keys = [['a', 'b', 'c', 'd', 'e']]
|
||||
data = {'a': {'b': 'c'}}
|
||||
self.assertRaises(errors.KeysPathUnreachable, self.processor.do_set,
|
||||
data, keys)
|
||||
|
||||
def test_delete_key_path_not_existed(self):
|
||||
keys = [['a', 'b']]
|
||||
data = {}
|
||||
self.assertRaises(errors.KeysPathNotExisted, self.processor.do_delete,
|
||||
data, keys)
|
||||
|
||||
keys = [[1]]
|
||||
data = ['a']
|
||||
self.assertRaises(errors.KeysPathNotExisted, self.processor.do_delete,
|
||||
data, keys)
|
||||
|
||||
def test_delete_key_path_unreachable(self):
|
||||
keys = [['a', 'b', 'value_b']]
|
||||
data = {'a': {'b': 'value_b'}}
|
||||
self.assertRaises(errors.KeysPathUnreachable, self.processor.do_delete,
|
||||
data, keys)
|
||||
|
||||
keys = [['a', 'b', 'value_c']]
|
||||
data = {'a': {'b': 'value_b'}}
|
||||
self.assertRaises(errors.KeysPathUnreachable, self.processor.do_delete,
|
||||
data, keys)
|
||||
|
||||
def test_delete(self):
|
||||
keys = [['a']]
|
||||
data = {'a': 'val_a', 'b': {'a': 'val_b_a'}}
|
||||
result = self.processor.do_delete(data, keys)
|
||||
self.assertEqual({'b': {'a': 'val_b_a'}}, result)
|
||||
|
||||
keys = [[0]]
|
||||
data = ['a']
|
||||
result = self.processor.do_delete(data, keys)
|
||||
self.assertEqual([], result)
|
||||
|
||||
keys = [['a', 0, 'b']]
|
||||
data = {'a': [{'b': 'val_a_0_b', 'c': 'val_a_0_c'}, 'd']}
|
||||
result = self.processor.do_delete(data, keys)
|
||||
self.assertEqual({'a': [{'c': 'val_a_0_c'}, 'd']}, result)
|
||||
|
||||
keys = [['a', 'b'], ['a']]
|
||||
data = {'a': {'b': 'val_a_b', 'c': 'val_a_c'}, 'b': 'val_b'}
|
||||
result = self.processor.do_delete(data, keys)
|
||||
self.assertEqual({'b': 'val_b'}, result)
|
||||
|
||||
def test_delete_not_modifies_storage(self):
|
||||
keys = [['a', 'b']]
|
||||
data = {'a': {'b': 'value_b'}}
|
||||
result = self.processor.do_delete(data, keys)
|
||||
self.assertEqual({'a': {'b': 'value_b'}}, data)
|
||||
self.assertEqual({'a': {}}, result)
|
Loading…
x
Reference in New Issue
Block a user