Created
May 8, 2014 11:28
-
-
Save danielholmstrom/218ba0ad89d24034cb2e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import copy | |
import functools | |
import json | |
from pyramid.httpexceptions import ( | |
HTTPUnsupportedMediaType, | |
) | |
from colander import ( | |
Invalid, | |
) | |
class ContentTypePredicate(object): | |
"""Predicate for content types | |
Sets request.content_type to the request content_type. | |
""" | |
def __init__(self, val, config): | |
self.val = val | |
def text(self): | |
return 'content type = {0}'.format(self.val) | |
phash = text | |
def __call__(self, context, request): | |
"""Check if the requests content type matches""" | |
return request.content_type == self.val | |
class RequestDataValidationError(Exception): | |
"""Error for when the data from the client was invalid""" | |
def __init__(self, errors=None, message=None, status=400): | |
"""Create a new Error | |
:param errors: Dict with validation errors \ | |
(Same format as :class:`colander.Invalid.asdict`) | |
:param message: An error message | |
:param status: HTTP Status code | |
""" | |
self._errors = errors or {} | |
self.message = message | |
self.status = int(status) | |
@property | |
def asdict(self): | |
"""Get as dict""" | |
return { | |
'message': self.message and str(self.message), | |
'errors': copy.copy(self._errors), | |
} | |
class InvalidDataError(RequestDataValidationError): | |
"""Error for when the validation was a success but the data was invalid | |
Error for when validation of client data was a success but other | |
requirements for the data like a foreign key id, failed. | |
""" | |
pass | |
def request_data_invalid_response_adapter(request): | |
"""Adapter that converts a `RequestDataValidationError` to a json response | |
:returns: A response with content-type 'application/json' | |
""" | |
response = request.response | |
response.content_type = 'application/json' | |
response.status = request.exception.status | |
response.text = json.dumps(request.exception.asdict) | |
return response | |
def get_request_json_data(request): | |
"""Get request json data | |
:raises: HTTPUnsupportedMediaType If the request.content_type isn't json | |
:returns: The request json body or None | |
""" | |
if request.content_type != 'application/json': | |
raise HTTPUnsupportedMediaType( | |
"Content-Type '{0}' not supported".format(request.content_type)) | |
else: | |
# Get json body or None | |
return getattr(request, 'json_body', None) | |
def validate_request_data(request, schema, error_message=None): | |
"""Validate data with a schema | |
:param request: The request | |
:param schema: Colander shema for validating the data | |
:param error_message: Additional error message if the validation failed | |
:raises: :class:`RequestDataValidationError` If data is None | |
:raises: :class:`RequestDataValidationError` If validation failed | |
:returns: Sanitized and validated data | |
""" | |
data = get_request_json_data(request) | |
if data is None: | |
raise RequestDataValidationError({}, message='Request data missing') | |
try: | |
return schema.deserialize(data) | |
except Invalid as e: | |
raise RequestDataValidationError(e.asdict(), message=error_message) | |
def client_data_schema(schema, message=None, status=400): | |
"""Adds client data validation to a view | |
The schema validates the client data, not route parameters. | |
The validated data will be added to the request under 'validated_data' | |
:returns: View wrapper | |
""" | |
def decorator(view): | |
@functools.wraps(view) | |
def wrapper(request): | |
try: | |
data = schema().deserialize(request.client_data()) | |
except Invalid as e: | |
raise InvalidDataError(message=message, | |
errors=e.asdict(), | |
status=status) | |
setattr(request, 'validated_data', data) | |
return view(request) | |
return wrapper | |
return decorator | |
def validate(validator, message=None, status=400): | |
"""Run a validator before the view | |
The validator method should raise an error if validation failed. | |
The validator will be called like this: validator(request). | |
""" | |
def decorator(view): | |
@functools.wraps(view) | |
def wrapper(request): | |
validator(request) | |
return view(request) | |
return wrapper | |
return decorator | |
def includeme(config): | |
config.add_view_predicate('content_type', ContentTypePredicate) | |
config.add_view(request_data_invalid_response_adapter, | |
context=RequestDataValidationError) | |
config.add_request_method(get_request_json_data, | |
'client_data', | |
reify=False) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment