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:
Flavio Percoco
2013-10-04 18:32:45 +02:00
parent 34688b2b88
commit fcccf5e1ba
8 changed files with 201 additions and 22 deletions

View File

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

View 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']
}
}

View File

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

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

View File

@@ -29,6 +29,10 @@ setup-hooks =
packages =
marconiclient
[entry_points]
marconiclient.transport =
http.v1 = marconiclient.transport.http:HttpTransport
[nosetests]
where=tests
verbosity=2

View File

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

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