pyramid_swagger/tween.py
def get_swagger_versions(settings):
    """
    Validates and returns the versions of the Swagger Spec that this pyramid
    application supports.

    :type settings: dict
    :return: list of strings. eg ['1.2', '2.0']
    :raises: ValueError when an unsupported Swagger version is encountered.
    """
    swagger_versions = set(aslist(settings.get(
        'pyramid_swagger.swagger_versions', DEFAULT_SWAGGER_VERSIONS)))

    if len(swagger_versions) == 0:
        raise ValueError('pyramid_swagger.swagger_versions is empty')

    for swagger_version in swagger_versions:
        if swagger_version not in SUPPORTED_SWAGGER_VERSIONS:
            raise ValueError('Swagger version {0} is not supported.'
                             .format(swagger_version))
    return swagger_versions
pyramid_swagger/tween.py
def validation_error(exc_class):
    def decorator(f):
        @functools.wraps(f)
        def _validate(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except (
                jsonschema.exceptions.ValidationError,
                SwaggerMappingError,
            ) as exc:
                # This will alter our stack trace slightly, but Pyramid knows
                # how to render it. And the real value is in the message
                # anyway.
                e = exc_class(str(exc), child=exc)
                e._traceback = sys.exc_info()[2]
                raise e

        return _validate

    return decorator
pyramid_swagger/tween.py
def should_exclude_path(exclude_path_regexes, path):
    # Skip validation for the specified endpoints
    return any(r.match(path) for r in exclude_path_regexes)
pyramid_swagger/tween.py
def get_exclude_paths(registry):
    """Compiles a list of paths that should not be validated against.
        :rtype: list of compiled validation regexes
    """
    # TODO(#63): remove deprecated `skip_validation` setting in v2.0.
    regexes = registry.settings.get(
        'pyramid_swagger.skip_validation',
        registry.settings.get(
            'pyramid_swagger.exclude_paths',
            DEFAULT_EXCLUDED_PATHS
        )
    )

    # being nice to users using strings :p
    if not isinstance(regexes, list) and not isinstance(regexes, tuple):
        regexes = [regexes]

    return [re.compile(r) for r in regexes]
pyramid_swagger/tween.py
@validation_error(ResponseValidationError)
def validate_response(response, validator_map):
    """Validates response against our schemas.

    :param response: the response object to validate
    :type response: :class:`pyramid.response.Response`
    :type validator_map: :class:`pyramid_swagger.load_schema.ValidatorMap`
    """
    validator = validator_map.response

    # Short circuit if we are supposed to not validate anything.
    returns_nothing = validator.schema.get('type') == 'void'
    body_empty = response.body in (None, b'', b'{}', b'null')
    if returns_nothing and body_empty:
        return

    # Don't attempt to validate non-success responses in v1.2
    if not 200 <= response.status_code <= 203:
        return

    validator.validate(prepare_body(response))
pyramid_swagger/exceptions.py
class RequestValidationError(HTTPBadRequest):
    def __init__(self, *args, **kwargs):
        self.child = kwargs.pop('child', None)
        super(RequestValidationError, self).__init__(*args, **kwargs)
pyramid_swagger/tween.py
def should_exclude_route(excluded_routes, route_info):
    return route_info.get('route') and route_info['route'].name in excluded_routes
pyramid_swagger/exceptions.py
class ResponseValidationError(HTTPInternalServerError):
    def __init__(self, *args, **kwargs):
        self.child = kwargs.pop('child', None)
        super(ResponseValidationError, self).__init__(*args, **kwargs)
pyramid_swagger/tween.py
def prepare_body(response):
    # content_type must be set to access response.text
    if not response.content_type:
        raise ResponseValidationError(
            'Response validation error: Content-Type must be set'
        )

    if 'application/json' in response.content_type:
        return simplejson.loads(response.text)
    else:
        return response.text
pyramid_swagger/tween.py
def get_swagger_objects(settings, route_info, registry):
    """Returns appropriate swagger handler and swagger spec schema.

    Swagger Handler contains callables that isolate implementation differences
    in the tween to handle both Swagger 1.2 and Swagger 2.0.

    Exception is made when `settings.prefer_20_routes` are non-empty and
    ['1.2', '2.0'] both are present in available swagger versions. In this
    special scenario, '2.0' spec is chosen only for requests which are listed
    in the `prefer_20_routes`. This helps in incremental migration of
    routes from v1.2 to v2.0 by making moving to v2.0 opt-in.

    :rtype: (:class:`SwaggerHandler`,
             :class:`pyramid_swagger.model.SwaggerSchema` OR
                :class:`bravado_core.spec.Spec`)
    """
    enabled_swagger_versions = get_swagger_versions(registry.settings)
    schema12 = registry.settings['pyramid_swagger.schema12']
    schema20 = registry.settings['pyramid_swagger.schema20']

    if (
        SWAGGER_20 in enabled_swagger_versions
        and SWAGGER_12 in enabled_swagger_versions
        and settings.prefer_20_routes
        and route_info.get('route')
        and route_info['route'].name not in settings.prefer_20_routes
    ):
        return settings.swagger12_handler, schema12

    if SWAGGER_20 in enabled_swagger_versions:
        return settings.swagger20_handler, schema20

    if SWAGGER_12 in enabled_swagger_versions:
        return settings.swagger12_handler, schema12
pyramid_swagger/tween.py
class Settings(namedtuple(
    'Settings',
    [
        'swagger12_handler',
        'swagger20_handler',
        'validate_request',
        'validate_response',
        'validate_path',
        'exclude_paths',
        'exclude_routes',
        'prefer_20_routes',
    ]
)):

    """A settings object for configurable options.

    :param swagger12_handler: a :class:`SwaggerHandler` for v1.2 or None
    :param swagger20_handler: a :class:`SwaggerHandler` for v2.0 or None
    :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.
    :param validate_path: check if request path is in schema. If disabled
        and path not found in schema, request / response validation is skipped.
    :param exclude_paths: list of paths (in regex format) that should be
        excluded from validation.
    :rtype: namedtuple
    :param exclude_routes: list of route names that should be excluded from
        validation.
    :param prefer_20_routes: list of route names that should be handled
        via v2.0 spec when `2.0` is in `swagger_versions`. All others will be
        handled via v1.2 spec. [i.e. Make v2.0 an opt-in feature]
    """
pyramid_swagger/tween.py
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: PathNotMatchedError when a matching Swagger operation is not
        found.
    """
    # pyramid.urldispath.Route
    route = route_info['route']
    if hasattr(route, 'path'):
        route_path = route.path
        if route_path[0] != '/':
            route_path = '/' + route_path
        op = spec.get_op_for_request(request.method, route_path)
        if op is not None:
            return op
        else:
            raise PathNotMatchedError(
                "Could not find a matching Swagger "
                "operation for {0} request {1}"
                .format(request.method, request.url))
    else:
        raise PathNotMatchedError(
            "Could not find a matching route for {0} request {1}. "
            "Have you registered this endpoint with Pyramid?"
            .format(request.method, request.url))
pyramid_swagger/load_schema.py
class ValidatorMap(
    namedtuple('_VMap', 'query path form headers body response')
):
    """
    A data object with validators for each part of the request and response
    objects. Each field is a :class:`SchemaValidator`.
    """
    __slots__ = ()

    @classmethod
    def from_operation(cls, operation, models, resolver):
        args = []
        for schema, validator in [
            (build_param_schema(operation, 'query'), Swagger12ParamValidator),
            (build_param_schema(operation, 'path'), Swagger12ParamValidator),
            (build_param_schema(operation, 'form'), Swagger12ParamValidator),
            (build_param_schema(operation, 'header'), Swagger12ParamValidator),
            (extract_body_schema(operation), get_body_validator(models)),
            (extract_response_body_schema(operation, models),
                Draft4Validator),
        ]:
            args.append(SchemaValidator.from_schema(
                schema,
                resolver,
                validator))

        return cls(*args)
pyramid_swagger/model.py
class PathNotMatchedError(Exception):
    """Raised when a SwaggerSchema object is given a request it cannot match
    against its stored schema."""
    pass
pyramid_swagger/tween.py
class PyramidSwaggerRequest(IncomingRequest):
    """Adapter for a :class:`pyramid.request.Request` which exposes request
    data for casting and validation.

    The following properties are exposed in order to comply with the interface
    of :class:`bravado_core.request.IncomingRequest`:

        path: a dictionary of URL path parameters
        query: a dictionary of parameters from the query string
        form: a dictionary of form parameters from a POST
        headers: a dictionary of request headers
        files: a dictionary of uploaded filename to content
    """

    FORM_TYPES = [
        'application/x-www-form-urlencoded',
        'multipart/form-data',
    ]

    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 headers(self):
        return self.request.headers

    @property
    def query(self):
        """
        :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):
        return self.route_info.get('match') or {}

    @property
    def form(self):
        """
        :rtype: dict
        """
        # Don't read the POST dict unless the body is form encoded
        if self.request.content_type in self.FORM_TYPES:
            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().items():
            if hasattr(v, 'file'):
                result[k] = v.file
        return result

    def json(self, **kwargs):
        if self.request.is_body_readable:
            return getattr(self.request, 'json_body', {})
        else:
            return None
