Remove resource and proxy

Step one in the final removal of resource/proxy v1. Doing it as a
two commit chain just as a sanity check.

All of the services have been migrated to Resource2/Proxy2. Before we
rename those back to Resource/Proxy, delete the v1 versions and make
sure everything is still happy.

The next patch will do the renames.

Change-Id: I254a7474236ea4959db1bd00b397320b0ca27387
This commit is contained in:
Monty Taylor 2018-01-12 17:43:25 -06:00
parent 3326bb01aa
commit aebf019660
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
9 changed files with 12 additions and 3260 deletions

View File

@ -1,7 +1,7 @@
# Apache 2 header omitted for brevity
from openstack.fake import fake_service
from openstack import resource
from openstack import resource2 as resource
class Fake(resource.Resource):
@ -9,21 +9,20 @@ class Fake(resource.Resource):
resources_key = "resources"
base_path = "/fake"
service = fake_service.FakeService()
id_attribute = "name"
allow_create = True
allow_retrieve = True
allow_get = True
allow_update = True
allow_delete = True
allow_list = True
allow_head = True
#: The transaction date and time.
timestamp = resource.prop("x-timestamp")
timestamp = resource.Header("x-timestamp")
#: The name of this resource.
name = resource.prop("name")
name = resource.Body("name", alternate_id=True)
#: The value of the resource. Also available in headers.
value = resource.prop("value", alias="x-resource-value")
value = resource.Body("value", alias="x-resource-value")
#: Is this resource cool? If so, set it to True.
#: This is a multi-line comment about cool stuff.
cool = resource.prop("cool", type=bool)
cool = resource.Body("cool", type=bool)

View File

@ -1,3 +1,5 @@
.. TODO(shade) Update this guide.
Creating a New Resource
=======================
@ -108,14 +110,6 @@ service is called in the service catalog. When a request is made for this
resource, the Session now knows how to construct the appropriate URL using
this ``FakeService`` instance.
``id_attribute``
****************
*Line 12* specifies that this resource uses a different identifier than
the default of ``id``. While IDs are used internally, such as for creating
request URLs to interact with an individual resource, they are exposed for
consistency so users always have one place to find the resource's identity.
Supported Operations
--------------------
@ -136,7 +130,7 @@ value by setting it to ``True``:
+----------------------------------------------+----------------+
| :class:`~openstack.resource.Resource.list` | allow_list |
+----------------------------------------------+----------------+
| :class:`~openstack.resource.Resource.get` | allow_retrieve |
| :class:`~openstack.resource.Resource.get` | allow_get |
+----------------------------------------------+----------------+
| :class:`~openstack.resource.Resource.update` | allow_update |
+----------------------------------------------+----------------+
@ -148,6 +142,8 @@ used for ``Resource.update``.
Properties
----------
.. TODO(shade) Especially this section
The way resource classes communicate values between the user and the server
are :class:`~openstack.resource.prop` objects. These act similarly to Python's
built-in property objects, but they share only the name - they're not the same.

View File

@ -137,7 +137,6 @@ can be customized.
.. toctree::
:maxdepth: 1
resource
resource2
service_filter
utils

View File

@ -1,39 +0,0 @@
**NOTE: This module is being phased out in favor of**
:mod:`openstack.resource2`. **Once all services have been moved over to use
resource2, that module will take this `resource` name.**
Resource
========
.. automodule:: openstack.resource
The prop class
--------------
.. autoclass:: openstack.resource.prop
:members:
The Resource class
------------------
.. autoclass:: openstack.resource.Resource
:members:
:member-order: bysource
How path_args are used
**********************
As :class:`Resource`\s often contain compound :data:`Resource.base_path`\s,
meaning the path is constructed from more than just that string, the
various request methods need a way to fill in the missing parts.
That's where ``path_args`` come in.
For example::
class ServerIP(resource.Resource):
base_path = "/servers/%(server_id)s/ips"
Making a GET request to obtain server IPs requires the ID of the server
to check. This is handled by passing ``{"server_id": "12345"}`` as the
``path_args`` argument when calling :meth:`Resource.get_by_id`. From there,
the method uses Python's string interpolation to fill in the ``server_id``
piece of the URL, and then makes the request.

