Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/Yelp/bravado
Browse files Browse the repository at this point in the history
  • Loading branch information
sjaensch committed Jan 11, 2018
2 parents 851e2ca + e6a160a commit c3e8075
Show file tree
Hide file tree
Showing 9 changed files with 305 additions and 41 deletions.
31 changes: 8 additions & 23 deletions aiobravado/client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""
The :class:`SwaggerClient` provides an interface for making API calls based on
a swagger spec, and returns responses of python objects which build from the
Expand Down Expand Up @@ -52,34 +53,16 @@
from six import iteritems
from six import itervalues

from aiobravado.aiohttp_client import AiohttpClient
from aiobravado.config_defaults import CONFIG_DEFAULTS
from aiobravado.config_defaults import REQUEST_OPTIONS_DEFAULTS
from aiobravado.docstring_property import docstring_property
from aiobravado.swagger_model import Loader
from aiobravado.warning import warn_for_deprecated_op

from aiobravado.aiohttp_client import AiohttpClient

log = logging.getLogger(__name__)


CONFIG_DEFAULTS = {
# See the constructor of :class:`aiobravado.http_future.HttpFuture` for an
# in depth explanation of what this means.
'also_return_response': False,
}

REQUEST_OPTIONS_DEFAULTS = {
# List of callbacks that are executed after the incoming response has been
# validated and the swagger_result has been unmarshalled.
#
# The callback should expect two arguments:
# param : incoming_response
# type : subclass of class:`bravado_core.response.IncomingResponse`
# param : operation
# type : class:`bravado_core.operation.Operation`
'response_callbacks': [],
}


class SwaggerClient(object):
"""A client for accessing a Swagger-documented RESTful service.
Expand All @@ -91,8 +74,7 @@ def __init__(self, swagger_spec, also_return_response=False):
self.swagger_spec = swagger_spec

@classmethod
async def from_url(cls, spec_url, http_client=None, request_headers=None,
config=None):
async def from_url(cls, spec_url, http_client=None, request_headers=None, config=None):
"""Build a :class:`SwaggerClient` from a url to the Swagger
specification for a RESTful API.
Expand Down Expand Up @@ -293,6 +275,9 @@ def construct_request(operation, request_options, **op_kwargs):
'params': {}, # filled in downstream
'headers': request_options.get('headers', {}),
}
# Adds Accept header to request for msgpack response if specified
if request_options.get('use_msgpack', False):
request['headers']['Accept'] = 'application/msgpack'

# Copy over optional request options
for request_option in ('connect_timeout', 'timeout'):
Expand Down
18 changes: 18 additions & 0 deletions aiobravado/config_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
CONFIG_DEFAULTS = {
# See the constructor of :class:`bravado.http_future.HttpFuture` for an
# in depth explanation of what this means.
'also_return_response': False,
}

REQUEST_OPTIONS_DEFAULTS = {
# List of callbacks that are executed after the incoming response has been
# validated and the swagger_result has been unmarshalled.
#
# The callback should expect two arguments:
# param : incoming_response
# type : subclass of class:`bravado_core.response.IncomingResponse`
# param : operation
# type : class:`bravado_core.operation.Operation`
'response_callbacks': [],
}
35 changes: 18 additions & 17 deletions aiobravado/http_future.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
from functools import wraps

import six
import umsgpack
from bravado_core.content_type import APP_JSON, APP_MSGPACK
from bravado_core.content_type import APP_JSON
from bravado_core.content_type import APP_MSGPACK
from bravado_core.exception import MatchingResponseNotFound
from bravado_core.response import get_response_spec
from bravado_core.unmarshal import unmarshal_schema_object
from bravado_core.validate import validate_schema_object
from msgpack import unpackb

from aiobravado.config_defaults import REQUEST_OPTIONS_DEFAULTS
from aiobravado.exception import BravadoTimeoutError
from aiobravado.exception import make_http_exception

