Skip to content

Commit

Permalink
Review updates
Browse files Browse the repository at this point in the history
  • Loading branch information
Semir Patel committed Apr 23, 2015
1 parent 3e99874 commit 0b5337d
Show file tree
Hide file tree
Showing 13 changed files with 469 additions and 293 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ language: python
env:
- TOX_ENV=py26
- TOX_ENV=py27
# TODO: re-enable when bravado-core supports py3.x
# TODO: re-enable when bravado-core supports py3.x - https://github.com/Yelp/bravado-core/issues/10
#- TOX_ENV=py33
#- TOX_ENV=py34
- TOX_ENV=docs
Expand Down
21 changes: 15 additions & 6 deletions pyramid_swagger/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,24 @@ def includeme(config):

# Add the SwaggerSchema to settings to make it avialable to the validation
# tween and `register_api_doc_endpoints`
settings['pyramid_swagger.schema'] = get_swagger_schema(settings)
settings['pyramid_swagger.spec'] = get_swagger_spec(settings)
swagger_version = settings.get('pyramid_swagger.swagger_version', '2.0')

if swagger_version == '1.2':
settings['pyramid_swagger.schema'] = get_swagger_schema(settings)
elif swagger_version == '2.0':
settings['pyramid_swagger.schema'] = get_swagger_spec(settings)
else:
raise TypeError('Unsupported pyramid_swagger.swagger_version: {0}'
.format(swagger_version))

config.add_tween(
# "pyramid_swagger.tween.validation_tween_factory", # 1.2
"pyramid_swagger.tween20.swagger_tween_factory", # 2.0
"pyramid_swagger.tween.validation_tween_factory",
under=pyramid.tweens.EXCVIEW
)

if settings.get('pyramid_swagger.enable_api_doc_views', True):
register_api_doc_endpoints(config)
register_swagger_json_endpoint(config)
if swagger_version == '1.2':
register_api_doc_endpoints(config)

if swagger_version == '2.0':
register_swagger_json_endpoint(config)
2 changes: 1 addition & 1 deletion pyramid_swagger/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def register_swagger_json_endpoint(config):
:param config: Configurator instance for our webapp
:type config: :class:`pyramid.config.Configurator`
"""
spec = config.registry.settings['pyramid_swagger.spec']
spec = config.registry.settings['pyramid_swagger.schema']

def view_for_swagger_json(request):
return spec.spec_dict
Expand Down
2 changes: 1 addition & 1 deletion pyramid_swagger/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(
self.api_declarations = api_declarations
self.resource_validators = resource_validators

def validators_for_request(self, request):
def validators_for_request(self, request, **kwargs):
"""Takes a request and returns a validator mapping for the request.
:param request: A Pyramid request to fetch schemas for
Expand Down
137 changes: 127 additions & 10 deletions pyramid_swagger/tween.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import logging
import re
import sys
from bravado_core.request import RequestLike, unmarshal_request