View File

@ -1,286 +0,0 @@
# 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 openstack import _adapter
from openstack import exceptions
from openstack import resource
# The _check_resource decorator is used on BaseProxy methods to ensure that
# the `actual` argument is in fact the type of the `expected` argument.
# It does so under two cases:
# 1. When strict=False, if and only if `actual` is a Resource instance,
# it is checked to see that it's an instance of the `expected` class.
# This allows `actual` to be other types, such as strings, when it makes
# sense to accept a raw id value.
# 2. When strict=True, `actual` must be an instance of the `expected` class.
def _check_resource(strict=False):
def wrap(method):
def check(self, expected, actual=None, *args, **kwargs):
if (strict and actual is not None and not
isinstance(actual, resource.Resource)):
raise ValueError("A %s must be passed" % expected.__name__)
elif (isinstance(actual, resource.Resource) and not
isinstance(actual, expected)):
raise ValueError("Expected %s but received %s" % (
expected.__name__, actual.__class__.__name__))
return method(self, expected, actual, *args, **kwargs)
return check
return wrap
class BaseProxy(_adapter.OpenStackSDKAdapter):
def _get_resource(self, resource_type, value, path_args=None):
"""Get a resource object to work on
:param resource_type: The type of resource to operate on. This should
be a subclass of
:class:`~openstack.resource.Resource` with a
``from_id`` method.
:param value: The ID of a resource or an object of ``resource_type``
class if using an existing instance, or None to create a
new instance.
:param path_args: A dict containing arguments for forming the request
URL, if needed.
"""
if value is None:
# Create a bare resource
res = resource_type()
elif not isinstance(value, resource_type):
# Create from an ID
args = {resource_type.id_attribute:
resource.Resource.get_id(value)}
res = resource_type.existing(**args)
else:
# An existing resource instance
res = value
# Set any intermediate path arguments, but don't overwrite Nones.
if path_args is not None:
res.update_attrs(ignore_none=True, **path_args)
return res
def _find(self, resource_type, name_or_id, path_args=None,
ignore_missing=True):
"""Find a resource
:param name_or_id: The name or ID of a resource to find.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be
raised when the resource does not exist.
When set to ``True``, None will be returned when
attempting to find a nonexistent resource.
:returns: An instance of ``resource_type`` or None
"""
return resource_type.find(self, name_or_id,
path_args=path_args,
ignore_missing=ignore_missing)
@_check_resource(strict=False)
def _delete(self, resource_type, value, path_args=None,
ignore_missing=True):
"""Delete a resource
:param resource_type: The type of resource to delete. This should
be a :class:`~openstack.resource.Resource`
subclass with a ``from_id`` method.
:param value: The value to delete. Can be either the ID of a
resource or a :class:`~openstack.resource.Resource`
subclass.
:param path_args: A dict containing arguments for forming the request
URL, if needed.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be
raised when the resource does not exist.
When set to ``True``, no exception will be set when
attempting to delete a nonexistent resource.
:returns: The result of the ``delete``
:raises: ``ValueError`` if ``value`` is a
:class:`~openstack.resource.Resource` that doesn't match
the ``resource_type``.
:class:`~openstack.exceptions.ResourceNotFound` when
ignore_missing if ``False`` and a nonexistent resource
is attempted to be deleted.
"""
res = self._get_resource(resource_type, value, path_args)
try:
rv = res.delete(
self,
error_message="No {resource_type} found for {value}".format(
resource_type=resource_type.__name__, value=value))
except exceptions.NotFoundException:
if ignore_missing:
return None
else:
raise
return rv
@_check_resource(strict=False)
def _update(self, resource_type, value, path_args=None, **attrs):
"""Update a resource
:param resource_type: The type of resource to update.
:type resource_type: :class:`~openstack.resource.Resource`
:param value: The resource to update. This must either be a
:class:`~openstack.resource.Resource` or an id
that corresponds to a resource.
:param path_args: A dict containing arguments for forming the request
URL, if needed.
:param **attrs: Attributes to update on a Resource object.
These attributes will be used in conjunction with
``resource_type``.
:returns: The result of the ``update``
:rtype: :class:`~openstack.resource.Resource`
"""
res = self._get_resource(resource_type, value, path_args)
res.update_attrs(attrs)
return res.update(self)
def _create(self, resource_type, path_args=None, **attrs):
"""Create a resource from attributes
:param resource_type: The type of resource to create.
:type resource_type: :class:`~openstack.resource.Resource`
:param path_args: A dict containing arguments for forming the request
URL, if needed.
:param **attrs: Attributes from which to create a Resource object.
These attributes will be used in conjunction with
``resource_type``.
:returns: The result of the ``create``
:rtype: :class:`~openstack.resource.Resource`
"""
res = resource_type.new(**attrs)
if path_args is not None:
res.update_attrs(path_args)
return res.create(self)
@_check_resource(strict=False)
def _get(self, resource_type, value=None, path_args=None, args=None):
"""Get a resource
:param resource_type: The type of resource to get.
:type resource_type: :class:`~openstack.resource.Resource`
:param value: The value to get. Can be either the ID of a
resource or a :class:`~openstack.resource.Resource`
subclass.
:param path_args: A dict containing arguments for forming the request
URL, if needed.
:param args: A optional dict containing arguments that will be
translated into query strings when forming the request URL.
:returns: The result of the ``get``
:rtype: :class:`~openstack.resource.Resource`
"""
res = self._get_resource(resource_type, value, path_args)
return res.get(
self, args=args,
error_message='No {resource} found for {value}'.format(
resource=resource_type.__name__, value=value))
def _list(self, resource_type, value=None, paginated=False,
path_args=None, **query):
"""List a resource
:param resource_type: The type of resource to delete. This should
be a :class:`~openstack.resource.Resource`
subclass with a ``from_id`` method.
:param value: The resource to list. It can be the ID of a resource, or
a :class:`~openstack.resource.Resource` object. When set
to None, a new bare resource is created.
:param bool paginated: When set to ``False``, expect all of the data
to be returned in one response. When set to
``True``, the resource supports data being
returned across multiple pages.
:param path_args: A dictionary containing arguments for use when
forming the request URL for resource retrieval.
:param kwargs **query: Keyword arguments that are sent to the list
method, which are then attached as query
parameters on the request URL.
:returns: A generator of Resource objects.
:raises: ``ValueError`` if ``value`` is a
:class:`~openstack.resource.Resource` that doesn't match
the ``resource_type``.
"""
res = self._get_resource(resource_type, value, path_args)
query = res.convert_ids(query)
return res.list(self, path_args=path_args,
paginated=paginated, params=query)
def _head(self, resource_type, value=None, path_args=None):
"""Retrieve a resource's header
:param resource_type: The type of resource to retrieve.
:type resource_type: :class:`~openstack.resource.Resource`
:param value: The value of a specific resource to retreive headers
for. Can be either the ID of a resource,
a :class:`~openstack.resource.Resource` subclass,
or ``None``.
:param path_args: A dict containing arguments for forming the request
URL, if needed.
:returns: The result of the ``head`` call
:rtype: :class:`~openstack.resource.Resource`
"""
res = self._get_resource(resource_type, value, path_args)
return res.head(self)
def wait_for_status(self, value, status, failures=None, interval=2,
wait=120):
"""Wait for a resource to be in a particular status.
:param value: The resource to wait on to reach the status. The
resource must have a status attribute.
:type value: :class:`~openstack.resource.Resource`
:param status: Desired status of the resource.
:param list failures: Statuses that would indicate the transition
failed such as 'ERROR'.
:param interval: Number of seconds to wait between checks.
:param wait: Maximum number of seconds to wait for the change.
:return: Method returns resource on success.
:raises: :class:`~openstack.exceptions.ResourceTimeout` transition
to status failed to occur in wait seconds.
:raises: :class:`~openstack.exceptions.ResourceFailure` resource
transitioned to one of the failure states.
:raises: :class:`~AttributeError` if the resource does not have a
status attribute
"""
failures = [] if failures is None else failures
return resource.wait_for_status(self, value, status,
failures, interval, wait)
def wait_for_delete(self, value, interval=2, wait=120):
"""Wait for the resource to be deleted.
:param value: The resource to wait on to be deleted.
:type value: :class:`~openstack.resource.Resource`
:param interval: Number of seconds to wait between checks.
:param wait: Maximum number of seconds to wait for the delete.
:return: Method returns resource on success.
:raises: :class:`~openstack.exceptions.ResourceTimeout` transition
to delete failed to occur in wait seconds.
"""
return resource.wait_for_delete(self, value, interval, wait)

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,6 @@ import warnings
import os_service_types
from openstack import _log
from openstack import proxy
from openstack import proxy2
_logger = _log.setup_logging('openstack')
@ -97,17 +96,11 @@ class ServiceDescription(object):
self._validate_proxy_class()
def _validate_proxy_class(self):
if not issubclass(
self.proxy_class, (proxy.BaseProxy, proxy2.BaseProxy)):
if not issubclass(self.proxy_class, proxy2.BaseProxy):
raise TypeError(
"{module}.{proxy_class} must inherit from BaseProxy".format(
module=self.proxy_class.__module__,
proxy_class=self.proxy_class.__name__))
if issubclass(self.proxy_class, proxy.BaseProxy) and self._warn_if_old:
warnings.warn(
"Use of proxy.BaseProxy is not supported."
" Please update to use proxy2.BaseProxy.",
DeprecationWarning)
class OpenStackServiceDescription(ServiceDescription):

