Add possibility to override base_path for resource operations

We currently have multiple places, where we inherit resource just to be
able to request it's Details or so under different url. With 619594
a change has been introduced to improve this by having possibility
to override base_path for a single resource operation.
This was done only for list operations. However there are also cases
when such possibility would be nice for create/update/etc operations.
One example is dry-run for heat, where the endpoint receives "/preview"
part. This is currently implemented by creating
additional resource StackPreview with different base_path. For create
it might be ok, but dry-run stack update would require another url.

With this change a possibility to override base_path for individual
operation on resource object is added. It can be given to proxy
function, which would pass it to resource and use it during URI
calculation.

Change-Id: Id8c8212249cb985d2e47eb1d4fb23ebf19b3871b
This commit is contained in:
Artem Goncharov 2018-12-20 16:21:42 +01:00
parent 841ea76fba
commit 8274409c9e
30 changed files with 278 additions and 104 deletions

View File

@ -51,7 +51,7 @@ class Keypair(resource.Resource):
return super(Keypair, self)._consume_attrs(mapping, attrs)
@classmethod
def list(cls, session, paginated=False):
def list(cls, session, paginated=False, base_path=None):
resp = session.get(cls.base_path,
headers={"Accept": "application/json"})
resp = resp.json()

View File

@ -76,7 +76,8 @@ class Limits(resource.Resource):
absolute = resource.Body("absolute", type=AbsoluteLimits)
rate = resource.Body("rate", type=list, list_type=RateLimit)
def fetch(self, session, requires_id=False, error_message=None):
def fetch(self, session, requires_id=False, error_message=None,
base_path=None):
"""Get the Limits resource.
:param session: The session to use for making this request.
@ -88,4 +89,5 @@ class Limits(resource.Resource):
# TODO(mordred) We shouldn't have to subclass just to declare
# requires_id = False.
return super(Limits, self).fetch(
session=session, requires_id=False, error_message=error_message)
session=session, requires_id=False, error_message=error_message,
base_path=base_path)

View File

@ -138,7 +138,8 @@ class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin):
#: only.
instance_name = resource.Body('OS-EXT-SRV-ATTR:instance_name')
def _prepare_request(self, requires_id=True, prepend_key=True):
def _prepare_request(self, requires_id=True, prepend_key=True,
base_path=None):
request = super(Server, self)._prepare_request(requires_id=requires_id,
prepend_key=prepend_key)

View File

@ -33,7 +33,7 @@ class ServerIP(resource.Resource):
@classmethod
def list(cls, session, paginated=False, server_id=None,
network_label=None, **params):
network_label=None, base_path=None, **params):
url = cls.base_path % {"server_id": server_id}
if network_label is not None:

View File

@ -34,7 +34,8 @@ class User(resource.Resource):
#: The password of the user
password = resource.Body('password')
def _prepare_request(self, requires_id=True, prepend_key=True):
def _prepare_request(self, requires_id=True, prepend_key=True,
base_path=None):
"""Prepare a request for the database service's create call
User.create calls require the resources_key.

View File

@ -43,7 +43,7 @@ class Extension(resource.Resource):
updated_at = resource.Body('updated')
@classmethod
def list(cls, session, paginated=False, **params):
def list(cls, session, paginated=False, base_path=None, **params):
resp = session.get(cls.base_path,
params=params)
resp = resp.json()

View File

@ -27,7 +27,7 @@ class Version(resource.Resource):
updated = resource.Body('updated')
@classmethod
def list(cls, session, paginated=False, **params):
def list(cls, session, paginated=False, base_path=None, **params):
resp = session.get(cls.base_path,
params=params)
resp = resp.json()

View File

@ -278,10 +278,11 @@ class Image(resource.Resource, resource.TagMixin):
return resp.content
def _prepare_request(self, requires_id=None, prepend_key=False,
patch=False):
patch=False, base_path=None):
request = super(Image, self)._prepare_request(requires_id=requires_id,
prepend_key=prepend_key,
patch=patch)
patch=patch,
base_path=base_path)
if patch:
headers = {
'Content-Type': 'application/openstack-images-v2.1-json-patch',

View File

@ -77,8 +77,10 @@ class Secret(resource.Resource):
#: (required if payload is encoded)
payload_content_encoding = resource.Body('payload_content_encoding')
def fetch(self, session, requires_id=True, error_message=None):
request = self._prepare_request(requires_id=requires_id)
def fetch(self, session, requires_id=True,
base_path=None, error_message=None):
request = self._prepare_request(requires_id=requires_id,
base_path=base_path)
response = session.get(request.url).json()

View File

@ -41,9 +41,11 @@ class Quota(resource.Resource):
#: The ID of the project this quota is associated with.
project_id = resource.Body('project_id', alternate_id=True)
def _prepare_request(self, requires_id=True, prepend_key=False):
def _prepare_request(self, requires_id=True,
base_path=None, prepend_key=False):
_request = super(Quota, self)._prepare_request(requires_id,
prepend_key)
prepend_key,
base_path=base_path)
if self.resource_key in _request.body:
_body = _request.body[self.resource_key]
else:

