Add base resource class
This has been describes as closer to the ORM approach whereby the resource contains all the information on how to communicate with a server. It is intended to be a relatively low level interface on which other things may be built. So I expect some helper wrappers will be built at a later time. The manager in this commit is no longer a requirement, but it shows how you can keep the current client pattern of managers and remove the manual handling of session objects. Change-Id: I46678215063c31928c0b372c039ed020bed837c9
This commit is contained in:
parent
40b23d8299
commit
c8105da07a
296
openstack/resource.py
Normal file
296
openstack/resource.py
Normal file
@ -0,0 +1,296 @@
|
||||
# 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 abc
|
||||
import collections
|
||||
|
||||
import six
|
||||
from six.moves.urllib import parse as url_parse
|
||||
|
||||
from openstack import utils
|
||||
|
||||
|
||||
class MethodNotSupported(Exception):
|
||||
"""The resource does not support this operation type."""
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class Resource(collections.MutableMapping):
|
||||
"""A base class that represents a remote resource.
|
||||
|
||||
Attributes of the resource are defined by the responses from the server
|
||||
rather than in code so that we don't have to try and keep up with all
|
||||
possible attributes and extensions. This may be changed in the future.
|
||||
|
||||
For update management we maintain a dirty list so when updating an object
|
||||
only the attributes that have actually been changed are sent to the server.
|
||||
|
||||
There is some support here for lazy loading that needs improvement.
|
||||
"""
|
||||
|
||||
# the singular and plural forms of the key element
|
||||
resource_key = None
|
||||
resources_key = None
|
||||
|
||||
# the base part of the url for this resource
|
||||
base_path = ''
|
||||
|
||||
# The service this belongs to. e.g. 'identity'
|
||||
# (unused, is a session/auth_plugin attribute for determining URL)
|
||||
service = None
|
||||
|
||||
# limit the abilities of a subclass. You should set these to true if your
|
||||
# resource supports that function.
|
||||
allow_create = False
|
||||
allow_retrieve = False
|
||||
allow_update = False
|
||||
allow_delete = False
|
||||
allow_list = False
|
||||
|
||||
def __init__(self, attrs=None, loaded=False):
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
|
||||
self._id = attrs.pop('id', None)
|
||||
self._attrs = attrs
|
||||
self._dirty = set() if loaded else set(attrs.keys())
|
||||
self._loaded = loaded
|
||||
|
||||
##
|
||||
# CONSTRUCTORS
|
||||
##
|
||||
|
||||
@classmethod
|
||||
def new(cls, **kwargs):
|
||||
"""Create a new instance of this resource.
|
||||
|
||||
Internally set flags such that it is marked as not present on the
|
||||
server.
|
||||
"""
|
||||
return cls(kwargs, loaded=False)
|
||||
|
||||
@classmethod
|
||||
def existing(cls, **kwargs):
|
||||
"""Create a new object representation of an existing resource.
|
||||
|
||||
It is marked as an exact replication of a resource present on a server.
|
||||
"""
|
||||
return cls(kwargs, loaded=True)
|
||||
|
||||
##
|
||||
# MUTABLE MAPPING IMPLEMENTATION
|
||||
##
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self._attrs[name]
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
try:
|
||||
orig = self._attrs[name]
|
||||
except KeyError:
|
||||
changed = True
|
||||
else:
|
||||
changed = orig != value
|
||||
|
||||
if changed:
|
||||
self._attrs[name] = value
|
||||
self._dirty.add(name)
|
||||
|
||||
def __delitem__(self, name):
|
||||
del self._attrs[name]
|
||||
self._dirty.add(name)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._attrs)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._attrs)
|
||||
|
||||
##
|
||||
# BASE PROPERTIES/OPERATIONS
|
||||
##
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
# id is read only
|
||||
return self._id
|
||||
|
||||
@id.deleter
|
||||
def id_del(self):
|
||||
self._id = None
|
||||
|
||||
@property
|
||||
def is_dirty(self):
|
||||
return len(self._dirty) > 0
|
||||
|
||||
def _reset_dirty(self):
|
||||
self._dirty = set()
|
||||
|
||||
##
|
||||
# CRUD OPERATIONS
|
||||
##
|
||||
|
||||
@classmethod
|
||||
def create_by_id(cls, session, attrs, r_id=None):
|
||||
if not cls.allow_create:
|
||||
raise MethodNotSupported('create')
|
||||
|
||||
if cls.resource_key:
|
||||
body = {cls.resource_key: attrs}
|
||||
else:
|
||||
body = attrs
|
||||
|
||||
if r_id:
|
||||
url = utils.urljoin(cls.base_path, r_id)
|
||||
resp = cls._http_put(session, url, json=body)
|
||||
else:
|
||||
resp = cls._http_post(session, cls.base_path, json=body)
|
||||
|
||||
resp_body = resp.json()
|
||||
|
||||
if cls.resource_key:
|
||||
resp_body = resp_body[cls.resource_key]
|
||||
|
||||
return resp_body
|
||||
|
||||
def create(self, session):
|
||||
resp_body = self.create_by_id(session, self._attrs, self.id)
|
||||
self._id = resp_body.pop('id')
|
||||
self._reset_dirty()
|
||||
|
||||
@classmethod
|
||||
def get_data_by_id(cls, session, r_id):
|
||||
if not cls.allow_retrieve:
|
||||
raise MethodNotSupported('retrieve')
|
||||
|
||||
url = utils.urljoin(cls.base_path, r_id)
|
||||
body = cls._http_get(session, url).json()
|
||||
|
||||
if cls.resource_key:
|
||||
body = body[cls.resource_key]
|
||||
|
||||
return body
|
||||
|
||||
@classmethod
|
||||
def get_by_id(cls, session, r_id):
|
||||
body = cls.get_data_by_id(session, r_id)
|
||||
return cls.existing(**body)
|
||||
|
||||
def get(self, session):
|
||||
body = self.get_data_by_id(session, self.id)
|
||||
self._attrs.update(body)
|
||||
self._loaded = True
|
||||
|
||||
@classmethod
|
||||
def update_by_id(cls, session, r_id, attrs):
|
||||
if not cls.allow_update:
|
||||
raise MethodNotSupported('update')
|
||||
|
||||
if cls.resource_key:
|
||||
body = {cls.resource_key: attrs}
|
||||
else:
|
||||
body = attrs
|
||||
|
||||
url = utils.urljoin(cls.base_path, r_id)
|
||||
resp_body = cls._http_patch(session, url, json=body).json()
|
||||
|
||||
if cls.resource_key:
|
||||
resp_body = resp_body[cls.resource_key]
|
||||
|
||||
return resp_body
|
||||
|
||||
def update(self, session):
|
||||
if not self.is_dirty:
|
||||
return
|
||||
|
||||
dirty_attrs = dict((k, self._attrs[k]) for k in self._dirty)
|
||||
resp_json = self.update_by_id(session, self.id, dirty_attrs)
|
||||
|
||||
try:
|
||||
resp_id = resp_json.pop('id')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
assert resp_id == self.id
|
||||
|
||||
self._reset_dirty()
|
||||
|
||||
@classmethod
|
||||
def delete_by_id(cls, session, r_id):
|
||||
if not cls.allow_delete:
|
||||
raise MethodNotSupported('delete')
|
||||
|
||||
cls._http_delete(session, utils.urljoin(cls.base_path, r_id))
|
||||
|
||||
def delete(self, session):
|
||||
self.delete_by_id(session, self.id)
|
||||
|
||||
@classmethod
|
||||
def list(cls, session, limit=None, marker=None):
|
||||
# NOTE(jamielennox): Is it possible we can return a generator from here
|
||||
# and allow us to keep paging rather than limit and marker?
|
||||
if not cls.allow_list:
|
||||
raise MethodNotSupported('retrieve')
|
||||
|
||||
filters = {}
|
||||
|
||||
if limit:
|
||||
filters['limit'] = limit
|
||||
if marker:
|
||||
filters['marker'] = marker
|
||||
|
||||
url = cls.base_path
|
||||
if filters:
|
||||
url = '%s?%s' % (url, url_parse.urlencode(filters))
|
||||
|
||||
resp_body = cls._http_get(session, url).json()
|
||||
|
||||
if cls.resources_key:
|
||||
resp_body = resp_body[cls.resources_key]
|
||||
|
||||
return [cls.existing(**data) for data in resp_body]
|
||||
|
||||
###
|
||||
# HTTP Operations
|
||||
###
|
||||
# these shouldn't live here long term
|
||||
|
||||
@classmethod
|
||||
def _http_request(cls, session, method, path, **kwargs):
|
||||
headers = kwargs.setdefault('headers', {})
|
||||
headers['Accept'] = 'application/json'
|
||||
|
||||
return session._request(cls.service, path, method, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _http_get(cls, session, url, **kwargs):
|
||||
return cls._http_request(session, 'GET', url, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _http_post(cls, session, url, **kwargs):
|
||||
return cls._http_request(session, 'POST', url, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _http_put(cls, session, url, **kwargs):
|
||||
return cls._http_request(session, 'PUT', url, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _http_patch(cls, session, url, **kwargs):
|
||||
return cls._http_request(session, 'PATCH', url, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _http_delete(cls, session, url, **kwargs):
|
||||
return cls._http_request(session, 'DELETE', url, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _http_head(cls, session, url, **kwargs):
|
||||
return cls._http_request(session, 'HEAD', url, **kwargs)
|
@ -12,6 +12,8 @@
|
||||
|
||||
import logging
|
||||
|
||||
from openstack import utils
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@ -53,7 +55,9 @@ class Session(object):
|
||||
if token:
|
||||
headers['X-Auth-Token'] = token
|
||||
|
||||
url = self.authenticator.get_endpoint(self.transport, service) + path
|
||||
endpoint = self.authenticator.get_endpoint(self.transport, service)
|
||||
url = utils.urljoin(endpoint, path)
|
||||
|
||||
return self.transport.request(method, url, **kwargs)
|
||||
|
||||
def head(self, service, path, **kwargs):
|
||||
|
@ -15,11 +15,15 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import fixtures
|
||||
import httpretty
|
||||
import testtools
|
||||
|
||||
from openstack import utils
|
||||
|
||||
_TRUE_VALUES = ('true', '1', 'yes')
|
||||
|
||||
|
||||
@ -51,3 +55,46 @@ class TestCase(testtools.TestCase):
|
||||
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
|
||||
|
||||
self.log_fixture = self.useFixture(fixtures.FakeLogger())
|
||||
|
||||
|
||||
class TestTransportBase(TestCase):
|
||||
|
||||
TEST_URL = 'http://www.root.url'
|
||||
|
||||
def stub_url(self, method, path=None, base_url=None, **kwargs):
|
||||
if not base_url:
|
||||
base_url = self.TEST_URL
|
||||
|
||||
if isinstance(path, (list, tuple)):
|
||||
base_url = utils.urljoin(base_url, *path)
|
||||
elif path:
|
||||
base_url = utils.urljoin(base_url, path)
|
||||
|
||||
if 'json' in kwargs:
|
||||
json_data = kwargs.pop('json')
|
||||
if json_data is not None:
|
||||
kwargs['body'] = json.dumps(json_data)
|
||||
kwargs['Content-Type'] = 'application/json'
|
||||
|
||||
httpretty.register_uri(method, base_url, **kwargs)
|
||||
|
||||
def assertRequestHeaderEqual(self, name, val):
|
||||
"""Verify that the last request made contains a header and its value
|
||||
|
||||
The request must have already been made and httpretty must have been
|
||||
activated for the request.
|
||||
|
||||
"""
|
||||
headers = httpretty.last_request().headers
|
||||
self.assertEqual(val, headers.get(name))
|
||||
|
||||
def assertResponseOK(self, resp, status=200, body=None):
|
||||
"""Verify the Response object contains expected values
|
||||
|
||||
Tests our defaults for a successful request.
|
||||
"""
|
||||
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertEqual(status, resp.status_code)
|
||||
if body:
|
||||
self.assertEqual(body, resp.text)
|
||||
|
@ -23,3 +23,15 @@ class FakeTransport(mock.Mock):
|
||||
super(FakeTransport, self).__init__()
|
||||
self.request = mock.Mock()
|
||||
self.request.return_value = self.RESPONSE
|
||||
|
||||
|
||||
class FakeAuthenticator(mock.Mock):
|
||||
TOKEN = 'fake_token'
|
||||
ENDPOINT = 'http://www.example.com/endpoint'
|
||||
|
||||
def __init__(self):
|
||||
super(FakeAuthenticator, self).__init__()
|
||||
self.get_token = mock.Mock()
|
||||
self.get_token.return_value = self.TOKEN
|
||||
self.get_endpoint = mock.Mock()
|
||||
self.get_endpoint.return_value = self.ENDPOINT
|
||||
|
152
openstack/tests/test_resource.py
Normal file
152
openstack/tests/test_resource.py
Normal file
@ -0,0 +1,152 @@
|
||||
# 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 httpretty
|
||||
|
||||
from openstack import resource
|
||||
from openstack import session
|
||||
from openstack.tests import base
|
||||
from openstack.tests import fakes
|
||||
from openstack import transport
|
||||
|
||||
fake_name = 'name'
|
||||
fake_id = 99
|
||||
fake_attr1 = 'attr1'
|
||||
fake_attr2 = 'attr2'
|
||||
|
||||
fake_resource = 'fake'
|
||||
fake_resources = 'fakes'
|
||||
fake_path = fake_resources
|
||||
|
||||
fake_data = {'id': fake_id,
|
||||
'name': fake_name,
|
||||
'attr1': fake_attr1,
|
||||
'attr2': fake_attr2}
|
||||
fake_body = {fake_resource: fake_data}
|
||||
|
||||
|
||||
class FakeResource(resource.Resource):
|
||||
|
||||
resource_key = fake_resource
|
||||
resources_key = fake_path
|
||||
base_path = '/%s' % fake_path
|
||||
|
||||
allow_create = allow_retrieve = allow_update = True
|
||||
allow_delete = allow_list = True
|
||||
|
||||
|
||||
class ResourceTests(base.TestTransportBase):
|
||||
|
||||
TEST_URL = fakes.FakeAuthenticator.ENDPOINT
|
||||
|
||||
def setUp(self):
|
||||
super(ResourceTests, self).setUp()
|
||||
self.transport = transport.Transport()
|
||||
self.auth = fakes.FakeAuthenticator()
|
||||
self.session = session.Session(self.transport, self.auth)
|
||||
|
||||
@httpretty.activate
|
||||
def test_create(self):
|
||||
self.stub_url(httpretty.POST, path=fake_path, json=fake_body)
|
||||
|
||||
obj = FakeResource.new(name=fake_name,
|
||||
attr1=fake_attr1, attr2=fake_attr2)
|
||||
obj.create(self.session)
|
||||
self.assertFalse(obj.is_dirty)
|
||||
|
||||
last_req = httpretty.last_request().parsed_body[fake_resource]
|
||||
|
||||
self.assertEqual(3, len(last_req))
|
||||
self.assertEqual(fake_name, last_req['name'])
|
||||
self.assertEqual(fake_attr1, last_req['attr1'])
|
||||
self.assertEqual(fake_attr2, last_req['attr2'])
|
||||
|
||||
self.assertEqual(fake_id, obj.id)
|
||||
self.assertEqual(fake_name, obj['name'])
|
||||
self.assertEqual(fake_attr1, obj['attr1'])
|
||||
self.assertEqual(fake_attr2, obj['attr2'])
|
||||
|
||||
@httpretty.activate
|
||||
def test_get(self):
|
||||
self.stub_url(httpretty.GET, path=[fake_path, fake_id], json=fake_body)
|
||||
obj = FakeResource.get_by_id(self.session, fake_id)
|
||||
|
||||
self.assertEqual(fake_name, obj['name'])
|
||||
self.assertEqual(fake_id, obj.id)
|
||||
self.assertEqual(fake_attr1, obj['attr1'])
|
||||
self.assertEqual(fake_attr2, obj['attr2'])
|
||||
|
||||
@httpretty.activate
|
||||
def test_update(self):
|
||||
new_attr1 = 'attr5'
|
||||
fake_body1 = fake_body.copy()
|
||||
fake_body1[fake_resource]['attr1'] = new_attr1
|
||||
|
||||
self.stub_url(httpretty.POST, path=fake_path, json=fake_body1)
|
||||
self.stub_url(httpretty.PATCH,
|
||||
path=[fake_path, fake_id],
|
||||
json=fake_body)
|
||||
|
||||
obj = FakeResource.new(name=fake_name,
|
||||
attr1=new_attr1,
|
||||
attr2=fake_attr2)
|
||||
obj.create(self.session)
|
||||
self.assertFalse(obj.is_dirty)
|
||||
self.assertEqual(new_attr1, obj['attr1'])
|
||||
|
||||
obj['attr1'] = fake_attr1
|
||||
self.assertTrue(obj.is_dirty)
|
||||
|
||||
obj.update(self.session)
|
||||
self.assertFalse(obj.is_dirty)
|
||||
|
||||
last_req = httpretty.last_request().parsed_body[fake_resource]
|
||||
self.assertEqual(1, len(last_req))
|
||||
self.assertEqual(fake_attr1, last_req['attr1'])
|
||||
|
||||
self.assertEqual(fake_id, obj.id)
|
||||
self.assertEqual(fake_name, obj['name'])
|
||||
self.assertEqual(fake_attr1, obj['attr1'])
|
||||
self.assertEqual(fake_attr2, obj['attr2'])
|
||||
|
||||
@httpretty.activate
|
||||
def test_delete(self):
|
||||
self.stub_url(httpretty.GET, path=[fake_path, fake_id], json=fake_body)
|
||||
self.stub_url(httpretty.DELETE, [fake_path, fake_id])
|
||||
obj = FakeResource.get_by_id(self.session, fake_id)
|
||||
|
||||
obj.delete(self.session)
|
||||
|
||||
last_req = httpretty.last_request()
|
||||
self.assertEqual('DELETE', last_req.method)
|
||||
self.assertEqual('/endpoint/%s/%s' % (fake_path, fake_id),
|
||||
last_req.path)
|
||||
|
||||
@httpretty.activate
|
||||
def test_list(self):
|
||||
results = [fake_data.copy(), fake_data.copy(), fake_data.copy()]
|
||||
for i in range(len(results)):
|
||||
results[i]['id'] = fake_id + i
|
||||
|
||||
self.stub_url(httpretty.GET,
|
||||
path=fake_path,
|
||||
json={fake_resources: results})
|
||||
|
||||
objs = FakeResource.list(self.session, marker='x')
|
||||
|
||||
self.assertIn('marker=x', httpretty.last_request().path)
|
||||
self.assertEqual(3, len(objs))
|
||||
|
||||
for obj in objs:
|
||||
self.assertIn(obj.id, range(fake_id, fake_id + 3))
|
||||
self.assertEqual(fake_name, obj['name'])
|
||||
self.assertIsInstance(obj, FakeResource)
|
@ -10,26 +10,12 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
|
||||
from openstack.auth import service
|
||||
from openstack import session
|
||||
from openstack.tests import base
|
||||
from openstack.tests import fakes
|
||||
|
||||
|
||||
class FakeAuthenticator(mock.Mock):
|
||||
TOKEN = 'fake_token'
|
||||
ENDPOINT = 'http://www.example.com/endpoint'
|
||||
|
||||
def __init__(self):
|
||||
super(FakeAuthenticator, self).__init__()
|
||||
self.get_token = mock.Mock()
|
||||
self.get_token.return_value = self.TOKEN
|
||||
self.get_endpoint = mock.Mock()
|
||||
self.get_endpoint.return_value = self.ENDPOINT
|
||||
|
||||
|
||||
class TestSession(base.TestCase):
|
||||
|
||||
TEST_PATH = '/test/path'
|
||||
@ -37,7 +23,7 @@ class TestSession(base.TestCase):
|
||||
def setUp(self):
|
||||
super(TestSession, self).setUp()
|
||||
self.xport = fakes.FakeTransport()
|
||||
self.auth = FakeAuthenticator()
|
||||
self.auth = fakes.FakeAuthenticator()
|
||||
self.serv = service.ServiceIdentifier('identity')
|
||||
self.sess = session.Session(self.xport, self.auth)
|
||||
self.expected = {'headers': {'X-Auth-Token': self.auth.TOKEN}}
|
||||
|
@ -23,7 +23,6 @@ from openstack.tests import base
|
||||
from openstack import transport
|
||||
|
||||
|
||||
fake_url = 'http://www.root.url'
|
||||
fake_request = 'Now is the time...'
|
||||
fake_response = 'for the quick brown fox...'
|
||||
fake_redirect = 'redirect text'
|
||||
@ -41,48 +40,13 @@ fake_record2 = {
|
||||
}
|
||||
|
||||
|
||||
class TestTransportBase(base.TestCase):
|
||||
|
||||
def stub_url(self, method, base_url=None, **kwargs):
|
||||
if not base_url:
|
||||
base_url = fake_url
|
||||
|
||||
if 'json' in kwargs:
|
||||
json_data = kwargs.pop('json')
|
||||
if json_data is not None:
|
||||
kwargs['body'] = json.dumps(json_data)
|
||||
kwargs['Content-Type'] = 'application/json'
|
||||
|
||||
httpretty.register_uri(method, base_url, **kwargs)
|
||||
|
||||
def assertRequestHeaderEqual(self, name, val):
|
||||
"""Verify that the last request made contains a header and its value
|
||||
|
||||
The request must have already been made and httpretty must have been
|
||||
activated for the request.
|
||||
|
||||
"""
|
||||
headers = httpretty.last_request().headers
|
||||
self.assertEqual(val, headers.get(name))
|
||||
|
||||
def assertResponseOK(self, resp, status=200, body=fake_response):
|
||||
"""Verify the Response object contains expected values
|
||||
|
||||
Tests our defaults for a successful request.
|
||||
"""
|
||||
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertEqual(status, resp.status_code)
|
||||
self.assertEqual(body, resp.text)
|
||||
|
||||
|
||||
class TestTransport(TestTransportBase):
|
||||
class TestTransport(base.TestTransportBase):
|
||||
|
||||
@httpretty.activate
|
||||
def test_request(self):
|
||||
self.stub_url(httpretty.GET, body=fake_response)
|
||||
sess = transport.Transport()
|
||||
resp = sess.request('GET', fake_url)
|
||||
resp = sess.request('GET', self.TEST_URL)
|
||||
self.assertEqual(httpretty.GET, httpretty.last_request().method)
|
||||
self.assertResponseOK(resp, body=fake_response)
|
||||
|
||||
@ -90,7 +54,7 @@ class TestTransport(TestTransportBase):
|
||||
def test_request_json(self):
|
||||
self.stub_url(httpretty.GET, json=fake_record1)
|
||||
sess = transport.Transport()
|
||||
resp = sess.request('GET', fake_url)
|
||||
resp = sess.request('GET', self.TEST_URL)
|
||||
self.assertEqual(httpretty.GET, httpretty.last_request().method)
|
||||
self.assertResponseOK(resp, body=json.dumps(fake_record1))
|
||||
self.assertEqual(fake_record1, resp.json())
|
||||
@ -99,7 +63,7 @@ class TestTransport(TestTransportBase):
|
||||
def test_delete(self):
|
||||
self.stub_url(httpretty.DELETE, body=fake_response)
|
||||
sess = transport.Transport()
|
||||
resp = sess.delete(fake_url)
|
||||
resp = sess.delete(self.TEST_URL)
|
||||
self.assertEqual(httpretty.DELETE, httpretty.last_request().method)
|
||||
self.assertResponseOK(resp, body=fake_response)
|
||||
|
||||
@ -107,7 +71,7 @@ class TestTransport(TestTransportBase):
|
||||
def test_get(self):
|
||||
self.stub_url(httpretty.GET, body=fake_response)
|
||||
sess = transport.Transport()
|
||||
resp = sess.get(fake_url)
|
||||
resp = sess.get(self.TEST_URL)
|
||||
self.assertEqual(httpretty.GET, httpretty.last_request().method)
|
||||
self.assertResponseOK(resp, body=fake_response)
|
||||
|
||||
@ -115,7 +79,7 @@ class TestTransport(TestTransportBase):
|
||||
def test_head(self):
|
||||
self.stub_url(httpretty.HEAD, body=fake_response)
|
||||
sess = transport.Transport()
|
||||
resp = sess.head(fake_url)
|
||||
resp = sess.head(self.TEST_URL)
|
||||
self.assertEqual(httpretty.HEAD, httpretty.last_request().method)
|
||||
self.assertResponseOK(resp, body='')
|
||||
|
||||
@ -123,7 +87,7 @@ class TestTransport(TestTransportBase):
|
||||
def test_options(self):
|
||||
self.stub_url(httpretty.OPTIONS, body=fake_response)
|
||||
sess = transport.Transport()
|
||||
resp = sess.options(fake_url)
|
||||
resp = sess.options(self.TEST_URL)
|
||||
self.assertEqual(httpretty.OPTIONS, httpretty.last_request().method)
|
||||
self.assertResponseOK(resp, body=fake_response)
|
||||
|
||||
@ -131,7 +95,7 @@ class TestTransport(TestTransportBase):
|
||||
def test_patch(self):
|
||||
self.stub_url(httpretty.PATCH, body=fake_response)
|
||||
sess = transport.Transport()
|
||||
resp = sess.patch(fake_url, json=fake_record2)
|
||||
resp = sess.patch(self.TEST_URL, json=fake_record2)
|
||||
self.assertEqual(httpretty.PATCH, httpretty.last_request().method)
|
||||
self.assertEqual(
|
||||
json.dumps(fake_record2),
|
||||
@ -143,7 +107,7 @@ class TestTransport(TestTransportBase):
|
||||
def test_post(self):
|
||||
self.stub_url(httpretty.POST, body=fake_response)
|
||||
sess = transport.Transport()
|
||||
resp = sess.post(fake_url, json=fake_record2)
|
||||
resp = sess.post(self.TEST_URL, json=fake_record2)
|
||||
self.assertEqual(httpretty.POST, httpretty.last_request().method)
|
||||
self.assertEqual(
|
||||
json.dumps(fake_record2),
|
||||
@ -156,7 +120,7 @@ class TestTransport(TestTransportBase):
|
||||
self.stub_url(httpretty.PUT, body=fake_response)
|
||||
sess = transport.Transport()
|
||||
|
||||
resp = sess.put(fake_url, data=fake_request)
|
||||
resp = sess.put(self.TEST_URL, data=fake_request)
|
||||
self.assertEqual(httpretty.PUT, httpretty.last_request().method)
|
||||
self.assertEqual(
|
||||
fake_request,
|
||||
@ -164,7 +128,7 @@ class TestTransport(TestTransportBase):
|
||||
)
|
||||
self.assertResponseOK(resp, body=fake_response)
|
||||
|
||||
resp = sess.put(fake_url, json=fake_record2)
|
||||
resp = sess.put(self.TEST_URL, json=fake_record2)
|
||||
self.assertEqual(httpretty.PUT, httpretty.last_request().method)
|
||||
self.assertEqual(
|
||||
json.dumps(fake_record2),
|
||||
@ -177,39 +141,39 @@ class TestTransport(TestTransportBase):
|
||||
self.stub_url(httpretty.GET, body=fake_response)
|
||||
sess = transport.Transport()
|
||||
|
||||
resp = sess.get(fake_url)
|
||||
resp = sess.get(self.TEST_URL)
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual(
|
||||
'User-Agent',
|
||||
transport.DEFAULT_USER_AGENT,
|
||||
)
|
||||
|
||||
resp = sess.get(fake_url, headers={'User-Agent': None})
|
||||
resp = sess.get(self.TEST_URL, headers={'User-Agent': None})
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', None)
|
||||
|
||||
resp = sess.get(fake_url, user_agent=None)
|
||||
resp = sess.get(self.TEST_URL, user_agent=None)
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', None)
|
||||
|
||||
resp = sess.get(fake_url, headers={'User-Agent': ''})
|
||||
resp = sess.get(self.TEST_URL, headers={'User-Agent': ''})
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', '')
|
||||
|
||||
resp = sess.get(fake_url, user_agent='')
|
||||
resp = sess.get(self.TEST_URL, user_agent='')
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', '')
|
||||
|
||||
resp = sess.get(fake_url, headers={'User-Agent': 'new-agent'})
|
||||
resp = sess.get(self.TEST_URL, headers={'User-Agent': 'new-agent'})
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', 'new-agent')
|
||||
|
||||
resp = sess.get(fake_url, user_agent='new-agent')
|
||||
resp = sess.get(self.TEST_URL, user_agent='new-agent')
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', 'new-agent')
|
||||
|
||||
resp = sess.get(
|
||||
fake_url,
|
||||
self.TEST_URL,
|
||||
headers={'User-Agent': 'new-agent'},
|
||||
user_agent=None,
|
||||
)
|
||||
@ -217,7 +181,7 @@ class TestTransport(TestTransportBase):
|
||||
self.assertRequestHeaderEqual('User-Agent', None)
|
||||
|
||||
resp = sess.get(
|
||||
fake_url,
|
||||
self.TEST_URL,
|
||||
headers={'User-Agent': None},
|
||||
user_agent='overrides-agent',
|
||||
)
|
||||
@ -225,7 +189,7 @@ class TestTransport(TestTransportBase):
|
||||
self.assertRequestHeaderEqual('User-Agent', 'overrides-agent')
|
||||
|
||||
resp = sess.get(
|
||||
fake_url,
|
||||
self.TEST_URL,
|
||||
headers={'User-Agent': 'new-agent'},
|
||||
user_agent='overrides-agent',
|
||||
)
|
||||
@ -237,39 +201,39 @@ class TestTransport(TestTransportBase):
|
||||
self.stub_url(httpretty.GET, body=fake_response)
|
||||
sess = transport.Transport(user_agent=None)
|
||||
|
||||
resp = sess.get(fake_url)
|
||||
resp = sess.get(self.TEST_URL)
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual(
|
||||
'User-Agent',
|
||||
transport.DEFAULT_USER_AGENT,
|
||||
)
|
||||
|
||||
resp = sess.get(fake_url, headers={'User-Agent': None})
|
||||
resp = sess.get(self.TEST_URL, headers={'User-Agent': None})
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', None)
|
||||
|
||||
resp = sess.get(fake_url, user_agent=None)
|
||||
resp = sess.get(self.TEST_URL, user_agent=None)
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', None)
|
||||
|
||||
resp = sess.get(fake_url, headers={'User-Agent': ''})
|
||||
resp = sess.get(self.TEST_URL, headers={'User-Agent': ''})
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', '')
|
||||
|
||||
resp = sess.get(fake_url, user_agent='')
|
||||
resp = sess.get(self.TEST_URL, user_agent='')
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', '')
|
||||
|
||||
resp = sess.get(fake_url, headers={'User-Agent': 'new-agent'})
|
||||
resp = sess.get(self.TEST_URL, headers={'User-Agent': 'new-agent'})
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', 'new-agent')
|
||||
|
||||
resp = sess.get(fake_url, user_agent='new-agent')
|
||||
resp = sess.get(self.TEST_URL, user_agent='new-agent')
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', 'new-agent')
|
||||
|
||||
resp = sess.get(
|
||||
fake_url,
|
||||
self.TEST_URL,
|
||||
headers={'User-Agent': 'new-agent'},
|
||||
user_agent=None,
|
||||
)
|
||||
@ -277,7 +241,7 @@ class TestTransport(TestTransportBase):
|
||||
self.assertRequestHeaderEqual('User-Agent', None)
|
||||
|
||||
resp = sess.get(
|
||||
fake_url,
|
||||
self.TEST_URL,
|
||||
headers={'User-Agent': None},
|
||||
user_agent='overrides-agent',
|
||||
)
|
||||
@ -285,7 +249,7 @@ class TestTransport(TestTransportBase):
|
||||
self.assertRequestHeaderEqual('User-Agent', 'overrides-agent')
|
||||
|
||||
resp = sess.get(
|
||||
fake_url,
|
||||
self.TEST_URL,
|
||||
headers={'User-Agent': 'new-agent'},
|
||||
user_agent='overrides-agent',
|
||||
)
|
||||
@ -297,36 +261,36 @@ class TestTransport(TestTransportBase):
|
||||
self.stub_url(httpretty.GET, body=fake_response)
|
||||
sess = transport.Transport(user_agent='test-agent')
|
||||
|
||||
resp = sess.get(fake_url)
|
||||
resp = sess.get(self.TEST_URL)
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', 'test-agent')
|
||||
|
||||
resp = sess.get(fake_url, headers={'User-Agent': None})
|
||||
resp = sess.get(self.TEST_URL, headers={'User-Agent': None})
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', None)
|
||||
|
||||
resp = sess.get(fake_url, user_agent=None)
|
||||
resp = sess.get(self.TEST_URL, user_agent=None)
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', None)
|
||||
|
||||
resp = sess.get(fake_url, headers={'User-Agent': ''})
|
||||
resp = sess.get(self.TEST_URL, headers={'User-Agent': ''})
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', '')
|
||||
|
||||
resp = sess.get(fake_url, user_agent='')
|
||||
resp = sess.get(self.TEST_URL, user_agent='')
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', '')
|
||||
|
||||
resp = sess.get(fake_url, headers={'User-Agent': 'new-agent'})
|
||||
resp = sess.get(self.TEST_URL, headers={'User-Agent': 'new-agent'})
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', 'new-agent')
|
||||
|
||||
resp = sess.get(fake_url, user_agent='new-agent')
|
||||
resp = sess.get(self.TEST_URL, user_agent='new-agent')
|
||||
self.assertTrue(resp.ok)
|
||||
self.assertRequestHeaderEqual('User-Agent', 'new-agent')
|
||||
|
||||
resp = sess.get(
|
||||
fake_url,
|
||||
self.TEST_URL,
|
||||
headers={'User-Agent': 'new-agent'},
|
||||
user_agent=None,
|
||||
)
|
||||
@ -334,7 +298,7 @@ class TestTransport(TestTransportBase):
|
||||
self.assertRequestHeaderEqual('User-Agent', None)
|
||||
|
||||
resp = sess.get(
|
||||
fake_url,
|
||||
self.TEST_URL,
|
||||
headers={'User-Agent': None},
|
||||
user_agent='overrides-agent',
|
||||
)
|
||||
@ -342,7 +306,7 @@ class TestTransport(TestTransportBase):
|
||||
self.assertRequestHeaderEqual('User-Agent', 'overrides-agent')
|
||||
|
||||
resp = sess.get(
|
||||
fake_url,
|
||||
self.TEST_URL,
|
||||
headers={'User-Agent': 'new-agent'},
|
||||
user_agent='overrides-agent',
|
||||
)
|
||||
@ -374,7 +338,7 @@ class TestTransport(TestTransportBase):
|
||||
sess = transport.Transport()
|
||||
self.stub_url(httpretty.GET, status=404)
|
||||
|
||||
resp = sess.get(fake_url)
|
||||
resp = sess.get(self.TEST_URL)
|
||||
self.assertFalse(resp.ok)
|
||||
self.assertEqual(404, resp.status_code)
|
||||
|
||||
@ -383,12 +347,12 @@ class TestTransport(TestTransportBase):
|
||||
sess = transport.Transport()
|
||||
self.stub_url(httpretty.GET, status=500)
|
||||
|
||||
resp = sess.get(fake_url)
|
||||
resp = sess.get(self.TEST_URL)
|
||||
self.assertFalse(resp.ok)
|
||||
self.assertEqual(500, resp.status_code)
|
||||
|
||||
|
||||
class TestTransportDebug(TestTransportBase):
|
||||
class TestTransportDebug(base.TestTransportBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestTransportDebug, self).setUp()
|
||||
@ -410,7 +374,7 @@ class TestTransportDebug(TestTransportBase):
|
||||
'ssh_config_dir': '~/myusername/.ssh',
|
||||
}
|
||||
resp = sess.post(
|
||||
fake_url,
|
||||
self.TEST_URL,
|
||||
headers=headers,
|
||||
params=params,
|
||||
json=fake_record2,
|
||||
@ -440,7 +404,7 @@ class TestTransportDebug(TestTransportBase):
|
||||
self.assertIn(v, self.log_fixture.output)
|
||||
|
||||
|
||||
class TestTransportRedirects(TestTransportBase):
|
||||
class TestTransportRedirects(base.TestTransportBase):
|
||||
|
||||
REDIRECT_CHAIN = [
|
||||
'http://myhost:3445/',
|
||||
@ -476,14 +440,14 @@ class TestTransportRedirects(TestTransportBase):
|
||||
self.setup_redirects()
|
||||
sess = transport.Transport()
|
||||
resp = sess.get(self.REDIRECT_CHAIN[-2])
|
||||
self.assertResponseOK(resp)
|
||||
self.assertResponseOK(resp, body=fake_response)
|
||||
|
||||
@httpretty.activate
|
||||
def test_post_keeps_correct_method(self):
|
||||
self.setup_redirects(method=httpretty.POST, status=301)
|
||||
sess = transport.Transport()
|
||||
resp = sess.post(self.REDIRECT_CHAIN[-2])
|
||||
self.assertResponseOK(resp)
|
||||
self.assertResponseOK(resp, body=fake_response)
|
||||
|
||||
@httpretty.activate
|
||||
def test_redirect_forever(self):
|
||||
|
@ -46,6 +46,7 @@ class Transport(requests.Session):
|
||||
user_agent=None,
|
||||
verify=True,
|
||||
redirect=DEFAULT_REDIRECT_LIMIT,
|
||||
base_url=None,
|
||||
):
|
||||
"""Wraps requests.Session to add some OpenStack-specific features
|
||||
|
||||
@ -76,6 +77,10 @@ class Transport(requests.Session):
|
||||
self.verify = verify
|
||||
self._redirect = redirect
|
||||
|
||||
# NOTE(jamielennox): This is a stub for having auth plugins and
|
||||
# discovery determine the correct base URL
|
||||
self.base_url = base_url
|
||||
|
||||
def request(self, method, url, redirect=None, **kwargs):
|
||||
"""Send a request
|
||||
|
||||
@ -100,6 +105,11 @@ class Transport(requests.Session):
|
||||
|
||||
headers = kwargs.setdefault('headers', {})
|
||||
|
||||
# NOTE(jamielennox): This is a stub for having auth plugins and
|
||||
# discovery determine the correct base URL
|
||||
if self.base_url:
|
||||
url = "%s/%s" % (self.base_url.rstrip('/'), url.lstrip('/'))
|
||||
|
||||
# JSON-encode the data in json arg if present
|
||||
# Overwrites any existing 'data' value
|
||||
json_data = kwargs.pop('json', None)
|
||||
|
21
openstack/utils.py
Normal file
21
openstack/utils.py
Normal file
@ -0,0 +1,21 @@
|
||||
# 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.
|
||||
|
||||
|
||||
def urljoin(*args):
|
||||
"""A custom version of urljoin that simply joins strings into a path.
|
||||
|
||||
The real urljoin takes into account web semantics like when joining a url
|
||||
like /path this should be joined to http://host/path as it is an anchored
|
||||
link. We generally won't care about that in client.
|
||||
"""
|
||||
return '/'.join(str(a).strip('/') for a in args)
|
Loading…
Reference in New Issue
Block a user