from pyramid.interfaces import IRoutesMapper
import jsonschema.exceptions
Expand All @@ -30,6 +31,7 @@ class Settings(namedtuple(
'Settings',
[
'schema',
'swagger_handler',
'validate_request',
'validate_response',
'validate_path',
Expand All @@ -41,6 +43,7 @@ class Settings(namedtuple(
"""A settings object for configuratble options.
:param schema: a :class:`pyramid_swagger.model.SwaggerSchema`
:param swagger_handler: a :class:`SwaggerHandler`
:param validate_swagger_spec: check Swagger files for correctness.
:param validate_request: check requests against Swagger spec.
:param validate_response: check responses against Swagger spec.
Expand Down Expand Up @@ -90,6 +93,7 @@ def validation_tween_factory(handler, registry):
route_mapper = registry.queryUtility(IRoutesMapper)

def validator_tween(request):
swagger_handler = settings.swagger_handler
# We don't have access to this yet but let's go ahead and build the
# matchdict so we can validate it and use it to exclude routes from
# validation.
Expand All @@ -101,7 +105,8 @@ def validator_tween(request):
validation_context = _get_validation_context(registry)

try:
validator_map = settings.schema.validators_for_request(request)
op_or_validators_map = swagger_handler.op_for_request(
request, route_info=route_info, spec=settings.schema)
except PathNotMatchedError as exc:
if settings.validate_path:
with validation_context(request):
Expand All @@ -110,10 +115,11 @@ def validator_tween(request):
return handler(request)

if settings.validate_request:
request_data = handle_request(
request_data = swagger_handler.handle_request(
PyramidSwaggerRequest(request, route_info),
validation_context,
validator_map)
validation_context=validation_context,
validator_map=op_or_validators_map,
op=op_or_validators_map)

def swagger_data(_):
return request_data
Expand All @@ -124,14 +130,16 @@ def swagger_data(_):

if settings.validate_response:
with validation_context(request, response=response):
validate_response(response, validator_map.response)
swagger_handler.handle_response(
response,
validator=getattr(op_or_validators_map, 'response', None))

return response

return validator_tween


class PyramidSwaggerRequest(object):
class PyramidSwaggerRequest(RequestLike):
"""Adapter for a :class:`pyramid.request.Request` which exposes request
data for casting and validation.
"""
Expand All @@ -142,12 +150,21 @@ class PyramidSwaggerRequest(object):
]

def __init__(self, request, route_info):
"""
:type request: :class:`pyramid.request.Request`
:type route_info: :class:`pyramid.urldispatch.Route`
"""
self.request = request
self.route_info = route_info

@property
def query(self):
return self.request.GET
"""
:rtype: dict
"""
# The `mixed` dict will return a list if a parameter has multiple
# values or a single primitive in the case of a single value.
return self.request.GET.mixed()

@property
def path(self):
Expand All @@ -159,17 +176,31 @@ def headers(self):

@property
def form(self):
"""
:rtype: dict
"""
# Don't read the POST dict unless the body is form encoded
if self.request.headers.get('Content-Type') in self.FORM_TYPES:
return self.request.POST
return self.request.POST.mixed()
return {}

@property
def body(self):
return self.json()

@property
def files(self):
result = {}
for k, v in self.request.params.mixed():
if hasattr(v, 'file'):
result[k] = v.file
return result

def json(self, **kwargs):
return getattr(self.request, 'json_body', {})


def handle_request(request, validation_context, validator_map):
def handle_request(request, validation_context, validator_map, **kwargs):
"""Validate the request against the swagger spec and return a dict with
all parameter values available in the request, casted to the expected
python type.
Expand Down Expand Up @@ -209,8 +240,10 @@ def handle_request(request, validation_context, validator_map):


def load_settings(registry):
schema = registry.settings['pyramid_swagger.schema']
return Settings(
schema=registry.settings['pyramid_swagger.schema'],
schema=schema,
swagger_handler=build_swagger_handler(registry.settings, schema),
validate_request=registry.settings.get(
'pyramid_swagger.enable_request_validation',
True
Expand All @@ -230,6 +263,38 @@ def load_settings(registry):
)


SwaggerHandler = namedtuple('SwaggerHandler',
'op_for_request handle_request handle_response')


def build_swagger_handler(settings, schema):
"""
Contains callables that isolate implementation differences in the tween to
handle both Swagger 1.2 and Swagger 2.0.
:type settings: dict
:type schema: :class:'
:rtype: :class:`SwaggerHandler`
"""
swagger_version = settings.get('pyramid_swagger.swagger_version', '2.0')

if swagger_version == '2.0':
return SwaggerHandler(
op_for_request=get_op_for_request,
handle_request=swaggerize_request,
handle_response=swaggerize_response,
)
elif swagger_version == '1.2':
return SwaggerHandler(
op_for_request=schema.validators_for_request,
handle_request=handle_request,
handle_response=validate_response,
)
raise TypeError(
"Invalid pyramid_swagger.swagger_version: {0}. "
"Should be either '1.2' or '2.0'".format(swagger_version))


def get_exclude_paths(registry):
"""Compiles a list of paths that should not be validated against.
:rtype: list of compiled validation regexes
Expand Down Expand Up @@ -367,3 +432,55 @@ def prepare_body(response):
return simplejson.loads(response.text)
else:
return response.text


@validation_error(RequestValidationError)
def swaggerize_request(request, **kwargs):
"""
Delegate handling the Swagger concerns of the request to bravado-core.
Post-invocation, the Swagger request parameters are available as a dict
named `swagger_data` on the Pyramid request.
:type request: :class:`pyramid.request.Request`
:type op: :class:`bravado_core.operation.Operation`
:type validatation_context: context manager
"""
op = kwargs['op']
validation_context = kwargs['validation_context']
with validation_context(request):
request_data = unmarshal_request(request, op)
return request_data


@validation_error(ResponseValidationError)
def swaggerize_response(response, **kwargs):
"""
Delegate handling the Swagger concerns of the response to bravado-core.
:type response: :class:`pyramid.response.Response`
"""
# TODO: validate, marshal, and transform the response object
log.warn('TODO: Implement swaggerize_response()')


def get_op_for_request(request, route_info, spec):
"""
Find out which operation in the Swagger schema corresponds to the given
pyramid request.
:type request: :class:`pyramid.request.Request`
:type route_info: dict (usually has 'match' and 'route' keys)
:type spec: :class:`bravado_core.spec.Spec`
:rtype: :class:`bravado_core.operation.Operation`
:raises: RequestValidationError when a matching Swagger operation is not
found.
"""
# pyramid.urldispath.Route
route = route_info['route']
if hasattr(route, 'path'):
op = spec.get_op_for_request(request.method, route.path)
if op is not None:
return op
raise PathNotMatchedError(
"Could not find a matching Swagger operation for {0} request {1}"
.format(request.method, request.url))

0 comments on commit 0b5337d

Please sign in to comment.