Allow key overrides in create and fetch methods

Provides override  arguments so that subresources that require different envelope/resource keys for requests/responses don't need to override base resource methods in a hacky manner. It also makes it so subresources don't need to "reinvent the wheel" as often by making custom request/response processing. These two patterns are relatively common and are often caused by the need to override these resource/envelope keys. For the sake of consistency across the SDK, this patch moves the logic needed in these cases into the base resource.

Change-Id: I9d928a55539c17ab54d983165afc7912cd0926f8
This commit is contained in:
Reynaldo Bontje 2023-03-21 10:51:56 -05:00
parent 2679719701
commit aab0235065
2 changed files with 171 additions and 24 deletions

View File

@ -1152,7 +1152,13 @@ class Resource(dict):
body['properties'] = props
return body
def _prepare_request_body(self, patch, prepend_key):
def _prepare_request_body(
self,
patch,
prepend_key,
*,
resource_request_key=None,
):
if patch:
if not self._store_unknown_attrs_as_properties:
# Default case
@ -1180,8 +1186,11 @@ class Resource(dict):
self._body.dirty
)
if prepend_key and self.resource_key is not None:
body = {self.resource_key: body}
if prepend_key:
if resource_request_key is not None:
body = {resource_request_key: body}
elif self.resource_key is not None:
body = {self.resource_key: body}
return body
def _prepare_request(
@ -1191,6 +1200,8 @@ class Resource(dict):
patch=False,
base_path=None,
params=None,
*,
resource_request_key=None,
**kwargs,
):
"""Prepare a request to be sent to the server
@ -1210,7 +1221,14 @@ class Resource(dict):
if requires_id is None:
requires_id = self.requires_id
body = self._prepare_request_body(patch, prepend_key)
# Conditionally construct arguments for _prepare_request_body
request_kwargs = {
"patch": patch,
"prepend_key": prepend_key
}
if resource_request_key is not None:
request_kwargs['resource_request_key'] = resource_request_key
body = self._prepare_request_body(**request_kwargs)
# TODO(mordred) Ensure headers have string values better than this
headers = {}
@ -1237,7 +1255,14 @@ class Resource(dict):
return _Request(uri, body, headers)
def _translate_response(self, response, has_body=None, error_message=None):
def _translate_response(
self,
response,
has_body=None,
error_message=None,
*,
resource_response_key=None,
):
"""Given a KSA response, inflate this instance with its data
DELETE operations don't return a body, so only try to work
@ -1254,7 +1279,9 @@ class Resource(dict):
if has_body:
try:
body = response.json()
if self.resource_key and self.resource_key in body:
if resource_response_key and resource_response_key in body:
body = body[resource_response_key]
elif self.resource_key and self.resource_key in body:
body = body[self.resource_key]
# Do not allow keys called "self" through. Glance chose
@ -1412,6 +1439,8 @@ class Resource(dict):
prepend_key=True,
base_path=None,
*,
resource_request_key=None,
resource_response_key=None,
microversion=None,
**params
):
@ -1424,6 +1453,12 @@ class Resource(dict):
True.
:param str base_path: Base part of the URI for creating resources, if
different from :data:`~openstack.resource.Resource.base_path`.
:param str resource_request_key: Overrides the usage of
self.resource_key when prepending a key to the request body.
Ignored if `prepend_key` is false.
:param str resource_response_key: Overrides the usage of
self.resource_key when processing response bodies.
Ignored if `prepend_key` is false.
:param str microversion: API version to override the negotiated one.
:param dict params: Additional params to pass.
:return: This :class:`Resource` instance.
@ -1442,15 +1477,20 @@ class Resource(dict):
else self.create_method == 'PUT'
)
# Construct request arguments.
request_kwargs = {
"requires_id": requires_id,
"prepend_key": prepend_key,
"base_path": base_path,
}
if resource_request_key is not None:
request_kwargs['resource_request_key'] = resource_request_key
if self.create_exclude_id_from_body:
self._body._dirty.discard("id")
if self.create_method == 'PUT':
request = self._prepare_request(
requires_id=requires_id,
prepend_key=prepend_key,
base_path=base_path,
)
request = self._prepare_request(**request_kwargs)
response = session.put(
request.url,
json=request.body,
@ -1459,11 +1499,7 @@ class Resource(dict):
params=params,
)
elif self.create_method == 'POST':
request = self._prepare_request(
requires_id=requires_id,
prepend_key=prepend_key,
base_path=base_path,
)
request = self._prepare_request(**request_kwargs)
response = session.post(
request.url,
json=request.body,
@ -1482,11 +1518,22 @@ class Resource(dict):
else self.create_returns_body
)
self.microversion = microversion
self._translate_response(response, has_body=has_body)
response_kwargs = {
"has_body": has_body,
}
if resource_response_key is not None:
response_kwargs['resource_response_key'] = resource_response_key
self._translate_response(response, **response_kwargs)
# direct comparision to False since we need to rule out None
if self.has_body and self.create_returns_body is False:
# fetch the body if it's required but not returned by create
return self.fetch(session)
fetch_kwargs = {}
if resource_response_key is not None:
fetch_kwargs = \
{'resource_response_key': resource_response_key}
return self.fetch(session, **fetch_kwargs)
return self
@classmethod
@ -1603,6 +1650,7 @@ class Resource(dict):
error_message=None,
skip_cache=False,
*,
resource_response_key=None,
microversion=None,
**params,
):
@ -1618,6 +1666,8 @@ class Resource(dict):
requested object does not exist.
:param bool skip_cache: A boolean indicating whether optional API
cache should be skipped for this invocation.
:param str resource_response_key: Overrides the usage of
self.resource_key when processing the response body.
:param str microversion: API version to override the negotiated one.
:param dict params: Additional parameters that can be consumed.
:return: This :class:`Resource` instance.
@ -1646,6 +1696,8 @@ class Resource(dict):
kwargs['error_message'] = error_message
self.microversion = microversion
if resource_response_key is not None:
kwargs['resource_response_key'] = resource_response_key
self._translate_response(response, **kwargs)
return self

