diff --git a/marconiclient/common/http.py b/marconiclient/common/http.py index a242011b..e62020dc 100644 --- a/marconiclient/common/http.py +++ b/marconiclient/common/http.py @@ -23,6 +23,10 @@ class Client(object): def __init__(self, *args, **kwargs): self.session = requests.session(*args, **kwargs) + def request(self, *args, **kwargs): + """Raw request.""" + return self.session.request(*args, **kwargs) + def get(self, *args, **kwargs): """Does http GET.""" return self.session.get(*args, **kwargs) diff --git a/marconiclient/tests/transport/__init__.py b/marconiclient/tests/transport/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/marconiclient/tests/transport/api.py b/marconiclient/tests/transport/api.py new file mode 100644 index 00000000..926ed4e0 --- /dev/null +++ b/marconiclient/tests/transport/api.py @@ -0,0 +1,32 @@ +# Copyright (c) 2013 Red Hat, Inc. +# +# 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 marconiclient.transport import api + + +class FakeApi(api.Api): + schema = { + 'test_operation': { + 'ref': 'test/{name}', + 'method': 'GET', + 'properties': { + 'name': {'type': 'string'}, + 'address': {'type': 'string'} + }, + + 'additionalProperties': False, + 'required': ['name'] + } + } diff --git a/marconiclient/transport/api.py b/marconiclient/transport/api.py index 90c1b8d2..7cefc13b 100644 --- a/marconiclient/transport/api.py +++ b/marconiclient/transport/api.py @@ -24,6 +24,26 @@ class Api(object): schema = {} validators = {} + def get_schema(self, operation): + """Returns the schema for an operation + + :param operation: Operation for which params need + to be validated. + :type operation: `six.text_type` + + :returns: Operation's schema + :rtype: dict + + :raises: `errors.InvalidOperation` if the operation + does not exist + """ + try: + return self.schema[operation] + except KeyError: + # TODO(flaper87): gettext support + msg = '{0} is not a valid operation'.format(operation) + raise errors.InvalidOperation(msg) + def validate(self, operation, params): """Validates the request data @@ -44,13 +64,8 @@ class Api(object): """ if operation not in self.validators: - try: - schema = self.schema[operation] - self.validators[operation] = validators.Draft4Validator(schema) - except KeyError: - # TODO(flaper87): gettext support - msg = '{0} is not a valid operation'.format(operation) - raise errors.InvalidOperation(msg) + schema = self.get_schema(operation) + self.validators[operation] = validators.Draft4Validator(schema) try: self.validators[operation].validate(params) diff --git a/marconiclient/transport/http.py b/marconiclient/transport/http.py new file mode 100644 index 00000000..bc987715 --- /dev/null +++ b/marconiclient/transport/http.py @@ -0,0 +1,59 @@ +# Copyright (c) 2013 Red Hat, Inc. +# +# 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 marconiclient.common import http +from marconiclient.transport import base + + +class HttpTransport(base.Transport): + + def __init__(self, conf): + super(HttpTransport, self).__init__(conf) + self.client = http.Client() + + def _prepare(self, request): + if not request.api: + return request.endpoint, 'GET', request + + # TODO(flaper87): Validate if the user + # explicitly wants so. Validation must + # happen before any other operation here. + # request.validate() + + schema = request.api.get_schema(request.operation) + ref = schema.get('ref', '') + ref_params = {} + + for param in list(request.params.keys()): + if '{{{0}}}'.format(param) in ref: + ref_params[param] = request.params.pop(param) + + url = '{0}/{1}'.format(request.endpoint.rstrip('/'), + ref.format(**ref_params)) + return url, schema.get('method', 'GET'), request + + def send(self, request): + url, method, request = self._prepare(request) + + # NOTE(flape87): Do not modify + # request's headers directly. + headers = request.headers.copy() + headers['content-type'] = 'application/json' + + return self.client.request(method, + url=url, + params=request.params, + headers=headers, + data=request.content) diff --git a/setup.cfg b/setup.cfg index 5bea5c48..2aa169f4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,10 @@ setup-hooks = packages = marconiclient +[entry_points] +marconiclient.transport = + http.v1 = marconiclient.transport.http:HttpTransport + [nosetests] where=tests verbosity=2 diff --git a/tests/unit/transport/test_api.py b/tests/unit/transport/test_api.py index 4be51a0e..d1414cf7 100644 --- a/tests/unit/transport/test_api.py +++ b/tests/unit/transport/test_api.py @@ -15,27 +15,14 @@ from marconiclient import errors from marconiclient.tests import base -from marconiclient.transport import api - - -class FakeApi(api.Api): - schema = { - 'test_operation': { - 'properties': { - 'name': {'type': 'string'} - }, - - 'additionalProperties': False, - 'required': ['name'] - } - } +from marconiclient.tests.transport import api as tapi class TestApi(base.TestBase): def setUp(self): super(TestApi, self).setUp() - self.api = FakeApi() + self.api = tapi.FakeApi() def test_valid_params(self): self.assertTrue(self.api.validate('test_operation', diff --git a/tests/unit/transport/test_http.py b/tests/unit/transport/test_http.py new file mode 100644 index 00000000..41537500 --- /dev/null +++ b/tests/unit/transport/test_http.py @@ -0,0 +1,78 @@ +# Copyright (c) 2013 Red Hat, Inc. +# +# 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 + +from marconiclient.tests import base +from marconiclient.tests.transport import api +from marconiclient.transport import http +from marconiclient.transport import request + + +class TestHttpTransport(base.TestBase): + """Tests for the HTTP transport.""" + + def setUp(self): + super(TestHttpTransport, self).setUp() + self.api = api.FakeApi() + self.transport = http.HttpTransport(self.conf) + + def test_basic_send(self): + params = {'name': 'Test', + 'address': 'Outer space'} + req = request.Request('http://example.org/', + operation='test_operation', + params=params) + + with mock.patch.object(self.transport.client, 'request', + autospec=True) as request_method: + + request_method.return_value = None + + # NOTE(flaper87): Bypass the API + # loading step by setting the _api + # attribute + req._api = self.api + self.transport.send(req) + + final_url = 'http://example.org/test/Test' + final_params = {'address': 'Outer space'} + final_headers = {'content-type': 'application/json'} + + request_method.assert_called_with('GET', url=final_url, + params=final_params, + headers=final_headers, + data=None) + + def test_send_without_api(self): + params = {'name': 'Test', + 'address': 'Outer space'} + req = request.Request('http://example.org/', + operation='test_operation', + params=params) + + with mock.patch.object(self.transport.client, 'request', + autospec=True) as request_method: + + request_method.return_value = None + self.transport.send(req) + + final_url = 'http://example.org/' + final_headers = {'content-type': 'application/json'} + + request_method.assert_called_with('GET', url=final_url, + params=params, + headers=final_headers, + data=None)