Add client resource base class

Change-Id: I0e1179fbf1804087c753e529ca67a6e503b6d1d4
This commit is contained in:
Rui Chen 2016-09-24 17:42:49 +08:00
parent ba4e567fb3
commit edc0ccd973
5 changed files with 717 additions and 1 deletions

356
nimbleclient/common/base.py Normal file
View File

@ -0,0 +1,356 @@
# Copyright 2016 Huawei, 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
from requests import Response
import six
from nimbleclient.common import exceptions
# Python 2.4 compat
try:
all
except NameError:
def all(iterable):
return True not in (not x for x in iterable)
def getid(obj):
"""Get obj's id or object itself if no 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
class Manager(object):
"""Interacts with type of API
Managers interact with a particular type of API (instances, types, etc.)
and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
def _list(self, url, response_key=None, obj_class=None,
data=None, headers=None):
if headers is None:
headers = {}
resp, body = self.api.get(url, headers=headers)
if obj_class is None:
obj_class = self.resource_class
if response_key:
if response_key not in body:
body[response_key] = []
data = body[response_key]
else:
data = body
items = [obj_class(self, res, loaded=True) for res in data if res]
return ListWithMeta(items, resp)
def _delete(self, url, headers=None):
if headers is None:
headers = {}
resp, body = self.api.delete(url, headers=headers)
return self.convert_into_with_meta(body, resp)
def _update(self, url, data, response_key=None, headers=None):
if headers is None:
headers = {}
resp, body = self.api.patch(url, data=data, headers=headers)
# PUT requests may not return a body
if body:
if response_key:
return self.resource_class(self, body[response_key], resp=resp)
return self.resource_class(self, body, resp=resp)
else:
return StrWithMeta(body, resp)
def _create(self, url, data=None, response_key=None, return_raw=False,
headers=None):
if headers is None:
headers = {}
if data:
resp, body = self.api.post(url, data=data, headers=headers)
else:
resp, body = self.api.post(url, headers=headers)
if return_raw:
if response_key:
body = body[response_key]
return self.convert_into_with_meta(body, resp)
if response_key:
return self.resource_class(self, body[response_key], resp=resp)
return self.resource_class(self, body, resp=resp)
def _get(self, url, response_key=None, return_raw=False, headers=None):
if headers is None:
headers = {}
resp, body = self.api.get(url, headers=headers)
if return_raw:
if response_key:
body = body[response_key]
return self.convert_into_with_meta(body, resp)
if response_key:
return self.resource_class(self, body[response_key], loaded=True,
resp=resp)
return self.resource_class(self, body, loaded=True, resp=resp)
def convert_into_with_meta(self, item, resp):
if isinstance(item, six.string_types):
if six.PY2 and isinstance(item, six.text_type):
return UnicodeWithMeta(item, resp)
else:
return StrWithMeta(item, resp)
elif isinstance(item, six.binary_type):
return BytesWithMeta(item, resp)
elif isinstance(item, list):
return ListWithMeta(item, resp)
elif isinstance(item, tuple):
return TupleWithMeta(item, resp)
elif item is None:
return TupleWithMeta((), resp)
else:
return DictWithMeta(item, resp)
@six.add_metaclass(abc.ABCMeta)
class ManagerWithFind(Manager):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num = len(matches)
if num == 0:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(msg)
elif num > 1:
raise exceptions.NoUniqueMatch
else:
return self.get(matches[0].id)
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class RequestIdMixin(object):
"""Wrapper class to expose x-openstack-request-id to the caller."""
def request_ids_setup(self):
self.x_openstack_request_ids = []
@property
def request_ids(self):
return self.x_openstack_request_ids
def append_request_ids(self, resp):
"""Add request_ids as an attribute to the object
:param resp: Response object or list of Response objects
"""
if isinstance(resp, list):
# Add list of request_ids if response is of type list.
for resp_obj in resp:
self._append_request_id(resp_obj)
elif resp is not None:
# Add request_ids if response contains single object.
self._append_request_id(resp)
def _append_request_id(self, resp):
if isinstance(resp, Response):
# Extract 'x-openstack-request-id' from headers if
# response is a Response object.
request_id = (resp.headers.get('x-openstack-request-id') or
resp.headers.get('x-compute-request-id'))
else:
# If resp is of type string or None.
request_id = resp
if request_id not in self.x_openstack_request_ids:
self.x_openstack_request_ids.append(request_id)
class Resource(RequestIdMixin):
"""Represents an instance of an object
A resource represents a particular instance of an object (instance, type,
etc). This is pretty much just a bag for attributes.
:param manager: BaseManager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
:param resp: Response or list of Response objects
"""
def __init__(self, manager, info, loaded=False, resp=None):
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
self.request_ids_setup()
self.append_request_ids(resp)
def _add_details(self, info):
for (k, v) in six.iteritems(info):
try:
setattr(self, k, v)
self._info[k] = v
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __setstate__(self, d):
for k, v in d.items():
setattr(self, k, v)
def __getattr__(self, k):
if k not in self.__dict__:
# NOTE(RuiChen): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def __repr__(self):
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
k not in ('manager', 'x_openstack_request_ids'))
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
return "<%s %s>" % (self.__class__.__name__, info)
def get(self):
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
# The 'request_ids' attribute has been added,
# so store the request id to it instead of _info
self.append_request_ids(new.request_ids)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return self._info == other._info
def __ne__(self, other):
return not self.__eq__(other)
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val
def to_dict(self):
return copy.deepcopy(self._info)
class ListWithMeta(list, RequestIdMixin):
def __init__(self, values, resp):
super(ListWithMeta, self).__init__(values)
self.request_ids_setup()
self.append_request_ids(resp)
class DictWithMeta(dict, RequestIdMixin):
def __init__(self, values, resp):
super(DictWithMeta, self).__init__(values)
self.request_ids_setup()
self.append_request_ids(resp)
class TupleWithMeta(tuple, RequestIdMixin):
def __new__(cls, values, resp):
return super(TupleWithMeta, cls).__new__(cls, values)
def __init__(self, values, resp):
self.request_ids_setup()
self.append_request_ids(resp)
class StrWithMeta(str, RequestIdMixin):
def __new__(cls, value, resp):
return super(StrWithMeta, cls).__new__(cls, value)
def __init__(self, values, resp):
self.request_ids_setup()
self.append_request_ids(resp)
class BytesWithMeta(six.binary_type, RequestIdMixin):
def __new__(cls, value, resp):
return super(BytesWithMeta, cls).__new__(cls, value)
def __init__(self, values, resp):
self.request_ids_setup()
self.append_request_ids(resp)
if six.PY2:
class UnicodeWithMeta(six.text_type, RequestIdMixin):
def __new__(cls, value, resp):
return super(UnicodeWithMeta, cls).__new__(cls, value)
def __init__(self, values, resp):
self.request_ids_setup()
self.append_request_ids(resp)