View File

@ -61,9 +61,10 @@ class Claim(resource.Resource):
# Extract claim ID from location
self.id = self.location.split("claims/")[1]
def create(self, session, prepend_key=False):
def create(self, session, prepend_key=False, base_path=None):
request = self._prepare_request(requires_id=False,
prepend_key=prepend_key)
prepend_key=prepend_key,
base_path=base_path)
headers = {
"Client-ID": self.client_id or str(uuid.uuid4()),
"X-PROJECT-ID": self.project_id or session.get_project_id()
@ -80,8 +81,10 @@ class Claim(resource.Resource):
return self
def fetch(self, session, requires_id=True, error_message=None):
request = self._prepare_request(requires_id=requires_id)
def fetch(self, session, requires_id=True,
base_path=None, error_message=None):
request = self._prepare_request(requires_id=requires_id,
base_path=base_path)
headers = {
"Client-ID": self.client_id or str(uuid.uuid4()),
"X-PROJECT-ID": self.project_id or session.get_project_id()
@ -94,8 +97,10 @@ class Claim(resource.Resource):
return self
def commit(self, session, prepend_key=False, has_body=False):
request = self._prepare_request(prepend_key=prepend_key)
def commit(self, session, prepend_key=False, has_body=False,
base_path=None):
request = self._prepare_request(prepend_key=prepend_key,
base_path=base_path)
headers = {
"Client-ID": self.client_id or str(uuid.uuid4()),
"X-PROJECT-ID": self.project_id or session.get_project_id()

View File

@ -67,7 +67,7 @@ class Message(resource.Resource):
return response.json()['resources']
@classmethod
def list(cls, session, paginated=True, **params):
def list(cls, session, paginated=True, base_path=None, **params):
"""This method is a generator which yields message objects.
This is almost the copy of list method of resource.Resource class.
@ -107,8 +107,10 @@ class Message(resource.Resource):
query_params["limit"] = yielded
query_params["marker"] = new_marker
def fetch(self, session, requires_id=True, error_message=None):
request = self._prepare_request(requires_id=requires_id)
def fetch(self, session, requires_id=True,
base_path=None, error_message=None):
request = self._prepare_request(requires_id=requires_id,
base_path=base_path)
headers = {
"Client-ID": self.client_id or str(uuid.uuid4()),
"X-PROJECT-ID": self.project_id or session.get_project_id()

View File

@ -50,9 +50,10 @@ class Queue(resource.Resource):
#: in case keystone auth is not enabled in Zaqar service.
project_id = resource.Header("X-PROJECT-ID")
def create(self, session, prepend_key=True):
def create(self, session, prepend_key=True, base_path=None):
request = self._prepare_request(requires_id=True,
prepend_key=prepend_key)
prepend_key=prepend_key,
base_path=None)
headers = {
"Client-ID": self.client_id or str(uuid.uuid4()),
"X-PROJECT-ID": self.project_id or session.get_project_id()
@ -65,7 +66,7 @@ class Queue(resource.Resource):
return self
@classmethod
def list(cls, session, paginated=False, **params):
def list(cls, session, paginated=False, base_path=None, **params):
"""This method is a generator which yields queue objects.
This is almost the copy of list method of resource.Resource class.
@ -105,8 +106,10 @@ class Queue(resource.Resource):
query_params["limit"] = yielded
query_params["marker"] = new_marker
def fetch(self, session, requires_id=True, error_message=None):
request = self._prepare_request(requires_id=requires_id)
def fetch(self, session, requires_id=True,
base_path=None, error_message=None):
request = self._prepare_request(requires_id=requires_id,
base_path=base_path)
headers = {
"Client-ID": self.client_id or str(uuid.uuid4()),
"X-PROJECT-ID": self.project_id or session.get_project_id()

View File

@ -58,7 +58,7 @@ class Subscription(resource.Resource):
#: authentication is not enabled in Zaqar service.
project_id = resource.Header("X-PROJECT-ID")
def create(self, session, prepend_key=True):
def create(self, session, prepend_key=True, base_path=None):
request = self._prepare_request(requires_id=False,
prepend_key=prepend_key)
headers = {
@ -73,7 +73,7 @@ class Subscription(resource.Resource):
return self
@classmethod
def list(cls, session, paginated=True, **params):
def list(cls, session, paginated=True, base_path=None, **params):
"""This method is a generator which yields subscription objects.
This is almost the copy of list method of resource.Resource class.
@ -113,8 +113,10 @@ class Subscription(resource.Resource):
query_params["limit"] = yielded
query_params["marker"] = new_marker
def fetch(self, session, requires_id=True, error_message=None):
request = self._prepare_request(requires_id=requires_id)
def fetch(self, session, requires_id=True,
base_path=None, error_message=None):
request = self._prepare_request(requires_id=requires_id,
base_path=base_path)
headers = {
"Client-ID": self.client_id or str(uuid.uuid4()),
"X-PROJECT-ID": self.project_id or session.get_project_id()

View File

@ -56,7 +56,8 @@ class Quota(resource.Resource):
#: The maximum amount of security groups you can create. *Type: int*
security_groups = resource.Body('security_group', type=int)
def _prepare_request(self, requires_id=True, prepend_key=False):
def _prepare_request(self, requires_id=True, prepend_key=False,
base_path=None):
_request = super(Quota, self)._prepare_request(requires_id,
prepend_key)
if self.resource_key in _request.body:

View File

@ -109,7 +109,7 @@ class Container(_base.BaseResource):
kwargs.setdefault('name', name)
return Container(_synchronized=True, **kwargs)
def create(self, session, prepend_key=True):
def create(self, session, prepend_key=True, base_path=None):
"""Create a remote resource based on this instance.
:param session: The session to use for making this request.
@ -123,7 +123,7 @@ class Container(_base.BaseResource):
:data:`Resource.allow_create` is not set to ``True``.
"""
request = self._prepare_request(
requires_id=True, prepend_key=prepend_key)
requires_id=True, prepend_key=prepend_key, base_path=base_path)
response = session.put(
request.url, json=request.body, headers=request.headers)

View File

@ -285,8 +285,8 @@ class Object(_base.BaseResource):
session, error_message=error_message, stream=True)
return response.iter_content(chunk_size, decode_unicode=False)
def create(self, session):
request = self._prepare_request()
def create(self, session, base_path=None):
request = self._prepare_request(base_path=base_path)
request.headers['Accept'] = ''
response = session.put(

View File

@ -45,7 +45,8 @@ class SoftwareConfig(resource.Resource):
#: produces.
outputs = resource.Body('outputs')
def create(self, session):
def create(self, session, base_path=None):
# This overrides the default behavior of resource creation because
# heat doesn't accept resource_key in its request.
return super(SoftwareConfig, self).create(session, prepend_key=False)
return super(SoftwareConfig, self).create(session, prepend_key=False,
base_path=base_path)

View File

@ -49,14 +49,14 @@ class SoftwareDeployment(resource.Resource):
#: The date and time when the software deployment resource was created.
updated_at = resource.Body('updated_time')
def create(self, session):
def create(self, session, base_path=None):
# This overrides the default behavior of resource creation because
# heat doesn't accept resource_key in its request.
return super(SoftwareDeployment, self).create(
session, prepend_key=False)
session, prepend_key=False, base_path=base_path)
def commit(self, session):
def commit(self, session, base_path=None):
# This overrides the default behavior of resource creation because
# heat doesn't accept resource_key in its request.
return super(SoftwareDeployment, self).commit(
session, prepend_key=False)
session, prepend_key=False, base_path=base_path)

View File

@ -74,16 +74,17 @@ class Stack(resource.Resource):
#: The ID of the user project created for this stack.
user_project_id = resource.Body('stack_user_project_id')
def create(self, session):
def create(self, session, base_path=None):
# This overrides the default behavior of resource creation because
# heat doesn't accept resource_key in its request.
return super(Stack, self).create(session, prepend_key=False)
return super(Stack, self).create(session, prepend_key=False,
base_path=base_path)
def commit(self, session):
def commit(self, session, base_path=None):
# This overrides the default behavior of resource creation because
# heat doesn't accept resource_key in its request.
return super(Stack, self).commit(session, prepend_key=False,
has_body=False)
has_body=False, base_path=None)
def _action(self, session, body):
"""Perform stack actions"""
@ -94,10 +95,12 @@ class Stack(resource.Resource):
def check(self, session):
return self._action(session, {'check': ''})
def fetch(self, session, requires_id=True, error_message=None):
def fetch(self, session, requires_id=True,
base_path=None, error_message=None):
stk = super(Stack, self).fetch(
session,
requires_id=requires_id,
base_path=base_path,
error_message=error_message)
if stk and stk.status in ['DELETE_COMPLETE', 'ADOPT_COMPLETE']:
raise exceptions.ResourceNotFound(

View File

@ -34,9 +34,9 @@ class StackFiles(resource.Resource):
# Backwards compat
stack_id = id
def fetch(self, session):
def fetch(self, session, base_path=None):
# The stack files response contains a map of filenames and file
# contents.
request = self._prepare_request(requires_id=False)
request = self._prepare_request(requires_id=False, base_path=base_path)
resp = session.get(request.url)
return resp.json()

View File

@ -161,7 +161,7 @@ class Proxy(_adapter.OpenStackSDKAdapter):
return rv
@_check_resource(strict=False)
def _update(self, resource_type, value, **attrs):
def _update(self, resource_type, value, base_path=None, **attrs):
"""Update a resource
:param resource_type: The type of resource to update.
@ -169,6 +169,9 @@ class Proxy(_adapter.OpenStackSDKAdapter):
:param value: The resource to update. This must either be a
:class:`~openstack.resource.Resource` or an id
that corresponds to a resource.
:param str base_path: Base part of the URI for updating resources, if
different from
:data:`~openstack.resource.Resource.base_path`.
:param dict attrs: Attributes to be passed onto the
:meth:`~openstack.resource.Resource.update`
method to be updated. These should correspond
@ -180,13 +183,16 @@ class Proxy(_adapter.OpenStackSDKAdapter):
:rtype: :class:`~openstack.resource.Resource`
"""
res = self._get_resource(resource_type, value, **attrs)
return res.commit(self)
return res.commit(self, base_path=base_path)
def _create(self, resource_type, **attrs):
def _create(self, resource_type, base_path=None, **attrs):
"""Create a resource from attributes
:param resource_type: The type of resource to create.
:type resource_type: :class:`~openstack.resource.Resource`
:param str base_path: Base part of the URI for creating resources, if
different from
:data:`~openstack.resource.Resource.base_path`.
:param path_args: A dict containing arguments for forming the request
URL, if needed.
:param dict attrs: Attributes to be passed onto the
@ -200,10 +206,11 @@ class Proxy(_adapter.OpenStackSDKAdapter):
:rtype: :class:`~openstack.resource.Resource`
"""
res = resource_type.new(**attrs)
return res.create(self)
return res.create(self, base_path=base_path)
@_check_resource(strict=False)
def _get(self, resource_type, value=None, requires_id=True, **attrs):
def _get(self, resource_type, value=None, requires_id=True,
base_path=None, **attrs):
"""Fetch a resource
:param resource_type: The type of resource to get.
@ -211,6 +218,9 @@ class Proxy(_adapter.OpenStackSDKAdapter):
:param value: The value to get. Can be either the ID of a
resource or a :class:`~openstack.resource.Resource`
subclass.
:param str base_path: Base part of the URI for fetching resources, if
different from
:data:`~openstack.resource.Resource.base_path`.
:param dict attrs: Attributes to be passed onto the
:meth:`~openstack.resource.Resource.get`
method. These should correspond
@ -224,11 +234,12 @@ class Proxy(_adapter.OpenStackSDKAdapter):
res = self._get_resource(resource_type, value, **attrs)
return res.fetch(
self, requires_id=requires_id,
self, requires_id=requires_id, base_path=base_path,
error_message="No {resource_type} found for {value}".format(
resource_type=resource_type.__name__, value=value))
def _list(self, resource_type, value=None, paginated=False, **attrs):
def _list(self, resource_type, value=None,
paginated=False, base_path=None, **attrs):
"""List a resource
:param resource_type: The type of resource to delete. This should
@ -241,6 +252,9 @@ class Proxy(_adapter.OpenStackSDKAdapter):
to be returned in one response. When set to
``True``, the resource supports data being
returned across multiple pages.
:param str base_path: Base part of the URI for listing resources, if
different from
:data:`~openstack.resource.Resource.base_path`.
:param dict attrs: Attributes to be passed onto the
:meth:`~openstack.resource.Resource.list` method. These should
correspond to either :class:`~openstack.resource.URI` values
@ -252,9 +266,10 @@ class Proxy(_adapter.OpenStackSDKAdapter):
the ``resource_type``.
"""
res = self._get_resource(resource_type, value, **attrs)
return res.list(self, paginated=paginated, **attrs)
return res.list(self, paginated=paginated,
base_path=base_path, **attrs)
def _head(self, resource_type, value=None, **attrs):
def _head(self, resource_type, value=None, base_path=None, **attrs):
"""Retrieve a resource's header
:param resource_type: The type of resource to retrieve.
@ -263,6 +278,9 @@ class Proxy(_adapter.OpenStackSDKAdapter):
for. Can be either the ID of a resource,
a :class:`~openstack.resource.Resource` subclass,
or ``None``.
:param str base_path: Base part of the URI for heading resources, if
different from
:data:`~openstack.resource.Resource.base_path`.
:param dict attrs: Attributes to be passed onto the
:meth:`~openstack.resource.Resource.head` method.
These should correspond to
@ -272,4 +290,4 @@ class Proxy(_adapter.OpenStackSDKAdapter):
:rtype: :class:`~openstack.resource.Resource`
"""
res = self._get_resource(resource_type, value, **attrs)
return res.head(self)
return res.head(self, base_path=base_path)

View File

@ -884,7 +884,7 @@ class Resource(dict):
return body
def _prepare_request(self, requires_id=None, prepend_key=False,
patch=False):
patch=False, base_path=None):
"""Prepare a request to be sent to the server
Create operations don't require an ID, but all others do,
@ -912,7 +912,9 @@ class Resource(dict):
else:
headers[k] = str(v)
uri = self.base_path % self._uri.attributes
if base_path is None:
base_path = self.base_path
uri = base_path % self._uri.attributes
if requires_id:
if self.id is None:
raise exceptions.InvalidRequest(
@ -1047,7 +1049,7 @@ class Resource(dict):
return actual
def create(self, session, prepend_key=True):
def create(self, session, prepend_key=True, base_path=None):
"""Create a remote resource based on this instance.
:param session: The session to use for making this request.
@ -1055,7 +1057,9 @@ class Resource(dict):
:param prepend_key: A boolean indicating whether the resource_key
should be prepended in a resource creation
request. Default to True.
:param str base_path: Base part of the URI for creating resources, if
different from
:data:`~openstack.resource.Resource.base_path`.
:return: This :class:`Resource` instance.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
:data:`Resource.allow_create` is not set to ``True``.
@ -1067,13 +1071,15 @@ class Resource(dict):
microversion = self._get_microversion_for(session, 'create')
if self.create_method == 'PUT':
request = self._prepare_request(requires_id=True,
prepend_key=prepend_key)
prepend_key=prepend_key,
base_path=base_path)
response = session.put(request.url,
json=request.body, headers=request.headers,
microversion=microversion)
elif self.create_method == 'POST':
request = self._prepare_request(requires_id=False,
prepend_key=prepend_key)
prepend_key=prepend_key,
base_path=base_path)
response = session.post(request.url,
json=request.body, headers=request.headers,
microversion=microversion)
@ -1085,13 +1091,19 @@ class Resource(dict):
self._translate_response(response)
return self
def fetch(self, session, requires_id=True, error_message=None):
def fetch(self, session, requires_id=True,
base_path=None, error_message=None):
"""Get a remote resource based on this instance.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param boolean requires_id: A boolean indicating whether resource ID
should be part of the requested URI.
:param str base_path: Base part of the URI for fetching resources, if
different from
:data:`~openstack.resource.Resource.base_path`.
:param str error_message: An Error message to be returned if
requested object does not exist.
:return: This :class:`Resource` instance.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
:data:`Resource.allow_fetch` is not set to ``True``.
@ -1101,7 +1113,8 @@ class Resource(dict):
if not self.allow_fetch:
raise exceptions.MethodNotSupported(self, "fetch")
request = self._prepare_request(requires_id=requires_id)
request = self._prepare_request(requires_id=requires_id,
base_path=base_path)
session = self._get_session(session)
microversion = self._get_microversion_for(session, 'fetch')
response = session.get(request.url, microversion=microversion)
@ -1113,11 +1126,14 @@ class Resource(dict):
self._translate_response(response, **kwargs)
return self
def head(self, session):
def head(self, session, base_path=None):
"""Get headers from a remote resource based on this instance.
:param session: The session to use for making this request.
:type session: :class:`~keystoneauth1.adapter.Adapter`
:param str base_path: Base part of the URI for fetching resources, if
different from
:data:`~openstack.resource.Resource.base_path`.
:return: This :class:`Resource` instance.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
@ -1128,7 +1144,7 @@ class Resource(dict):
if not self.allow_head:
raise exceptions.MethodNotSupported(self, "head")
request = self._prepare_request()
request = self._prepare_request(base_path=base_path)
session = self._get_session(session)
microversion = self._get_microversion_for(session, 'fetch')
@ -1141,7 +1157,7 @@ class Resource(dict):
return self
def commit(self, session, prepend_key=True, has_body=True,
retry_on_conflict=None):
retry_on_conflict=None, base_path=None):
"""Commit the state of the instance to the remote resource.
:param session: The session to use for making this request.
@ -1152,6 +1168,9 @@ class Resource(dict):
:param bool retry_on_conflict: Whether to enable retries on HTTP
CONFLICT (409). Value of ``None`` leaves
the `Adapter` defaults.
:param str base_path: Base part of the URI for modifying resources, if
different from
:data:`~openstack.resource.Resource.base_path`.
:return: This :class:`Resource` instance.
:raises: :exc:`~openstack.exceptions.MethodNotSupported` if
@ -1173,7 +1192,9 @@ class Resource(dict):
if self.commit_jsonpatch:
kwargs['patch'] = True
request = self._prepare_request(prepend_key=prepend_key, **kwargs)
request = self._prepare_request(prepend_key=prepend_key,
base_path=base_path,
**kwargs)
session = self._get_session(session)
kwargs = {}

View File

@ -130,7 +130,8 @@ class TestMessageProxy(test_proxy_base.TestProxyBase):
def test_subscription_create(self):
self._verify("openstack.message.v2.subscription.Subscription.create",
self.proxy.create_subscription,
method_args=["test_queue"])
method_args=["test_queue"],
expected_kwargs={"base_path": None})
@mock.patch.object(proxy_base.Proxy, '_get_resource')
def test_subscription_get(self, mock_get_resource):
@ -175,7 +176,8 @@ class TestMessageProxy(test_proxy_base.TestProxyBase):
def test_claim_create(self):
self._verify("openstack.message.v2.claim.Claim.create",
self.proxy.create_claim,
method_args=["test_queue"])
method_args=["test_queue"],
expected_kwargs={"base_path": None})
def test_claim_get(self):
self._verify2("openstack.proxy.Proxy._get",

View File

@ -93,7 +93,8 @@ class TestStack(base.TestCase):
res = sot.create(sess)
mock_create.assert_called_once_with(sess, prepend_key=False)
mock_create.assert_called_once_with(sess, prepend_key=False,
base_path=None)
self.assertEqual(mock_create.return_value, res)
@mock.patch.object(resource.Resource, 'commit')
@ -104,7 +105,8 @@ class TestStack(base.TestCase):
res = sot.commit(sess)
mock_commit.assert_called_once_with(sess, prepend_key=False,
has_body=False)
has_body=False,
base_path=None)
self.assertEqual(mock_commit.return_value, res)
def test_check(self):

View File

@ -254,13 +254,22 @@ class TestProxyUpdate(base.TestCase):
self.assertEqual(rv, self.fake_result)
self.res._update.assert_called_once_with(**self.attrs)
self.res.commit.assert_called_once_with(self.sot)
self.res.commit.assert_called_once_with(self.sot, base_path=None)
def test_update_resource_override_base_path(self):
base_path = 'dummy'
rv = self.sot._update(UpdateableResource, self.res,
base_path=base_path, **self.attrs)
self.assertEqual(rv, self.fake_result)
self.res._update.assert_called_once_with(**self.attrs)
self.res.commit.assert_called_once_with(self.sot, base_path=base_path)
def test_update_id(self):
rv = self.sot._update(UpdateableResource, self.fake_id, **self.attrs)
self.assertEqual(rv, self.fake_result)
self.res.commit.assert_called_once_with(self.sot)
self.res.commit.assert_called_once_with(self.sot, base_path=None)
class TestProxyCreate(base.TestCase):
@ -284,7 +293,18 @@ class TestProxyCreate(base.TestCase):
self.assertEqual(rv, self.fake_result)
CreateableResource.new.assert_called_once_with(**attrs)
self.res.create.assert_called_once_with(self.sot)
self.res.create.assert_called_once_with(self.sot, base_path=None)
def test_create_attributes_override_base_path(self):
CreateableResource.new = mock.Mock(return_value=self.res)
base_path = 'dummy'
attrs = {"x": 1, "y": 2, "z": 3}
rv = self.sot._create(CreateableResource, base_path=base_path, **attrs)
self.assertEqual(rv, self.fake_result)
CreateableResource.new.assert_called_once_with(**attrs)
self.res.create.assert_called_once_with(self.sot, base_path=base_path)
class TestProxyGet(base.TestCase):
@ -309,6 +329,7 @@ class TestProxyGet(base.TestCase):
self.res.fetch.assert_called_with(
self.sot, requires_id=True,
base_path=None,
error_message=mock.ANY)
self.assertEqual(rv, self.fake_result)
@ -318,7 +339,7 @@ class TestProxyGet(base.TestCase):
self.res._update.assert_called_once_with(**args)
self.res.fetch.assert_called_with(
self.sot, requires_id=True,
self.sot, requires_id=True, base_path=None,
error_message=mock.ANY)
self.assertEqual(rv, self.fake_result)
@ -327,7 +348,18 @@ class TestProxyGet(base.TestCase):
RetrieveableResource.new.assert_called_with(id=self.fake_id)
self.res.fetch.assert_called_with(
self.sot, requires_id=True,
self.sot, requires_id=True, base_path=None,
error_message=mock.ANY)
self.assertEqual(rv, self.fake_result)
def test_get_base_path(self):
base_path = 'dummy'
rv = self.sot._get(RetrieveableResource, self.fake_id,
base_path=base_path)
RetrieveableResource.new.assert_called_with(id=self.fake_id)
self.res.fetch.assert_called_with(
self.sot, requires_id=True, base_path=base_path,
error_message=mock.ANY)
self.assertEqual(rv, self.fake_result)
@ -354,12 +386,13 @@ class TestProxyList(base.TestCase):
ListableResource.list = mock.Mock()
ListableResource.list.return_value = self.fake_response
def _test_list(self, paginated):
rv = self.sot._list(ListableResource, paginated=paginated, **self.args)
def _test_list(self, paginated, base_path=None):
rv = self.sot._list(ListableResource, paginated=paginated,
base_path=base_path, **self.args)
self.assertEqual(self.fake_response, rv)
ListableResource.list.assert_called_once_with(
self.sot, paginated=paginated, **self.args)
self.sot, paginated=paginated, base_path=base_path, **self.args)
def test_list_paginated(self):
self._test_list(True)
@ -367,6 +400,9 @@ class TestProxyList(base.TestCase):
def test_list_non_paginated(self):
self._test_list(False)
def test_list_override_base_path(self):
self._test_list(False, base_path='dummy')
class TestProxyHead(base.TestCase):
@ -388,12 +424,19 @@ class TestProxyHead(base.TestCase):
def test_head_resource(self):
rv = self.sot._head(HeadableResource, self.res)
self.res.head.assert_called_with(self.sot)
self.res.head.assert_called_with(self.sot, base_path=None)
self.assertEqual(rv, self.fake_result)
def test_head_resource_base_path(self):
base_path = 'dummy'
rv = self.sot._head(HeadableResource, self.res, base_path=base_path)
self.res.head.assert_called_with(self.sot, base_path=base_path)
self.assertEqual(rv, self.fake_result)
def test_head_id(self):
rv = self.sot._head(HeadableResource, self.fake_id)
HeadableResource.new.assert_called_with(id=self.fake_id)
self.res.head.assert_called_with(self.sot)
self.res.head.assert_called_with(self.sot, base_path=None)
self.assertEqual(rv, self.fake_result)

View File

@ -76,7 +76,17 @@ class TestProxyBase(base.TestCase):
else:
self.assertEqual(expected_result, test_method(*method_args,
**method_kwargs))
mocked.assert_called_with(*expected_args, **expected_kwargs)
# Check how the mock was called in detail
(called_args, called_kwargs) = mocked.call_args
self.assertEqual(list(called_args), expected_args)
base_path = expected_kwargs.get('base_path', None)
# NOTE(gtema): if base_path is not in epected_kwargs or empty
# exclude it from the comparison, since some methods might
# still invoke method with None value
if not base_path:
expected_kwargs.pop('base_path', None)
called_kwargs.pop('base_path', None)
self.assertDictEqual(called_kwargs, expected_kwargs)
else:
self.assertEqual(expected_result, test_method())
mocked.assert_called_with(test_method.__self__)
@ -87,7 +97,9 @@ class TestProxyBase(base.TestCase):
the_kwargs = {"x": 1, "y": 2, "z": 3}
method_kwargs = kwargs.pop("method_kwargs", the_kwargs)
expected_args = [resource_type]
expected_kwargs = kwargs.pop("expected_kwargs", the_kwargs)
# Default the_kwargs should be copied, since we might need to extend it
expected_kwargs = kwargs.pop("expected_kwargs", the_kwargs.copy())
expected_kwargs["base_path"] = kwargs.pop("base_path", None)
self._verify2(mock_method, test_method,
expected_result=expected_result,
@ -147,6 +159,7 @@ class TestProxyBase(base.TestCase):
proxy._get(resource_type)
res.fetch.assert_called_once_with(
proxy, requires_id=True,
base_path=None,
error_message=mock.ANY)
def verify_head(self, test_method, resource_type,
@ -216,7 +229,8 @@ class TestProxyBase(base.TestCase):
method_kwargs = kwargs.pop("method_kwargs", {})
method_kwargs.update({"x": 1, "y": 2, "z": 3})
expected_args = kwargs.pop("expected_args", ["resource_or_id"])
expected_kwargs = method_kwargs.copy()
expected_kwargs = kwargs.pop("expected_kwargs", method_kwargs.copy())
expected_kwargs["base_path"] = kwargs.pop("base_path", None)
self._add_path_args_for_verify(path_args, method_args, expected_kwargs,
value=value)

View File

@ -1076,16 +1076,18 @@ class TestResourceActions(base.TestCase):
self.session.get_endpoint_data.return_value = self.endpoint_data
def _test_create(self, cls, requires_id=False, prepend_key=False,
microversion=None):
microversion=None, base_path=None):
id = "id" if requires_id else None
sot = cls(id=id)
sot._prepare_request = mock.Mock(return_value=self.request)
sot._translate_response = mock.Mock()
result = sot.create(self.session, prepend_key=prepend_key)
result = sot.create(self.session, prepend_key=prepend_key,
base_path=base_path)
sot._prepare_request.assert_called_once_with(
requires_id=requires_id, prepend_key=prepend_key)
requires_id=requires_id, prepend_key=prepend_key,
base_path=base_path)
if requires_id:
self.session.put.assert_called_once_with(
self.request.url,
@ -1130,10 +1132,21 @@ class TestResourceActions(base.TestCase):
self._test_create(Test, requires_id=False, prepend_key=True)
def test_post_create_base_path(self):
class Test(resource.Resource):
service = self.service_name
base_path = self.base_path
allow_create = True
create_method = 'POST'
self._test_create(Test, requires_id=False, prepend_key=True,
base_path='dummy')
def test_fetch(self):
result = self.sot.fetch(self.session)
self.sot._prepare_request.assert_called_once_with(requires_id=True)
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)
@ -1154,7 +1167,8 @@ class TestResourceActions(base.TestCase):
result = sot.fetch(self.session)
sot._prepare_request.assert_called_once_with(requires_id=True)
sot._prepare_request.assert_called_once_with(
requires_id=True, base_path=None)
self.session.get.assert_called_once_with(
self.request.url, microversion='1.42')
@ -1165,7 +1179,20 @@ class TestResourceActions(base.TestCase):
def test_fetch_not_requires_id(self):
result = self.sot.fetch(self.session, False)
self.sot._prepare_request.assert_called_once_with(requires_id=False)
self.sot._prepare_request.assert_called_once_with(
requires_id=False, base_path=None)
self.session.get.assert_called_once_with(
self.request.url, microversion=None)
self.sot._translate_response.assert_called_once_with(self.response)
self.assertEqual(result, self.sot)
def test_fetch_base_path(self):
result = self.sot.fetch(self.session, False, base_path='dummy')
self.sot._prepare_request.assert_called_once_with(
requires_id=False,
base_path='dummy')
self.session.get.assert_called_once_with(
self.request.url, microversion=None)
@ -1175,7 +1202,21 @@ class TestResourceActions(base.TestCase):
def test_head(self):
result = self.sot.head(self.session)
self.sot._prepare_request.assert_called_once_with()
self.sot._prepare_request.assert_called_once_with(base_path=None)
self.session.head.assert_called_once_with(
self.request.url,
headers={"Accept": ""},
microversion=None)
self.assertIsNone(self.sot.microversion)
self.sot._translate_response.assert_called_once_with(
self.response, has_body=False)
self.assertEqual(result, self.sot)
def test_head_base_path(self):
result = self.sot.head(self.session, base_path='dummy')
self.sot._prepare_request.assert_called_once_with(base_path='dummy')
self.session.head.assert_called_once_with(
self.request.url,
headers={"Accept": ""},
@ -1199,7 +1240,7 @@ class TestResourceActions(base.TestCase):
result = sot.head(self.session)
sot._prepare_request.assert_called_once_with()
sot._prepare_request.assert_called_once_with(base_path=None)
self.session.head.assert_called_once_with(
self.request.url,
headers={"Accept": ""},
@ -1212,7 +1253,7 @@ class TestResourceActions(base.TestCase):
def _test_commit(self, commit_method='PUT', prepend_key=True,
has_body=True, microversion=None,
commit_args=None, expected_args=None):
commit_args=None, expected_args=None, base_path=None):
self.sot.commit_method = commit_method
# Need to make sot look dirty so we can attempt an update
@ -1220,10 +1261,11 @@ class TestResourceActions(base.TestCase):
self.sot._body.dirty = mock.Mock(return_value={"x": "y"})
self.sot.commit(self.session, prepend_key=prepend_key,
has_body=has_body, **(commit_args or {}))
has_body=has_body, base_path=base_path,
**(commit_args or {}))
self.sot._prepare_request.assert_called_once_with(
prepend_key=prepend_key)
prepend_key=prepend_key, base_path=base_path)
if commit_method == 'PATCH':
self.session.patch.assert_called_once_with(
@ -1252,6 +1294,10 @@ class TestResourceActions(base.TestCase):
self._test_commit(
commit_method='PATCH', prepend_key=False, has_body=False)
def test_commit_base_path(self):
self._test_commit(commit_method='PUT', prepend_key=True, has_body=True,
base_path='dummy')
def test_commit_patch_retry_on_conflict(self):
self._test_commit(
commit_method='PATCH',

View File

@ -50,9 +50,10 @@ class Execution(resource.Resource):
#: The time at which the Execution was updated
updated_at = resource.Body("updated_at")
def create(self, session, prepend_key=True):
def create(self, session, prepend_key=True, base_path=None):
request = self._prepare_request(requires_id=False,
prepend_key=prepend_key)
prepend_key=prepend_key,
base_path=base_path)
request_body = request.body["execution"]
response = session.post(request.url,

View File

@ -46,9 +46,10 @@ class Workflow(resource.Resource):
#: The time at which the workflow was created
updated_at = resource.Body("updated_at")
def create(self, session, prepend_key=True):
def create(self, session, prepend_key=True, base_path=None):
request = self._prepare_request(requires_id=False,
prepend_key=prepend_key)
prepend_key=prepend_key,
base_path=base_path)
headers = {
"Content-Type": 'text/plain'