provide-crud-api-operation
Add abstract crud api for client. Change-Id: I545a8ab7af3791f9c5eda8c79370ea8e77fae26a
This commit is contained in:
parent
61bccadc37
commit
09c301c28c
168
valenceclient/common/base.py
Normal file
168
valenceclient/common/base.py
Normal file
@ -0,0 +1,168 @@
|
||||
# Copyright 2017 99cloud, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of
|
||||
"""
|
||||
|
||||
import abc
|
||||
import copy
|
||||
import six
|
||||
from valenceclient.common.apiclient import base
|
||||
from valenceclient import exc
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""Wrapper to get object's ID
|
||||
|
||||
Abstracts the common pattern of allowing both an object or an
|
||||
object's ID (UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Manager(object):
|
||||
"""Provides CRUD operations with a particular API."""
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def _path(self, resource_id=None):
|
||||
"""Return a request path for a given resource identifier.
|
||||
|
||||
:param resource_id: identifier of the resource to generate the request
|
||||
path
|
||||
"""
|
||||
return ('/v1/%s/%s' % (self._resource_name, resource_id) if
|
||||
resource_id else '/v1/%s' % self._resource_name)
|
||||
|
||||
@abc.abstractproperty
|
||||
def resource_class(self):
|
||||
"""The resource class"""
|
||||
|
||||
@abc.abstractproperty
|
||||
def _resource_name(self):
|
||||
"""The resource name"""
|
||||
|
||||
def _get(self, resource_id, fields=None):
|
||||
"""Retrieve a resource.
|
||||
|
||||
:param resource_id: Identifier of the resource.
|
||||
:param fields: List of specific fields to be returned.
|
||||
"""
|
||||
|
||||
if fields is not None:
|
||||
resource_id = '/%s' % resource_id
|
||||
resource_id += ','.join(fields)
|
||||
|
||||
try:
|
||||
return self._list(self._path(resource_id))[0]
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _get_as_dict(self, resource_id, fields=None):
|
||||
"""Retrieve a resource as a dictionary
|
||||
|
||||
:param resource_id: Identifier of the resource.
|
||||
:param fields: List of specific fields to be returned.
|
||||
:returns: a dictionary representing the resource; may be empty
|
||||
"""
|
||||
|
||||
resource = self._get(resource_id, fields=fields)
|
||||
if resource:
|
||||
return resource.to_dict()
|
||||
else:
|
||||
return {}
|
||||
|
||||
def _list(self, url, obj_class=None,):
|
||||
resp, body = self.api.json_request('GET', url)
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
|
||||
if not isinstance(body, list):
|
||||
body = [body]
|
||||
|
||||
return [obj_class(self, res, loaded=True) for res in body if res]
|
||||
|
||||
def _update(self, resource_id, patch, method='PATCH'):
|
||||
"""Update a resource.
|
||||
|
||||
:param resource_id: Resource identifier.
|
||||
:param patch: New version of a given resource.
|
||||
:param method: Name of the method for the request.
|
||||
"""
|
||||
|
||||
url = self._path(resource_id)
|
||||
resp, body = self.api.json_request(method, url, body=patch)
|
||||
# PATCH/PUT requests may not return a body
|
||||
if body:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _delete(self, resource_id):
|
||||
"""Delete a resource.
|
||||
|
||||
:param resource_id: Resource identifier.
|
||||
"""
|
||||
self.api.json_request('DELETE', self._path(resource_id))
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class CreateManager(Manager):
|
||||
"""Provides creation operations with a particular API."""
|
||||
|
||||
@abc.abstractproperty
|
||||
def _creation_attributes(self):
|
||||
"""A list of required creation attributes for a resource type"""
|
||||
|
||||
def create(self, **kwargs):
|
||||
"""Create a resource based on a kwargs dictionary of attributes.
|
||||
|
||||
:param kwargs: A dictionary containing the attributes of the resource
|
||||
that will be created.
|
||||
:raises exc.InvalidAttribution: For invalid attributes that are not
|
||||
needed to create the resource.
|
||||
"""
|
||||
new = {}
|
||||
invalid = []
|
||||
for (key, value) in kwargs.items():
|
||||
if key in self._creation_attributes:
|
||||
new[key] = value
|
||||
else:
|
||||
invalid.append(key)
|
||||
if invalid:
|
||||
raise exc.InvalidAttribution(
|
||||
'The attribution(s) "%s(attrs)s" are invalid: they are not '
|
||||
'needed to create %(resource)s.' %
|
||||
{'resource': self._resource_name,
|
||||
'attrs': '","'.join(invalid)})
|
||||
|
||||
url = self._path()
|
||||
resp, body = self.api.json_request('POST', url, body=new)
|
||||
if body:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
|
||||
class Resource(base.Resource):
|
||||
"""Represents a particular instance of an object (tenant, user, etc).
|
||||
|
||||
This is pretty much just a bag for attributes.
|
||||
"""
|
||||
|
||||
def to_dict(self):
|
||||
return copy.deepcopy(self._info)
|
189
valenceclient/tests/unit/common/test_base.py
Normal file
189
valenceclient/tests/unit/common/test_base.py
Normal file
@ -0,0 +1,189 @@
|
||||
# Copyright 2017 99cloud Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from valenceclient.common import base
|
||||
from valenceclient.tests.unit import utils
|
||||
|
||||
|
||||
TESTABLE_RESOURCE = {
|
||||
'uuid': '11111111-2222-3333-4444-555555555555',
|
||||
'attribute1': '1',
|
||||
'attribute2': '2',
|
||||
}
|
||||
|
||||
CREATE_TESTABLE_RESOURCE = copy.deepcopy(TESTABLE_RESOURCE)
|
||||
del CREATE_TESTABLE_RESOURCE['uuid']
|
||||
|
||||
INVALID_ATTRIBUTE_TESTABLE_RESOURCE = {
|
||||
'non-existent-attribute': 'blablabla',
|
||||
'attribute1': '1',
|
||||
'attribute2': '2',
|
||||
}
|
||||
|
||||
UPDATED_TESTABLE_RESOURCE = copy.deepcopy(TESTABLE_RESOURCE)
|
||||
NEW_ATTRIBUTE_VALUE = 'brand-new-attribute-value'
|
||||
UPDATED_TESTABLE_RESOURCE['attribute1'] = NEW_ATTRIBUTE_VALUE
|
||||
|
||||
fake_responses = {
|
||||
'/redfish/v1/testableresources':
|
||||
{
|
||||
'GET': (
|
||||
{},
|
||||
{"testableresources": [TESTABLE_RESOURCE]},
|
||||
),
|
||||
'POST': (
|
||||
{},
|
||||
CREATE_TESTABLE_RESOURCE,
|
||||
),
|
||||
},
|
||||
'/redfish/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid']:
|
||||
{
|
||||
'GET': (
|
||||
{},
|
||||
TESTABLE_RESOURCE,
|
||||
),
|
||||
'DELETE': (
|
||||
{},
|
||||
None,
|
||||
),
|
||||
'PATCH': (
|
||||
{},
|
||||
UPDATED_TESTABLE_RESOURCE,
|
||||
),
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
class TestableResource(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<TestableResource %s>" % self._info
|
||||
|
||||
|
||||
class TestableManager(base.CreateManager):
|
||||
resource_class = TestableResource
|
||||
_creation_attributes = ['attribute1', 'attribute2']
|
||||
_resource_name = 'testableresources'
|
||||
|
||||
def _path(self, id=None):
|
||||
return ('/redfish/v1/testableresources/%s' % id if id
|
||||
else '/redfish/v1/testableresources')
|
||||
|
||||
def get(self, testable_resource_id, fields=None):
|
||||
return self._get(resource_id=testable_resource_id,
|
||||
fields=fields)
|
||||
|
||||
def delete(self, testable_resource_id):
|
||||
return self._delete(resource_id=testable_resource_id)
|
||||
|
||||
def update(self, testable_resource_id, patch):
|
||||
return self._update(resource_id=testable_resource_id,
|
||||
patch=patch)
|
||||
|
||||
|
||||
class ManagerTestCase(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ManagerTestCase, self).setUp()
|
||||
self.api = utils.FakeAPI(fake_responses)
|
||||
self.manager = TestableManager(self.api)
|
||||
|
||||
def test_create(self):
|
||||
resource = self.manager.create(**CREATE_TESTABLE_RESOURCE)
|
||||
expect = [
|
||||
('POST',
|
||||
'/redfish/v1/testableresources',
|
||||
{},
|
||||
CREATE_TESTABLE_RESOURCE),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertTrue(resource)
|
||||
self.assertIsInstance(resource, TestableResource)
|
||||
|
||||
def test__get(self):
|
||||
resource_id = TESTABLE_RESOURCE['uuid']
|
||||
resource = self.manager._get(resource_id)
|
||||
expect = [
|
||||
('GET', '/redfish/v1/testableresources/%s' % resource_id,
|
||||
{}, None),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual(resource_id, resource.uuid)
|
||||
self.assertEqual(TESTABLE_RESOURCE['attribute1'], resource.attribute1)
|
||||
|
||||
def test__get_as_dict(self):
|
||||
resource_id = TESTABLE_RESOURCE['uuid']
|
||||
resource = self.manager._get_as_dict(resource_id)
|
||||
expect = [
|
||||
('GET', '/redfish/v1/testableresources/%s' % resource_id,
|
||||
{}, None),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual(TESTABLE_RESOURCE, resource)
|
||||
|
||||
@mock.patch.object(base.Manager, '_get', autospec=True)
|
||||
def test__get_as_dict_empty(self, mock_get):
|
||||
mock_get.return_value = None
|
||||
resource_id = TESTABLE_RESOURCE['uuid']
|
||||
resource = self.manager._get_as_dict(resource_id)
|
||||
mock_get.assert_called_once_with(mock.ANY, resource_id, fields=None)
|
||||
self.assertEqual({}, resource)
|
||||
|
||||
def test_get(self):
|
||||
resource = self.manager.get(TESTABLE_RESOURCE['uuid'])
|
||||
expect = [
|
||||
('GET',
|
||||
'/redfish/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid'],
|
||||
{},
|
||||
None),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual(TESTABLE_RESOURCE['uuid'], resource.uuid)
|
||||
self.assertEqual(TESTABLE_RESOURCE['attribute1'], resource.attribute1)
|
||||
|
||||
def test_update(self):
|
||||
patch = {'op': 'replace',
|
||||
'value': NEW_ATTRIBUTE_VALUE,
|
||||
'path': '/attribute1'}
|
||||
resource = self.manager.update(
|
||||
testable_resource_id=TESTABLE_RESOURCE['uuid'],
|
||||
patch=patch
|
||||
)
|
||||
expect = [
|
||||
('PATCH',
|
||||
'/redfish/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid'],
|
||||
{},
|
||||
patch),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertEqual(NEW_ATTRIBUTE_VALUE, resource.attribute1)
|
||||
|
||||
def test_delete(self):
|
||||
resource = self.manager.delete(
|
||||
testable_resource_id=TESTABLE_RESOURCE['uuid']
|
||||
)
|
||||
expect = [
|
||||
('DELETE',
|
||||
'/redfish/v1/testableresources/%s' % TESTABLE_RESOURCE['uuid'],
|
||||
{},
|
||||
None),
|
||||
]
|
||||
self.assertEqual(expect, self.api.calls)
|
||||
self.assertIsNone(resource)
|
Loading…
x
Reference in New Issue
Block a user