View File

@ -0,0 +1,277 @@
# Copyright 2016 Huawei, 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 six
from nimbleclient.common import base
from nimbleclient.common import exceptions
from nimbleclient.tests.unit import base as test_base
from nimbleclient.tests.unit import fakes
class TestResource(test_base.TestBase):
def test_resource_repr(self):
r = base.Resource(None, dict(foo='bar', baz='spam'))
self.assertEqual('<Resource baz=spam, foo=bar>', repr(r))
def test_getid(self):
self.assertEqual(4, base.getid(4))
class TmpObject(object):
id = 4
self.assertEqual(4, base.getid(TmpObject))
def test_init_with_attribute_info(self):
r = base.Resource(None, dict(foo='bar', baz='spam'))
self.assertTrue(hasattr(r, 'foo'))
self.assertEqual('bar', r.foo)
self.assertTrue(hasattr(r, 'baz'))
self.assertEqual('spam', r.baz)
def test_resource_lazy_getattr(self):
fake_manager = mock.Mock()
return_resource = base.Resource(None, dict(id=mock.sentinel.fake_id,
foo='bar',
name='fake_name'))
fake_manager.get.return_value = return_resource
r = base.Resource(fake_manager,
dict(id=mock.sentinel.fake_id, foo='bar'))
self.assertTrue(hasattr(r, 'foo'))
self.assertEqual('bar', r.foo)
self.assertFalse(r.is_loaded())
# Trigger load
self.assertEqual('fake_name', r.name)
fake_manager.get.assert_called_once_with(mock.sentinel.fake_id)
self.assertTrue(r.is_loaded())
# Missing stuff still fails after a second get
self.assertRaises(AttributeError, getattr, r, 'blahblah')
def test_eq(self):
# Two resources of the same type with the same id: not equal
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
self.assertNotEqual(r1, r2)
# Two resources of different types: never equal
r1 = base.Resource(None, {'id': 1})
r2 = fakes.FaksResource(None, {'id': 1})
self.assertNotEqual(r1, r2)
# Two resources with no ID: equal if their info is equal
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
self.assertEqual(r1, r2)
def test_resource_object_with_request_ids(self):
resp_obj = fakes.create_response_obj_with_header()
r = base.Resource(None, {'name': '1'}, resp=resp_obj)
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, r.request_ids)
def test_resource_object_with_compute_request_ids(self):
resp_obj = fakes.create_response_obj_with_compute_header()
r = base.Resource(None, {'name': '1'}, resp=resp_obj)
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, r.request_ids)
class TestManager(test_base.TestBase):
fake_manager = fakes.create_resource_manager()
@mock.patch.object(fakes.FakeHTTPClient, 'get')
def test_manager_get(self, mock_get):
mock_get.return_value = (fakes.create_response_obj_with_header(),
mock.MagicMock())
fake_resource = fakes.FaksResource(
None, dict(id=fakes.FAKE_RESOURCE_ID,
name=fakes.FAKE_RESOURCE_NAME))
result = self.fake_manager.get(fake_resource)
self.assertIsInstance(result, base.Resource)
self.assertIsInstance(result._info, mock.MagicMock)
self.assertTrue(result.is_loaded())
expect_url = (fakes.FAKE_RESOURCE_ITEM_URL % fakes.FAKE_RESOURCE_ID)
mock_get.assert_called_once_with(expect_url, headers={})
@mock.patch.object(fakes.FakeHTTPClient, 'get')
def test_manager_list(self, mock_get):
mock_get.return_value = (fakes.create_response_obj_with_header(),
mock.MagicMock())
result = self.fake_manager.list()
self.assertIsInstance(result, base.ListWithMeta)
self.assertEqual([], result)
expect_url = fakes.FAKE_RESOURCE_COLLECTION_URL
mock_get.assert_called_once_with(expect_url, headers={})
@mock.patch.object(fakes.FakeHTTPClient, 'patch')
def test_manager_update(self, mock_patch):
mock_patch.return_value = (fakes.create_response_obj_with_header(),
mock.MagicMock())
fake_resource = fakes.FaksResource(
None, dict(id=fakes.FAKE_RESOURCE_ID,
name=fakes.FAKE_RESOURCE_NAME))
result = self.fake_manager.update(fake_resource)
self.assertIsInstance(result, base.Resource)
self.assertIsInstance(result._info, mock.MagicMock)
self.assertFalse(result.is_loaded())
expect_url = (fakes.FAKE_RESOURCE_ITEM_URL % fakes.FAKE_RESOURCE_ID)
mock_patch.assert_called_once_with(expect_url, data=fake_resource,
headers={})
@mock.patch.object(fakes.FakeHTTPClient, 'delete')
def test_manager_delete(self, mock_delete):
mock_delete.return_value = (fakes.create_response_obj_with_header(),
None)
fake_resource = fakes.FaksResource(
None, dict(id=fakes.FAKE_RESOURCE_ID,
name=fakes.FAKE_RESOURCE_NAME))
result = self.fake_manager.delete(fake_resource)
self.assertIsInstance(result, base.TupleWithMeta)
self.assertEqual(tuple(), result)
expect_url = (fakes.FAKE_RESOURCE_ITEM_URL % fakes.FAKE_RESOURCE_ID)
mock_delete.assert_called_once_with(expect_url, headers={})
@mock.patch.object(fakes.FakeHTTPClient, 'post')
def test_manager_create(self, mock_post):
mock_post.return_value = (fakes.create_response_obj_with_header(),
mock.MagicMock())
fake_resource = fakes.FaksResource(
None, dict(id=fakes.FAKE_RESOURCE_ID,
name=fakes.FAKE_RESOURCE_NAME))
result = self.fake_manager.create(fake_resource)
self.assertIsInstance(result, base.Resource)
self.assertIsInstance(result._info, mock.MagicMock)
self.assertFalse(result.is_loaded())
expect_url = fakes.FAKE_RESOURCE_COLLECTION_URL
mock_post.assert_called_once_with(expect_url, data=fake_resource,
headers={})
@mock.patch.object(fakes.FakeHTTPClient, 'get')
def test_manager_find(self, mock_get):
fake_json_body_1 = dict(id=fakes.FAKE_RESOURCE_ID,
name=fakes.FAKE_RESOURCE_NAME)
fake_json_body_2 = dict(id='no_existed_id',
name='no_existed_name')
mock_get.side_effect = [
(fakes.create_response_obj_with_header(),
{'resources': [fake_json_body_1,
fake_json_body_2]}),
(fakes.create_response_obj_with_header(),
fake_json_body_1)
]
result = self.fake_manager.find(id=fakes.FAKE_RESOURCE_ID,
name=fakes.FAKE_RESOURCE_NAME)
self.assertIsInstance(result, base.Resource)
self.assertEqual(fakes.FAKE_RESOURCE_ID, result.id)
self.assertEqual(fakes.FAKE_RESOURCE_NAME, result.name)
self.assertTrue(result.is_loaded())
expect_collection_url = fakes.FAKE_RESOURCE_COLLECTION_URL
expect_item_url = (fakes.FAKE_RESOURCE_ITEM_URL %
fakes.FAKE_RESOURCE_ID)
mock_get.assert_has_calls(
[mock.call(expect_collection_url, headers={}),
mock.call(expect_item_url, headers={})])
@mock.patch.object(fakes.FakeHTTPClient, 'get')
def test_manager_find_no_result(self, mock_get):
mock_get.return_value = (fakes.create_response_obj_with_header(),
{'resources': []})
self.assertRaises(exceptions.NotFound,
self.fake_manager.find,
id=fakes.FAKE_RESOURCE_ID,
name=fakes.FAKE_RESOURCE_NAME)
expect_collection_url = fakes.FAKE_RESOURCE_COLLECTION_URL
mock_get.assert_called_once_with(expect_collection_url, headers={})
@mock.patch.object(fakes.FakeHTTPClient, 'get')
def test_manager_find_more_than_one_result(self, mock_get):
fake_json_body_1 = dict(id=fakes.FAKE_RESOURCE_ID,
name=fakes.FAKE_RESOURCE_NAME)
fake_json_body_2 = copy.deepcopy(fake_json_body_1)
mock_get.return_value = (fakes.create_response_obj_with_header(),
{'resources': [fake_json_body_1,
fake_json_body_2]})
self.assertRaises(exceptions.NoUniqueMatch,
self.fake_manager.find,
id=fakes.FAKE_RESOURCE_ID,
name=fakes.FAKE_RESOURCE_NAME)
expect_collection_url = fakes.FAKE_RESOURCE_COLLECTION_URL
mock_get.assert_called_once_with(expect_collection_url, headers={})
class ListWithMetaTest(test_base.TestBase):
def test_list_with_meta(self):
resp = fakes.create_response_obj_with_header()
obj = base.ListWithMeta([], resp)
self.assertEqual([], obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)
class DictWithMetaTest(test_base.TestBase):
def test_dict_with_meta(self):
resp = fakes.create_response_obj_with_header()
obj = base.DictWithMeta({}, resp)
self.assertEqual({}, obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)
class TupleWithMetaTest(test_base.TestBase):
def test_tuple_with_meta(self):
resp = fakes.create_response_obj_with_header()
expected_tuple = (1, 2)
obj = base.TupleWithMeta(expected_tuple, resp)
self.assertEqual(expected_tuple, obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)
class StrWithMetaTest(test_base.TestBase):
def test_str_with_meta(self):
resp = fakes.create_response_obj_with_header()
obj = base.StrWithMeta('test-str', resp)
self.assertEqual('test-str', obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)
class BytesWithMetaTest(test_base.TestBase):
def test_bytes_with_meta(self):
resp = fakes.create_response_obj_with_header()
obj = base.BytesWithMeta(b'test-bytes', resp)
self.assertEqual(b'test-bytes', obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)
if six.PY2:
class UnicodeWithMetaTest(test_base.TestBase):
def test_unicode_with_meta(self):
resp = fakes.create_response_obj_with_header()
obj = base.UnicodeWithMeta(u'test-unicode', resp)
self.assertEqual(u'test-unicode', obj)
# Check request_ids attribute is added to obj
self.assertTrue(hasattr(obj, 'request_ids'))
self.assertEqual(fakes.FAKE_REQUEST_ID_LIST, obj.request_ids)