pyramid_swagger/tween.py
SWAGGER_20 = '2.0'
pyramid_swagger/tween.py
def handle_request(request, 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.

    :param request: a :class:`PyramidSwaggerRequest` to validate
    :param validator_map: a :class:`pyramid_swagger.load_schema.ValidatorMap`
        used to validate the request
    :returns: a :class:`dict` of request data for each parameter in the swagger
        spec
    :raises: RequestValidationError when the request is not valid for the
        swagger spec
    """
    request_data = {}
    validation_pairs = []

    for validator, values in [
        (validator_map.query, request.query),
        (validator_map.path, request.path),
        (validator_map.form, request.form),
        (validator_map.headers, request.headers),
    ]:
        values = cast_params(validator.schema, values)
        validation_pairs.append((validator, values))
        request_data.update(values)

    # Body is a special case because the key for the request_data comes
    # from the name in the schema, instead of keys in the values
    if validator_map.body.schema:
        param_name = validator_map.body.schema['name']
        validation_pairs.append((validator_map.body, request.body))
        request_data[param_name] = request.body

    validate_request(validation_pairs)

    return request_data
pyramid_swagger/tween.py
SWAGGER_12 = '1.2'
pyramid_swagger/tween.py
class PyramidSwaggerResponse(OutgoingResponse):
    """Adapter for a :class:`pyramid.response.Response` which exposes response
    data for validation.

    The following properties are exposed in order to comply with the interface
    of :class:`bravado_core.response.OutgoingResponse`:

        content_type: a standard content-type string
        text: the response body as a string
        headers: a dictionary of response headers
    """

    def __init__(self, response):
        """
        :type response: :class:`pyramid.response.Response`
        """
        self.response = response

    @property
    def content_type(self):
        return self.response.content_type

    @property
    def headers(self):
        return self.response.headers

    @property
    def raw_bytes(self):
        return self.response.body

    @property
    def text(self):

        # Treating webob.Response carefully: first check if there's a
        # non-empty body attribute, since if not, we can short circuit
        if not self.response.body:
            return None
        # if there's a response body, try to read it as text, but carefully
        try:
            return self.response.text
        except AttributeError as prev_ex:
            # pyramid.response.Response raises AttributeError if you try to
            # read this property without setting the charset first: trap that
            # so it doesn't show up as a missing "text" attribute (and get
            # its error message swallowed by the base class)
            raise Exception(str(prev_ex))

    def json(self, **kwargs):
        return getattr(self.response, 'json_body', {})
pyramid_swagger/tween.py
DEFAULT_EXCLUDED_PATHS = [
    r'^/static/?',
    r'^/api-docs/?',
    r'^/swagger.(json|yaml)',
]
pyramid_swagger/load_schema.py
class SchemaValidator(object):
    """A Validator used by :mod:`pyramid_swagger.tween` to validate a
    field from the request or response.

    :param schema: a :class:`dict` jsonschema that was used by the
        validator
    :param validator: a Validator which a func:`validate` method
        for validating a field from a request or response. This
        will often be a :class:`jsonschema.validator.Validator`.
    """

    def __init__(self, schema, validator):
        self.schema = schema
        self.validator = validator

    @classmethod
    def from_schema(cls, schema, resolver, validator_class):
        type_checker = deepcopy(validator_class.TYPE_CHECKER)
        type_checker.redefine_many({
            type_name: lambda checker, value: all(check(value) for check in checks)
            for type_name, checks in iteritems(EXTENDED_TYPES)
        })
        extended_validator_class = jsonschema.validators.extend(
            validator_class,
            type_checker=type_checker,
        )
        return cls(
            schema,
            extended_validator_class(schema, resolver=resolver))

    def validate(self, values):
        """Validate a :class:`dict` of values. If `self.schema` is falsy this
        is a noop.
        """
        if not self.schema or (values is None and not self.schema.get('required', False)):
            return
        self.validator.validate(values)


Question: import re
import mock
import pytest
import simplejson
from bravado_core.exception import SwaggerMappingError
from bravado_core.operation import Operation
from bravado_core.spec import Spec
from mock import Mock
from pyramid.request import Request
from pyramid.response import Response
from pyramid.urldispatch import Route
from pyramid_swagger.exceptions import RequestValidationError
from pyramid_swagger.exceptions import ResponseValidationError
from pyramid_swagger.load_schema import SchemaValidator
from pyramid_swagger.load_schema import ValidatorMap
from pyramid_swagger.model import PathNotMatchedError
from pyramid_swagger.tween import DEFAULT_EXCLUDED_PATHS
from pyramid_swagger.tween import get_exclude_paths
from pyramid_swagger.tween import get_op_for_request
from pyramid_swagger.tween import get_swagger_objects
from pyramid_swagger.tween import get_swagger_versions
from pyramid_swagger.tween import handle_request
from pyramid_swagger.tween import prepare_body
from pyramid_swagger.tween import PyramidSwaggerRequest
from pyramid_swagger.tween import PyramidSwaggerResponse
from pyramid_swagger.tween import Settings
from pyramid_swagger.tween import should_exclude_path
from pyramid_swagger.tween import should_exclude_route
from pyramid_swagger.tween import SWAGGER_12
from pyramid_swagger.tween import SWAGGER_20
from pyramid_swagger.tween import validate_response
from pyramid_swagger.tween import validation_error
    from pyramid_swagger.tween import is_swagger_documentation_route
# -*- coding: utf-8 -*-
"""Unit tests for tweens.py"""
from __future__ import absolute_import





def assert_eq_regex_lists(left, right):
    assert [r.pattern for r in left] == [r.pattern for r in right]


def test_default_exclude_paths():
    assert_eq_regex_lists(
        get_exclude_paths(Mock(settings={})),
        [re.compile(r) for r in DEFAULT_EXCLUDED_PATHS]
    )


def test_exclude_path_with_string():
    path_string = r'/foo/'
    registry = Mock(settings={'pyramid_swagger.exclude_paths': path_string})
    assert_eq_regex_lists(
        get_exclude_paths(registry),
        [re.compile(r) for r in [path_string]]
    )


def test_exclude_path_with_overrides():
    paths = [r'/foo/', r'/bar/']
    compiled = get_exclude_paths(
        Mock(settings={'pyramid_swagger.exclude_paths': paths}))
    assert_eq_regex_lists(
        compiled,
        [re.compile(r) for r in paths]
    )


def test_exclude_path_with_old_setting():
    # TODO(#63): remove deprecated `skip_validation` setting in v2.0.
    paths = [r'/foo/', r'/bar/']
    assert_eq_regex_lists(
        get_exclude_paths(
            Mock(settings={'pyramid_swagger.skip_validation': paths})),
        [re.compile(r) for r in paths]
    )


def test_response_content_type_missing_raises_5xx():

Answer:     with pytest.raises(ResponseValidationError) as excinfo: