diff --git a/keystoneclient/adapter.py b/keystoneclient/adapter.py new file mode 100644 index 000000000..6e5af481a --- /dev/null +++ b/keystoneclient/adapter.py @@ -0,0 +1,115 @@ +# 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 keystoneclient import utils + + +class Adapter(object): + """An instance of a session with local variables. + + A session is a global object that is shared around amongst many clients. It + therefore contains state that is relevant to everyone. There is a lot of + state such as the service type and region_name that are only relevant to a + particular client that is using the session. An adapter provides a wrapper + of client local data around the global session object. + """ + + @utils.positional() + def __init__(self, session, service_type=None, service_name=None, + interface=None, region_name=None, auth=None, + user_agent=None): + """Create a new adapter. + + :param Session session: The session object to wrap. + :param str service_type: The default service_type for URL discovery. + :param str service_name: The default service_name for URL discovery. + :param str interface: The default interface for URL discovery. + :param str region_name: The default region_name for URL discovery. + :param auth.BaseAuthPlugin auth: An auth plugin to use instead of the + session one. + :param str user_agent: The User-Agent string to set. + """ + + self.session = session + self.service_type = service_type + self.service_name = service_name + self.interface = interface + self.region_name = region_name + self.user_agent = user_agent + self.auth = auth + + def request(self, url, method, **kwargs): + endpoint_filter = kwargs.setdefault('endpoint_filter', {}) + + if self.service_type: + endpoint_filter.setdefault('service_type', self.service_type) + if self.service_name: + endpoint_filter.setdefault('service_name', self.service_name) + if self.interface: + endpoint_filter.setdefault('interface', self.interface) + if self.region_name: + endpoint_filter.setdefault('region_name', self.region_name) + + if self.auth: + kwargs.setdefault('auth', self.auth) + if self.user_agent: + kwargs.setdefault('user_agent', self.user_agent) + + return self.session.request(url, method, **kwargs) + + def get(self, url, **kwargs): + return self.request(url, 'GET', **kwargs) + + def head(self, url, **kwargs): + return self.request(url, 'HEAD', **kwargs) + + def post(self, url, **kwargs): + return self.request(url, 'POST', **kwargs) + + def put(self, url, **kwargs): + return self.request(url, 'PUT', **kwargs) + + def patch(self, url, **kwargs): + return self.request(url, 'PATCH', **kwargs) + + def delete(self, url, **kwargs): + return self.request(url, 'DELETE', **kwargs) + + +class LegacyJsonAdapter(Adapter): + """Make something that looks like an old HTTPClient. + + A common case when using an adapter is that we want an interface similar to + the HTTPClients of old which returned the body as JSON as well. + + You probably don't want this if you are starting from scratch. + """ + + def request(self, *args, **kwargs): + headers = kwargs.setdefault('headers', {}) + headers.setdefault('Accept', 'application/json') + + try: + kwargs['json'] = kwargs.pop('body') + except KeyError: + pass + + resp = super(LegacyJsonAdapter, self).request(*args, **kwargs) + + body = None + if resp.text: + try: + body = resp.json() + except ValueError: + pass + + return resp, body diff --git a/keystoneclient/tests/test_session.py b/keystoneclient/tests/test_session.py index f1e616e12..98dd990d8 100644 --- a/keystoneclient/tests/test_session.py +++ b/keystoneclient/tests/test_session.py @@ -10,14 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid import httpretty import mock import requests import six +from keystoneclient import adapter from keystoneclient.auth import base from keystoneclient import exceptions +from keystoneclient.openstack.common import jsonutils from keystoneclient import session as client_session from keystoneclient.tests import utils @@ -314,6 +317,7 @@ class CalledAuthPlugin(base.BaseAuthPlugin): def __init__(self, invalidate=True): self.get_token_called = False self.get_endpoint_called = False + self.endpoint_arguments = {} self.invalidate_called = False self._invalidate = invalidate @@ -323,6 +327,7 @@ class CalledAuthPlugin(base.BaseAuthPlugin): def get_endpoint(self, session, **kwargs): self.get_endpoint_called = True + self.endpoint_arguments = kwargs return self.ENDPOINT def invalidate(self): @@ -506,3 +511,91 @@ class SessionAuthTests(utils.TestCase): self.assertRaises(exceptions.Unauthorized, sess.get, self.TEST_URL, authenticated=True, allow_reauth=False) self.assertFalse(auth.invalidate_called) + + +class AdapterTest(utils.TestCase): + + SERVICE_TYPE = uuid.uuid4().hex + SERVICE_NAME = uuid.uuid4().hex + INTERFACE = uuid.uuid4().hex + REGION_NAME = uuid.uuid4().hex + USER_AGENT = uuid.uuid4().hex + + TEST_URL = CalledAuthPlugin.ENDPOINT + + @httpretty.activate + def test_setting_variables(self): + response = uuid.uuid4().hex + self.stub_url(httpretty.GET, body=response) + + auth = CalledAuthPlugin() + sess = client_session.Session() + adpt = adapter.Adapter(sess, + auth=auth, + service_type=self.SERVICE_TYPE, + service_name=self.SERVICE_NAME, + interface=self.INTERFACE, + region_name=self.REGION_NAME, + user_agent=self.USER_AGENT) + + resp = adpt.get('/') + self.assertEqual(resp.text, response) + + self.assertEqual(self.SERVICE_TYPE, + auth.endpoint_arguments['service_type']) + self.assertEqual(self.SERVICE_NAME, + auth.endpoint_arguments['service_name']) + self.assertEqual(self.INTERFACE, + auth.endpoint_arguments['interface']) + self.assertEqual(self.REGION_NAME, + auth.endpoint_arguments['region_name']) + + self.assertTrue(auth.get_token_called) + self.assertRequestHeaderEqual('User-Agent', self.USER_AGENT) + + @httpretty.activate + def test_legacy_binding(self): + key = uuid.uuid4().hex + val = uuid.uuid4().hex + response = jsonutils.dumps({key: val}) + + self.stub_url(httpretty.GET, body=response) + + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + adpt = adapter.LegacyJsonAdapter(sess, + service_type=self.SERVICE_TYPE, + user_agent=self.USER_AGENT) + + resp, body = adpt.get('/') + self.assertEqual(self.SERVICE_TYPE, + auth.endpoint_arguments['service_type']) + self.assertEqual(resp.text, response) + self.assertEqual(val, body[key]) + + @httpretty.activate + def test_legacy_binding_non_json_resp(self): + response = uuid.uuid4().hex + self.stub_url(httpretty.GET, body=response, content_type='text/html') + + auth = CalledAuthPlugin() + sess = client_session.Session(auth=auth) + adpt = adapter.LegacyJsonAdapter(sess, + service_type=self.SERVICE_TYPE, + user_agent=self.USER_AGENT) + + resp, body = adpt.get('/') + self.assertEqual(self.SERVICE_TYPE, + auth.endpoint_arguments['service_type']) + self.assertEqual(resp.text, response) + self.assertIsNone(body) + + def test_methods(self): + sess = client_session.Session() + adpt = adapter.Adapter(sess) + url = 'http://url' + + for method in ['get', 'head', 'post', 'put', 'patch', 'delete']: + with mock.patch.object(adpt, 'request') as m: + getattr(adpt, method)(url) + m.assert_called_once_with(url, method.upper())