View File

@ -14,6 +14,88 @@
# #
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
from requests import Response
from nimbleclient.common import base
# fake request id
FAKE_REQUEST_ID = 'req-0594c66b-6973-405c-ae2c-43fcfc00f2e3'
FAKE_REQUEST_ID_LIST = [FAKE_REQUEST_ID]
# fake resource id
FAKE_RESOURCE_ID = '0594c66b-6973-405c-ae2c-43fcfc00f2e3'
FAKE_RESOURCE_NAME = 'name-0594c66b-6973-405c-ae2c-43fcfc00f2e3'
# fake resource response key
FAKE_RESOURCE_ITEM_URL = '/resources/%s'
FAKE_RESOURCE_COLLECTION_URL = '/resources'
def create_response_obj_with_header():
resp = Response()
resp.headers['x-openstack-request-id'] = FAKE_REQUEST_ID
return resp
def create_response_obj_with_compute_header():
resp = Response()
resp.headers['x-compute-request-id'] = FAKE_REQUEST_ID
return resp
def create_resource_manager():
return FakeManager()
class FakeHTTPClient(object):
def get(self):
pass
def head(self):
pass
def post(self):
pass
def put(self):
pass
def delete(self):
pass
def patch(self):
pass
class FaksResource(base.Resource):
id = 'N/A'
class FakeManager(base.ManagerWithFind):
resource_class = FaksResource
def __init__(self, api=None):
if not api:
api = FakeHTTPClient()
super(FakeManager, self).__init__(api)
def get(self, resource):
return self._get(FAKE_RESOURCE_ITEM_URL % base.getid(resource))
def list(self):
return self._list(FAKE_RESOURCE_COLLECTION_URL,
response_key='resources')
def update(self, resource):
return self._update(FAKE_RESOURCE_ITEM_URL % base.getid(resource),
resource)
def create(self, resource):
return self._create(FAKE_RESOURCE_COLLECTION_URL, resource)
def delete(self, resource):
return self._delete(FAKE_RESOURCE_ITEM_URL % base.getid(resource))
class FakeRaw(object): class FakeRaw(object):

View File

@ -8,6 +8,7 @@ mock>=2.0 # BSD
python-openstackclient>=2.1.0 # Apache-2.0 python-openstackclient>=2.1.0 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD python-subunit>=0.0.18 # Apache-2.0/BSD
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0
reno>=1.8.0 # Apache2 reno>=1.8.0 # Apache2
requests-mock>=1.0 # Apache-2.0 requests-mock>=1.0 # Apache-2.0
sphinx!=1.3b1,<1.3,>=1.2.1 # BSD sphinx!=1.3b1,<1.3,>=1.2.1 # BSD

View File

@ -28,7 +28,7 @@ commands =
sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[testenv:debug] [testenv:debug]
commands = oslo_debug_helper {posargs} commands = oslo_debug_helper -t nimbleclient/tests {posargs}
[flake8] [flake8]
# E123, E125 skipped as they are invalid PEP-8. # E123, E125 skipped as they are invalid PEP-8.