View File

@ -1,358 +0,0 @@
# 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 mock
import testtools
from openstack import exceptions
from openstack import proxy
from openstack import resource
class DeleteableResource(resource.Resource):
allow_delete = True
class UpdateableResource(resource.Resource):
allow_update = True
class CreateableResource(resource.Resource):
allow_create = True
class RetrieveableResource(resource.Resource):
allow_retrieve = True
class ListableResource(resource.Resource):
allow_list = True
class HeadableResource(resource.Resource):
allow_head = True
class Test_check_resource(testtools.TestCase):
def setUp(self):
super(Test_check_resource, self).setUp()
def method(self, expected_type, value):
return value
self.sot = mock.Mock()
self.sot.method = method
def _test_correct(self, value):
decorated = proxy._check_resource(strict=False)(self.sot.method)
rv = decorated(self.sot, resource.Resource, value)
self.assertEqual(value, rv)
def test_correct_resource(self):
res = resource.Resource()
self._test_correct(res)
def test_notstrict_id(self):
self._test_correct("abc123-id")
def test_strict_id(self):
decorated = proxy._check_resource(strict=True)(self.sot.method)
self.assertRaisesRegex(ValueError, "A Resource must be passed",
decorated, self.sot, resource.Resource,
"this-is-not-a-resource")
def test_incorrect_resource(self):
class OneType(resource.Resource):
pass
class AnotherType(resource.Resource):
pass
value = AnotherType()
decorated = proxy._check_resource(strict=False)(self.sot.method)
self.assertRaisesRegex(ValueError,
"Expected OneType but received AnotherType",
decorated, self.sot, OneType, value)
class TestProxyDelete(testtools.TestCase):
def setUp(self):
super(TestProxyDelete, self).setUp()
self.session = mock.Mock()
self.fake_id = 1
self.res = mock.Mock(spec=DeleteableResource)
self.res.id = self.fake_id
self.res.delete = mock.Mock()
self.sot = proxy.BaseProxy(self.session)
DeleteableResource.existing = mock.Mock(return_value=self.res)
def test_delete(self):
self.sot._delete(DeleteableResource, self.res)
self.res.delete.assert_called_with(self.sot, error_message=mock.ANY)
self.sot._delete(DeleteableResource, self.fake_id)
DeleteableResource.existing.assert_called_with(id=self.fake_id)
self.res.delete.assert_called_with(self.sot, error_message=mock.ANY)
# Delete generally doesn't return anything, so we will normally
# swallow any return from within a service's proxy, but make sure
# we can still return for any cases where values are returned.
self.res.delete.return_value = self.fake_id
rv = self.sot._delete(DeleteableResource, self.fake_id)
self.assertEqual(rv, self.fake_id)
def test_delete_ignore_missing(self):
self.res.delete.side_effect = exceptions.NotFoundException(
message="test", http_status=404)
rv = self.sot._delete(DeleteableResource, self.fake_id)
self.assertIsNone(rv)
def test_delete_NotFound(self):
self.res.delete.side_effect = exceptions.NotFoundException(
message="test", http_status=404)
self.assertRaisesRegex(
exceptions.NotFoundException, "test",
self.sot._delete, DeleteableResource, self.res,
ignore_missing=False)
def test_delete_HttpException(self):
self.res.delete.side_effect = exceptions.HttpException(
message="test", http_status=500)
self.assertRaises(exceptions.HttpException, self.sot._delete,
DeleteableResource, self.res, ignore_missing=False)
class TestProxyUpdate(testtools.TestCase):
def setUp(self):
super(TestProxyUpdate, self).setUp()
self.session = mock.Mock()
self.fake_id = 1
self.fake_result = "fake_result"
self.res = mock.Mock(spec=UpdateableResource)
self.res.update = mock.Mock(return_value=self.fake_result)
self.res.update_attrs = mock.Mock()
self.sot = proxy.BaseProxy(self.session)
self.attrs = {"x": 1, "y": 2, "z": 3}
UpdateableResource.existing = mock.Mock(return_value=self.res)
def _test_update(self, value):
rv = self.sot._update(UpdateableResource, value, **self.attrs)
self.assertEqual(rv, self.fake_result)
self.res.update_attrs.assert_called_once_with(self.attrs)
self.res.update.assert_called_once_with(self.sot)
def test_update_resource(self):
self._test_update(self.res)
def test_update_id(self):
self._test_update(self.fake_id)
class TestProxyCreate(testtools.TestCase):
def setUp(self):
super(TestProxyCreate, self).setUp()
self.session = mock.Mock()
self.fake_result = "fake_result"
self.res = mock.Mock(spec=CreateableResource)
self.res.create = mock.Mock(return_value=self.fake_result)
self.sot = proxy.BaseProxy(self.session)
def test_create_attributes(self):
CreateableResource.new = mock.Mock(return_value=self.res)
attrs = {"x": 1, "y": 2, "z": 3}
rv = self.sot._create(CreateableResource, **attrs)
self.assertEqual(rv, self.fake_result)
CreateableResource.new.assert_called_once_with(**attrs)
self.res.create.assert_called_once_with(self.sot)
class TestProxyGet(testtools.TestCase):
def setUp(self):
super(TestProxyGet, self).setUp()
self.session = mock.Mock()
self.fake_id = 1
self.fake_name = "fake_name"
self.fake_result = "fake_result"
self.res = mock.Mock(spec=RetrieveableResource)
self.res.id = self.fake_id
self.res.get = mock.Mock(return_value=self.fake_result)
self.sot = proxy.BaseProxy(self.session)
RetrieveableResource.existing = mock.Mock(return_value=self.res)
def test_get_resource(self):
rv = self.sot._get(RetrieveableResource, self.res)
self.res.get.assert_called_with(self.sot, args=None,
error_message=mock.ANY)
self.assertEqual(rv, self.fake_result)
def test_get_resource_with_args(self):
rv = self.sot._get(RetrieveableResource, self.res, args={'K': 'V'})
self.res.get.assert_called_with(
self.sot, args={'K': 'V'},
error_message='No RetrieveableResource found for {res}'.format(
res=str(self.res)))
self.assertEqual(rv, self.fake_result)
def test_get_id(self):
rv = self.sot._get(RetrieveableResource, self.fake_id)
RetrieveableResource.existing.assert_called_with(id=self.fake_id)
self.res.get.assert_called_with(self.sot, args=None,
error_message=mock.ANY)
self.assertEqual(rv, self.fake_result)
def test_get_not_found(self):
self.res.get.side_effect = exceptions.NotFoundException(
message="test", http_status=404)
# TODO(shade) The mock here does not mock the right things, so we're
# not testing the actual exception mechanism.
self.assertRaisesRegex(
exceptions.NotFoundException, "test",
self.sot._get, RetrieveableResource, self.res)
class TestProxyList(testtools.TestCase):
def setUp(self):
super(TestProxyList, self).setUp()
self.session = mock.Mock()
self.fake_a = 1
self.fake_b = 2
self.fake_c = 3
self.fake_resource = resource.Resource.new(id=self.fake_a)
self.fake_response = [resource.Resource()]
self.fake_query = {"a": self.fake_resource, "b": self.fake_b}
self.fake_path_args = {"c": self.fake_c}
self.sot = proxy.BaseProxy(self.session)
ListableResource.list = mock.Mock()
ListableResource.list.return_value = self.fake_response
def _test_list(self, path_args, paginated, **query):
rv = self.sot._list(ListableResource, path_args=path_args,
paginated=paginated, **query)
self.assertEqual(self.fake_response, rv)
ListableResource.list.assert_called_once_with(
self.sot, path_args=path_args, paginated=paginated,
params={'a': self.fake_a, 'b': self.fake_b})
def test_list_paginated(self):
self._test_list(self.fake_path_args, True, **self.fake_query)
def test_list_non_paginated(self):
self._test_list(self.fake_path_args, False, **self.fake_query)
class TestProxyHead(testtools.TestCase):
def setUp(self):
super(TestProxyHead, self).setUp()
self.session = mock.Mock()
self.fake_id = 1
self.fake_name = "fake_name"
self.fake_result = "fake_result"
self.res = mock.Mock(spec=HeadableResource)
self.res.id = self.fake_id
self.res.head = mock.Mock(return_value=self.fake_result)
self.sot = proxy.BaseProxy(self.session)
HeadableResource.existing = mock.Mock(return_value=self.res)
def test_head_resource(self):
rv = self.sot._head(HeadableResource, self.res)
self.res.head.assert_called_with(self.sot)
self.assertEqual(rv, self.fake_result)
def test_head_id(self):
rv = self.sot._head(HeadableResource, self.fake_id)
HeadableResource.existing.assert_called_with(id=self.fake_id)
self.res.head.assert_called_with(self.sot)
self.assertEqual(rv, self.fake_result)
def test_head_no_value(self):
MockHeadResource = mock.Mock(spec=HeadableResource)
instance = mock.Mock()
MockHeadResource.return_value = instance
self.sot._head(MockHeadResource)
MockHeadResource.assert_called_with()
instance.head.assert_called_with(self.sot)
@mock.patch("openstack.resource.wait_for_status")
def test_wait_for(self, mock_wait):
mock_resource = mock.Mock()
mock_wait.return_value = mock_resource
self.sot.wait_for_status(mock_resource, 'ACTIVE')
mock_wait.assert_called_once_with(
self.sot, mock_resource, 'ACTIVE', [], 2, 120)
@mock.patch("openstack.resource.wait_for_status")
def test_wait_for_params(self, mock_wait):
mock_resource = mock.Mock()
mock_wait.return_value = mock_resource
self.sot.wait_for_status(mock_resource, 'ACTIVE', ['ERROR'], 1, 2)
mock_wait.assert_called_once_with(
self.sot, mock_resource, 'ACTIVE', ['ERROR'], 1, 2)
@mock.patch("openstack.resource.wait_for_delete")
def test_wait_for_delete(self, mock_wait):
mock_resource = mock.Mock()
mock_wait.return_value = mock_resource
self.sot.wait_for_delete(mock_resource)
mock_wait.assert_called_once_with(
self.sot, mock_resource, 2, 120)
@mock.patch("openstack.resource.wait_for_delete")
def test_wait_for_delete_params(self, mock_wait):
mock_resource = mock.Mock()
mock_wait.return_value = mock_resource
self.sot.wait_for_delete(mock_resource, 1, 2)
mock_wait.assert_called_once_with(
self.sot, mock_resource, 1, 2)

File diff suppressed because it is too large Load Diff