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):
|
def __init__(self, *args, **kwargs):
|
||||||
self.session = requests.session(*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):
|
def get(self, *args, **kwargs):
|
||||||
"""Does http GET."""
|
"""Does http GET."""
|
||||||
return self.session.get(*args, **kwargs)
|
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 = {}
|
schema = {}
|
||||||
validators = {}
|
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):
|
def validate(self, operation, params):
|
||||||
"""Validates the request data
|
"""Validates the request data
|
||||||
|
|
||||||
@@ -44,13 +64,8 @@ class Api(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if operation not in self.validators:
|
if operation not in self.validators:
|
||||||
try:
|
schema = self.get_schema(operation)
|
||||||
schema = self.schema[operation]
|
self.validators[operation] = validators.Draft4Validator(schema)
|
||||||
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)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.validators[operation].validate(params)
|
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 =
|
packages =
|
||||||
marconiclient
|
marconiclient
|
||||||
|
|
||||||
|
[entry_points]
|
||||||
|
marconiclient.transport =
|
||||||
|
http.v1 = marconiclient.transport.http:HttpTransport
|
||||||
|
|
||||||
[nosetests]
|
[nosetests]
|
||||||
where=tests
|
where=tests
|
||||||
verbosity=2
|
verbosity=2
|
||||||
|
@@ -15,27 +15,14 @@
|
|||||||
|
|
||||||
from marconiclient import errors
|
from marconiclient import errors
|
||||||
from marconiclient.tests import base
|
from marconiclient.tests import base
|
||||||
from marconiclient.transport import api
|
from marconiclient.tests.transport import api as tapi
|
||||||
|
|
||||||
|
|
||||||
class FakeApi(api.Api):
|
|
||||||
schema = {
|
|
||||||
'test_operation': {
|
|
||||||
'properties': {
|
|
||||||
'name': {'type': 'string'}
|
|
||||||
},
|
|
||||||
|
|
||||||
'additionalProperties': False,
|
|
||||||
'required': ['name']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestApi(base.TestBase):
|
class TestApi(base.TestBase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestApi, self).setUp()
|
super(TestApi, self).setUp()
|
||||||
self.api = FakeApi()
|
self.api = tapi.FakeApi()
|
||||||
|
|
||||||
def test_valid_params(self):
|
def test_valid_params(self):
|
||||||
self.assertTrue(self.api.validate('test_operation',
|
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