View File

@ -1095,7 +1095,7 @@ class TestResource(base.TestCase):
self.assertRaises(exceptions.InvalidRequest,
sot._prepare_request, requires_id=True)
def test__prepare_request_with_key(self):
def test__prepare_request_with_resource_key(self):
key = "key"
class Test(resource.Resource):
@ -1115,6 +1115,30 @@ class TestResource(base.TestCase):
self.assertEqual({key: {"x": body_value}}, result.body)
self.assertEqual({"y": header_value}, result.headers)
def test__prepare_request_with_override_key(self):
default_key = "key"
override_key = "other_key"
class Test(resource.Resource):
base_path = "/something"
resource_key = default_key
body_attr = resource.Body("x")
header_attr = resource.Header("y")
body_value = "body"
header_value = "header"
sot = Test(body_attr=body_value, header_attr=header_value,
_synchronized=False)
result = sot._prepare_request(
requires_id=False,
prepend_key=True,
resource_request_key=override_key)
self.assertEqual("/something", result.url)
self.assertEqual({override_key: {"x": body_value}}, result.body)
self.assertEqual({"y": header_value}, result.headers)
def test__prepare_request_with_patch(self):
class Test(resource.Resource):
commit_jsonpatch = True
@ -1528,7 +1552,8 @@ class TestResourceActions(base.TestCase):
def _test_create(self, cls, requires_id=False, prepend_key=False,
microversion=None, base_path=None, params=None,
id_marked_dirty=True, explicit_microversion=None):
id_marked_dirty=True, explicit_microversion=None,
resource_request_key=None, resource_response_key=None):
id = "id" if requires_id else None
sot = cls(id=id)
sot._prepare_request = mock.Mock(return_value=self.request)
@ -1540,14 +1565,20 @@ class TestResourceActions(base.TestCase):
kwargs['microversion'] = explicit_microversion
microversion = explicit_microversion
result = sot.create(self.session, prepend_key=prepend_key,
base_path=base_path, **kwargs)
base_path=base_path,
resource_request_key=resource_request_key,
resource_response_key=resource_response_key,
**kwargs)
id_is_dirty = ('id' in sot._body._dirty)
self.assertEqual(id_marked_dirty, id_is_dirty)
prepare_kwargs = {}
if resource_request_key is not None:
prepare_kwargs['resource_request_key'] = resource_request_key
sot._prepare_request.assert_called_once_with(
requires_id=requires_id, prepend_key=prepend_key,
base_path=base_path)
base_path=base_path, **prepare_kwargs)
if requires_id:
self.session.put.assert_called_once_with(
self.request.url,
@ -1560,8 +1591,13 @@ class TestResourceActions(base.TestCase):
microversion=microversion, params=params)
self.assertEqual(sot.microversion, microversion)
sot._translate_response.assert_called_once_with(self.response,
has_body=sot.has_body)
res_kwargs = {}
if resource_response_key is not None:
res_kwargs['resource_response_key'] = resource_response_key
sot._translate_response.assert_called_once_with(
self.response,
has_body=sot.has_body,
**res_kwargs)
self.assertEqual(result, sot)
def test_put_create(self):
@ -1625,6 +1661,49 @@ class TestResourceActions(base.TestCase):
self._test_create(Test, requires_id=False, prepend_key=True)
def test_post_create_override_request_key(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
allow_create = True
create_method = 'POST'
resource_key = 'SomeKey'
self._test_create(
Test,
requires_id=False,
prepend_key=True,
resource_request_key="OtherKey")
def test_post_create_override_response_key(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
allow_create = True
create_method = 'POST'
resource_key = 'SomeKey'
self._test_create(
Test,
requires_id=False,
prepend_key=True,
resource_response_key="OtherKey")
def test_post_create_override_key_both(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
allow_create = True
create_method = 'POST'
resource_key = 'SomeKey'
self._test_create(
Test,
requires_id=False,
prepend_key=True,
resource_request_key="OtherKey",
resource_response_key="SomeOtherKey")
def test_post_create_base_path(self):
class Test(resource.Resource):
service = self.service_name
@ -1658,6 +1737,22 @@ class TestResourceActions(base.TestCase):
self.sot._translate_response.assert_called_once_with(self.response)
self.assertEqual(result, self.sot)
def test_fetch_with_override_key(self):
result = self.sot.fetch(
self.session, resource_response_key="SomeKey")
self.sot._prepare_request.assert_called_once_with(
requires_id=True, base_path=None)
self.session.get.assert_called_once_with(
self.request.url, microversion=None, params={},
skip_cache=False)
self.assertIsNone(self.sot.microversion)
self.sot._translate_response.assert_called_once_with(
self.response,
resource_response_key="SomeKey")
self.assertEqual(result, self.sot)
def test_fetch_with_params(self):
result = self.sot.fetch(self.session, fields='a,b')