Expand Down Expand Up @@ -96,7 +98,7 @@ def __init__(self, future, response_adapter, operation=None,
self.future = future
self.response_adapter = response_adapter
self.operation = operation
self.response_callbacks = response_callbacks or []
self.response_callbacks = response_callbacks or REQUEST_OPTIONS_DEFAULTS['response_callbacks']
self.also_return_response = also_return_response

@reraise_errors
Expand Down Expand Up @@ -134,12 +136,10 @@ async def unmarshal_response(incoming_response, operation, response_callbacks=No
This hands the response over to bravado_core for validation and
unmarshalling and then runs any response callbacks. On success, the
swagger_result is available as ``incoming_response.swagger_result``.
:type incoming_response: :class:`bravado_core.response.IncomingResponse`
:type operation: :class:`bravado_core.operation.Operation`
:type response_callbacks: list of callable. See
bravado_core.client.REQUEST_OPTIONS_DEFAULTS.
:raises: HTTPError
- On 5XX status code, the HTTPError has minimal information.
- On non-2XX status code with no matching response, the HTTPError
Expand All @@ -152,8 +152,8 @@ async def unmarshal_response(incoming_response, operation, response_callbacks=No
try:
raise_on_unexpected(incoming_response)
incoming_response.swagger_result = await unmarshal_response_inner(
incoming_response,
operation,
response=incoming_response,
op=operation,
)
except MatchingResponseNotFound as e:
exception = make_http_exception(
Expand All @@ -173,21 +173,18 @@ async def unmarshal_response(incoming_response, operation, response_callbacks=No


async def unmarshal_response_inner(response, op):
"""Unmarshal incoming http response into a value based on the
"""
Unmarshal incoming http response into a value based on the
response specification.
:type response: :class:`bravado_core.response.IncomingResponse`
:type op: :class:`bravado_core.operation.Operation`
:returns: value where type(value) matches response_spec['schema']['type']
if it exists, None otherwise.
"""
deref = op.swagger_spec.deref
response_spec = get_response_spec(response.status_code, op)

def has_content(response_spec):
return 'schema' in response_spec
response_spec = get_response_spec(status_code=response.status_code, op=op)

if not has_content(response_spec):
if 'schema' not in response_spec:
return None

content_type = response.headers.get('content-type', '').lower()
Expand All @@ -197,12 +194,16 @@ def has_content(response_spec):
if content_type.startswith(APP_JSON):
content_value = await response.json()
else:
content_value = umsgpack.loads(await response.raw_bytes)
if op.swagger_spec.config['validate_responses']:
content_value = unpackb(await response.raw_bytes, encoding='utf-8')

if op.swagger_spec.config.get('validate_responses', False):
validate_schema_object(op.swagger_spec, content_spec, content_value)

return unmarshal_schema_object(
op.swagger_spec, content_spec, content_value)
swagger_spec=op.swagger_spec,
schema_object_spec=content_spec,
value=content_value,
)

# TODO: Non-json response contents
return response.text
Expand Down
4 changes: 4 additions & 0 deletions changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
Changelog
=========

0.9.1 (2018-XX-XX)
------------------
- bravado version: 9.2.2

0.9.0 (2017-11-17)
------------------
- Initial release, based on the source code of bravado 9.2.0.
2 changes: 2 additions & 0 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,6 @@ Config key Type Default Description
| - ``operation`` of type ``bravado_core.operation.Operation``
*timeout* float N/A | TCP idle timeout in seconds. This is passed along to the
| http_client when making a service call.
*use_msgpack* boolean False | If a msgpack serialization is desired for the response. This
| will add a Accept: application/msgpack header to the request.
========================= =============== ========= ===============================================================
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
],
install_requires=[
'aiohttp',
'bravado-core >= 4.2.2',
'bravado-core >= 4.11.0',
'msgpack-python',
'python-dateutil',
'pyyaml',
],
Expand Down
22 changes: 22 additions & 0 deletions tests/client/construct_request_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,25 @@ def test_with_not_string_headers(
assert request['headers'][header_name] == str(header_value)
unmarshalled_request = unmarshal_request(request_object, operation)
assert unmarshalled_request[header_name] == header_value


def test_use_msgpack(
minimal_swagger_spec,
getPetById_spec,
):
op = CallableOperation(
Operation.from_spec(
minimal_swagger_spec,
'/pet/{petId}',
'get',
getPetById_spec
)
)
request = construct_request(
op,
request_options={
'use_msgpack': True,
},
petId=1,
)
assert request['headers']['Accept'] == 'application/msgpack'
133 changes: 133 additions & 0 deletions tests/http_future/unmarshall_response_inner_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
from asyncio import coroutine

import mock
import msgpack
import pytest
from bravado_core.content_type import APP_JSON
from bravado_core.content_type import APP_MSGPACK
from bravado_core.response import IncomingResponse
from bravado_core.spec import Spec

from aiobravado.http_future import unmarshal_response_inner


@pytest.fixture
def empty_swagger_spec():
return Spec(spec_dict={})


@pytest.fixture
def response_spec():
return {
'description': "Day of the week",
'schema': {
'type': 'string',
}
}


@pytest.fixture
def mock_get_response_spec():
with mock.patch('aiobravado.http_future.get_response_spec') as m:
yield m


@pytest.fixture
def mock_validate_schema_object():
with mock.patch('aiobravado.http_future.validate_schema_object') as m:
yield m


def test_no_content(mock_get_response_spec, empty_swagger_spec, event_loop):
response_spec = {
'description': "I don't have a 'schema' key so I return nothing",
}
response = mock.Mock(spec=IncomingResponse, status_code=200)

mock_get_response_spec.return_value = response_spec
op = mock.Mock(swagger_spec=empty_swagger_spec)
result = event_loop.run_until_complete(unmarshal_response_inner(response, op))
assert result is None


def test_json_content(mock_get_response_spec, empty_swagger_spec, response_spec, event_loop):
response = mock.Mock(
spec=IncomingResponse,
status_code=200,
headers={'content-type': APP_JSON},
json=coroutine(mock.Mock(return_value='Monday')),
)

mock_get_response_spec.return_value = response_spec
op = mock.Mock(swagger_spec=empty_swagger_spec)
assert 'Monday' == event_loop.run_until_complete(unmarshal_response_inner(response, op))


def test_msgpack_content(mock_get_response_spec, empty_swagger_spec, response_spec, event_loop):
message = 'Monday'
response = mock.Mock(
spec=IncomingResponse,
status_code=200,
headers={'content-type': APP_MSGPACK},
raw_bytes=coroutine(lambda: msgpack.dumps(message))(),
)

mock_get_response_spec.return_value = response_spec
op = mock.Mock(swagger_spec=empty_swagger_spec)
assert message == event_loop.run_until_complete(unmarshal_response_inner(response, op))


def test_text_content(mock_get_response_spec, empty_swagger_spec, response_spec, event_loop):
response = mock.Mock(
spec=IncomingResponse,
status_code=200,
headers={'content-type': 'text/plain'},
text='Monday',
)

mock_get_response_spec.return_value = response_spec
op = mock.Mock(swagger_spec=empty_swagger_spec)
assert 'Monday' == event_loop.run_until_complete(unmarshal_response_inner(response, op))


def test_skips_validation(
mock_validate_schema_object,
mock_get_response_spec,
empty_swagger_spec,
response_spec,
event_loop,
):
empty_swagger_spec.config['validate_responses'] = False
response = mock.Mock(
spec=IncomingResponse,
status_code=200,
headers={'content-type': APP_JSON},
json=coroutine(mock.Mock(return_value='Monday')),
)

mock_get_response_spec.return_value = response_spec
op = mock.Mock(swagger_spec=empty_swagger_spec)
event_loop.run_until_complete(unmarshal_response_inner(response, op))
assert mock_validate_schema_object.call_count == 0


def test_performs_validation(
mock_validate_schema_object,
mock_get_response_spec,
empty_swagger_spec,
response_spec,
event_loop,
):
empty_swagger_spec.config['validate_responses'] = True
response = mock.Mock(
spec=IncomingResponse,
status_code=200,
headers={'content-type': APP_JSON},
json=coroutine(mock.Mock(return_value='Monday')),
)

mock_get_response_spec.return_value = response_spec
op = mock.Mock(swagger_spec=empty_swagger_spec)
event_loop.run_until_complete(unmarshal_response_inner(response, op))
assert mock_validate_schema_object.call_count == 1
Loading

0 comments on commit c3e8075

Please sign in to comment.