Implement HTTP Transport
This patch implements the HTTP transport. Some notes: - Consider not having a version for the transport implementation since we already have one for the API definition. - Consider moving transport implementations under a private package since they're not suppose to be inherited nor imported. To load a transport users have to use `transport.get_transport(_for)` Partially-Implements blueprint python-marconiclient-v1 Change-Id: Ie2c70b3c160c5a331bd79765b2f4d69433e5bd73
This commit is contained in:
@@ -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)
|
||||
|
0
marconiclient/tests/transport/__init__.py
Normal file
0
marconiclient/tests/transport/__init__.py
Normal file
32
marconiclient/tests/transport/api.py
Normal file
32
marconiclient/tests/transport/api.py
Normal file
@@ -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']
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
59
marconiclient/transport/http.py
Normal file
59
marconiclient/transport/http.py
Normal file
@@ -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)
|
@@ -29,6 +29,10 @@ setup-hooks =
|
||||
packages =
|
||||
marconiclient
|
||||
|
||||
[entry_points]
|
||||
marconiclient.transport =
|
||||
http.v1 = marconiclient.transport.http:HttpTransport
|
||||
|
||||
[nosetests]
|
||||
where=tests
|
||||
verbosity=2
|
||||
|
@@ -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',
|
||||
|
78
tests/unit/transport/test_http.py
Normal file
78
tests/unit/transport/test_http.py
Normal file
@@ -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)
|
Reference in New Issue
Block a user