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:
Jamie Lennox 2014-04-09 13:12:00 +10:00
parent 40b23d8299
commit c8105da07a
9 changed files with 592 additions and 100 deletions

296
openstack/resource.py Normal file
View 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)

View File

@ -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):

View File

@ -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)

View File

@ -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

View 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)

View File

@ -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}}

View File

@ -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):

View File

@ -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
View 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)